@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,312 @@
|
|
|
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 { isPgTableCall } from "../utils";
|
|
6
|
+
|
|
7
|
+
export const RULE_NAME = "tables-must-have-timestamps";
|
|
8
|
+
|
|
9
|
+
export interface TablesMustHaveTimestampsOptions {
|
|
10
|
+
readonly requireColumns?: readonly string[];
|
|
11
|
+
readonly requireOnUpdate?: readonly string[];
|
|
12
|
+
readonly ignoreTablePattern?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RuleOptions = [TablesMustHaveTimestampsOptions];
|
|
16
|
+
type MessageIds = "missingTimestamp" | "missingOnUpdate";
|
|
17
|
+
|
|
18
|
+
const DEFAULT_REQUIRE_COLUMNS = ["createdAt"] as const;
|
|
19
|
+
const DEFAULT_REQUIRE_ON_UPDATE: readonly string[] = [];
|
|
20
|
+
const ON_UPDATE_METHODS = new Set(["$onUpdate", "$onUpdateFn"]);
|
|
21
|
+
|
|
22
|
+
const optionSchema: JSONSchema4 = {
|
|
23
|
+
type: "object",
|
|
24
|
+
additionalProperties: false,
|
|
25
|
+
properties: {
|
|
26
|
+
requireColumns: {
|
|
27
|
+
type: "array",
|
|
28
|
+
items: {
|
|
29
|
+
type: "string",
|
|
30
|
+
},
|
|
31
|
+
uniqueItems: true,
|
|
32
|
+
},
|
|
33
|
+
requireOnUpdate: {
|
|
34
|
+
type: "array",
|
|
35
|
+
items: {
|
|
36
|
+
type: "string",
|
|
37
|
+
},
|
|
38
|
+
uniqueItems: true,
|
|
39
|
+
},
|
|
40
|
+
ignoreTablePattern: {
|
|
41
|
+
type: "string",
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const tablesMustHaveTimestampsRule = createRule<RuleOptions, MessageIds>(
|
|
47
|
+
{
|
|
48
|
+
name: RULE_NAME,
|
|
49
|
+
meta: {
|
|
50
|
+
type: "suggestion",
|
|
51
|
+
docs: {
|
|
52
|
+
description:
|
|
53
|
+
"Require Drizzle tables to declare standard timestamp columns (createdAt by default).",
|
|
54
|
+
},
|
|
55
|
+
schema: [optionSchema],
|
|
56
|
+
messages: {
|
|
57
|
+
missingTimestamp:
|
|
58
|
+
"Table '{{name}}' missing required column(s): {{missing}}.",
|
|
59
|
+
missingOnUpdate:
|
|
60
|
+
"Column '{{column}}' on table '{{name}}' is in `requireOnUpdate` but its `timestamp(...)` chain does not include `.$onUpdate(...)` — without it, the column will not auto-update on row mutations.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultOptions: [
|
|
64
|
+
{
|
|
65
|
+
requireColumns: [...DEFAULT_REQUIRE_COLUMNS],
|
|
66
|
+
requireOnUpdate: [...DEFAULT_REQUIRE_ON_UPDATE],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
create(context, [options]) {
|
|
70
|
+
const requireColumns = options.requireColumns ?? DEFAULT_REQUIRE_COLUMNS;
|
|
71
|
+
const requireOnUpdate =
|
|
72
|
+
options.requireOnUpdate ?? DEFAULT_REQUIRE_ON_UPDATE;
|
|
73
|
+
const ignorePattern = compilePattern(options.ignoreTablePattern);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
VariableDeclarator(node) {
|
|
77
|
+
if (node.init?.type !== AST_NODE_TYPES.CallExpression) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!isPgTableCall(node.init)) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const tableName = getTableName(node);
|
|
86
|
+
|
|
87
|
+
if (!tableName) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (ignorePattern?.test(tableName)) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const columnsArg = node.init.arguments[1];
|
|
96
|
+
|
|
97
|
+
const definedColumns =
|
|
98
|
+
columnsArg?.type === AST_NODE_TYPES.ObjectExpression
|
|
99
|
+
? columnsArg
|
|
100
|
+
: null;
|
|
101
|
+
|
|
102
|
+
const reportNode =
|
|
103
|
+
node.id.type === AST_NODE_TYPES.Identifier ? node.id : node;
|
|
104
|
+
|
|
105
|
+
if (requireColumns.length > 0) {
|
|
106
|
+
const missing = requireColumns.filter(
|
|
107
|
+
(column) => !hasTimestampColumn(definedColumns, column)
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
if (missing.length > 0) {
|
|
111
|
+
context.report({
|
|
112
|
+
node: reportNode,
|
|
113
|
+
messageId: "missingTimestamp",
|
|
114
|
+
data: {
|
|
115
|
+
name: tableName,
|
|
116
|
+
missing: missing.join(", "),
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (definedColumns) {
|
|
123
|
+
for (const violation of findOnUpdateViolations(
|
|
124
|
+
definedColumns,
|
|
125
|
+
requireOnUpdate
|
|
126
|
+
)) {
|
|
127
|
+
context.report({
|
|
128
|
+
node: violation.property,
|
|
129
|
+
messageId: "missingOnUpdate",
|
|
130
|
+
data: {
|
|
131
|
+
name: tableName,
|
|
132
|
+
column: violation.column,
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
function compilePattern(source: string | undefined): RegExp | null {
|
|
144
|
+
if (!source) {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
return new RegExp(source);
|
|
150
|
+
} catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function getTableName(node: TSESTree.VariableDeclarator): string | null {
|
|
156
|
+
if (node.id.type === AST_NODE_TYPES.Identifier) {
|
|
157
|
+
return node.id.name;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (node.init?.type === AST_NODE_TYPES.CallExpression) {
|
|
161
|
+
const firstArg = node.init.arguments[0];
|
|
162
|
+
|
|
163
|
+
if (
|
|
164
|
+
firstArg?.type === AST_NODE_TYPES.Literal &&
|
|
165
|
+
typeof firstArg.value === "string"
|
|
166
|
+
) {
|
|
167
|
+
return firstArg.value;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function findTimestampProperty(
|
|
175
|
+
columns: TSESTree.ObjectExpression,
|
|
176
|
+
columnName: string
|
|
177
|
+
): TSESTree.Property | null {
|
|
178
|
+
for (const property of columns.properties) {
|
|
179
|
+
if (property.type !== AST_NODE_TYPES.Property) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!matchesPropertyKey(property, columnName)) {
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (
|
|
188
|
+
property.value.type === AST_NODE_TYPES.CallExpression &&
|
|
189
|
+
isTimestampInitializer(property.value)
|
|
190
|
+
) {
|
|
191
|
+
return property;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function matchesPropertyKey(
|
|
199
|
+
property: TSESTree.Property,
|
|
200
|
+
name: string
|
|
201
|
+
): boolean {
|
|
202
|
+
if (
|
|
203
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
204
|
+
property.key.name === name
|
|
205
|
+
) {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
property.key.type === AST_NODE_TYPES.Literal &&
|
|
211
|
+
property.key.value === name
|
|
212
|
+
) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function isTimestampInitializer(node: TSESTree.CallExpression): boolean {
|
|
220
|
+
const calleeName = getCalleeIdentifierName(node);
|
|
221
|
+
|
|
222
|
+
return calleeName === "timestamp";
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getCalleeIdentifierName(node: TSESTree.CallExpression): string | null {
|
|
226
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
227
|
+
return node.callee.name;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (
|
|
231
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
232
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier
|
|
233
|
+
) {
|
|
234
|
+
return node.callee.property.name;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function chainHasOnUpdate(startCall: TSESTree.CallExpression): boolean {
|
|
241
|
+
let current: TSESTree.Node = startCall;
|
|
242
|
+
let parent = getParent(current);
|
|
243
|
+
|
|
244
|
+
while (parent !== undefined) {
|
|
245
|
+
if (
|
|
246
|
+
parent.type === AST_NODE_TYPES.MemberExpression &&
|
|
247
|
+
parent.object === current &&
|
|
248
|
+
parent.property.type === AST_NODE_TYPES.Identifier &&
|
|
249
|
+
ON_UPDATE_METHODS.has(parent.property.name)
|
|
250
|
+
) {
|
|
251
|
+
const methodCall = getParent(parent);
|
|
252
|
+
|
|
253
|
+
if (methodCall?.type === AST_NODE_TYPES.CallExpression) {
|
|
254
|
+
return true;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
parent.type === AST_NODE_TYPES.MemberExpression ||
|
|
260
|
+
parent.type === AST_NODE_TYPES.CallExpression
|
|
261
|
+
) {
|
|
262
|
+
current = parent;
|
|
263
|
+
parent = getParent(current);
|
|
264
|
+
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function getParent(node: TSESTree.Node): TSESTree.Node | undefined {
|
|
275
|
+
return node.parent;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Columns from `requireOnUpdate` whose timestamp chain lacks `.$onUpdate(...)`. */
|
|
279
|
+
function findOnUpdateViolations(
|
|
280
|
+
definedColumns: TSESTree.ObjectExpression,
|
|
281
|
+
requireOnUpdate: readonly string[]
|
|
282
|
+
): { property: TSESTree.Property; column: string }[] {
|
|
283
|
+
const violations: { property: TSESTree.Property; column: string }[] = [];
|
|
284
|
+
|
|
285
|
+
for (const column of requireOnUpdate) {
|
|
286
|
+
const property = findTimestampProperty(definedColumns, column);
|
|
287
|
+
|
|
288
|
+
if (!property) {
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (
|
|
293
|
+
property.value.type === AST_NODE_TYPES.CallExpression &&
|
|
294
|
+
!chainHasOnUpdate(property.value)
|
|
295
|
+
) {
|
|
296
|
+
violations.push({ property, column });
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return violations;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function hasTimestampColumn(
|
|
304
|
+
columns: TSESTree.ObjectExpression | null,
|
|
305
|
+
columnName: string
|
|
306
|
+
): boolean {
|
|
307
|
+
if (!columns) {
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return findTimestampProperty(columns, columnName) !== null;
|
|
312
|
+
}
|
|
@@ -0,0 +1,166 @@
|
|
|
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 = "timestamp-must-specify-mode";
|
|
7
|
+
|
|
8
|
+
export interface TimestampMustSpecifyModeOptions {
|
|
9
|
+
readonly allowedModes?: readonly ("date" | "string")[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RuleOptions = [TimestampMustSpecifyModeOptions];
|
|
13
|
+
type MessageIds = "missingMode" | "invalidMode";
|
|
14
|
+
|
|
15
|
+
const DEFAULT_ALLOWED_MODES = ["date", "string"] as const;
|
|
16
|
+
|
|
17
|
+
const optionSchema: JSONSchema4 = {
|
|
18
|
+
type: "object",
|
|
19
|
+
additionalProperties: false,
|
|
20
|
+
properties: {
|
|
21
|
+
allowedModes: {
|
|
22
|
+
type: "array",
|
|
23
|
+
uniqueItems: true,
|
|
24
|
+
items: {
|
|
25
|
+
type: "string",
|
|
26
|
+
enum: ["date", "string"],
|
|
27
|
+
},
|
|
28
|
+
minItems: 1,
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const timestampMustSpecifyModeRule = createRule<RuleOptions, MessageIds>(
|
|
34
|
+
{
|
|
35
|
+
name: RULE_NAME,
|
|
36
|
+
meta: {
|
|
37
|
+
type: "problem",
|
|
38
|
+
docs: {
|
|
39
|
+
description:
|
|
40
|
+
"Require every Drizzle timestamp(...) call to explicitly set `mode: 'date'` or `mode: 'string'`.",
|
|
41
|
+
},
|
|
42
|
+
schema: [optionSchema],
|
|
43
|
+
messages: {
|
|
44
|
+
missingMode:
|
|
45
|
+
"timestamp(...) call does not specify `mode` — pass `{ mode: 'date' }` or `{ mode: 'string' }` so return types are deterministic across drivers.",
|
|
46
|
+
invalidMode:
|
|
47
|
+
"timestamp(...) `mode` must be one of: {{allowed}}. Got: {{actual}}.",
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
defaultOptions: [{ allowedModes: [...DEFAULT_ALLOWED_MODES] }],
|
|
51
|
+
create(context, [options]) {
|
|
52
|
+
const allowedModes = options.allowedModes ?? DEFAULT_ALLOWED_MODES;
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
if (!isTimestampCallee(node)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const optionsArg = node.arguments[1];
|
|
61
|
+
|
|
62
|
+
if (optionsArg?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
63
|
+
context.report({ node, messageId: "missingMode" });
|
|
64
|
+
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const modeProperty = findModeProperty(optionsArg);
|
|
69
|
+
|
|
70
|
+
if (!modeProperty) {
|
|
71
|
+
context.report({ node: optionsArg, messageId: "missingMode" });
|
|
72
|
+
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (
|
|
77
|
+
modeProperty.value.type !== AST_NODE_TYPES.Literal ||
|
|
78
|
+
typeof modeProperty.value.value !== "string"
|
|
79
|
+
) {
|
|
80
|
+
context.report({
|
|
81
|
+
node: modeProperty,
|
|
82
|
+
messageId: "invalidMode",
|
|
83
|
+
data: {
|
|
84
|
+
allowed: allowedModes.join(" | "),
|
|
85
|
+
actual: "<non-literal>",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const modeValue = modeProperty.value.value;
|
|
93
|
+
|
|
94
|
+
if (!allowedModesIncludes(allowedModes, modeValue)) {
|
|
95
|
+
context.report({
|
|
96
|
+
node: modeProperty,
|
|
97
|
+
messageId: "invalidMode",
|
|
98
|
+
data: {
|
|
99
|
+
allowed: allowedModes.join(" | "),
|
|
100
|
+
actual: modeValue,
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
};
|
|
106
|
+
},
|
|
107
|
+
}
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
function isTimestampCallee(node: TSESTree.CallExpression): boolean {
|
|
111
|
+
if (
|
|
112
|
+
node.callee.type === AST_NODE_TYPES.Identifier &&
|
|
113
|
+
node.callee.name === "timestamp"
|
|
114
|
+
) {
|
|
115
|
+
return true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
120
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier &&
|
|
121
|
+
node.callee.property.name === "timestamp"
|
|
122
|
+
) {
|
|
123
|
+
return true;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function findModeProperty(
|
|
130
|
+
obj: TSESTree.ObjectExpression
|
|
131
|
+
): TSESTree.Property | null {
|
|
132
|
+
for (const property of obj.properties) {
|
|
133
|
+
if (property.type !== AST_NODE_TYPES.Property) {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (propertyKeyMatches(property, "mode")) {
|
|
138
|
+
return property;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function propertyKeyMatches(
|
|
146
|
+
property: TSESTree.Property,
|
|
147
|
+
name: string
|
|
148
|
+
): boolean {
|
|
149
|
+
if (
|
|
150
|
+
property.key.type === AST_NODE_TYPES.Identifier &&
|
|
151
|
+
property.key.name === name
|
|
152
|
+
) {
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return (
|
|
157
|
+
property.key.type === AST_NODE_TYPES.Literal && property.key.value === name
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function allowedModesIncludes(
|
|
162
|
+
allowedModes: readonly string[],
|
|
163
|
+
value: string
|
|
164
|
+
): boolean {
|
|
165
|
+
return allowedModes.includes(value);
|
|
166
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { AST_NODE_TYPES, type TSESTree } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { walkAll } from "../utils";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Helper utilities for Drizzle rules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export function isPgTableCall(node: TSESTree.CallExpression): boolean {
|
|
10
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
11
|
+
return false;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const name = node.callee.name;
|
|
15
|
+
|
|
16
|
+
return name === "pgTable" || name === "pgTableCreator";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isRelationsCall(node: TSESTree.CallExpression): boolean {
|
|
20
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return node.callee.name === "relations";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function isForeignKeyCall(node: TSESTree.CallExpression): boolean {
|
|
28
|
+
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return node.callee.name === "foreignKey";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function isSchemaBuilderCall(node: TSESTree.CallExpression): boolean {
|
|
36
|
+
const calleeName = getCalleeIdentifierName(node);
|
|
37
|
+
|
|
38
|
+
if (!calleeName) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Drizzle schema builders: tables, relations, indices, checks, uniqueConstraints, etc.
|
|
43
|
+
const schemaBuilders = new Set([
|
|
44
|
+
"pgTable",
|
|
45
|
+
"pgTableCreator",
|
|
46
|
+
"relations",
|
|
47
|
+
"index",
|
|
48
|
+
"uniqueIndex",
|
|
49
|
+
"primaryKey",
|
|
50
|
+
"foreignKey",
|
|
51
|
+
"check",
|
|
52
|
+
"unique",
|
|
53
|
+
"schema",
|
|
54
|
+
"serial",
|
|
55
|
+
"smallserial",
|
|
56
|
+
"bigserial",
|
|
57
|
+
"varchar",
|
|
58
|
+
"char",
|
|
59
|
+
"text",
|
|
60
|
+
"integer",
|
|
61
|
+
"smallint",
|
|
62
|
+
"bigint",
|
|
63
|
+
"decimal",
|
|
64
|
+
"numeric",
|
|
65
|
+
"real",
|
|
66
|
+
"doublePrecision",
|
|
67
|
+
"boolean",
|
|
68
|
+
"date",
|
|
69
|
+
"time",
|
|
70
|
+
"timestamp",
|
|
71
|
+
"interval",
|
|
72
|
+
"json",
|
|
73
|
+
"jsonb",
|
|
74
|
+
"uuid",
|
|
75
|
+
"bytea",
|
|
76
|
+
"citext",
|
|
77
|
+
"inet",
|
|
78
|
+
"array",
|
|
79
|
+
"enum",
|
|
80
|
+
"geometry",
|
|
81
|
+
"geography",
|
|
82
|
+
]);
|
|
83
|
+
|
|
84
|
+
return schemaBuilders.has(calleeName);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function getCalleeIdentifierName(node: TSESTree.CallExpression): string | null {
|
|
88
|
+
if (node.callee.type === AST_NODE_TYPES.Identifier) {
|
|
89
|
+
return node.callee.name;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
node.callee.type === AST_NODE_TYPES.MemberExpression &&
|
|
94
|
+
node.callee.property.type === AST_NODE_TYPES.Identifier
|
|
95
|
+
) {
|
|
96
|
+
return node.callee.property.name;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function findCallExpressionsDeep(
|
|
103
|
+
root: TSESTree.Node,
|
|
104
|
+
predicate: (node: TSESTree.CallExpression) => boolean
|
|
105
|
+
): TSESTree.CallExpression[] {
|
|
106
|
+
const results: TSESTree.CallExpression[] = [];
|
|
107
|
+
|
|
108
|
+
walkAll(root, (node) => {
|
|
109
|
+
if (node.type === AST_NODE_TYPES.CallExpression && predicate(node)) {
|
|
110
|
+
results.push(node);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return results;
|
|
115
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { TSESLint } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { consistentStatusViaSetRule } from "./rules/consistent-status-via-set";
|
|
4
|
+
import { noDecorateStateCollisionRule } from "./rules/no-decorate-state-collision";
|
|
5
|
+
import { noSeparateModelInterfacesRule } from "./rules/no-separate-model-interfaces";
|
|
6
|
+
import { preferDestructuredContextRule } from "./rules/prefer-destructured-context";
|
|
7
|
+
import { preferDirectReturnRule } from "./rules/prefer-direct-return";
|
|
8
|
+
import { preferStaticServicesRule } from "./rules/prefer-static-services";
|
|
9
|
+
import { preferThrowStatusRule } from "./rules/prefer-throw-status";
|
|
10
|
+
import { requireHooksBeforeRoutesRule } from "./rules/require-hooks-before-routes";
|
|
11
|
+
import { requirePluginNameRule } from "./rules/require-plugin-name";
|
|
12
|
+
import type { IRulePack } from "../rule-packs.types";
|
|
13
|
+
|
|
14
|
+
const rules: Record<string, TSESLint.RuleModule<string, readonly unknown[]>> = {
|
|
15
|
+
"consistent-status-via-set": consistentStatusViaSetRule,
|
|
16
|
+
"no-decorate-state-collision": noDecorateStateCollisionRule,
|
|
17
|
+
"no-separate-model-interfaces": noSeparateModelInterfacesRule,
|
|
18
|
+
"prefer-destructured-context": preferDestructuredContextRule,
|
|
19
|
+
"prefer-direct-return": preferDirectReturnRule,
|
|
20
|
+
"prefer-static-services": preferStaticServicesRule,
|
|
21
|
+
"prefer-throw-status": preferThrowStatusRule,
|
|
22
|
+
"require-hooks-before-routes": requireHooksBeforeRoutesRule,
|
|
23
|
+
"require-plugin-name": requirePluginNameRule,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export const elysiaPack: IRulePack = {
|
|
27
|
+
id: "elysia",
|
|
28
|
+
description: "Elysia framework best practices and type-safety patterns",
|
|
29
|
+
rules,
|
|
30
|
+
rulesConfig: {
|
|
31
|
+
"consistent-status-via-set": "error",
|
|
32
|
+
"no-decorate-state-collision": "error",
|
|
33
|
+
"no-separate-model-interfaces": "warn",
|
|
34
|
+
"prefer-destructured-context": "warn",
|
|
35
|
+
"prefer-direct-return": "warn",
|
|
36
|
+
"prefer-static-services": "warn",
|
|
37
|
+
"prefer-throw-status": "warn",
|
|
38
|
+
"require-hooks-before-routes": "error",
|
|
39
|
+
"require-plugin-name": "error",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export default elysiaPack;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { AST_NODE_TYPES } from "@typescript-eslint/utils";
|
|
2
|
+
|
|
3
|
+
import { createRule } from "../../create-rule";
|
|
4
|
+
import {
|
|
5
|
+
collectElysiaVariables,
|
|
6
|
+
findEnclosingRouteHandler,
|
|
7
|
+
findObjectProperty,
|
|
8
|
+
} from "../utils/elysiaChain";
|
|
9
|
+
|
|
10
|
+
export const RULE_NAME = "consistent-status-via-set";
|
|
11
|
+
|
|
12
|
+
type RuleOptions = [];
|
|
13
|
+
type MessageIds = "useSetStatus";
|
|
14
|
+
|
|
15
|
+
export const consistentStatusViaSetRule = createRule<RuleOptions, MessageIds>({
|
|
16
|
+
name: RULE_NAME,
|
|
17
|
+
meta: {
|
|
18
|
+
type: "problem",
|
|
19
|
+
docs: {
|
|
20
|
+
description:
|
|
21
|
+
"Inside Elysia route handlers, set HTTP status via `set.status = N`, not by returning a `new Response(body, { status: N })`.",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
messages: {
|
|
25
|
+
useSetStatus:
|
|
26
|
+
"Do not return `new Response(body, { status })` from an Elysia route handler — assign `set.status = N` and return the body directly.",
|
|
27
|
+
},
|
|
28
|
+
},
|
|
29
|
+
defaultOptions: [],
|
|
30
|
+
create(context) {
|
|
31
|
+
let elysiaVars = new Set<string>();
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
Program(program) {
|
|
35
|
+
elysiaVars = collectElysiaVariables(program);
|
|
36
|
+
},
|
|
37
|
+
ReturnStatement(node) {
|
|
38
|
+
if (node.argument?.type !== AST_NODE_TYPES.NewExpression) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const newExpr = node.argument;
|
|
43
|
+
|
|
44
|
+
if (
|
|
45
|
+
newExpr.callee.type !== AST_NODE_TYPES.Identifier ||
|
|
46
|
+
newExpr.callee.name !== "Response"
|
|
47
|
+
) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const optionsArg = newExpr.arguments[1];
|
|
52
|
+
|
|
53
|
+
if (optionsArg?.type !== AST_NODE_TYPES.ObjectExpression) {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!findObjectProperty(optionsArg, "status")) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!findEnclosingRouteHandler(node, elysiaVars)) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
context.report({ node: newExpr, messageId: "useSetStatus" });
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|