website refactor
This commit is contained in:
@@ -1,166 +1,115 @@
|
||||
/**
|
||||
* ESLint rule to enforce PageQueries use Builders
|
||||
* ESLint Rule: Page Query Must 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.
|
||||
* Ensures page queries use builders to transform DTOs into ViewData
|
||||
* This prevents DTOs from leaking to the client
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce PageQueries use Builders for data transformation',
|
||||
description: 'Ensure page queries use builders to transform DTOs into ViewData',
|
||||
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.',
|
||||
mustUseBuilder: 'PageQueries must use ViewDataBuilder to transform DTOs - see apps/website/lib/contracts/builders/ViewDataBuilder.ts',
|
||||
noDirectDtoReturn: 'PageQueries must not return DTOs directly, use builders to create ViewData',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
let inPageQueryExecute = false;
|
||||
let hasManualTransformation = false;
|
||||
let hasMultipleExports = false;
|
||||
let hasBuilderCall = false;
|
||||
let pageQueryClassName = null;
|
||||
const filename = context.getFilename();
|
||||
const isPageQuery = filename.includes('/page-queries/') && filename.endsWith('.ts');
|
||||
|
||||
if (!isPageQuery) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let hasBuilderImport = false;
|
||||
const builderUsages = [];
|
||||
const returnStatements = [];
|
||||
|
||||
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') {
|
||||
// Check for builder imports
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (importPath.includes('/builders/view-data/')) {
|
||||
hasBuilderImport = true;
|
||||
|
||||
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;
|
||||
// Track which builder is imported
|
||||
node.specifiers.forEach(spec => {
|
||||
if (spec.type === 'ImportSpecifier') {
|
||||
builderUsages.push({
|
||||
name: spec.imported.name,
|
||||
localName: spec.local.name,
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
// Detect object literal assignments (manual transformation)
|
||||
VariableDeclarator(node) {
|
||||
if (inPageQueryExecute && node.init && node.init.type === 'ObjectExpression') {
|
||||
hasManualTransformation = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Detect object literal in return statements
|
||||
// Track 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;
|
||||
if (node.argument) {
|
||||
returnStatements.push(node);
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
// Only report if no builder was used
|
||||
if (hasManualTransformation && !hasBuilderCall) {
|
||||
if (!hasBuilderImport) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'mustUseBuilder',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (hasMultipleExports) {
|
||||
context.report({
|
||||
node: context.getSourceCode().ast,
|
||||
messageId: 'multipleExports',
|
||||
});
|
||||
}
|
||||
// Check if return statements use builders
|
||||
returnStatements.forEach(returnNode => {
|
||||
const returnExpr = returnNode.argument;
|
||||
|
||||
// Check if it's a builder call
|
||||
if (returnExpr && returnExpr.type === 'CallExpression') {
|
||||
const callee = returnExpr.callee;
|
||||
|
||||
// Check if it's a builder method call (e.g., ViewDataBuilder.build())
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'build') {
|
||||
// This is good - using a builder
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if it's a direct Result.ok() with DTO
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'Result' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
callee.property.name === 'ok') {
|
||||
|
||||
// Check if the argument is a variable that might be a DTO
|
||||
if (returnExpr.arguments && returnExpr.arguments[0]) {
|
||||
const arg = returnExpr.arguments[0];
|
||||
|
||||
// If it's an identifier, check if it's likely a DTO
|
||||
if (arg.type === 'Identifier') {
|
||||
const varName = arg.name;
|
||||
// Common DTO patterns: result, data, dto, apiResult, etc.
|
||||
if (varName.match(/(result|data|dto|apiResult|response)/i)) {
|
||||
context.report({
|
||||
node: returnNode,
|
||||
messageId: 'noDirectDtoReturn',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user