website refactor
This commit is contained in:
227
apps/website/eslint-rules/mutation-must-map-errors.js
Normal file
227
apps/website/eslint-rules/mutation-must-map-errors.js
Normal file
@@ -0,0 +1,227 @@
|
||||
/**
|
||||
* ESLint Rule: Mutation Must Map Errors
|
||||
*
|
||||
* Enforces the architecture documented in `lib/contracts/mutations/Mutation.ts`:
|
||||
* - Mutations call Services.
|
||||
* - Service returns Result<..., DomainError>.
|
||||
* - Mutation maps DomainError -> MutationError via mapToMutationError.
|
||||
* - Mutation returns Result<..., MutationError>.
|
||||
*
|
||||
* Strict enforcement (scoped to service results):
|
||||
* - Disallow returning a Service Result directly.
|
||||
* - `return result;` where `result` came from `await service.*(...)`
|
||||
* - `return service.someMethod(...)`
|
||||
* - `return await service.someMethod(...)`
|
||||
* - If a mutation does `if (result.isErr()) return Result.err(...)`,
|
||||
* it must be `Result.err(mapToMutationError(result.getError()))`.
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Require mutations to map service errors via mapToMutationError',
|
||||
category: 'Architecture',
|
||||
recommended: true,
|
||||
},
|
||||
schema: [],
|
||||
messages: {
|
||||
mustMapMutationError:
|
||||
'Mutations must map service errors via mapToMutationError(result.getError()) before returning Result.err(...) - see apps/website/lib/contracts/mutations/Mutation.ts',
|
||||
mustNotReturnServiceResult:
|
||||
'Mutations must not return a Service Result directly; map errors and return a Mutation-layer Result instead - see apps/website/lib/contracts/mutations/Mutation.ts',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
if (!filename.includes('/lib/mutations/') || !filename.endsWith('.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const resultVarsFromServiceCall = new Set();
|
||||
const serviceVarNames = new Set();
|
||||
const mapToMutationErrorNames = new Set(['mapToMutationError']);
|
||||
|
||||
function isIdentifier(node, name) {
|
||||
return node && node.type === 'Identifier' && node.name === name;
|
||||
}
|
||||
|
||||
function isResultErrCall(node) {
|
||||
return (
|
||||
node &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
!node.callee.computed &&
|
||||
node.callee.object &&
|
||||
node.callee.object.type === 'Identifier' &&
|
||||
node.callee.object.name === 'Result' &&
|
||||
node.callee.property &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'err'
|
||||
);
|
||||
}
|
||||
|
||||
function isMapToMutationErrorCall(node) {
|
||||
return (
|
||||
node &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee &&
|
||||
node.callee.type === 'Identifier' &&
|
||||
mapToMutationErrorNames.has(node.callee.name)
|
||||
);
|
||||
}
|
||||
|
||||
function isGetErrorCall(node, resultVarName) {
|
||||
return (
|
||||
node &&
|
||||
node.type === 'CallExpression' &&
|
||||
node.callee &&
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
!node.callee.computed &&
|
||||
node.callee.object &&
|
||||
isIdentifier(node.callee.object, resultVarName) &&
|
||||
node.callee.property &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
node.callee.property.name === 'getError'
|
||||
);
|
||||
}
|
||||
|
||||
function isServiceObjectName(name) {
|
||||
return typeof name === 'string' && (name.endsWith('Service') || serviceVarNames.has(name));
|
||||
}
|
||||
|
||||
function isServiceMemberCallExpression(callExpression) {
|
||||
if (!callExpression || callExpression.type !== 'CallExpression') return false;
|
||||
const callee = callExpression.callee;
|
||||
if (!callee || callee.type !== 'MemberExpression' || callee.computed) return false;
|
||||
if (!callee.object || callee.object.type !== 'Identifier') return false;
|
||||
return isServiceObjectName(callee.object.name);
|
||||
}
|
||||
|
||||
function isAwaitedServiceCall(init) {
|
||||
if (!init || init.type !== 'AwaitExpression') return false;
|
||||
return isServiceMemberCallExpression(init.argument);
|
||||
}
|
||||
|
||||
function collectReturnStatements(statementNode, callback) {
|
||||
if (!statementNode) return;
|
||||
|
||||
if (statementNode.type !== 'BlockStatement') {
|
||||
if (statementNode.type === 'ReturnStatement') callback(statementNode);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const s of statementNode.body || []) {
|
||||
if (!s) continue;
|
||||
if (s.type === 'ReturnStatement') callback(s);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source && node.source.value;
|
||||
if (typeof importPath !== 'string') return;
|
||||
|
||||
const isMutationErrorImport =
|
||||
importPath.includes('/lib/contracts/mutations/MutationError') ||
|
||||
importPath.endsWith('lib/contracts/mutations/MutationError') ||
|
||||
importPath.endsWith('contracts/mutations/MutationError') ||
|
||||
importPath.endsWith('contracts/mutations/MutationError.ts');
|
||||
|
||||
if (!isMutationErrorImport) return;
|
||||
|
||||
for (const spec of node.specifiers || []) {
|
||||
// import { mapToMutationError as X } from '...'
|
||||
if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.type === 'Identifier') {
|
||||
if (spec.imported.name === 'mapToMutationError') {
|
||||
mapToMutationErrorNames.add(spec.local.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
VariableDeclarator(node) {
|
||||
if (!node.id || node.id.type !== 'Identifier') return;
|
||||
|
||||
// const service = new SomeService();
|
||||
if (node.init && node.init.type === 'NewExpression' && node.init.callee && node.init.callee.type === 'Identifier') {
|
||||
if (node.init.callee.name.endsWith('Service')) {
|
||||
serviceVarNames.add(node.id.name);
|
||||
}
|
||||
}
|
||||
|
||||
// const result = await service.method(...)
|
||||
if (!isAwaitedServiceCall(node.init)) return;
|
||||
resultVarsFromServiceCall.add(node.id.name);
|
||||
},
|
||||
|
||||
ReturnStatement(node) {
|
||||
if (!node.argument) return;
|
||||
|
||||
// return result;
|
||||
if (node.argument.type === 'Identifier') {
|
||||
if (resultVarsFromServiceCall.has(node.argument.name)) {
|
||||
context.report({ node, messageId: 'mustNotReturnServiceResult' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// return service.method(...)
|
||||
if (node.argument.type === 'CallExpression') {
|
||||
if (isServiceMemberCallExpression(node.argument)) {
|
||||
context.report({ node, messageId: 'mustNotReturnServiceResult' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// return await service.method(...)
|
||||
if (node.argument.type === 'AwaitExpression') {
|
||||
if (isServiceMemberCallExpression(node.argument.argument)) {
|
||||
context.report({ node, messageId: 'mustNotReturnServiceResult' });
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
IfStatement(node) {
|
||||
// if (result.isErr()) { return Result.err(...) }
|
||||
const test = node.test;
|
||||
if (!test || test.type !== 'CallExpression') return;
|
||||
const testCallee = test.callee;
|
||||
if (
|
||||
!testCallee ||
|
||||
testCallee.type !== 'MemberExpression' ||
|
||||
testCallee.computed ||
|
||||
!testCallee.object ||
|
||||
testCallee.object.type !== 'Identifier' ||
|
||||
!testCallee.property ||
|
||||
testCallee.property.type !== 'Identifier' ||
|
||||
testCallee.property.name !== 'isErr'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const resultVarName = testCallee.object.name;
|
||||
if (!resultVarsFromServiceCall.has(resultVarName)) return;
|
||||
|
||||
collectReturnStatements(node.consequent, (returnNode) => {
|
||||
const arg = returnNode.argument;
|
||||
if (!isResultErrCall(arg)) return;
|
||||
|
||||
const errArg = arg.arguments && arg.arguments[0];
|
||||
if (!isMapToMutationErrorCall(errArg)) {
|
||||
context.report({ node: returnNode, messageId: 'mustMapMutationError' });
|
||||
return;
|
||||
}
|
||||
|
||||
const mappedFrom = errArg.arguments && errArg.arguments[0];
|
||||
if (!isGetErrorCall(mappedFrom, resultVarName)) {
|
||||
context.report({ node: returnNode, messageId: 'mustMapMutationError' });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user