Files
gridpilot.gg/apps/website/eslint-rules/page-query-must-use-builders.js
2026-01-14 02:02:24 +01:00

115 lines
3.6 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 = [];
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 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;
// Common DTO patterns: result, data, dto, apiResult, etc.
if (varName.match(/(result|data|dto|apiResult|response)/i)) {
context.report({
node: returnNode,
messageId: 'noDirectDtoReturn',
});
}
}
}
}
}
});
},
};
},
};