@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.
Files changed (216) hide show
  1. package/bin/tsforge.js +2 -0
  2. package/package.json +35 -0
  3. package/src/agent/agent.constants.ts +382 -0
  4. package/src/agent/agent.types.ts +34 -0
  5. package/src/agent/index.ts +4 -0
  6. package/src/agent/model-agent.ts +297 -0
  7. package/src/agent/tool-repair.ts +194 -0
  8. package/src/agent/tools.ts +190 -0
  9. package/src/browser/checks.ts +96 -0
  10. package/src/browser/index.ts +8 -0
  11. package/src/browser/oracle.ts +303 -0
  12. package/src/classify.ts +48 -0
  13. package/src/cli.ts +1333 -0
  14. package/src/config/config.constants.ts +9 -0
  15. package/src/config/flags.ts +32 -0
  16. package/src/config/index.ts +8 -0
  17. package/src/config/tsforge-config.ts +301 -0
  18. package/src/constitution/baseline.ts +257 -0
  19. package/src/detect-gate.ts +498 -0
  20. package/src/eval/eval.types.ts +36 -0
  21. package/src/eval/index.ts +3 -0
  22. package/src/eval/judge.ts +62 -0
  23. package/src/eval/score.ts +39 -0
  24. package/src/files/create.ts +22 -0
  25. package/src/files/edit.ts +193 -0
  26. package/src/files/files.constants.ts +11 -0
  27. package/src/files/files.types.ts +81 -0
  28. package/src/files/hashline-format.ts +110 -0
  29. package/src/files/hashline.ts +689 -0
  30. package/src/files/index.ts +19 -0
  31. package/src/index.ts +8 -0
  32. package/src/inference/index.ts +6 -0
  33. package/src/inference/inference.constants.ts +34 -0
  34. package/src/inference/inference.types.ts +123 -0
  35. package/src/inference/openai-compatible.ts +113 -0
  36. package/src/inference/stream-guard.ts +161 -0
  37. package/src/inference/stream.ts +370 -0
  38. package/src/inference/transport.ts +78 -0
  39. package/src/inference/wire.ts +0 -0
  40. package/src/lib/fs/fs.ts +126 -0
  41. package/src/lib/fs/fs.types.ts +5 -0
  42. package/src/lib/fs/index.ts +3 -0
  43. package/src/lib/fs/process.ts +146 -0
  44. package/src/lib/guards/guards.ts +9 -0
  45. package/src/lib/guards/index.ts +1 -0
  46. package/src/lib/json/index.ts +1 -0
  47. package/src/lib/json/json.ts +12 -0
  48. package/src/lib/scope/index.ts +2 -0
  49. package/src/lib/scope/scope.constants.ts +3 -0
  50. package/src/lib/scope/scope.ts +40 -0
  51. package/src/loop/astgrep-fix.ts +228 -0
  52. package/src/loop/feedback/feedback.ts +138 -0
  53. package/src/loop/feedback/index.ts +8 -0
  54. package/src/loop/feedback/meta-rule-docs.ts +41 -0
  55. package/src/loop/feedback/meta-rule-feedback.ts +61 -0
  56. package/src/loop/feedback/rule-docs.generated.json +112 -0
  57. package/src/loop/feedback/rule-docs.ts +342 -0
  58. package/src/loop/index.ts +19 -0
  59. package/src/loop/loop.constants.ts +68 -0
  60. package/src/loop/loop.types.ts +99 -0
  61. package/src/loop/prompt/index.ts +2 -0
  62. package/src/loop/prompt/project-map.ts +69 -0
  63. package/src/loop/prompt/prompt.ts +107 -0
  64. package/src/loop/quality.ts +174 -0
  65. package/src/loop/rule-docs.generated.json +367 -0
  66. package/src/loop/run-spec.ts +88 -0
  67. package/src/loop/run.ts +400 -0
  68. package/src/loop/session.ts +1410 -0
  69. package/src/loop/tools/add-dependency.ts +71 -0
  70. package/src/loop/tools/condense.ts +498 -0
  71. package/src/loop/tools/edit-hashline.ts +80 -0
  72. package/src/loop/tools/execute-tool.ts +80 -0
  73. package/src/loop/tools/file-ops.ts +323 -0
  74. package/src/loop/tools/index.ts +2 -0
  75. package/src/loop/tools/lsp-ops.ts +222 -0
  76. package/src/loop/tools/scaffold-routes.ts +68 -0
  77. package/src/loop/tools/scaffold-ui.ts +62 -0
  78. package/src/loop/tools/scaffold-web.ts +35 -0
  79. package/src/loop/tools/tool-context.ts +126 -0
  80. package/src/loop/ttsr-defaults.ts +53 -0
  81. package/src/loop/ttsr.ts +322 -0
  82. package/src/loop/turn.ts +856 -0
  83. package/src/lsp/index.ts +2 -0
  84. package/src/lsp/lsp.types.ts +56 -0
  85. package/src/lsp/service.ts +500 -0
  86. package/src/meta-rules/context.ts +195 -0
  87. package/src/meta-rules/index.ts +9 -0
  88. package/src/meta-rules/meta-rules.types.ts +47 -0
  89. package/src/meta-rules/parsers/package-json-parser.ts +51 -0
  90. package/src/meta-rules/registry.ts +37 -0
  91. package/src/meta-rules/rules/ci/workflow-actions-pinned.ts +59 -0
  92. package/src/meta-rules/rules/ci/workflow-runner-pinned.ts +57 -0
  93. package/src/meta-rules/rules/ci/workflow-timeout-required.ts +114 -0
  94. package/src/meta-rules/rules/config/tsconfig-paths-exist.ts +117 -0
  95. package/src/meta-rules/rules/config/tsconfig-strict.ts +91 -0
  96. package/src/meta-rules/rules/source-text/no-eslint-disable-comments.ts +34 -0
  97. package/src/meta-rules/rules/source-text/no-ts-suppressions.ts +38 -0
  98. package/src/meta-rules/rules/supply-chain/no-overlapping-libs.ts +57 -0
  99. package/src/meta-rules/rules/supply-chain/package-exact-deps.ts +55 -0
  100. package/src/meta-rules/rules/testing/test-sibling-required.ts +110 -0
  101. package/src/meta-rules/runner.ts +64 -0
  102. package/src/models-config.ts +196 -0
  103. package/src/render/ansi.ts +289 -0
  104. package/src/render/banner.ts +113 -0
  105. package/src/render/box.ts +134 -0
  106. package/src/render/index.ts +7 -0
  107. package/src/render/markdown.ts +123 -0
  108. package/src/render/render.types.ts +21 -0
  109. package/src/render/stream-markdown.ts +128 -0
  110. package/src/render/style.ts +26 -0
  111. package/src/rule-packs/bullmq/index.ts +39 -0
  112. package/src/rule-packs/bullmq/rules/index.ts +7 -0
  113. package/src/rule-packs/bullmq/rules/job-name-must-be-constant.ts +141 -0
  114. package/src/rule-packs/bullmq/rules/job-options-must-set-attempts.ts +174 -0
  115. package/src/rule-packs/bullmq/rules/no-blocking-concurrency-zero.ts +103 -0
  116. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeoncomplete.ts +130 -0
  117. package/src/rule-packs/bullmq/rules/queue-options-must-set-removeonfail.ts +130 -0
  118. package/src/rule-packs/bullmq/rules/worker-must-implement-close.ts +182 -0
  119. package/src/rule-packs/bullmq/rules/worker-must-listen-failed.ts +140 -0
  120. package/src/rule-packs/bullmq/utils.ts +334 -0
  121. package/src/rule-packs/code-flow/index.ts +25 -0
  122. package/src/rule-packs/code-flow/rules/index.ts +3 -0
  123. package/src/rule-packs/code-flow/rules/no-bare-date-now.ts +138 -0
  124. package/src/rule-packs/code-flow/rules/no-template-trim-empty-ternary.ts +87 -0
  125. package/src/rule-packs/code-flow/rules/prefer-early-return.ts +80 -0
  126. package/src/rule-packs/code-flow/utils/prefer-early-return.ts +132 -0
  127. package/src/rule-packs/comment-hygiene/index.ts +25 -0
  128. package/src/rule-packs/comment-hygiene/rules/index.ts +3 -0
  129. package/src/rule-packs/comment-hygiene/rules/no-historical-comments.ts +102 -0
  130. package/src/rule-packs/comment-hygiene/rules/no-narration-comments.ts +83 -0
  131. package/src/rule-packs/comment-hygiene/rules/no-pr-reference-comments.ts +90 -0
  132. package/src/rule-packs/create-rule.ts +9 -0
  133. package/src/rule-packs/drizzle/index.ts +41 -0
  134. package/src/rule-packs/drizzle/rules/account-scoped-tables-require-where.ts +371 -0
  135. package/src/rule-packs/drizzle/rules/index.ts +8 -0
  136. package/src/rule-packs/drizzle/rules/no-nested-db-transaction.ts +127 -0
  137. package/src/rule-packs/drizzle/rules/no-raw-sql-outside-allowlist.ts +100 -0
  138. package/src/rule-packs/drizzle/rules/relations-must-cover-fks.ts +209 -0
  139. package/src/rule-packs/drizzle/rules/schema-files-must-not-import-driver.ts +127 -0
  140. package/src/rule-packs/drizzle/rules/schema-files-must-only-export-schema.ts +149 -0
  141. package/src/rule-packs/drizzle/rules/tables-must-have-timestamps.ts +312 -0
  142. package/src/rule-packs/drizzle/rules/timestamp-must-specify-mode.ts +166 -0
  143. package/src/rule-packs/drizzle/utils.ts +115 -0
  144. package/src/rule-packs/elysia/index.ts +43 -0
  145. package/src/rule-packs/elysia/rules/consistent-status-via-set.ts +69 -0
  146. package/src/rule-packs/elysia/rules/no-decorate-state-collision.ts +276 -0
  147. package/src/rule-packs/elysia/rules/no-separate-model-interfaces.ts +144 -0
  148. package/src/rule-packs/elysia/rules/prefer-destructured-context.ts +155 -0
  149. package/src/rule-packs/elysia/rules/prefer-direct-return.ts +176 -0
  150. package/src/rule-packs/elysia/rules/prefer-static-services.ts +159 -0
  151. package/src/rule-packs/elysia/rules/prefer-throw-status.ts +151 -0
  152. package/src/rule-packs/elysia/rules/require-hooks-before-routes.ts +209 -0
  153. package/src/rule-packs/elysia/rules/require-plugin-name.ts +107 -0
  154. package/src/rule-packs/elysia/utils/elysiaChain.ts +306 -0
  155. package/src/rule-packs/env-access/index.ts +23 -0
  156. package/src/rule-packs/env-access/rules/index.ts +2 -0
  157. package/src/rule-packs/env-access/rules/no-direct-process-env.ts +133 -0
  158. package/src/rule-packs/env-access/rules/no-process-exit.ts +95 -0
  159. package/src/rule-packs/i18n-keys/index.ts +19 -0
  160. package/src/rule-packs/i18n-keys/rules/static-translation-key-exists.ts +173 -0
  161. package/src/rule-packs/index.ts +139 -0
  162. package/src/rule-packs/jwt-cookies/index.ts +25 -0
  163. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-httponly.ts +150 -0
  164. package/src/rule-packs/jwt-cookies/rules/auth-cookie-must-be-secure-in-prod.ts +149 -0
  165. package/src/rule-packs/jwt-cookies/rules/bcrypt-rounds-min.ts +195 -0
  166. package/src/rule-packs/jwt-cookies/utils.ts +188 -0
  167. package/src/rule-packs/oauth-security/index.ts +25 -0
  168. package/src/rule-packs/oauth-security/rules/pkce-required-for-oidc.ts +296 -0
  169. package/src/rule-packs/oauth-security/rules/state-must-be-redis-backed.ts +193 -0
  170. package/src/rule-packs/oauth-security/rules/state-ttl-bounded.ts +219 -0
  171. package/src/rule-packs/oauth-security/utils.ts +127 -0
  172. package/src/rule-packs/react-component-architecture/index.ts +35 -0
  173. package/src/rule-packs/react-component-architecture/rules/component-folder-structure.ts +123 -0
  174. package/src/rule-packs/react-component-architecture/rules/forwardref-display-name.ts +93 -0
  175. package/src/rule-packs/react-component-architecture/rules/index-must-reexport-default.ts +123 -0
  176. package/src/rule-packs/react-component-architecture/rules/max-hooks-per-file.ts +122 -0
  177. package/src/rule-packs/react-component-architecture/rules/no-cross-feature-imports.ts +170 -0
  178. package/src/rule-packs/react-component-architecture/rules/no-inline-jsx-functions.ts +66 -0
  179. package/src/rule-packs/react-component-architecture/utils.ts +47 -0
  180. package/src/rule-packs/rule-packs.types.ts +18 -0
  181. package/src/rule-packs/structured-logging/index.ts +26 -0
  182. package/src/rule-packs/structured-logging/rules/mask-pii-fields.ts +221 -0
  183. package/src/rule-packs/structured-logging/rules/no-error-stringify.ts +217 -0
  184. package/src/rule-packs/structured-logging/rules/require-event-field.ts +136 -0
  185. package/src/rule-packs/structured-logging/utils/logger.ts +104 -0
  186. package/src/rule-packs/tanstack-query/index.ts +20 -0
  187. package/src/rule-packs/tanstack-query/rules/prefix-query-key-must-use-set-queries-data.ts +321 -0
  188. package/src/rule-packs/test-conventions/index.ts +23 -0
  189. package/src/rule-packs/test-conventions/rules/index.ts +2 -0
  190. package/src/rule-packs/test-conventions/rules/no-focused-tests.ts +170 -0
  191. package/src/rule-packs/test-conventions/rules/test-file-mirrors-source.ts +127 -0
  192. package/src/rule-packs/utils.ts +142 -0
  193. package/src/session-store.ts +359 -0
  194. package/src/spec/generate-tests.ts +213 -0
  195. package/src/spec/index.ts +5 -0
  196. package/src/spec/parse.ts +152 -0
  197. package/src/spec/review-tests.ts +162 -0
  198. package/src/spec/spec.constants.ts +13 -0
  199. package/src/spec/spec.types.ts +79 -0
  200. package/src/stack-detection/detect.ts +246 -0
  201. package/src/stack-detection/index.ts +3 -0
  202. package/src/stack-detection/packs.ts +174 -0
  203. package/src/stack-detection/stack-detection.types.ts +47 -0
  204. package/src/validate/accept.ts +49 -0
  205. package/src/validate/errors.ts +35 -0
  206. package/src/validate/index.ts +12 -0
  207. package/src/validate/parse.ts +148 -0
  208. package/src/validate/run-tests.ts +59 -0
  209. package/src/validate/validate.ts +40 -0
  210. package/src/validate/validate.types.ts +52 -0
  211. package/src/web-components.ts +638 -0
  212. package/src/web-coverage.ts +89 -0
  213. package/src/web-routes.ts +151 -0
  214. package/src/web-templates.ts +1011 -0
  215. package/strict.eslint.config.mjs +84 -0
  216. 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
+ );