@agjs/tsforge 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tsforge.js +2 -0
- package/package.json +35 -0
- package/src/agent/agent.constants.ts +382 -0
- package/src/agent/agent.types.ts +34 -0
- package/src/agent/index.ts +4 -0
- package/src/agent/model-agent.ts +297 -0
- package/src/agent/tool-repair.ts +194 -0
- package/src/agent/tools.ts +190 -0
- package/src/browser/checks.ts +96 -0
- package/src/browser/index.ts +8 -0
- package/src/browser/oracle.ts +303 -0
- package/src/classify.ts +48 -0
- package/src/cli.ts +1333 -0
- package/src/config/config.constants.ts +9 -0
- package/src/config/flags.ts +32 -0
- package/src/config/index.ts +8 -0
- package/src/config/tsforge-config.ts +301 -0
- package/src/constitution/baseline.ts +257 -0
- package/src/detect-gate.ts +498 -0
- package/src/eval/eval.types.ts +36 -0
- package/src/eval/index.ts +3 -0
- package/src/eval/judge.ts +62 -0
- package/src/eval/score.ts +39 -0
- package/src/files/create.ts +22 -0
- package/src/files/edit.ts +193 -0
- package/src/files/files.constants.ts +11 -0
- package/src/files/files.types.ts +81 -0
- package/src/files/hashline-format.ts +110 -0
- package/src/files/hashline.ts +689 -0
- package/src/files/index.ts +19 -0
- package/src/index.ts +8 -0
- package/src/inference/index.ts +6 -0
- package/src/inference/inference.constants.ts +34 -0
- package/src/inference/inference.types.ts +123 -0
- package/src/inference/openai-compatible.ts +113 -0
- package/src/inference/stream-guard.ts +161 -0
- package/src/inference/stream.ts +370 -0
- package/src/inference/transport.ts +78 -0
- package/src/inference/wire.ts +0 -0
- package/src/lib/fs/fs.ts +126 -0
- package/src/lib/fs/fs.types.ts +5 -0
- package/src/lib/fs/index.ts +3 -0
- package/src/lib/fs/process.ts +146 -0
- package/src/lib/guards/guards.ts +9 -0
- package/src/lib/guards/index.ts +1 -0
- package/src/lib/json/index.ts +1 -0
- package/src/lib/json/json.ts +12 -0
- package/src/lib/scope/index.ts +2 -0
- package/src/lib/scope/scope.constants.ts +3 -0
- package/src/lib/scope/scope.ts +40 -0
- package/src/loop/astgrep-fix.ts +228 -0
- package/src/loop/feedback/feedback.ts +138 -0
- package/src/loop/feedback/index.ts +8 -0
- package/src/loop/feedback/meta-rule-docs.ts +41 -0
- package/src/loop/feedback/meta-rule-feedback.ts +61 -0
- package/src/loop/feedback/rule-docs.generated.json +112 -0
- package/src/loop/feedback/rule-docs.ts +342 -0
- package/src/loop/index.ts +19 -0
- package/src/loop/loop.constants.ts +68 -0
- package/src/loop/loop.types.ts +99 -0
- package/src/loop/prompt/index.ts +2 -0
- package/src/loop/prompt/project-map.ts +69 -0
- package/src/loop/prompt/prompt.ts +107 -0
- package/src/loop/quality.ts +174 -0
- package/src/loop/rule-docs.generated.json +367 -0
- package/src/loop/run-spec.ts +88 -0
- package/src/loop/run.ts +400 -0
- package/src/loop/session.ts +1410 -0
- package/src/loop/tools/add-dependency.ts +71 -0
- package/src/loop/tools/condense.ts +498 -0
- package/src/loop/tools/edit-hashline.ts +80 -0
- package/src/loop/tools/execute-tool.ts +80 -0
- package/src/loop/tools/file-ops.ts +323 -0
- package/src/loop/tools/index.ts +2 -0
- package/src/loop/tools/lsp-ops.ts +222 -0
- package/src/loop/tools/scaffold-routes.ts +68 -0
- package/src/loop/tools/scaffold-ui.ts +62 -0
- package/src/loop/tools/scaffold-web.ts +35 -0
- package/src/loop/tools/tool-context.ts +126 -0
- package/src/loop/ttsr-defaults.ts +53 -0
- package/src/loop/ttsr.ts +322 -0
- package/src/loop/turn.ts +856 -0
- package/src/lsp/index.ts +2 -0
- package/src/lsp/lsp.types.ts +56 -0
- package/src/lsp/service.ts +500 -0
- package/src/meta-rules/context.ts +195 -0
- package/src/meta-rules/index.ts +9 -0
- package/src/meta-rules/meta-rules.types.ts +47 -0
- package/src/meta-rules/parsers/package-json-parser.ts +51 -0
- package/src/meta-rules/registry.ts +37 -0
- package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
- package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
- package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
- package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
- package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
- package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
- package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
- package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
- package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
- package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
- package/src/meta-rules/runner.ts +64 -0
- package/src/models-config.ts +196 -0
- package/src/render/ansi.ts +289 -0
- package/src/render/banner.ts +113 -0
- package/src/render/box.ts +134 -0
- package/src/render/index.ts +7 -0
- package/src/render/markdown.ts +123 -0
- package/src/render/render.types.ts +21 -0
- package/src/render/stream-markdown.ts +128 -0
- package/src/render/style.ts +26 -0
- package/src/rule-packs/bullmq/index.ts +39 -0
- package/src/rule-packs/bullmq/rules/index.ts +7 -0
- package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
- package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
- package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
- package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
- package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
- package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
- package/src/rule-packs/bullmq/utils.ts +334 -0
- package/src/rule-packs/code-flow/index.ts +25 -0
- package/src/rule-packs/code-flow/rules/index.ts +3 -0
- package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
- package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
- package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
- package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
- package/src/rule-packs/comment-hygiene/index.ts +25 -0
- package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
- package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
- package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
- package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
- package/src/rule-packs/create-rule.ts +9 -0
- package/src/rule-packs/drizzle/index.ts +41 -0
- package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
- package/src/rule-packs/drizzle/rules/index.ts +8 -0
- package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
- package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
- package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
- package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
- package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
- package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
- package/src/rule-packs/drizzle/utils.ts +115 -0
- package/src/rule-packs/elysia/index.ts +43 -0
- package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
- package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
- package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
- package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
- package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
- package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
- package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
- package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
- package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
- package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
- package/src/rule-packs/env-access/index.ts +23 -0
- package/src/rule-packs/env-access/rules/index.ts +2 -0
- package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
- package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
- package/src/rule-packs/i18n-keys/index.ts +19 -0
- package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
- package/src/rule-packs/index.ts +139 -0
- package/src/rule-packs/jwt-cookies/index.ts +25 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
- package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
- package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
- package/src/rule-packs/jwt-cookies/utils.ts +188 -0
- package/src/rule-packs/oauth-security/index.ts +25 -0
- package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
- package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
- package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
- package/src/rule-packs/oauth-security/utils.ts +127 -0
- package/src/rule-packs/react-component-architecture/index.ts +35 -0
- package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
- package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
- package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
- package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
- package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
- package/src/rule-packs/react-component-architecture/utils.ts +47 -0
- package/src/rule-packs/rule-packs.types.ts +18 -0
- package/src/rule-packs/structured-logging/index.ts +26 -0
- package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
- package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
- package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
- package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
- package/src/rule-packs/tanstack-query/index.ts +20 -0
- package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
- package/src/rule-packs/test-conventions/index.ts +23 -0
- package/src/rule-packs/test-conventions/rules/index.ts +2 -0
- package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
- package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
- package/src/rule-packs/utils.ts +142 -0
- package/src/session-store.ts +359 -0
- package/src/spec/generate-tests.ts +213 -0
- package/src/spec/index.ts +5 -0
- package/src/spec/parse.ts +152 -0
- package/src/spec/review-tests.ts +162 -0
- package/src/spec/spec.constants.ts +13 -0
- package/src/spec/spec.types.ts +79 -0
- package/src/stack-detection/detect.ts +246 -0
- package/src/stack-detection/index.ts +3 -0
- package/src/stack-detection/packs.ts +174 -0
- package/src/stack-detection/stack-detection.types.ts +47 -0
- package/src/validate/accept.ts +49 -0
- package/src/validate/errors.ts +35 -0
- package/src/validate/index.ts +12 -0
- package/src/validate/parse.ts +148 -0
- package/src/validate/run-tests.ts +59 -0
- package/src/validate/validate.ts +40 -0
- package/src/validate/validate.types.ts +52 -0
- package/src/web-components.ts +638 -0
- package/src/web-coverage.ts +89 -0
- package/src/web-routes.ts +151 -0
- package/src/web-templates.ts +1011 -0
- package/strict.eslint.config.mjs +84 -0
- package/strict.web.eslint.config.mjs +185 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import { matchesAnyGlobPattern, pushChildNodes } from "../../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "account-scoped-tables-require-where";
|
|
8
|
+
|
|
9
|
+
export interface AccountScopedTablesRequireWhereOptions {
|
|
10
|
+
readonly tables?: readonly string[];
|
|
11
|
+
readonly scopeColumn?: string;
|
|
12
|
+
readonly alternateScopeColumns?: readonly string[];
|
|
13
|
+
readonly allowFiles?: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
type RuleOptions = [AccountScopedTablesRequireWhereOptions];
|
|
17
|
+
type MessageIds = "missingScopeFilter";
|
|
18
|
+
|
|
19
|
+
const DEFAULT_SCOPE_COLUMN = "accountId";
|
|
20
|
+
|
|
21
|
+
const optionSchema: JSONSchema4 = {
|
|
22
|
+
type: "object",
|
|
23
|
+
additionalProperties: false,
|
|
24
|
+
properties: {
|
|
25
|
+
tables: {
|
|
26
|
+
type: "array",
|
|
27
|
+
items: { type: "string" },
|
|
28
|
+
uniqueItems: true,
|
|
29
|
+
minItems: 1,
|
|
30
|
+
},
|
|
31
|
+
scopeColumn: { type: "string", minLength: 1 },
|
|
32
|
+
alternateScopeColumns: {
|
|
33
|
+
type: "array",
|
|
34
|
+
items: { type: "string", minLength: 1 },
|
|
35
|
+
uniqueItems: true,
|
|
36
|
+
},
|
|
37
|
+
allowFiles: {
|
|
38
|
+
type: "array",
|
|
39
|
+
items: { type: "string", minLength: 1 },
|
|
40
|
+
uniqueItems: true,
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export const accountScopedTablesRequireWhereRule = createRule<
|
|
46
|
+
RuleOptions,
|
|
47
|
+
MessageIds
|
|
48
|
+
>({
|
|
49
|
+
name: RULE_NAME,
|
|
50
|
+
meta: {
|
|
51
|
+
type: "problem",
|
|
52
|
+
docs: {
|
|
53
|
+
description:
|
|
54
|
+
"Require every Drizzle query against a configured account-scoped table to filter by a scope column (accountId by default).",
|
|
55
|
+
},
|
|
56
|
+
schema: [optionSchema],
|
|
57
|
+
messages: {
|
|
58
|
+
missingScopeFilter:
|
|
59
|
+
"Drizzle query against account-scoped table `{{table}}` is missing a `{{scopeColumn}}` filter. Account-scoped queries must include `{{scopeColumn}}` in their WHERE / values / insert payload — otherwise data from other tenants leaks.",
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
defaultOptions: [{}],
|
|
63
|
+
create(context, [options]) {
|
|
64
|
+
const tables = new Set(options.tables ?? []);
|
|
65
|
+
const scopeColumn = options.scopeColumn ?? DEFAULT_SCOPE_COLUMN;
|
|
66
|
+
const alternateScopeColumns = options.alternateScopeColumns ?? [];
|
|
67
|
+
const allowFiles = options.allowFiles ?? [];
|
|
68
|
+
|
|
69
|
+
if (tables.size === 0) {
|
|
70
|
+
return {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (matchesAnyGlobPattern(context.filename, allowFiles)) {
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const scopeColumns = [scopeColumn, ...alternateScopeColumns];
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
CallExpression(node) {
|
|
81
|
+
const queryShape = identifyQuery(node, tables);
|
|
82
|
+
|
|
83
|
+
if (!queryShape) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (
|
|
88
|
+
queryShape.kind === "from" ||
|
|
89
|
+
queryShape.kind === "update" ||
|
|
90
|
+
queryShape.kind === "delete"
|
|
91
|
+
) {
|
|
92
|
+
if (chainContainsWhereWithAnyScope(node, scopeColumns)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
} else if (queryShape.kind === "insert") {
|
|
96
|
+
if (chainContainsValuesWithScope(node, scopeColumn)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
} else if (
|
|
100
|
+
queryShape.kind === "queryBuilder" &&
|
|
101
|
+
objectArgumentContainsAnyScope(node, scopeColumns)
|
|
102
|
+
) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
context.report({
|
|
107
|
+
node,
|
|
108
|
+
messageId: "missingScopeFilter",
|
|
109
|
+
data: { table: queryShape.table, scopeColumn },
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
function chainContainsWhereWithAnyScope(
|
|
117
|
+
startCall: TSESTree.CallExpression,
|
|
118
|
+
scopeColumns: readonly string[]
|
|
119
|
+
): boolean {
|
|
120
|
+
return scopeColumns.some((col) =>
|
|
121
|
+
chainContainsWhereWithScope(startCall, col)
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function objectArgumentContainsAnyScope(
|
|
126
|
+
node: TSESTree.CallExpression,
|
|
127
|
+
scopeColumns: readonly string[]
|
|
128
|
+
): boolean {
|
|
129
|
+
return scopeColumns.some((col) => objectArgumentContainsScope(node, col));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
interface QueryShape {
|
|
133
|
+
readonly kind: "from" | "insert" | "update" | "delete" | "queryBuilder";
|
|
134
|
+
readonly table: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function identifyQuery(
|
|
138
|
+
node: TSESTree.CallExpression,
|
|
139
|
+
tables: Set<string>
|
|
140
|
+
): QueryShape | null {
|
|
141
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const property = node.callee.property;
|
|
146
|
+
|
|
147
|
+
if (property.type !== AST_NODE_TYPES.Identifier) {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const directKinds = ["from", "insert", "update", "delete"] as const;
|
|
152
|
+
const direct = directKinds.find((kind) => kind === property.name);
|
|
153
|
+
|
|
154
|
+
if (direct !== undefined) {
|
|
155
|
+
const arg = node.arguments[0];
|
|
156
|
+
|
|
157
|
+
if (arg?.type === AST_NODE_TYPES.Identifier && tables.has(arg.name)) {
|
|
158
|
+
return { kind: direct, table: arg.name };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (property.name === "findFirst" || property.name === "findMany") {
|
|
165
|
+
const tableName = extractQueryBuilderTable(node);
|
|
166
|
+
|
|
167
|
+
if (tableName !== null && tables.has(tableName)) {
|
|
168
|
+
return { kind: "queryBuilder", table: tableName };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractQueryBuilderTable(
|
|
176
|
+
node: TSESTree.CallExpression
|
|
177
|
+
): string | null {
|
|
178
|
+
if (node.callee.type !== AST_NODE_TYPES.MemberExpression) {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const obj = node.callee.object;
|
|
183
|
+
|
|
184
|
+
if (obj.type !== AST_NODE_TYPES.MemberExpression) {
|
|
185
|
+
return null;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (obj.property.type !== AST_NODE_TYPES.Identifier) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return obj.property.name;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getParent(node: TSESTree.Node): TSESTree.Node | undefined {
|
|
196
|
+
return node.parent;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Walk up a fluent call chain (`db.update(t).set(x).where(...)`) looking for a
|
|
201
|
+
* `.<methodName>(...)` link whose call satisfies `callMatches`.
|
|
202
|
+
*/
|
|
203
|
+
function chainCallProvides(
|
|
204
|
+
startCall: TSESTree.CallExpression,
|
|
205
|
+
methodName: string,
|
|
206
|
+
callMatches: (call: TSESTree.CallExpression) => boolean
|
|
207
|
+
): boolean {
|
|
208
|
+
let current: TSESTree.Node = startCall;
|
|
209
|
+
let parent = getParent(current);
|
|
210
|
+
|
|
211
|
+
while (parent !== undefined) {
|
|
212
|
+
if (
|
|
213
|
+
parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
214
|
+
parent.object === current &&
|
|
215
|
+
parent.property.type === AST_NODE_TYPES.Identifier &&
|
|
216
|
+
parent.property.name === methodName
|
|
217
|
+
) {
|
|
218
|
+
const call = getParent(parent);
|
|
219
|
+
|
|
220
|
+
if (call?.type === AST_NODE_TYPES.CallExpression && callMatches(call)) {
|
|
221
|
+
return true;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (
|
|
226
|
+
parent.type === AST_NODE_TYPES.MemberExpression ||
|
|
227
|
+
parent.type === AST_NODE_TYPES.CallExpression
|
|
228
|
+
) {
|
|
229
|
+
current = parent;
|
|
230
|
+
parent = getParent(current);
|
|
231
|
+
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function chainContainsWhereWithScope(
|
|
242
|
+
startCall: TSESTree.CallExpression,
|
|
243
|
+
scopeColumn: string
|
|
244
|
+
): boolean {
|
|
245
|
+
return chainCallProvides(startCall, "where", (call) => {
|
|
246
|
+
const firstArg = call.arguments[0];
|
|
247
|
+
|
|
248
|
+
return (
|
|
249
|
+
firstArg !== undefined &&
|
|
250
|
+
subtreeReferencesIdentifier(firstArg, scopeColumn)
|
|
251
|
+
);
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function chainContainsValuesWithScope(
|
|
256
|
+
startCall: TSESTree.CallExpression,
|
|
257
|
+
scopeColumn: string
|
|
258
|
+
): boolean {
|
|
259
|
+
return chainCallProvides(startCall, "values", (call) =>
|
|
260
|
+
valuesCallProvidesScope(call, scopeColumn)
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function valuesCallProvidesScope(
|
|
265
|
+
valuesCall: TSESTree.CallExpression,
|
|
266
|
+
scopeColumn: string
|
|
267
|
+
): boolean {
|
|
268
|
+
const firstArg = valuesCall.arguments[0];
|
|
269
|
+
|
|
270
|
+
if (firstArg === undefined) {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (objectExpressionMentionsKey(firstArg, scopeColumn)) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (firstArg.type === AST_NODE_TYPES.ArrayExpression) {
|
|
279
|
+
return firstArg.elements.some(
|
|
280
|
+
(element) =>
|
|
281
|
+
element !== null && objectExpressionMentionsKey(element, scopeColumn)
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function objectArgumentContainsScope(
|
|
289
|
+
node: TSESTree.CallExpression,
|
|
290
|
+
scopeColumn: string
|
|
291
|
+
): boolean {
|
|
292
|
+
const arg = node.arguments[0];
|
|
293
|
+
|
|
294
|
+
if (arg?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
for (const property of arg.properties) {
|
|
299
|
+
if (
|
|
300
|
+
property.type === AST_NODE_TYPES.Property &&
|
|
301
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
302
|
+
property.key.name === "where" &&
|
|
303
|
+
subtreeReferencesIdentifier(property.value, scopeColumn)
|
|
304
|
+
) {
|
|
305
|
+
return true;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function objectExpressionMentionsKey(
|
|
313
|
+
node: TSESTree.Node,
|
|
314
|
+
key: string
|
|
315
|
+
): boolean {
|
|
316
|
+
if (node.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
317
|
+
return false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
for (const property of node.properties) {
|
|
321
|
+
if (
|
|
322
|
+
property.type === AST_NODE_TYPES.Property &&
|
|
323
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
324
|
+
property.key.name === key
|
|
325
|
+
) {
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (property.type === AST_NODE_TYPES.SpreadElement) {
|
|
330
|
+
return true;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return false;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function nodeReferencesIdentifier(node: TSESTree.Node, name: string): boolean {
|
|
338
|
+
if (node.type === AST_NODE_TYPES.Identifier && node.name === name) {
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return (
|
|
343
|
+
node.type === AST_NODE_TYPES.MemberExpression &&
|
|
344
|
+
node.property.type === AST_NODE_TYPES.Identifier &&
|
|
345
|
+
node.property.name === name
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function subtreeReferencesIdentifier(
|
|
350
|
+
root: TSESTree.Node,
|
|
351
|
+
name: string
|
|
352
|
+
): boolean {
|
|
353
|
+
const stack: TSESTree.Node[] = [root];
|
|
354
|
+
const visited = new WeakSet();
|
|
355
|
+
|
|
356
|
+
for (let node = stack.pop(); node !== undefined; node = stack.pop()) {
|
|
357
|
+
if (visited.has(node)) {
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
visited.add(node);
|
|
362
|
+
|
|
363
|
+
if (nodeReferencesIdentifier(node, name)) {
|
|
364
|
+
return true;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
pushChildNodes(node, stack);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { accountScopedTablesRequireWhereRule } from "./account-scoped-tables-require-where";
|
|
2
|
+
export { noNestedDbTransactionRule } from "./no-nested-db-transaction";
|
|
3
|
+
export { noRawSqlOutsideAllowlistRule } from "./no-raw-sql-outside-allowlist";
|
|
4
|
+
export { relationsMustCoverFksRule } from "./relations-must-cover-fks";
|
|
5
|
+
export { schemaFilesMustNotImportDriverRule } from "./schema-files-must-not-import-driver";
|
|
6
|
+
export { schemaFilesMustOnlyExportSchemaRule } from "./schema-files-must-only-export-schema";
|
|
7
|
+
export { tablesMustHaveTimestampsRule } from "./tables-must-have-timestamps";
|
|
8
|
+
export { timestampMustSpecifyModeRule } from "./timestamp-must-specify-mode";
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
|
|
6
|
+
export const RULE_NAME = "no-nested-db-transaction";
|
|
7
|
+
|
|
8
|
+
export interface NoNestedDbTransactionOptions {
|
|
9
|
+
readonly transactionMethod?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RuleOptions = [NoNestedDbTransactionOptions];
|
|
13
|
+
type MessageIds = "nestedTransaction";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_TRANSACTION_METHOD = "transaction";
|
|
16
|
+
|
|
17
|
+
const optionSchema: JSONSchema4 = {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
transactionMethod: {
|
|
22
|
+
type: "string",
|
|
23
|
+
minLength: 1,
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const noNestedDbTransactionRule = createRule<RuleOptions, MessageIds>({
|
|
29
|
+
name: RULE_NAME,
|
|
30
|
+
meta: {
|
|
31
|
+
type: "problem",
|
|
32
|
+
docs: {
|
|
33
|
+
description:
|
|
34
|
+
"Forbid invoking the outer db's `.transaction(...)` method inside a transaction callback — use the callback's `tx` parameter instead to avoid deadlocks.",
|
|
35
|
+
},
|
|
36
|
+
schema: [optionSchema],
|
|
37
|
+
messages: {
|
|
38
|
+
nestedTransaction:
|
|
39
|
+
"Nested call to `{{receiver}}.{{method}}(...)` inside a transaction callback — use the callback's transaction parameter (`tx.{{method}}(...)`) instead. Calling the original db inside an open transaction deadlocks or silently fails.",
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultOptions: [{ transactionMethod: DEFAULT_TRANSACTION_METHOD }],
|
|
43
|
+
create(context, [options]) {
|
|
44
|
+
const transactionMethod =
|
|
45
|
+
options.transactionMethod ?? DEFAULT_TRANSACTION_METHOD;
|
|
46
|
+
|
|
47
|
+
const callbackParamStack: string[] = [];
|
|
48
|
+
|
|
49
|
+
function isTransactionCall(node: TSESTree.CallExpression): boolean {
|
|
50
|
+
return (
|
|
51
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
52
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
53
|
+
node.callee.property.name === transactionMethod
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getReceiverName(node: TSESTree.CallExpression): string | null {
|
|
58
|
+
if (
|
|
59
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
60
|
+
node.callee.object.type === AST_NODE_TYPES.Identifier
|
|
61
|
+
) {
|
|
62
|
+
return node.callee.object.name;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getCallbackParamName(
|
|
69
|
+
node: TSESTree.CallExpression
|
|
70
|
+
): string | null {
|
|
71
|
+
const arg = node.arguments[0];
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
!arg ||
|
|
75
|
+
(arg.type !== AST_NODE_TYPES.ArrowFunctionExpression &&
|
|
76
|
+
arg.type !== AST_NODE_TYPES.FunctionExpression)
|
|
77
|
+
) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const firstParam = arg.params[0];
|
|
82
|
+
|
|
83
|
+
if (firstParam?.type === AST_NODE_TYPES.Identifier) {
|
|
84
|
+
return firstParam.name;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
CallExpression(node) {
|
|
92
|
+
if (!isTransactionCall(node)) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (callbackParamStack.length > 0) {
|
|
97
|
+
const receiverName = getReceiverName(node);
|
|
98
|
+
|
|
99
|
+
if (
|
|
100
|
+
receiverName !== null &&
|
|
101
|
+
!callbackParamStack.includes(receiverName)
|
|
102
|
+
) {
|
|
103
|
+
context.report({
|
|
104
|
+
node,
|
|
105
|
+
messageId: "nestedTransaction",
|
|
106
|
+
data: {
|
|
107
|
+
receiver: receiverName,
|
|
108
|
+
method: transactionMethod,
|
|
109
|
+
},
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const paramName = getCallbackParamName(node);
|
|
115
|
+
|
|
116
|
+
callbackParamStack.push(paramName ?? "");
|
|
117
|
+
},
|
|
118
|
+
"CallExpression:exit"(node: TSESTree.CallExpression) {
|
|
119
|
+
if (!isTransactionCall(node)) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
callbackParamStack.pop();
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
},
|
|
127
|
+
});
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
import type { JSONSchema4 } from "@typescript-eslint/utils/json-schema";
|
|
3
|
+
|
|
4
|
+
import { createRule } from "../../create-rule";
|
|
5
|
+
import { matchesAnyGlobPattern } from "../../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "no-raw-sql-outside-allowlist";
|
|
8
|
+
|
|
9
|
+
export interface NoRawSqlOutsideAllowlistOptions {
|
|
10
|
+
readonly allowFiles?: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type RuleOptions = [NoRawSqlOutsideAllowlistOptions];
|
|
14
|
+
type MessageIds = "noRawSql";
|
|
15
|
+
|
|
16
|
+
const DEFAULT_ALLOW_FILES = [
|
|
17
|
+
"**/migrations/**",
|
|
18
|
+
"**/raw/**",
|
|
19
|
+
"**/health/**",
|
|
20
|
+
"**/*.check.ts",
|
|
21
|
+
"**/tests/**",
|
|
22
|
+
"**/__tests__/**",
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
const optionSchema: JSONSchema4 = {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
allowFiles: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: {
|
|
32
|
+
type: "string",
|
|
33
|
+
},
|
|
34
|
+
uniqueItems: true,
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const noRawSqlOutsideAllowlistRule = createRule<RuleOptions, MessageIds>(
|
|
40
|
+
{
|
|
41
|
+
name: RULE_NAME,
|
|
42
|
+
meta: {
|
|
43
|
+
type: "problem",
|
|
44
|
+
docs: {
|
|
45
|
+
description:
|
|
46
|
+
"Disallow drizzle-orm `sql` tagged template literals outside an allowlist of files (migrations, raw queries).",
|
|
47
|
+
},
|
|
48
|
+
schema: [optionSchema],
|
|
49
|
+
messages: {
|
|
50
|
+
noRawSql:
|
|
51
|
+
"Raw `sql` template literals are not allowed outside the configured allowlist (migrations / raw).",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
defaultOptions: [{ allowFiles: [...DEFAULT_ALLOW_FILES] }],
|
|
55
|
+
create(context, [options]) {
|
|
56
|
+
const allowFiles = options.allowFiles ?? DEFAULT_ALLOW_FILES;
|
|
57
|
+
|
|
58
|
+
if (matchesAnyGlobPattern(context.filename, allowFiles)) {
|
|
59
|
+
return {};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const sqlBindings = new Set<string>();
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
ImportDeclaration(node) {
|
|
66
|
+
if (node.source.value !== "drizzle-orm") {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
for (const specifier of node.specifiers) {
|
|
71
|
+
if (specifier.type !== AST_NODE_TYPES.ImportSpecifier) {
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
specifier.imported.type === AST_NODE_TYPES.Identifier &&
|
|
77
|
+
specifier.imported.name === "sql"
|
|
78
|
+
) {
|
|
79
|
+
sqlBindings.add(specifier.local.name);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
TaggedTemplateExpression(node) {
|
|
84
|
+
if (node.tag.type !== AST_NODE_TYPES.Identifier) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!sqlBindings.has(node.tag.name)) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
context.report({
|
|
93
|
+
node,
|
|
94
|
+
messageId: "noRawSql",
|
|
95
|
+
});
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
);
|