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 +26 -0
- package/README.md +234 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +128 -0
- package/dist/cli.js.map +1 -0
- package/dist/drizzle.grit +34 -0
- package/dist/generator.d.ts +31 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +120 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +28 -0
- package/dist/utils.js.map +1 -0
- package/docs/rules/enforce-delete-with-where.md +113 -0
- package/docs/rules/enforce-update-with-where.md +129 -0
- package/package.json +77 -0
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
|
package/dist/cli.js.map
ADDED
|
@@ -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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"}
|
package/dist/index.d.ts
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 { 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"}
|
package/dist/utils.d.ts
ADDED
|
@@ -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
|
+
}
|