website refactor
This commit is contained in:
211
apps/website/eslint-rules/mutation-must-use-builders.js
Normal file
211
apps/website/eslint-rules/mutation-must-use-builders.js
Normal file
@@ -0,0 +1,211 @@
|
||||
/**
|
||||
* ESLint Rule: Mutation Must Use Builders
|
||||
*
|
||||
* Ensures mutations use builders to transform DTOs into ViewData
|
||||
* or return appropriate simple types (void, string, etc.)
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure mutations use builders or return appropriate types, not DTOs',
|
||||
category: 'Mutation',
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
mustUseBuilder: 'Mutations must use ViewDataBuilder to transform DTOs or return simple types - see apps/website/lib/contracts/builders/ViewDataBuilder.ts',
|
||||
noDirectDtoReturn: 'Mutations must not return DTOs directly, use builders to create ViewData or return simple types like void, string, or primitive values',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isMutation = filename.includes('/lib/mutations/') && filename.endsWith('.ts');
|
||||
|
||||
if (!isMutation) {
|
||||
return {};
|
||||
}
|
||||
|
||||
let hasBuilderImport = false;
|
||||
const returnStatements = [];
|
||||
const methodDefinitions = [];
|
||||
|
||||
return {
|
||||
// Check for builder imports
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source.value;
|
||||
if (importPath.includes('/builders/view-data/')) {
|
||||
hasBuilderImport = true;
|
||||
}
|
||||
},
|
||||
|
||||
// Track method definitions
|
||||
MethodDefinition(node) {
|
||||
if (node.key.type === 'Identifier' && node.key.name === 'execute') {
|
||||
methodDefinitions.push(node);
|
||||
}
|
||||
},
|
||||
|
||||
// Track return statements in execute method
|
||||
ReturnStatement(node) {
|
||||
// Only track returns inside execute method
|
||||
let parent = node;
|
||||
while (parent) {
|
||||
if (parent.type === 'MethodDefinition' &&
|
||||
parent.key.type === 'Identifier' &&
|
||||
parent.key.name === 'execute') {
|
||||
if (node.argument) {
|
||||
returnStatements.push(node);
|
||||
}
|
||||
break;
|
||||
}
|
||||
parent = parent.parent;
|
||||
}
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
if (methodDefinitions.length === 0) {
|
||||
return; // No execute method found
|
||||
}
|
||||
|
||||
// Check each return statement in execute method
|
||||
returnStatements.forEach(returnNode => {
|
||||
const returnExpr = returnNode.argument;
|
||||
|
||||
if (!returnExpr) return;
|
||||
|
||||
// Check if it's a Result type
|
||||
if (returnExpr.type === 'CallExpression') {
|
||||
const callee = returnExpr.callee;
|
||||
|
||||
// Check for Result.ok() or Result.err()
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'Result' &&
|
||||
callee.property.type === 'Identifier' &&
|
||||
(callee.property.name === 'ok' || callee.property.name === 'err')) {
|
||||
|
||||
const resultArg = returnExpr.arguments[0];
|
||||
|
||||
if (callee.property.name === 'ok') {
|
||||
// Check what's being returned in Result.ok()
|
||||
|
||||
// If it's a builder call, that's good
|
||||
if (resultArg && resultArg.type === 'CallExpression') {
|
||||
const builderCallee = resultArg.callee;
|
||||
if (builderCallee.type === 'MemberExpression' &&
|
||||
builderCallee.property.type === 'Identifier' &&
|
||||
builderCallee.property.name === 'build') {
|
||||
return; // Good: using builder
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a method call that might be unwrap() or similar
|
||||
if (resultArg && resultArg.type === 'CallExpression') {
|
||||
const methodCallee = resultArg.callee;
|
||||
if (methodCallee.type === 'MemberExpression' &&
|
||||
methodCallee.property.type === 'Identifier') {
|
||||
const methodName = methodCallee.property.name;
|
||||
// Common DTO extraction methods
|
||||
if (['unwrap', 'getValue', 'getData', 'getResult'].includes(methodName)) {
|
||||
context.report({
|
||||
node: returnNode,
|
||||
messageId: 'noDirectDtoReturn',
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If it's a simple type (string, number, boolean, void), that's OK
|
||||
if (resultArg && (
|
||||
resultArg.type === 'Literal' ||
|
||||
resultArg.type === 'Identifier' &&
|
||||
['void', 'undefined', 'null'].includes(resultArg.name) ||
|
||||
resultArg.type === 'UnaryExpression' // e.g., undefined
|
||||
)) {
|
||||
return; // Good: simple type
|
||||
}
|
||||
|
||||
// If it's an identifier that might be a DTO
|
||||
if (resultArg && resultArg.type === 'Identifier') {
|
||||
const varName = resultArg.name;
|
||||
// Check if it's likely a DTO (ends with DTO, or common patterns)
|
||||
if (varName.match(/(DTO|Result|Response|Data)$/i) &&
|
||||
!varName.includes('ViewData') &&
|
||||
!varName.includes('ViewModel')) {
|
||||
context.report({
|
||||
node: returnNode,
|
||||
messageId: 'noDirectDtoReturn',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If it's an object literal with DTO-like properties
|
||||
if (resultArg && resultArg.type === 'ObjectExpression') {
|
||||
const hasDtoLikeProps = resultArg.properties.some(prop => {
|
||||
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
||||
const keyName = prop.key.name;
|
||||
return keyName.match(/(id|Id|URL|Url|DTO)$/i);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasDtoLikeProps) {
|
||||
context.report({
|
||||
node: returnNode,
|
||||
messageId: 'noDirectDtoReturn',
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// If no builder import and we have DTO returns, report
|
||||
if (!hasBuilderImport && returnStatements.length > 0) {
|
||||
const hasDtoReturn = returnStatements.some(returnNode => {
|
||||
const returnExpr = returnNode.argument;
|
||||
if (returnExpr && returnExpr.type === 'CallExpression') {
|
||||
const callee = returnExpr.callee;
|
||||
if (callee.type === 'MemberExpression' &&
|
||||
callee.object.type === 'Identifier' &&
|
||||
callee.object.name === 'Result' &&
|
||||
callee.property.name === 'ok') {
|
||||
const arg = returnExpr.arguments[0];
|
||||
|
||||
// Check for method calls like result.unwrap()
|
||||
if (arg && arg.type === 'CallExpression') {
|
||||
const methodCallee = arg.callee;
|
||||
if (methodCallee.type === 'MemberExpression' &&
|
||||
methodCallee.property.type === 'Identifier') {
|
||||
const methodName = methodCallee.property.name;
|
||||
if (['unwrap', 'getValue', 'getData', 'getResult'].includes(methodName)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for identifier
|
||||
if (arg && arg.type === 'Identifier') {
|
||||
return arg.name.match(/(DTO|Result|Response|Data)$/i);
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
if (hasDtoReturn) {
|
||||
context.report({
|
||||
node: methodDefinitions[0],
|
||||
messageId: 'mustUseBuilder',
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user