/** * ESLint rule to enforce PageQueries use Builders * * PageQueries should not manually transform API DTOs or return them directly. * They must use Builder classes to transform API DTOs to View Data. */ module.exports = { meta: { type: 'problem', docs: { description: 'Enforce PageQueries use Builders for data transformation', category: 'Page Query', recommended: true, }, fixable: null, schema: [], messages: { mustUseBuilder: 'PageQueries must use Builder classes to transform API DTOs to View Data. Found manual transformation or direct API DTO return.', multipleExports: 'PageQuery files should only export the PageQuery class, not DTOs.', }, }, create(context) { let inPageQueryExecute = false; let hasManualTransformation = false; let hasMultipleExports = false; let hasBuilderCall = false; let pageQueryClassName = null; return { // Track PageQuery class name ClassDeclaration(node) { if (node.id && node.id.name && node.id.name.endsWith('PageQuery')) { pageQueryClassName = node.id.name; } }, // Track PageQuery class execute method MethodDefinition(node) { if (node.key.type === 'Identifier' && node.key.name === 'execute' && node.parent.type === 'ClassBody') { const classNode = node.parent.parent; if (classNode && classNode.id && classNode.id.name && classNode.id.name.endsWith('PageQuery')) { inPageQueryExecute = true; } } }, // Detect Builder calls CallExpression(node) { if (inPageQueryExecute) { // Check for Builder.build() or Builder.createViewData() if (node.callee.type === 'MemberExpression' && node.callee.property.type === 'Identifier' && (node.callee.property.name === 'build' || node.callee.property.name === 'createViewData')) { // Check if the object is a Builder if (node.callee.object.type === 'Identifier' && (node.callee.object.name.includes('Builder') || node.callee.object.name.includes('builder'))) { hasBuilderCall = true; } } } }, // Detect object literal assignments (manual transformation) VariableDeclarator(node) { if (inPageQueryExecute && node.init && node.init.type === 'ObjectExpression') { hasManualTransformation = true; } }, // Detect object literal in return statements ReturnStatement(node) { if (inPageQueryExecute && node.argument) { // Direct object literal return if (node.argument.type === 'ObjectExpression') { hasManualTransformation = true; } // Direct identifier return (likely API DTO) else if (node.argument.type === 'Identifier' && !node.argument.name.includes('ViewData') && !node.argument.name.includes('viewData')) { // This might be returning an API DTO directly // We'll flag it as manual transformation since no builder was used if (!hasBuilderCall) { hasManualTransformation = true; } } // CallExpression like Result.ok(apiDto) or Result.err() else if (node.argument.type === 'CallExpression') { // Check if it's a Result call with an identifier argument const callExpr = node.argument; if (callExpr.callee.type === 'MemberExpression' && callExpr.callee.object.type === 'Identifier' && callExpr.callee.object.name === 'Result' && callExpr.callee.property.type === 'Identifier' && (callExpr.callee.property.name === 'ok' || callExpr.callee.property.name === 'err')) { // If it's Result.ok(someIdentifier), check if the identifier is likely an API DTO if (callExpr.callee.property.name === 'ok' && callExpr.arguments.length > 0 && callExpr.arguments[0].type === 'Identifier') { const argName = callExpr.arguments[0].name; // Common API DTO naming patterns const isApiDto = argName.includes('Dto') || argName.includes('api') || argName === 'result' || argName === 'data'; if (isApiDto && !hasBuilderCall) { hasManualTransformation = true; } } } } } }, // Track exports ExportNamedDeclaration(node) { if (node.declaration) { if (node.declaration.type === 'ClassDeclaration') { const className = node.declaration.id?.name; if (className && !className.endsWith('PageQuery')) { hasMultipleExports = true; } } else if (node.declaration.type === 'InterfaceDeclaration' || node.declaration.type === 'TypeAlias') { hasMultipleExports = true; } } else if (node.specifiers && node.specifiers.length > 0) { hasMultipleExports = true; } }, 'MethodDefinition:exit'(node) { if (node.key.type === 'Identifier' && node.key.name === 'execute') { inPageQueryExecute = false; } }, 'Program:exit'() { // Only report if no builder was used if (hasManualTransformation && !hasBuilderCall) { context.report({ node: context.getSourceCode().ast, messageId: 'mustUseBuilder', }); } if (hasMultipleExports) { context.report({ node: context.getSourceCode().ast, messageId: 'multipleExports', }); } }, }; }, };