biome-plugin-drizzle 0.0.2

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/LICENSE ADDED
@@ -0,0 +1,26 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 biome-plugin-drizzle contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
23
+ ---
24
+
25
+ This plugin is inspired by eslint-plugin-drizzle from the Drizzle Team.
26
+ See: https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle
package/README.md ADDED
@@ -0,0 +1,234 @@
1
+ # biome-plugin-drizzle
2
+
3
+ Biome linter plugin for Drizzle ORM safety rules. Enforces `.where()` clauses on delete and update operations to prevent accidental data loss.
4
+
5
+ This plugin is a port of [eslint-plugin-drizzle](https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle) for the [Biome](https://biomejs.dev/) toolchain.
6
+
7
+ ## Features
8
+
9
+ - **enforce-delete-with-where**: Catches `.delete()` calls without `.where()` that would delete all rows
10
+ - **enforce-update-with-where**: Catches `.update().set()` calls without `.where()` that would update all rows
11
+ - **Object name filtering**: Optionally restrict rules to specific Drizzle object names (e.g., `db`, `tx`)
12
+ - **CLI generator**: Generate customized plugin configurations
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install -D biome-plugin-drizzle @biomejs/biome
18
+ ```
19
+
20
+ ## Quick Setup
21
+
22
+ ### Option 1: Using the CLI (Recommended)
23
+
24
+ ```bash
25
+ # Initialize with default settings (broad matching)
26
+ npx biome-plugin-drizzle init
27
+
28
+ # Or initialize with object name filtering (reduces false positives)
29
+ npx biome-plugin-drizzle init --object-names db,tx
30
+ ```
31
+
32
+ ### Option 2: Manual Configuration
33
+
34
+ Add the plugin to your `biome.json`:
35
+
36
+ ```json
37
+ {
38
+ "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
39
+ "plugins": ["./node_modules/biome-plugin-drizzle/dist/drizzle.grit"],
40
+ "linter": {
41
+ "enabled": true
42
+ }
43
+ }
44
+ ```
45
+
46
+ > **Note:** This plugin requires Biome 2.0.0 or later, which introduced support for GritQL plugins.
47
+
48
+ Then run Biome:
49
+
50
+ ```bash
51
+ npx biome lint .
52
+ ```
53
+
54
+ ## Configuration
55
+
56
+ ### Default Plugin (Broad Matching)
57
+
58
+ The default plugin matches any `.delete()` or `.update().set()` call, which may cause false positives if you have other classes with similar method names.
59
+
60
+ ```json
61
+ {
62
+ "plugins": ["./node_modules/biome-plugin-drizzle/dist/drizzle.grit"]
63
+ }
64
+ ```
65
+
66
+ ### Generated Plugin (Object Name Filtering)
67
+
68
+ To reduce false positives, generate a customized plugin that only matches specific object names:
69
+
70
+ ```bash
71
+ # Generate a plugin that only matches 'db' and 'tx' objects
72
+ npx biome-plugin-drizzle generate --out ./.biome/drizzle.grit --object-names db,tx
73
+ ```
74
+
75
+ Then update your `biome.json`:
76
+
77
+ ```json
78
+ {
79
+ "plugins": ["./.biome/drizzle.grit"]
80
+ }
81
+ ```
82
+
83
+ This is equivalent to the ESLint plugin's `drizzleObjectName` option:
84
+
85
+ ```javascript
86
+ // ESLint equivalent
87
+ {
88
+ "drizzle/enforce-delete-with-where": ["error", { "drizzleObjectName": ["db", "tx"] }]
89
+ }
90
+ ```
91
+
92
+ ## Rules
93
+
94
+ ### drizzle/enforce-delete-with-where
95
+
96
+ Ensures all `.delete()` operations include a `.where()` clause to prevent accidental deletion of entire tables.
97
+
98
+ #### Failing Examples
99
+
100
+ ```typescript
101
+ // Will delete ALL rows in the users table!
102
+ db.delete(users);
103
+
104
+ // Still deletes all rows (returning doesn't add safety)
105
+ db.delete(users).returning();
106
+
107
+ // Async operations without where
108
+ await db.delete(posts);
109
+ ```
110
+
111
+ #### Passing Examples
112
+
113
+ ```typescript
114
+ // Safe: has where clause
115
+ db.delete(users).where(eq(users.id, 1));
116
+
117
+ // Safe: where can be anywhere in the chain
118
+ db.delete(users).returning().where(eq(users.id, 1));
119
+
120
+ // Safe: with CTEs
121
+ db.with(someCte).delete(users).where(eq(users.id, 1));
122
+ ```
123
+
124
+ ### drizzle/enforce-update-with-where
125
+
126
+ Ensures all `.update().set()` operations include a `.where()` clause to prevent accidental updates to entire tables.
127
+
128
+ #### Failing Examples
129
+
130
+ ```typescript
131
+ // Will update ALL rows in the users table!
132
+ db.update(users).set({ name: "John" });
133
+
134
+ // Still updates all rows
135
+ db.update(users).set({ status: "active" }).returning();
136
+
137
+ // Async operations without where
138
+ await db.update(posts).set({ views: 0 });
139
+ ```
140
+
141
+ #### Passing Examples
142
+
143
+ ```typescript
144
+ // Safe: has where clause
145
+ db.update(users).set({ name: "John" }).where(eq(users.id, 1));
146
+
147
+ // Safe: where can be anywhere in the chain
148
+ db.update(users).set({ email: "new@email.com" }).returning().where(eq(users.id, 1));
149
+
150
+ // Safe: complex conditions
151
+ db.update(users)
152
+ .set({ verified: true })
153
+ .where(and(eq(users.status, "pending"), gt(users.age, 18)));
154
+ ```
155
+
156
+ ## CLI Reference
157
+
158
+ ### `init`
159
+
160
+ Initialize biome-plugin-drizzle in your project.
161
+
162
+ ```bash
163
+ npx biome-plugin-drizzle init [options]
164
+ ```
165
+
166
+ Options:
167
+ - `-c, --config <path>`: Path to biome.json (default: `./biome.json`)
168
+ - `--object-names <names>`: Comma-separated list of Drizzle object names
169
+ - `--out <path>`: Output path for generated .grit file (default: `./.biome/drizzle.grit`)
170
+
171
+ ### `generate`
172
+
173
+ Generate a customized .grit plugin file.
174
+
175
+ ```bash
176
+ npx biome-plugin-drizzle generate --out <path> [options]
177
+ ```
178
+
179
+ Options:
180
+ - `-o, --out <path>`: Output path for the generated .grit file (required)
181
+ - `--object-names <names>`: Comma-separated list of Drizzle object names
182
+ - `--no-delete-rule`: Exclude the enforce-delete-with-where rule
183
+ - `--no-update-rule`: Exclude the enforce-update-with-where rule
184
+
185
+ ### `print-path`
186
+
187
+ Print the path to the installed default plugin.
188
+
189
+ ```bash
190
+ npx biome-plugin-drizzle print-path [--absolute]
191
+ ```
192
+
193
+ Options:
194
+ - `--absolute`: Print the absolute path instead of relative
195
+
196
+ ## Limitations
197
+
198
+ ### Biome Plugin System
199
+
200
+ - Biome linter plugins are currently GritQL-based and can only report diagnostics
201
+ - Unlike ESLint, Biome plugins cannot accept configuration options in `biome.json`
202
+ - Workaround: Use the CLI generator to create customized plugins with object name filtering
203
+
204
+ ### Pattern Matching
205
+
206
+ - The plugin uses structural pattern matching, which may not catch all edge cases
207
+ - Complex dynamic code patterns may not be detected
208
+ - When in doubt, add explicit `.where()` clauses to your queries
209
+
210
+ ## How to Release
211
+
212
+ 1. Update version in `package.json`
213
+ 2. Update CHANGELOG if you have one
214
+ 3. Build and test:
215
+ ```bash
216
+ npm run build
217
+ npm test
218
+ ```
219
+ 4. Publish to npm:
220
+ ```bash
221
+ npm publish
222
+ ```
223
+
224
+ ## Contributing
225
+
226
+ Contributions are welcome! Please feel free to submit issues and pull requests.
227
+
228
+ ## License
229
+
230
+ MIT
231
+
232
+ ---
233
+
234
+ Inspired by [eslint-plugin-drizzle](https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle) from the Drizzle Team.
package/dist/cli.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for biome-plugin-drizzle
4
+ *
5
+ * Commands:
6
+ * - init: Initialize plugin configuration in the current project
7
+ * - generate: Generate a customized .grit plugin file
8
+ * - print-path: Print the path to the installed plugin
9
+ */
10
+ export {};
11
+ //# sourceMappingURL=cli.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAEA;;;;;;;GAOG"}
package/dist/cli.js ADDED
@@ -0,0 +1,128 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * CLI for biome-plugin-drizzle
4
+ *
5
+ * Commands:
6
+ * - init: Initialize plugin configuration in the current project
7
+ * - generate: Generate a customized .grit plugin file
8
+ * - print-path: Print the path to the installed plugin
9
+ */
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { program } from "commander";
13
+ import { generatePlugin } from "./generator.js";
14
+ import { getPluginPath, getRelativePluginPath } from "./utils.js";
15
+ const packageJson = JSON.parse(readFileSync(join(dirname(getPluginPath()), "..", "package.json"), "utf-8"));
16
+ program
17
+ .name("biome-plugin-drizzle")
18
+ .description("CLI for biome-plugin-drizzle - Drizzle ORM safety rules")
19
+ .version(packageJson.version);
20
+ program
21
+ .command("init")
22
+ .description("Initialize biome-plugin-drizzle in your project")
23
+ .option("-c, --config <path>", "Path to biome.json config file", "./biome.json")
24
+ .option("--object-names <names>", "Comma-separated list of Drizzle object names (e.g., db,tx)")
25
+ .option("--out <path>", "Output path for generated .grit file (only used with --object-names)")
26
+ .action((options) => {
27
+ const configPath = resolve(options.config);
28
+ // Determine plugin path
29
+ let pluginPath;
30
+ if (options.objectNames) {
31
+ // Generate a custom plugin with object name filtering
32
+ const objectNames = options.objectNames
33
+ .split(",")
34
+ .map((n) => n.trim())
35
+ .filter(Boolean);
36
+ const outputPath = options.out || "./.biome/drizzle.grit";
37
+ const absoluteOutputPath = resolve(outputPath);
38
+ // Ensure output directory exists
39
+ const outputDir = dirname(absoluteOutputPath);
40
+ if (!existsSync(outputDir)) {
41
+ mkdirSync(outputDir, { recursive: true });
42
+ }
43
+ // Generate and write the plugin
44
+ const content = generatePlugin({ objectNames });
45
+ writeFileSync(absoluteOutputPath, content);
46
+ console.log(`Generated custom plugin at: ${absoluteOutputPath}`);
47
+ console.log(`Object name filter: ${objectNames.join(", ")}`);
48
+ pluginPath = outputPath;
49
+ }
50
+ else {
51
+ // Use the default plugin from node_modules
52
+ pluginPath = getRelativePluginPath();
53
+ console.log(`Using default plugin from: ${pluginPath}`);
54
+ }
55
+ // Update or create biome.json
56
+ let biomeConfig = {};
57
+ if (existsSync(configPath)) {
58
+ try {
59
+ biomeConfig = JSON.parse(readFileSync(configPath, "utf-8"));
60
+ }
61
+ catch {
62
+ console.error(`Error: Could not parse ${configPath}`);
63
+ process.exit(1);
64
+ }
65
+ }
66
+ // Add or update plugins array
67
+ const plugins = biomeConfig.plugins || [];
68
+ if (!plugins.includes(pluginPath)) {
69
+ plugins.push(pluginPath);
70
+ }
71
+ biomeConfig.plugins = plugins;
72
+ writeFileSync(configPath, `${JSON.stringify(biomeConfig, null, 2)}\n`);
73
+ console.log(`Updated ${configPath} with plugin configuration`);
74
+ console.log("\nSetup complete! Run 'biome lint' to check your code.");
75
+ });
76
+ program
77
+ .command("generate")
78
+ .description("Generate a customized .grit plugin file")
79
+ .requiredOption("-o, --out <path>", "Output path for the generated .grit file")
80
+ .option("--object-names <names>", "Comma-separated list of Drizzle object names to match (e.g., db,tx)")
81
+ .option("--no-delete-rule", "Exclude the enforce-delete-with-where rule")
82
+ .option("--no-update-rule", "Exclude the enforce-update-with-where rule")
83
+ .action((options) => {
84
+ const outputPath = resolve(options.out);
85
+ // Parse object names if provided
86
+ const objectNames = options.objectNames
87
+ ?.split(",")
88
+ .map((n) => n.trim())
89
+ .filter(Boolean);
90
+ // Generate the plugin content
91
+ const content = generatePlugin({
92
+ objectNames,
93
+ includeDeleteRule: options.deleteRule,
94
+ includeUpdateRule: options.updateRule,
95
+ });
96
+ // Ensure output directory exists
97
+ const outputDir = dirname(outputPath);
98
+ if (!existsSync(outputDir)) {
99
+ mkdirSync(outputDir, { recursive: true });
100
+ }
101
+ // Write the plugin file
102
+ writeFileSync(outputPath, content);
103
+ console.log(`Generated plugin at: ${outputPath}`);
104
+ if (objectNames?.length) {
105
+ console.log(`Object name filter: ${objectNames.join(", ")}`);
106
+ }
107
+ else {
108
+ console.log("Object name filter: none (matches all objects)");
109
+ }
110
+ console.log(`Delete rule: ${options.deleteRule ? "enabled" : "disabled"}`);
111
+ console.log(`Update rule: ${options.updateRule ? "enabled" : "disabled"}`);
112
+ console.log("\nAdd the following to your biome.json:");
113
+ console.log(` "plugins": ["${options.out}"]`);
114
+ });
115
+ program
116
+ .command("print-path")
117
+ .description("Print the path to the installed default plugin")
118
+ .option("--absolute", "Print the absolute path instead of relative")
119
+ .action((options) => {
120
+ if (options.absolute) {
121
+ console.log(getPluginPath());
122
+ }
123
+ else {
124
+ console.log(getRelativePluginPath());
125
+ }
126
+ });
127
+ program.parse();
128
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,34 @@
1
+ engine biome(1.0)
2
+ language js
3
+
4
+ // biome-plugin-drizzle: Safety rules for Drizzle ORM
5
+ // Enforces .where() clauses on delete and update operations to prevent accidental data loss.
6
+ //
7
+ // Rules included:
8
+ // - drizzle/enforce-delete-with-where: Catches .delete() without .where()
9
+ // - drizzle/enforce-update-with-where: Catches .update().set() without .where()
10
+
11
+ or {
12
+ // Rule: drizzle/enforce-delete-with-where
13
+ // Bad: db.delete(users)
14
+ // Good: db.delete(users).where(eq(users.id, 1))
15
+ `$obj.delete($args)` as $call where {
16
+ not $call <: within `$_.where($_)`,
17
+ register_diagnostic(
18
+ span = $call,
19
+ message = "[drizzle/enforce-delete-with-where] Missing .where() clause. Calling .delete() without .where() will delete all rows in the table. Use db.delete(table).where(...) instead.",
20
+ severity = "error"
21
+ )
22
+ },
23
+ // Rule: drizzle/enforce-update-with-where
24
+ // Bad: db.update(users).set({ name: 'John' })
25
+ // Good: db.update(users).set({ name: 'John' }).where(eq(users.id, 1))
26
+ `$obj.update($table).set($values)` as $call where {
27
+ not $call <: within `$_.where($_)`,
28
+ register_diagnostic(
29
+ span = $call,
30
+ message = "[drizzle/enforce-update-with-where] Missing .where() clause. Calling .update().set() without .where() will update all rows in the table. Use db.update(table).set(...).where(...) instead.",
31
+ severity = "error"
32
+ )
33
+ }
34
+ }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Generator for customized Drizzle GritQL plugin files.
3
+ *
4
+ * This module generates GritQL plugin content with optional object name filtering,
5
+ * similar to the ESLint plugin's `drizzleObjectName` option.
6
+ */
7
+ export interface GeneratePluginOptions {
8
+ /**
9
+ * Array of Drizzle object names to match against.
10
+ * When provided, rules will only trigger when the receiver object
11
+ * matches one of these names, reducing false positives.
12
+ *
13
+ * Example: ['db', 'tx', 'database']
14
+ */
15
+ objectNames?: string[];
16
+ /**
17
+ * Whether to include the delete rule.
18
+ * @default true
19
+ */
20
+ includeDeleteRule?: boolean;
21
+ /**
22
+ * Whether to include the update rule.
23
+ * @default true
24
+ */
25
+ includeUpdateRule?: boolean;
26
+ }
27
+ /**
28
+ * Generates a complete GritQL plugin file with the specified options.
29
+ */
30
+ export declare function generatePlugin(options?: GeneratePluginOptions): string;
31
+ //# sourceMappingURL=generator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,qBAAqB;IACpC;;;;;;OAMG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IAEvB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAE5B;;;OAGG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;CAC7B;AA4FD;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,GAAE,qBAA0B,GAAG,MAAM,CAuC1E"}
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Generator for customized Drizzle GritQL plugin files.
3
+ *
4
+ * This module generates GritQL plugin content with optional object name filtering,
5
+ * similar to the ESLint plugin's `drizzleObjectName` option.
6
+ */
7
+ /**
8
+ * Generates the GritQL pattern for matching object names.
9
+ * If no object names are provided, matches any object.
10
+ */
11
+ function generateObjectPattern(objectNames) {
12
+ if (!objectNames || objectNames.length === 0) {
13
+ return "$obj";
14
+ }
15
+ if (objectNames.length === 1) {
16
+ return `\`${objectNames[0]}\``;
17
+ }
18
+ // Generate an "or" pattern for multiple object names
19
+ const patterns = objectNames.map((name) => `\`${name}\``).join(", ");
20
+ return `or { ${patterns} }`;
21
+ }
22
+ /**
23
+ * Generates the enforce-delete-with-where rule pattern body.
24
+ */
25
+ function generateDeleteRuleBody(objectNames) {
26
+ const objPattern = generateObjectPattern(objectNames);
27
+ const objDescription = objectNames?.length
28
+ ? `Drizzle object (${objectNames.join(", ")})`
29
+ : "any object";
30
+ // If we have specific object names, we need a different pattern structure
31
+ if (objectNames && objectNames.length > 0) {
32
+ return ` // Rule: drizzle/enforce-delete-with-where
33
+ // Configured to match: ${objDescription}
34
+ \`$obj.delete($args)\` as $call where {
35
+ $obj <: ${objPattern},
36
+ not $call <: within \`$_.where($_)\`,
37
+ register_diagnostic(
38
+ span = $call,
39
+ message = "[drizzle/enforce-delete-with-where] Missing .where() clause. Calling .delete() without .where() will delete all rows in the table. Use db.delete(table).where(...) instead.",
40
+ severity = "error"
41
+ )
42
+ }`;
43
+ }
44
+ return ` // Rule: drizzle/enforce-delete-with-where
45
+ // Configured to match: ${objDescription}
46
+ \`$obj.delete($args)\` as $call where {
47
+ not $call <: within \`$_.where($_)\`,
48
+ register_diagnostic(
49
+ span = $call,
50
+ message = "[drizzle/enforce-delete-with-where] Missing .where() clause. Calling .delete() without .where() will delete all rows in the table. Use db.delete(table).where(...) instead.",
51
+ severity = "error"
52
+ )
53
+ }`;
54
+ }
55
+ /**
56
+ * Generates the enforce-update-with-where rule pattern body.
57
+ */
58
+ function generateUpdateRuleBody(objectNames) {
59
+ const objPattern = generateObjectPattern(objectNames);
60
+ const objDescription = objectNames?.length
61
+ ? `Drizzle object (${objectNames.join(", ")})`
62
+ : "any object";
63
+ // If we have specific object names, we need a different pattern structure
64
+ if (objectNames && objectNames.length > 0) {
65
+ return ` // Rule: drizzle/enforce-update-with-where
66
+ // Configured to match: ${objDescription}
67
+ \`$obj.update($table).set($values)\` as $call where {
68
+ $obj <: ${objPattern},
69
+ not $call <: within \`$_.where($_)\`,
70
+ register_diagnostic(
71
+ span = $call,
72
+ message = "[drizzle/enforce-update-with-where] Missing .where() clause. Calling .update().set() without .where() will update all rows in the table. Use db.update(table).set(...).where(...) instead.",
73
+ severity = "error"
74
+ )
75
+ }`;
76
+ }
77
+ return ` // Rule: drizzle/enforce-update-with-where
78
+ // Configured to match: ${objDescription}
79
+ \`$obj.update($table).set($values)\` as $call where {
80
+ not $call <: within \`$_.where($_)\`,
81
+ register_diagnostic(
82
+ span = $call,
83
+ message = "[drizzle/enforce-update-with-where] Missing .where() clause. Calling .update().set() without .where() will update all rows in the table. Use db.update(table).set(...).where(...) instead.",
84
+ severity = "error"
85
+ )
86
+ }`;
87
+ }
88
+ /**
89
+ * Generates a complete GritQL plugin file with the specified options.
90
+ */
91
+ export function generatePlugin(options = {}) {
92
+ const { objectNames, includeDeleteRule = true, includeUpdateRule = true, } = options;
93
+ const header = [
94
+ "engine biome(1.0)",
95
+ "language js",
96
+ "",
97
+ "// Generated by biome-plugin-drizzle",
98
+ `// Object filter: ${objectNames?.length ? objectNames.join(", ") : "none (matches all)"}`,
99
+ "",
100
+ ];
101
+ const ruleBodies = [];
102
+ if (includeDeleteRule) {
103
+ ruleBodies.push(generateDeleteRuleBody(objectNames));
104
+ }
105
+ if (includeUpdateRule) {
106
+ ruleBodies.push(generateUpdateRuleBody(objectNames));
107
+ }
108
+ // If we have multiple rules, wrap them in or {}
109
+ if (ruleBodies.length === 0) {
110
+ return `${header.join("\n")}// No rules enabled\n`;
111
+ }
112
+ if (ruleBodies.length === 1) {
113
+ // Single rule - no need for or {} wrapper
114
+ const singleRule = ruleBodies[0].replace(/^ {4}/gm, "");
115
+ return `${header.join("\n") + singleRule}\n`;
116
+ }
117
+ // Multiple rules - wrap in or {}
118
+ return `${header.join("\n")}or {\n${ruleBodies.join(",\n")}\n}\n`;
119
+ }
120
+ //# sourceMappingURL=generator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"generator.js","sourceRoot":"","sources":["../src/generator.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * biome-plugin-drizzle
3
+ *
4
+ * Biome linter plugin for Drizzle ORM safety rules.
5
+ * Enforces .where() clauses on delete and update operations.
6
+ */
7
+ export { type GeneratePluginOptions, generatePlugin } from "./generator.js";
8
+ export { getDefaultPluginContent, getPluginPath } from "./utils.js";
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,KAAK,qBAAqB,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * biome-plugin-drizzle
3
+ *
4
+ * Biome linter plugin for Drizzle ORM safety rules.
5
+ * Enforces .where() clauses on delete and update operations.
6
+ */
7
+ export { generatePlugin } from "./generator.js";
8
+ export { getDefaultPluginContent, getPluginPath } from "./utils.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAA8B,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAC5E,OAAO,EAAE,uBAAuB,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Utility functions for biome-plugin-drizzle.
3
+ */
4
+ /**
5
+ * Returns the absolute path to the default plugin .grit file.
6
+ * Useful for wiring up biome.json configuration.
7
+ */
8
+ export declare function getPluginPath(): string;
9
+ /**
10
+ * Returns the content of the default plugin .grit file.
11
+ */
12
+ export declare function getDefaultPluginContent(): string;
13
+ /**
14
+ * Resolves the plugin path relative to node_modules.
15
+ * Returns a path suitable for use in biome.json plugins array.
16
+ */
17
+ export declare function getRelativePluginPath(): string;
18
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAQH;;;GAGG;AACH,wBAAgB,aAAa,IAAI,MAAM,CAEtC;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,MAAM,CAEhD;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,CAE9C"}
package/dist/utils.js ADDED
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Utility functions for biome-plugin-drizzle.
3
+ */
4
+ import { readFileSync } from "node:fs";
5
+ import { dirname, join } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ const __dirname = dirname(fileURLToPath(import.meta.url));
8
+ /**
9
+ * Returns the absolute path to the default plugin .grit file.
10
+ * Useful for wiring up biome.json configuration.
11
+ */
12
+ export function getPluginPath() {
13
+ return join(__dirname, "drizzle.grit");
14
+ }
15
+ /**
16
+ * Returns the content of the default plugin .grit file.
17
+ */
18
+ export function getDefaultPluginContent() {
19
+ return readFileSync(getPluginPath(), "utf-8");
20
+ }
21
+ /**
22
+ * Resolves the plugin path relative to node_modules.
23
+ * Returns a path suitable for use in biome.json plugins array.
24
+ */
25
+ export function getRelativePluginPath() {
26
+ return "./node_modules/biome-plugin-drizzle/dist/drizzle.grit";
27
+ }
28
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC1C,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAEzC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D;;;GAGG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,SAAS,EAAE,cAAc,CAAC,CAAC;AACzC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,uBAAuB;IACrC,OAAO,YAAY,CAAC,aAAa,EAAE,EAAE,OAAO,CAAC,CAAC;AAChD,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB;IACnC,OAAO,uDAAuD,CAAC;AACjE,CAAC"}
@@ -0,0 +1,113 @@
1
+ # drizzle/enforce-delete-with-where
2
+
3
+ Ensures all Drizzle ORM `.delete()` operations include a `.where()` clause to prevent accidental deletion of entire tables.
4
+
5
+ ## Rule Details
6
+
7
+ This rule prevents dangerous delete operations that would affect all rows in a table. In Drizzle ORM, calling `.delete()` without `.where()` will delete every row in the specified table - often not the intended behavior.
8
+
9
+ ### Why This Rule Exists
10
+
11
+ ```typescript
12
+ // This deletes ALL users - probably not what you wanted!
13
+ db.delete(users);
14
+
15
+ // This is what you likely meant
16
+ db.delete(users).where(eq(users.id, userId));
17
+ ```
18
+
19
+ Forgetting a `.where()` clause can result in catastrophic data loss. This rule catches such mistakes at lint time.
20
+
21
+ ## Examples
22
+
23
+ ### Failing Code
24
+
25
+ ```typescript
26
+ import { users, posts } from "./schema";
27
+
28
+ // Simple delete without where
29
+ db.delete(users);
30
+
31
+ // Delete with returning but no where
32
+ db.delete(users).returning();
33
+
34
+ // Delete in expression
35
+ const result = db.delete(posts);
36
+
37
+ // Async delete without where
38
+ async function deleteAllUsers() {
39
+ await db.delete(users);
40
+ }
41
+
42
+ // Chained methods but no where
43
+ db.delete(users).returning().execute();
44
+ ```
45
+
46
+ ### Passing Code
47
+
48
+ ```typescript
49
+ import { users, posts } from "./schema";
50
+ import { eq, and, gt } from "drizzle-orm";
51
+
52
+ // Simple delete with where
53
+ db.delete(users).where(eq(users.id, 1));
54
+
55
+ // Delete with where and returning
56
+ db.delete(users).where(eq(users.id, 1)).returning();
57
+
58
+ // Where can come after returning
59
+ db.delete(users).returning().where(eq(users.id, 1));
60
+
61
+ // Complex where conditions
62
+ db.delete(users).where(
63
+ and(
64
+ eq(users.status, "inactive"),
65
+ gt(users.lastLogin, thirtyDaysAgo)
66
+ )
67
+ );
68
+
69
+ // With CTEs
70
+ db.with(archivedUsers).delete(users).where(eq(users.archived, true));
71
+
72
+ // Async with where
73
+ async function deleteUser(id: number) {
74
+ await db.delete(users).where(eq(users.id, id));
75
+ }
76
+ ```
77
+
78
+ ## Configuration
79
+
80
+ ### Default (Broad Matching)
81
+
82
+ By default, the rule matches any `.delete()` call on any object:
83
+
84
+ ```json
85
+ {
86
+ "plugins": ["./node_modules/biome-plugin-drizzle/dist/drizzle.grit"]
87
+ }
88
+ ```
89
+
90
+ ### Object Name Filtering
91
+
92
+ To reduce false positives (e.g., if you have other classes with `.delete()` methods), generate a plugin with object name filtering:
93
+
94
+ ```bash
95
+ npx biome-plugin-drizzle generate --out ./.biome/drizzle.grit --object-names db,tx
96
+ ```
97
+
98
+ This will only trigger on `db.delete()` and `tx.delete()`, not on `myArray.delete()` or `myCustomClass.delete()`.
99
+
100
+ ## When Not To Use This Rule
101
+
102
+ - If you intentionally want to delete all rows and understand the implications
103
+ - If you have a separate validation layer that ensures `.where()` is always called
104
+ - If you're using a different ORM or query builder that happens to have similar method names (use object name filtering instead)
105
+
106
+ ## Related Rules
107
+
108
+ - [enforce-update-with-where](./enforce-update-with-where.md) - Similar rule for update operations
109
+
110
+ ## Further Reading
111
+
112
+ - [Drizzle ORM Delete Documentation](https://orm.drizzle.team/docs/delete)
113
+ - [ESLint Plugin Drizzle](https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle)
@@ -0,0 +1,129 @@
1
+ # drizzle/enforce-update-with-where
2
+
3
+ Ensures all Drizzle ORM `.update().set()` operations include a `.where()` clause to prevent accidental updates to entire tables.
4
+
5
+ ## Rule Details
6
+
7
+ This rule prevents dangerous update operations that would affect all rows in a table. In Drizzle ORM, calling `.update().set()` without `.where()` will update every row in the specified table - often not the intended behavior.
8
+
9
+ ### Why This Rule Exists
10
+
11
+ ```typescript
12
+ // This updates ALL users - probably not what you wanted!
13
+ db.update(users).set({ role: "admin" });
14
+
15
+ // This is what you likely meant
16
+ db.update(users).set({ role: "admin" }).where(eq(users.id, userId));
17
+ ```
18
+
19
+ Forgetting a `.where()` clause can result in mass data corruption. This rule catches such mistakes at lint time.
20
+
21
+ ## Examples
22
+
23
+ ### Failing Code
24
+
25
+ ```typescript
26
+ import { users, posts } from "./schema";
27
+
28
+ // Simple update without where
29
+ db.update(users).set({ name: "John" });
30
+
31
+ // Update with returning but no where
32
+ db.update(users).set({ status: "active" }).returning();
33
+
34
+ // Update in expression
35
+ const result = db.update(posts).set({ views: 0 });
36
+
37
+ // Async update without where
38
+ async function resetAllUserNames() {
39
+ await db.update(users).set({ name: "Anonymous" });
40
+ }
41
+
42
+ // Chained methods but no where
43
+ db.update(users).set({ lastLogin: new Date() }).returning().execute();
44
+ ```
45
+
46
+ ### Passing Code
47
+
48
+ ```typescript
49
+ import { users, posts } from "./schema";
50
+ import { eq, and, gt, sql } from "drizzle-orm";
51
+
52
+ // Simple update with where
53
+ db.update(users).set({ name: "John" }).where(eq(users.id, 1));
54
+
55
+ // Update with where and returning
56
+ db.update(users)
57
+ .set({ status: "active" })
58
+ .where(eq(users.id, 1))
59
+ .returning();
60
+
61
+ // Where can come after returning
62
+ db.update(users)
63
+ .set({ email: "new@email.com" })
64
+ .returning()
65
+ .where(eq(users.id, 1));
66
+
67
+ // Complex where conditions
68
+ db.update(users)
69
+ .set({ verified: true })
70
+ .where(
71
+ and(
72
+ eq(users.status, "pending"),
73
+ gt(users.createdAt, thirtyDaysAgo)
74
+ )
75
+ );
76
+
77
+ // Using SQL expressions
78
+ db.update(posts)
79
+ .set({ views: sql`views + 1` })
80
+ .where(eq(posts.id, postId));
81
+
82
+ // With CTEs
83
+ db.with(activeUsers)
84
+ .update(users)
85
+ .set({ tier: "premium" })
86
+ .where(eq(users.active, true));
87
+
88
+ // Async with where
89
+ async function updateUser(id: number, data: UserUpdate) {
90
+ await db.update(users).set(data).where(eq(users.id, id));
91
+ }
92
+ ```
93
+
94
+ ## Configuration
95
+
96
+ ### Default (Broad Matching)
97
+
98
+ By default, the rule matches any `.update().set()` call on any object:
99
+
100
+ ```json
101
+ {
102
+ "plugins": ["./node_modules/biome-plugin-drizzle/dist/drizzle.grit"]
103
+ }
104
+ ```
105
+
106
+ ### Object Name Filtering
107
+
108
+ To reduce false positives (e.g., if you have other classes with `.update()` methods), generate a plugin with object name filtering:
109
+
110
+ ```bash
111
+ npx biome-plugin-drizzle generate --out ./.biome/drizzle.grit --object-names db,tx
112
+ ```
113
+
114
+ This will only trigger on `db.update().set()` and `tx.update().set()`, not on other objects.
115
+
116
+ ## When Not To Use This Rule
117
+
118
+ - If you intentionally want to update all rows and understand the implications
119
+ - If you have a separate validation layer that ensures `.where()` is always called
120
+ - If you're using a different ORM or query builder that happens to have similar method names (use object name filtering instead)
121
+
122
+ ## Related Rules
123
+
124
+ - [enforce-delete-with-where](./enforce-delete-with-where.md) - Similar rule for delete operations
125
+
126
+ ## Further Reading
127
+
128
+ - [Drizzle ORM Update Documentation](https://orm.drizzle.team/docs/update)
129
+ - [ESLint Plugin Drizzle](https://github.com/drizzle-team/drizzle-orm/tree/main/eslint-plugin-drizzle)
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "biome-plugin-drizzle",
3
+ "version": "0.0.2",
4
+ "description": "Biome linter plugin for Drizzle ORM safety rules - enforces .where() clauses on delete and update operations",
5
+ "keywords": [
6
+ "biome",
7
+ "linter",
8
+ "plugin",
9
+ "drizzle",
10
+ "drizzle-orm",
11
+ "database",
12
+ "sql",
13
+ "safety",
14
+ "gritql"
15
+ ],
16
+ "author": "",
17
+ "license": "MIT",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/TeamWarp/biome-plugin-drizzle"
21
+ },
22
+ "homepage": "https://github.com/TeamWarp/biome-plugin-drizzle#readme",
23
+ "bugs": {
24
+ "url": "https://github.com/TeamWarp/biome-plugin-drizzle/issues"
25
+ },
26
+ "type": "module",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js"
31
+ },
32
+ "./plugin": "./dist/drizzle.grit",
33
+ "./plugin.grit": "./dist/drizzle.grit"
34
+ },
35
+ "main": "./dist/index.js",
36
+ "types": "./dist/index.d.ts",
37
+ "bin": {
38
+ "biome-plugin-drizzle": "./dist/cli.js"
39
+ },
40
+ "files": [
41
+ "dist",
42
+ "docs",
43
+ "README.md",
44
+ "LICENSE"
45
+ ],
46
+ "scripts": {
47
+ "build": "tsc && npm run copy-grit",
48
+ "copy-grit": "cp grit/*.grit dist/",
49
+ "clean": "rm -rf dist",
50
+ "test": "vitest run",
51
+ "test:watch": "vitest",
52
+ "lint": "biome check .",
53
+ "format": "biome format --write .",
54
+ "prepublishOnly": "npm run clean && npm run build && npm test",
55
+ "pack": "npm pack"
56
+ },
57
+ "dependencies": {
58
+ "commander": "^12.1.0"
59
+ },
60
+ "devDependencies": {
61
+ "@biomejs/biome": "^2.3.11",
62
+ "@types/node": "^22.10.2",
63
+ "typescript": "^5.7.2",
64
+ "vitest": "^2.1.8"
65
+ },
66
+ "engines": {
67
+ "node": ">=18"
68
+ },
69
+ "peerDependencies": {
70
+ "@biomejs/biome": ">=2.0.0"
71
+ },
72
+ "peerDependenciesMeta": {
73
+ "@biomejs/biome": {
74
+ "optional": true
75
+ }
76
+ }
77
+ }