website refactor
This commit is contained in:
@@ -1,19 +1,24 @@
|
||||
/**
|
||||
* ESLint Rule: Mutation Contract
|
||||
*
|
||||
* Ensures mutations return Result type
|
||||
* Enforces the basic Mutation contract:
|
||||
* - Mutation classes should `implement Mutation<...>` (type-level contract)
|
||||
* - `execute()` must return `Promise<Result<...>>`
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Ensure mutations return Result type',
|
||||
description: 'Ensure mutations implement the Mutation contract and return Result',
|
||||
category: 'Mutation Contract',
|
||||
recommended: true,
|
||||
},
|
||||
messages: {
|
||||
wrongReturnType: 'Mutations must return Promise<Result<void, string>> - see apps/website/lib/contracts/Result.ts',
|
||||
mustImplementMutationInterface:
|
||||
'Mutation classes must implement Mutation<TInput, TOutput, TError> - see apps/website/lib/contracts/mutations/Mutation.ts',
|
||||
wrongReturnType:
|
||||
'Mutation execute() must return Promise<Result<TOutput, TError>> - see apps/website/lib/contracts/Result.ts',
|
||||
},
|
||||
schema: [],
|
||||
},
|
||||
@@ -21,50 +26,112 @@ module.exports = {
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
|
||||
// Only apply to mutation files
|
||||
if (!filename.includes('/lib/mutations/') || !filename.endsWith('.ts')) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const mutationInterfaceNames = new Set(['Mutation']);
|
||||
|
||||
function isIdentifier(node, nameSet) {
|
||||
return node && node.type === 'Identifier' && nameSet.has(node.name);
|
||||
}
|
||||
|
||||
function isPromiseType(typeAnnotation) {
|
||||
if (!typeAnnotation || typeAnnotation.type !== 'TSTypeReference') return false;
|
||||
const typeName = typeAnnotation.typeName;
|
||||
return typeName && typeName.type === 'Identifier' && typeName.name === 'Promise';
|
||||
}
|
||||
|
||||
function isResultType(typeNode) {
|
||||
if (!typeNode || typeNode.type !== 'TSTypeReference') return false;
|
||||
const typeName = typeNode.typeName;
|
||||
if (!typeName) return false;
|
||||
|
||||
// Common case: Result<...>
|
||||
if (typeName.type === 'Identifier') {
|
||||
return typeName.name === 'Result';
|
||||
}
|
||||
|
||||
// Fallback: handle qualified names (rare)
|
||||
if (typeName.type === 'TSQualifiedName') {
|
||||
return typeName.right && typeName.right.type === 'Identifier' && typeName.right.name === 'Result';
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return {
|
||||
ImportDeclaration(node) {
|
||||
const importPath = node.source && node.source.value;
|
||||
if (typeof importPath !== 'string') return;
|
||||
|
||||
// Accept both alias and relative imports.
|
||||
const isMutationContractImport =
|
||||
importPath.includes('/lib/contracts/mutations/Mutation') ||
|
||||
importPath.endsWith('lib/contracts/mutations/Mutation') ||
|
||||
importPath.endsWith('contracts/mutations/Mutation') ||
|
||||
importPath.endsWith('contracts/mutations/Mutation.ts');
|
||||
|
||||
if (!isMutationContractImport) return;
|
||||
|
||||
for (const spec of node.specifiers || []) {
|
||||
// import { Mutation as X } from '...'
|
||||
if (spec.type === 'ImportSpecifier' && spec.imported && spec.imported.type === 'Identifier') {
|
||||
mutationInterfaceNames.add(spec.local.name);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
ClassDeclaration(node) {
|
||||
if (!node.id || node.id.type !== 'Identifier') return;
|
||||
if (!node.id.name.endsWith('Mutation')) return;
|
||||
|
||||
const implementsNodes = node.implements || [];
|
||||
const implementsMutation = implementsNodes.some((impl) => {
|
||||
// `implements Mutation<...>`
|
||||
return impl && isIdentifier(impl.expression, mutationInterfaceNames);
|
||||
});
|
||||
|
||||
if (!implementsMutation) {
|
||||
context.report({
|
||||
node: node.id,
|
||||
messageId: 'mustImplementMutationInterface',
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
MethodDefinition(node) {
|
||||
if (node.key.type === 'Identifier' &&
|
||||
node.key.name === 'execute' &&
|
||||
node.value.type === 'FunctionExpression') {
|
||||
|
||||
const returnType = node.value.returnType;
|
||||
|
||||
// Check if it returns Promise<Result<...>>
|
||||
if (!returnType ||
|
||||
!returnType.typeAnnotation ||
|
||||
!returnType.typeAnnotation.typeName ||
|
||||
returnType.typeAnnotation.typeName.name !== 'Promise') {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (node.key.type !== 'Identifier' || node.key.name !== 'execute') return;
|
||||
if (!node.value) return;
|
||||
|
||||
// Check for Result type
|
||||
const typeArgs = returnType.typeAnnotation.typeParameters;
|
||||
if (!typeArgs || !typeArgs.params || typeArgs.params.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
return;
|
||||
}
|
||||
const returnType = node.value.returnType;
|
||||
const typeAnnotation = returnType && returnType.typeAnnotation;
|
||||
|
||||
const resultType = typeArgs.params[0];
|
||||
if (resultType.type !== 'TSTypeReference' ||
|
||||
!resultType.typeName ||
|
||||
(resultType.typeName.type === 'Identifier' && resultType.typeName.name !== 'Result')) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
}
|
||||
// Must be Promise<...>
|
||||
if (!isPromiseType(typeAnnotation)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const promiseTypeArgs = typeAnnotation.typeParameters;
|
||||
if (!promiseTypeArgs || !promiseTypeArgs.params || promiseTypeArgs.params.length === 0) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Must be Promise<Result<...>> (we don't constrain the generics here)
|
||||
const inner = promiseTypeArgs.params[0];
|
||||
if (!isResultType(inner)) {
|
||||
context.report({
|
||||
node,
|
||||
messageId: 'wrongReturnType',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user