workspace/projects/gamilit/apps/frontend/eslint-rules/no-api-route-issues.cjs
rckrdmrd ea1879f4ad feat: Initial workspace structure with multi-level Git configuration
- 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>
2025-12-08 10:44:23 -06:00

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}'`);
},
});
}
},
};
},
};