156 lines
5.0 KiB
JavaScript
156 lines
5.0 KiB
JavaScript
/**
|
|
* ESLint Rule: Page Query Must Use Builders
|
|
*
|
|
* 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: 'Ensure page queries use builders to transform DTOs into ViewData',
|
|
category: 'Page Query',
|
|
recommended: true,
|
|
},
|
|
messages: {
|
|
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) {
|
|
const filename = context.getFilename();
|
|
const isPageQuery = filename.includes('/page-queries/') && filename.endsWith('.ts');
|
|
|
|
if (!isPageQuery) {
|
|
return {};
|
|
}
|
|
|
|
let hasBuilderImport = false;
|
|
const builderUsages = [];
|
|
const returnStatements = [];
|
|
const builtVariables = new Set();
|
|
|
|
return {
|
|
// Check for builder imports
|
|
ImportDeclaration(node) {
|
|
const importPath = node.source.value;
|
|
if (importPath.includes('/builders/view-data/')) {
|
|
hasBuilderImport = true;
|
|
|
|
// Track which builder is imported
|
|
node.specifiers.forEach(spec => {
|
|
if (spec.type === 'ImportSpecifier') {
|
|
builderUsages.push({
|
|
name: spec.imported.name,
|
|
localName: spec.local.name,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
// Track variable assignments from builders
|
|
VariableDeclarator(node) {
|
|
if (node.init && node.init.type === 'CallExpression') {
|
|
const callee = node.init.callee;
|
|
if (callee.type === 'MemberExpression' &&
|
|
callee.property.type === 'Identifier' &&
|
|
callee.property.name === 'build') {
|
|
builtVariables.add(node.id.name);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Track variable assignments from builders
|
|
VariableDeclarator(node) {
|
|
if (node.init && node.init.type === 'CallExpression') {
|
|
const callee = node.init.callee;
|
|
if (callee.type === 'MemberExpression' &&
|
|
callee.property.type === 'Identifier' &&
|
|
callee.property.name === 'build') {
|
|
builtVariables.add(node.id.name);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Track variable assignments from builders
|
|
VariableDeclarator(node) {
|
|
if (node.init && node.init.type === 'CallExpression') {
|
|
const callee = node.init.callee;
|
|
if (callee.type === 'MemberExpression' &&
|
|
callee.property.type === 'Identifier' &&
|
|
callee.property.name === 'build') {
|
|
builtVariables.add(node.id.name);
|
|
}
|
|
}
|
|
},
|
|
|
|
// Track return statements
|
|
ReturnStatement(node) {
|
|
if (node.argument) {
|
|
returnStatements.push(node);
|
|
}
|
|
},
|
|
|
|
'Program:exit'() {
|
|
if (!hasBuilderImport) {
|
|
context.report({
|
|
node: context.getSourceCode().ast,
|
|
messageId: 'mustUseBuilder',
|
|
});
|
|
return;
|
|
}
|
|
|
|
// 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;
|
|
// Skip if it's a variable built from a builder
|
|
if (builtVariables.has(varName)) {
|
|
return;
|
|
}
|
|
// Common DTO patterns: result, data, dto, apiResult, etc.
|
|
if (varName.match(/(result|data|dto|apiResult|response)/i)) {
|
|
context.report({
|
|
node: returnNode,
|
|
messageId: 'noDirectDtoReturn',
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
},
|
|
};
|
|
},
|
|
}; |