Files
twenty/packages/twenty-eslint-rules/rules/graphql-resolvers-should-be-guarded.ts
T
Félix Malfait c737028dd6 Move tools/eslint-rules to packages/twenty-eslint-rules (#17203)
## Summary

Moves the custom ESLint rules from `tools/eslint-rules` to
`packages/twenty-eslint-rules` for better organization within the
monorepo packages structure.

## Changes

- Move `eslint-rules` from `tools/` to `packages/twenty-eslint-rules`
- Use `loadWorkspaceRules` from `@nx/eslint-plugin` to load custom rules
- Update all ESLint configs to use the `twenty/` rule prefix instead of
`@nx/workspace-`
- Update `project.json`, `jest.config.mjs` with new paths
- Update `package.json` workspaces and `nx.json` cache inputs
- Update Dockerfile reference

## Technical Details

The custom ESLint rules are now loaded using Nx's `loadWorkspaceRules`
utility which:
- Handles TypeScript transpilation automatically
- Allows loading workspace rules from any directory
- Provides a cleaner approach than the previous `@nx/workspace-`
convention

## Testing

- Verified all 17 custom ESLint rules load correctly from the new
location
- Verified linting works on dependent packages (twenty-front,
twenty-server, etc.)
2026-01-17 07:37:17 +01:00

81 lines
2.8 KiB
TypeScript

import { TSESTree } from '@typescript-eslint/utils';
import { createRule } from '../utils/createRule';
import { typedTokenHelpers } from '../utils/typedTokenHelpers';
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-graphql-resolvers-should-be-guarded"
export const RULE_NAME = 'graphql-resolvers-should-be-guarded';
export const graphqlResolversShouldBeGuarded = (
node: TSESTree.MethodDefinition,
) => {
const hasGraphQLResolverDecorator = typedTokenHelpers.nodeHasDecoratorsNamed(
node,
['Query', 'Mutation', 'Subscription'],
);
const hasAuthGuards = typedTokenHelpers.nodeHasAuthGuards(node);
const hasPermissionsGuard = typedTokenHelpers.nodeHasPermissionsGuard(node);
const findClassDeclaration = (
node: TSESTree.Node,
): TSESTree.ClassDeclaration | null => {
if (node.type === TSESTree.AST_NODE_TYPES.ClassDeclaration) {
return node;
}
if (node.parent) {
return findClassDeclaration(node.parent);
}
return null;
};
const classNode = findClassDeclaration(node);
const hasAuthGuardsOnResolver = classNode
? typedTokenHelpers.nodeHasAuthGuards(classNode)
: false;
const hasPermissionsGuardOnResolver = classNode
? typedTokenHelpers.nodeHasPermissionsGuard(classNode)
: false;
// All resolvers need both auth guards and permission guards
const missingAuthGuard =
hasGraphQLResolverDecorator && !hasAuthGuards && !hasAuthGuardsOnResolver;
const missingPermissionGuard =
hasGraphQLResolverDecorator && !hasPermissionsGuard && !hasPermissionsGuardOnResolver;
return missingAuthGuard || missingPermissionGuard;
};
export const rule = createRule<[], 'graphqlResolversShouldBeGuarded'>({
name: RULE_NAME,
meta: {
docs: {
description:
'GraphQL root resolvers (Query, Mutation, Subscription) should have authentication guards (UserAuthGuard or WorkspaceAuthGuard) or be explicitly marked as public (PublicEndpointGuard) and permission guards (SettingsPermissionsGuard or CustomPermissionGuard) to maintain our security model.',
},
messages: {
graphqlResolversShouldBeGuarded:
'All GraphQL resolvers must have authentication guards (@UseGuards(UserAuthGuard/WorkspaceAuthGuard)) and permission guards (@UseGuards(..., SettingsPermissionsGuard(PermissionFlagType.XXX)), CustomPermissionGuard for custom logic, or NoPermissionGuard for special cases like onboarding).',
},
schema: [],
hasSuggestions: false,
type: 'suggestion',
},
defaultOptions: [],
create: (context) => {
return {
MethodDefinition: (node: TSESTree.MethodDefinition): void => {
if (graphqlResolversShouldBeGuarded(node)) {
context.report({
node: node,
messageId: 'graphqlResolversShouldBeGuarded',
});
}
},
};
},
});