Files
upstream-twenty/packages/twenty-oxlint-rules/rules/prefer-workspace-scoped-repository.ts
upstream-mirror e7da97194a
Some checks failed
App Prod-Parity E2E Dispatch / dispatch (push) Failing after 10s
CD Storybook UI / build (push) Failing after 3m25s
CD Storybook UI / dispatch (push) Has been skipped
CD deploy main / website-changed-files (push) Successful in 18m8s
CI Agent Skills / changed-files-check (push) Successful in 18m9s
CI Create App E2E minimal / changed-files-check (push) Successful in 24s
CI Create App / changed-files-check (push) Successful in 26s
CI Docs / changed-files-check (push) Successful in 26s
CI Emails / changed-files-check (push) Successful in 28s
CI Example App Hello World / changed-files-check (push) Successful in 23s
CI Front / changed-files-check (push) Has been skipped
CI Example App Postcard / changed-files-check (push) Successful in 27s
CI UI / changed-files-check (push) Has been skipped
CI Utils Test / changed-files-check (push) Successful in 45s
CI Twenty Apps / discover (push) Successful in 1m3s
CD deploy main / deploy-main (push) Failing after 3s
CI Agent Skills / agent-skills-validate (push) Has been skipped
CI Create App / create-app-test (lint) (push) Has been skipped
CI Emails / emails-test (push) Has been skipped
CI Front / front-task (lingui:compile) (push) Has been skipped
CI Front / front-task (lint) (push) Has been skipped
CI Example App Postcard / example-app-postcard (push) Has been skipped
CI UI / ui-task (lint) (push) Has been skipped
CI UI / ui-task (pack-size) (push) Has been skipped
CI UI / ui-task (size) (push) Has been skipped
CI UI / ui-task (test) (push) Has been skipped
CI UI / ui-task (typecheck) (push) Has been skipped
CI Utils Test / utils-test (push) Has been skipped
CI Twenty Apps / ${{ matrix.app.name }} (push) Has been skipped
CI Emails / ci-emails-status-check (push) Successful in 2s
CI Example App Hello World / ci-example-app-hello-world-status-check (push) Successful in 2s
CI Example App Postcard / ci-example-app-postcard-status-check (push) Successful in 3s
CI UI / ui-sb-test (push) Has been skipped
CI Utils Test / ci-utils-test-status-check (push) Successful in 2s
CI Twenty Apps / ci-twenty-apps-status-check (push) Successful in 2s
CI UI / ci-ui-status-check (push) Successful in 2s
CI Desktop / desktop (push) Failing after 1m8s
Push docs to Crowdin / Push documentation to Crowdin (push) Failing after 55s
Push translations to Crowdin / Extract and upload translations (push) Failing after 5s
Push website translations to Crowdin / Extract and upload website translations (push) Failing after 5s
CI Create App / create-app-test (test) (push) Has been skipped
CI Create App / create-app-test (typecheck) (push) Has been skipped
CI Docs / docs-lint (push) Has been skipped
CI Example App Hello World / example-app-hello-world (push) Has been skipped
CI Front / front-task (lingui:extract) (push) Has been skipped
CI Front / front-task (test) (push) Has been skipped
CI Front / front-task (typecheck) (push) Has been skipped
CI UI / ui-sb-build (push) Failing after 1m32s
CI Twenty Apps / ${{ matrix.app.name }} (${{ matrix.server-source }}) (push) Has been skipped
CI Agent Skills / agent-skills-publish (push) Has been skipped
CI Create App / ci-create-app-status-check (push) Successful in 2s
CI Agent Skills / agent-skills-remote-source (push) Has been skipped
CI Create App E2E minimal / create-app-e2e-minimal (push) Has been cancelled
CI E2E Main / e2e-test (push) Has been cancelled
CI Front / front-sb-build (push) Has been cancelled
CI Front / front-build (push) Has been cancelled
CI Create App E2E minimal / ci-create-app-e2e-minimal-status-check (push) Has been cancelled
CI E2E Main / ci-e2e-main-status-check (push) Has been cancelled
CI E2E Main / notify-main-ci-failure (push) Has been cancelled
CI E2E Main / qa-scout-comment (push) Has been cancelled
CI Front / front-sb-test (push) Has been cancelled
CI Front / front-sb-screenshots (push) Has been cancelled
CI Front / ci-front-status-check (push) Has been cancelled
DPA Sub-Processor Sync / sync-subprocessors (push) Failing after 3s
AI Catalog Sync / sync-catalog (push) Failing after 3s
snapshot of https://github.com/twentyhq/twenty.git (main, 2026-09-17; blobs > 131072B omitted)
2026-09-17 23:28:13 +05:00

136 lines
4.5 KiB
TypeScript

import { defineRule } from '@oxlint/plugins';
export const RULE_NAME = 'prefer-workspace-scoped-repository';
// Entities that do not fit the scoped wrapper: workspace itself, pivots,
// nullable-workspaceId rows (instance-level config / migrations / tokens),
// and global tables with no workspaceId column at all.
const STRUCTURAL_EXEMPTIONS = new Set<string>([
'WorkspaceEntity',
'UserWorkspaceEntity',
'AppTokenEntity',
'ApplicationRegistrationEntity',
'ApplicationRegistrationClaimEntity',
'ApplicationRegistrationVariableEntity',
// nullable workspaceId — both rows support instance-level and per-workspace use
'KeyValuePairEntity',
'UpgradeMigrationEntity',
// user-scoped auth sessions; workspaceId is null for workspace-agnostic sessions
'UserSessionEntity',
'ApplicationVariableEntity',
'BillingMeterEntity',
'BillingPriceEntity',
'BillingProductEntity',
'BillingSubscriptionItemEntity',
'ConnectedAccountEntity',
'ConnectionProviderEntity',
'FrontComponentEntity',
'LogicFunctionEntity',
'MessageFolderEntity',
'RolePermissionFlagEntity',
'SigningKeyEntity',
'UserEntity',
'WorkspaceSsoIdentityProviderEntity',
]);
// Workspace-scoped entities the wrapper could technically wrap, but where
// the dominant access patterns are cross-workspace (request routing, auth,
// metadata sync, file storage, transaction-bound channel updates) and the
// payoff doesn't justify dual-injecting every call site or growing the
// wrapper API. Treat as a "deliberately not migrated" list, not a backlog.
const WORKSPACE_SCOPED_EXEMPTIONS = new Set<string>([
// Resolved by id alone at auth/request-routing time and inside file-storage
// transactions; very few of the ~50 call sites carry a workspaceId.
'ApplicationEntity',
// Read by user across every workspace they belong to (the "apps you
// authorized" screen) and from the OAuth token endpoint, which has no
// request workspace to scope by.
'ApplicationAuthorizationEntity',
// 20+ call sites across calendar/messaging modules; staged for a dedicated PR.
'CalendarChannelEntity',
'MessageChannelEntity',
// The owning services `extends TypeOrmQueryService<E>` and pass the raw
// repo to `super(...)`; the superclass type doesn't accept the wrapper.
'FieldMetadataEntity',
'ObjectMetadataEntity',
// Only injection lives in a frozen historical upgrade-version-command
// directory that CI's mutation-guard refuses to let us edit.
'DataSourceEntity',
// The domain column is globally unique across workspaces, so duplicate
// preflight checks must query cross-workspace; writes stay on the wrapper.
'EmailingDomainEntity',
]);
// Everything else must use @InjectWorkspaceScopedRepository.
const EXCLUSIONS = new Set<string>([
...STRUCTURAL_EXEMPTIONS,
...WORKSPACE_SCOPED_EXEMPTIONS,
]);
const matchInjectRepositoryEntity = (decorator: any): string | null => {
if (decorator.expression?.type !== 'CallExpression') {
return null;
}
const callee = decorator.expression.callee;
if (callee?.type !== 'Identifier' || callee.name !== 'InjectRepository') {
return null;
}
const [arg] = decorator.expression.arguments;
if (arg?.type !== 'Identifier') {
return null;
}
if (!arg.name.endsWith('Entity')) {
return null;
}
if (EXCLUSIONS.has(arg.name)) {
return null;
}
return arg.name;
};
export const rule = defineRule({
meta: {
type: 'problem',
docs: {
description:
'Disallow raw @InjectRepository for workspace-scoped entities. Use @InjectWorkspaceScopedRepository so workspaceId is enforced on every read/write.',
},
schema: [],
messages: {
preferWorkspaceScopedRepository:
'Use @InjectWorkspaceScopedRepository({{entityName}}) instead of raw @InjectRepository so the workspaceId guard is enforced. If {{entityName}} genuinely does not fit, add it to EXCLUSIONS or suppress with `// eslint-disable-next-line twenty/prefer-workspace-scoped-repository` and a short reason.',
},
},
create: (context) => {
return {
MethodDefinition: (node: any) => {
if (node.kind !== 'constructor') {
return;
}
for (const param of node.value.params ?? []) {
for (const decorator of param.decorators ?? []) {
const entityName = matchInjectRepositoryEntity(decorator);
if (entityName !== null) {
context.report({
node: decorator,
messageId: 'preferWorkspaceScopedRepository',
data: { entityName },
});
}
}
}
},
};
},
});