/** * ESLint rule to enforce clean error handling architecture * * PageQueries and Mutations must: * 1. Use Services for data access * 2. Services must return Result types */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce clean error handling architecture in PageQueries and Mutations', category: 'Architecture', recommended: true, }, fixable: null, schema: [], messages: { mustUseServices: 'PageQueries and Mutations must use Services for data access, not API Clients directly.', servicesMustReturnResult: 'Services must return Result for type-safe error handling.', }, }, create(context) { const filename = context.getFilename(); const isPageQuery = filename.includes('/lib/page-queries/'); const isMutation = filename.includes('/lib/mutations/'); const isService = filename.includes('/lib/services/'); const isRelevant = isPageQuery || isMutation || isService; if (!isRelevant) return {}; // Track imports const apiClientImports = new Set(); const serviceImports = new Set(); return { // Track imports ImportDeclaration(node) { node.specifiers.forEach(spec => { const importPath = node.source.value; if (importPath.includes('/lib/api/')) { apiClientImports.add(spec.local.name); } if (importPath.includes('/lib/services/')) { serviceImports.add(spec.local.name); } }); }, // Check PageQueries/Mutations for direct API Client usage NewExpression(node) { if (node.callee.type === 'Identifier') { const className = node.callee.name; // Only check in PageQueries and Mutations if ((isPageQuery || isMutation) && (className.endsWith('ApiClient') || className.endsWith('Api'))) { context.report({ node, messageId: 'mustUseServices', }); } } }, // Check Services for Result return type MethodDefinition(node) { if (isService && node.key.type === 'Identifier' && node.key.name === 'execute') { const returnType = node.value.returnType; if (!returnType || !returnType.typeAnnotation || !returnType.typeAnnotation.typeName || returnType.typeAnnotation.typeName.name !== 'Promise') { // Missing Promise return type context.report({ node, messageId: 'servicesMustReturnResult', }); return; } // Check for Result type const typeArgs = returnType.typeAnnotation.typeParameters; if (!typeArgs || !typeArgs.params || typeArgs.params.length === 0) { context.report({ node, messageId: 'servicesMustReturnResult', }); return; } const resultType = typeArgs.params[0]; if (resultType.type !== 'TSTypeReference' || !resultType.typeName || (resultType.typeName.type === 'Identifier' && resultType.typeName.name !== 'Result')) { context.report({ node, messageId: 'servicesMustReturnResult', }); } } }, // Check that PageQueries/Mutations have Service imports 'Program:exit'() { if ((isPageQuery || isMutation) && serviceImports.size === 0) { context.report({ node: context.getSourceCode().ast, messageId: 'mustUseServices', }); } }, }; }, };