- Configure workspace Git repository with comprehensive .gitignore - Add Odoo as submodule for ERP reference code - Include documentation: SETUP.md, GIT-STRUCTURE.md - Add gitignore templates for projects (backend, frontend, database) - Structure supports independent repos per project/subproject level Workspace includes: - core/ - Reusable patterns, modules, orchestration system - projects/ - Active projects (erp-suite, gamilit, trading-platform, etc.) - knowledge-base/ - Reference code and patterns (includes Odoo submodule) - devtools/ - Development tools and templates - customers/ - Client implementations template 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
167 lines
5.7 KiB
JavaScript
167 lines
5.7 KiB
JavaScript
/**
|
|
* Custom ESLint Rule: no-api-route-issues
|
|
*
|
|
* Prevents common API route configuration errors:
|
|
* 1. Duplicate /api/ prefix in route paths
|
|
* 2. New axios instances (must use official apiClient)
|
|
* 3. Direct fetch() calls (must use apiClient)
|
|
* 4. Hardcoded API URLs (must use constants)
|
|
*/
|
|
|
|
module.exports = {
|
|
meta: {
|
|
type: 'problem',
|
|
docs: {
|
|
description: 'Prevent API route configuration issues',
|
|
category: 'Best Practices',
|
|
recommended: true,
|
|
},
|
|
fixable: 'code',
|
|
schema: [],
|
|
messages: {
|
|
duplicateApiPrefix:
|
|
'Duplicate /api/ prefix detected. The baseURL already includes /api, remove it from the endpoint path.\n' +
|
|
'Example: Change `apiClient.get("/api/v1/users")` to `apiClient.get("/v1/users")`',
|
|
newAxiosInstance:
|
|
'Creating new axios instance is forbidden. Use the official apiClient from @/services/api/apiClient instead.\n' +
|
|
'Import: import { apiClient } from "@/services/api/apiClient"',
|
|
directFetch:
|
|
'Direct fetch() calls bypass authentication and error handling. Use apiClient instead.\n' +
|
|
'Example: Change `fetch("/api/users")` to `apiClient.get("/api/users")`',
|
|
hardcodedUrl:
|
|
'Hardcoded API URL detected. Use API_ENDPOINTS constants instead.\n' +
|
|
'Import: import { API_ENDPOINTS } from "@/shared/constants/api-endpoints"',
|
|
},
|
|
},
|
|
|
|
create(context) {
|
|
const OFFICIAL_API_CLIENT = '@/services/api/apiClient';
|
|
const API_PATTERN = /\/api\//;
|
|
const DOUBLE_API_PATTERN = /\/api\/api\//;
|
|
const HTTP_URL_PATTERN = /https?:\/\//;
|
|
|
|
return {
|
|
// Rule 1: Detect duplicate /api/api/ prefix
|
|
CallExpression(node) {
|
|
// Check apiClient.get/post/put/patch/delete calls
|
|
if (
|
|
node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.name === 'apiClient' &&
|
|
['get', 'post', 'put', 'patch', 'delete'].includes(node.callee.property.name)
|
|
) {
|
|
const firstArg = node.arguments[0];
|
|
|
|
if (firstArg && firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
// Check for duplicate /api/api/
|
|
if (DOUBLE_API_PATTERN.test(firstArg.value)) {
|
|
context.report({
|
|
node: firstArg,
|
|
messageId: 'duplicateApiPrefix',
|
|
fix(fixer) {
|
|
// Auto-fix: Remove the first /api/
|
|
const fixed = firstArg.value.replace(/\/api\/api\//, '/api/');
|
|
return fixer.replaceText(firstArg, `'${fixed}'`);
|
|
},
|
|
});
|
|
}
|
|
|
|
// Check for /api/v1/ pattern (should be /v1/ since baseURL already has /api)
|
|
if (firstArg.value.startsWith('/api/v1')) {
|
|
context.report({
|
|
node: firstArg,
|
|
messageId: 'duplicateApiPrefix',
|
|
fix(fixer) {
|
|
// Auto-fix: Remove /api/ prefix
|
|
const fixed = firstArg.value.replace(/^\/api\//, '/');
|
|
return fixer.replaceText(firstArg, `'${fixed}'`);
|
|
},
|
|
});
|
|
}
|
|
|
|
// Check for hardcoded HTTP URLs
|
|
if (HTTP_URL_PATTERN.test(firstArg.value)) {
|
|
context.report({
|
|
node: firstArg,
|
|
messageId: 'hardcodedUrl',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rule 3: Detect direct fetch() calls
|
|
if (
|
|
node.callee.type === 'Identifier' &&
|
|
node.callee.name === 'fetch'
|
|
) {
|
|
const firstArg = node.arguments[0];
|
|
|
|
// Only report if it's an API call (contains /api/ or HTTP URL)
|
|
if (firstArg && firstArg.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
if (API_PATTERN.test(firstArg.value) || HTTP_URL_PATTERN.test(firstArg.value)) {
|
|
context.report({
|
|
node,
|
|
messageId: 'directFetch',
|
|
});
|
|
}
|
|
}
|
|
|
|
// Also check template literals
|
|
if (firstArg && firstArg.type === 'TemplateLiteral') {
|
|
const templateString = firstArg.quasis.map(q => q.value.cooked).join('');
|
|
if (API_PATTERN.test(templateString) || HTTP_URL_PATTERN.test(templateString)) {
|
|
context.report({
|
|
node,
|
|
messageId: 'directFetch',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
// Rule 2: Detect new axios instances
|
|
CallExpression(node) {
|
|
if (
|
|
node.callee.type === 'MemberExpression' &&
|
|
node.callee.object.name === 'axios' &&
|
|
node.callee.property.name === 'create'
|
|
) {
|
|
// Allow in the official apiClient file
|
|
const filename = context.getFilename();
|
|
if (filename.includes('services/api/apiClient')) {
|
|
return;
|
|
}
|
|
|
|
context.report({
|
|
node,
|
|
messageId: 'newAxiosInstance',
|
|
});
|
|
}
|
|
},
|
|
|
|
// Additional check: Detect imports of deleted files
|
|
ImportDeclaration(node) {
|
|
const source = node.source.value;
|
|
|
|
// Check for deleted files
|
|
const deletedFiles = [
|
|
'@/lib/api/client',
|
|
'@/shared/utils/api.util',
|
|
'@/features/auth/api/apiClient',
|
|
'./apiClient', // Relative import in features/auth/api
|
|
'./api.util', // Relative import in shared/utils
|
|
];
|
|
|
|
if (deletedFiles.some(deleted => source.includes(deleted))) {
|
|
context.report({
|
|
node,
|
|
message: `This file has been deleted. Use '${OFFICIAL_API_CLIENT}' instead.`,
|
|
fix(fixer) {
|
|
return fixer.replaceText(node.source, `'${OFFICIAL_API_CLIENT}'`);
|
|
},
|
|
});
|
|
}
|
|
},
|
|
};
|
|
},
|
|
};
|