website refactor
This commit is contained in:
133
apps/website/eslint-rules/services-implement-contract.js
Normal file
133
apps/website/eslint-rules/services-implement-contract.js
Normal file
@@ -0,0 +1,133 @@
|
||||
/**
|
||||
* ESLint rule to enforce Services follow clean architecture patterns
|
||||
*
|
||||
* Services must:
|
||||
* 1. Return Result types for type-safe error handling
|
||||
* 2. Use DomainError types (not strings)
|
||||
* 3. Be classes named *Service
|
||||
* 4. Create their own dependencies (API Client, Logger, ErrorReporter)
|
||||
* 5. Have NO constructor parameters (self-contained)
|
||||
*
|
||||
* Note: Method names can vary (execute(), getSomething(), etc.)
|
||||
*/
|
||||
|
||||
module.exports = {
|
||||
meta: {
|
||||
type: 'problem',
|
||||
docs: {
|
||||
description: 'Enforce Services follow clean architecture patterns',
|
||||
category: 'Services',
|
||||
recommended: true,
|
||||
},
|
||||
fixable: null,
|
||||
schema: [],
|
||||
messages: {
|
||||
mustReturnResult: 'Service methods must return Promise<Result<T, DomainError>>',
|
||||
mustUseDomainError: 'Error types must be DomainError objects, not strings',
|
||||
noConstructorParams: 'Services must be self-contained. Constructor cannot have parameters. Dependencies should be created inside the constructor.',
|
||||
},
|
||||
},
|
||||
|
||||
create(context) {
|
||||
const filename = context.getFilename();
|
||||
const isInServices = filename.includes('/lib/services/');
|
||||
|
||||
if (!isInServices) return {};
|
||||
|
||||
let hasResultReturningMethod = false;
|
||||
let usesStringErrors = false;
|
||||
let hasConstructorParams = false;
|
||||
let classNode = null;
|
||||
|
||||
return {
|
||||
ClassDeclaration(node) {
|
||||
classNode = node;
|
||||
const className = node.id?.name;
|
||||
|
||||
if (!className || !className.endsWith('Service')) {
|
||||
return; // Not a service class
|
||||
}
|
||||
|
||||
// Check all methods for Result return types
|
||||
node.body.body.forEach(member => {
|
||||
if (member.type === 'MethodDefinition' &&
|
||||
member.key.type === 'Identifier') {
|
||||
|
||||
// Check constructor parameters
|
||||
if (member.kind === 'constructor') {
|
||||
const params = member.value.params;
|
||||
if (params && params.length > 0) {
|
||||
hasConstructorParams = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check return types
|
||||
const returnType = member.value?.returnType;
|
||||
|
||||
if (returnType &&
|
||||
returnType.typeAnnotation &&
|
||||
returnType.typeAnnotation.typeName &&
|
||||
returnType.typeAnnotation.typeName.name === 'Promise' &&
|
||||
returnType.typeAnnotation.typeParameters &&
|
||||
returnType.typeAnnotation.typeParameters.params.length > 0) {
|
||||
|
||||
const resultType = returnType.typeAnnotation.typeParameters.params[0];
|
||||
|
||||
// Check for Result<...>
|
||||
if (resultType.type === 'TSTypeReference' &&
|
||||
resultType.typeName &&
|
||||
resultType.typeName.type === 'Identifier' &&
|
||||
resultType.typeName.name === 'Result') {
|
||||
hasResultReturningMethod = true;
|
||||
}
|
||||
|
||||
// Check for string error type
|
||||
if (resultType.type === 'TSTypeReference' &&
|
||||
resultType.typeParameters &&
|
||||
resultType.typeParameters.params.length > 1) {
|
||||
|
||||
const errorType = resultType.typeParameters.params[1];
|
||||
// Check if error is string literal or string type
|
||||
if (errorType.type === 'TSStringKeyword' ||
|
||||
(errorType.type === 'TSLiteralType' && errorType.literal.type === 'StringLiteral')) {
|
||||
usesStringErrors = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
'Program:exit'() {
|
||||
if (!isInServices || !classNode) return;
|
||||
|
||||
const className = classNode.id?.name;
|
||||
if (!className || !className.endsWith('Service')) return;
|
||||
|
||||
// Error if constructor has parameters
|
||||
if (hasConstructorParams) {
|
||||
context.report({
|
||||
node: classNode,
|
||||
messageId: 'noConstructorParams',
|
||||
});
|
||||
}
|
||||
|
||||
// Error if no methods return Result
|
||||
if (!hasResultReturningMethod) {
|
||||
context.report({
|
||||
node: classNode,
|
||||
messageId: 'mustReturnResult',
|
||||
});
|
||||
}
|
||||
|
||||
// Error if using string errors
|
||||
if (usesStringErrors) {
|
||||
context.report({
|
||||
node: classNode,
|
||||
messageId: 'mustUseDomainError',
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user