eslint-plugin-slonik 1.0.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/LICENSE +48 -0
- package/README.md +368 -0
- package/dist/config.cjs +61 -0
- package/dist/config.cjs.map +1 -0
- package/dist/config.d.cts +192 -0
- package/dist/config.d.mts +192 -0
- package/dist/config.d.ts +192 -0
- package/dist/config.mjs +59 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.cjs +27 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +319 -0
- package/dist/index.d.mts +319 -0
- package/dist/index.d.ts +319 -0
- package/dist/index.mjs +20 -0
- package/dist/index.mjs.map +1 -0
- package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.cts +611 -0
- package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.mts +611 -0
- package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.ts +611 -0
- package/dist/shared/eslint-plugin-slonik.BxexVlk1.cjs +1539 -0
- package/dist/shared/eslint-plugin-slonik.BxexVlk1.cjs.map +1 -0
- package/dist/shared/eslint-plugin-slonik.C0xTyWZ2.mjs +2866 -0
- package/dist/shared/eslint-plugin-slonik.C0xTyWZ2.mjs.map +1 -0
- package/dist/shared/eslint-plugin-slonik.DbzoLz5_.mjs +1514 -0
- package/dist/shared/eslint-plugin-slonik.DbzoLz5_.mjs.map +1 -0
- package/dist/shared/eslint-plugin-slonik.rlOTrCdf.cjs +2929 -0
- package/dist/shared/eslint-plugin-slonik.rlOTrCdf.cjs.map +1 -0
- package/dist/workers/check-sql.worker.cjs +2436 -0
- package/dist/workers/check-sql.worker.cjs.map +1 -0
- package/dist/workers/check-sql.worker.d.cts +171 -0
- package/dist/workers/check-sql.worker.d.mts +171 -0
- package/dist/workers/check-sql.worker.d.ts +171 -0
- package/dist/workers/check-sql.worker.mjs +2412 -0
- package/dist/workers/check-sql.worker.mjs.map +1 -0
- package/package.json +103 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Copyright (c) 2026, Gajus Kuizinas (https://gajus.com/)
|
|
2
|
+
All rights reserved.
|
|
3
|
+
|
|
4
|
+
Redistribution and use in source and binary forms, with or without
|
|
5
|
+
modification, are permitted provided that the following conditions are met:
|
|
6
|
+
* Redistributions of source code must retain the above copyright
|
|
7
|
+
notice, this list of conditions and the following disclaimer.
|
|
8
|
+
* Redistributions in binary form must reproduce the above copyright
|
|
9
|
+
notice, this list of conditions and the following disclaimer in the
|
|
10
|
+
documentation and/or other materials provided with the distribution.
|
|
11
|
+
* Neither the name of the Gajus Kuizinas (https://gajus.com/) nor the
|
|
12
|
+
names of its contributors may be used to endorse or promote products
|
|
13
|
+
derived from this software without specific prior written permission.
|
|
14
|
+
|
|
15
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
16
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
17
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
18
|
+
DISCLAIMED. IN NO EVENT SHALL GAJUS KUIZINAS BE LIABLE FOR ANY
|
|
19
|
+
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
|
20
|
+
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
|
21
|
+
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
|
22
|
+
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
|
23
|
+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
|
24
|
+
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
MIT License
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2022 ts-safeql
|
|
31
|
+
|
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
33
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
34
|
+
in the Software without restriction, including without limitation the rights
|
|
35
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
36
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
37
|
+
furnished to do so, subject to the following conditions:
|
|
38
|
+
|
|
39
|
+
The above copyright notice and this permission notice shall be included in all
|
|
40
|
+
copies or substantial portions of the Software.
|
|
41
|
+
|
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
43
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
44
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
45
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
46
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
47
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
48
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
# eslint-plugin-slonik
|
|
2
|
+
|
|
3
|
+
Provides compile-time SQL query validation by checking your raw SQL strings against your actual database schema, catching errors before runtime.
|
|
4
|
+
|
|
5
|
+
This is a fork of [@ts-safeql/eslint-plugin](https://github.com/ts-safeql/safeql) with native support for Slonik's SQL tag builders (`sql.array`, `sql.fragment`, `sql.identifier`, `sql.unnest`, etc.).
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🔍 **SQL Validation** — Validates SQL queries against your PostgreSQL database schema at lint time
|
|
10
|
+
- 🏷️ **Slonik SQL Tags** — Native support for all Slonik SQL tag builders
|
|
11
|
+
- 🎯 **Type Inference** — Extracts type hints from `sql.array()`, `sql.unnest()`, and `sql.identifier()`
|
|
12
|
+
- 📝 **Fragment Support** — Properly handles `sql.fragment` for dynamic query composition
|
|
13
|
+
- ✨ **Graceful Degradation** — Skips validation for runtime-dependent constructs like `sql.join()`
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install eslint-plugin-slonik --save-dev
|
|
19
|
+
# or
|
|
20
|
+
pnpm add eslint-plugin-slonik --save-dev
|
|
21
|
+
# or
|
|
22
|
+
yarn add eslint-plugin-slonik --dev
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Peer Dependencies
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npm install @typescript-eslint/utils libpg-query --save-dev
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Configuration
|
|
32
|
+
|
|
33
|
+
### ESLint Flat Config (eslint.config.js)
|
|
34
|
+
|
|
35
|
+
```js
|
|
36
|
+
import slonik from "eslint-plugin-slonik/config";
|
|
37
|
+
|
|
38
|
+
export default [
|
|
39
|
+
// ... other configs
|
|
40
|
+
slonik.configs.connections({
|
|
41
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
42
|
+
overrides: {
|
|
43
|
+
types: {
|
|
44
|
+
// Map PostgreSQL types to Slonik token types
|
|
45
|
+
date: "DateSqlToken",
|
|
46
|
+
timestamp: "TimestampSqlToken",
|
|
47
|
+
interval: "IntervalSqlToken",
|
|
48
|
+
json: "JsonSqlToken",
|
|
49
|
+
jsonb: "JsonBinarySqlToken",
|
|
50
|
+
uuid: "UuidSqlToken",
|
|
51
|
+
"int4[]": 'ArraySqlToken<"int4">',
|
|
52
|
+
"text[]": 'ArraySqlToken<"text">',
|
|
53
|
+
"uuid[]": 'ArraySqlToken<"uuid">',
|
|
54
|
+
"numeric[]": 'ArraySqlToken<"numeric">',
|
|
55
|
+
"real[]": "VectorSqlToken",
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
targets: [
|
|
59
|
+
{
|
|
60
|
+
// Match Slonik's typed query methods
|
|
61
|
+
tag: "sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)",
|
|
62
|
+
skipTypeAnnotations: true,
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
}),
|
|
66
|
+
];
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Using a Config File (slonik.config.ts)
|
|
70
|
+
|
|
71
|
+
Alternatively, use a separate config file:
|
|
72
|
+
|
|
73
|
+
**eslint.config.js:**
|
|
74
|
+
```js
|
|
75
|
+
import slonik from "eslint-plugin-slonik/config";
|
|
76
|
+
|
|
77
|
+
export default [
|
|
78
|
+
slonik.configs.useConfigFile,
|
|
79
|
+
];
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
**slonik.config.ts:**
|
|
83
|
+
```ts
|
|
84
|
+
import { defineConfig } from "eslint-plugin-slonik";
|
|
85
|
+
|
|
86
|
+
export default defineConfig({
|
|
87
|
+
connections: {
|
|
88
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
89
|
+
overrides: {
|
|
90
|
+
types: {
|
|
91
|
+
date: "DateSqlToken",
|
|
92
|
+
timestamp: "TimestampSqlToken",
|
|
93
|
+
interval: "IntervalSqlToken",
|
|
94
|
+
json: "JsonSqlToken",
|
|
95
|
+
jsonb: "JsonBinarySqlToken",
|
|
96
|
+
uuid: "UuidSqlToken",
|
|
97
|
+
"int4[]": 'ArraySqlToken<"int4">',
|
|
98
|
+
"text[]": 'ArraySqlToken<"text">',
|
|
99
|
+
"uuid[]": 'ArraySqlToken<"uuid">',
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
targets: [
|
|
103
|
+
{
|
|
104
|
+
tag: "sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)",
|
|
105
|
+
skipTypeAnnotations: true,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Slonik SQL Tag Support
|
|
113
|
+
|
|
114
|
+
| SQL Tag | Support | Behavior |
|
|
115
|
+
|---------|---------|----------|
|
|
116
|
+
| `sql.array([1,2], 'int4')` | ✅ Full | Extracts type → `$1::int4[]` |
|
|
117
|
+
| `sql.array([1,2], sql.fragment\`int[]\`)` | ✅ Graceful | Falls back to `$1` |
|
|
118
|
+
| `sql.unnest([[...]], ['int4','text'])` | ✅ Full | Extracts types → `unnest($1::int4[], $2::text[])` |
|
|
119
|
+
| `sql.identifier(['schema','table'])` | ✅ Full | Embeds → `"schema"."table"` |
|
|
120
|
+
| `sql.fragment\`...\`` | ✅ Full | Embeds SQL content directly |
|
|
121
|
+
| `sql.join([...], glue)` | ✅ Skip | Skipped (runtime content) |
|
|
122
|
+
| `sql.binary(buffer)` | ✅ Skip | Skipped |
|
|
123
|
+
| `sql.date(date)` | ✅ Skip | Skipped |
|
|
124
|
+
| `sql.timestamp(date)` | ✅ Skip | Skipped |
|
|
125
|
+
| `sql.interval({...})` | ✅ Skip | Skipped |
|
|
126
|
+
| `sql.json(value)` | ✅ Skip | Skipped |
|
|
127
|
+
| `sql.jsonb(value)` | ✅ Skip | Skipped |
|
|
128
|
+
| `sql.uuid(str)` | ✅ Skip | Skipped |
|
|
129
|
+
| `sql.literalValue(str)` | ✅ Skip | Skipped |
|
|
130
|
+
|
|
131
|
+
### How It Works
|
|
132
|
+
|
|
133
|
+
**Full Support** means the plugin extracts type information and generates accurate PostgreSQL placeholders for validation:
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// sql.array with type hint
|
|
137
|
+
sql.type(z.object({ ids: z.array(z.number()) }))`
|
|
138
|
+
SELECT * FROM users WHERE id = ANY(${sql.array(userIds, 'int4')})
|
|
139
|
+
`;
|
|
140
|
+
// → Validates: SELECT * FROM users WHERE id = ANY($1::int4[])
|
|
141
|
+
|
|
142
|
+
// sql.identifier for dynamic table/column names
|
|
143
|
+
sql.type(z.object({ id: z.number() }))`
|
|
144
|
+
SELECT id FROM ${sql.identifier(['public', 'users'])}
|
|
145
|
+
`;
|
|
146
|
+
// → Validates: SELECT id FROM "public"."users"
|
|
147
|
+
|
|
148
|
+
// sql.fragment for query composition
|
|
149
|
+
const whereClause = sql.fragment`WHERE active = true`;
|
|
150
|
+
sql.type(z.object({ id: z.number() }))`
|
|
151
|
+
SELECT id FROM users ${whereClause}
|
|
152
|
+
`;
|
|
153
|
+
// → Validates: SELECT id FROM users WHERE active = true
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
**Graceful Skip** means the plugin recognizes Slonik tokens and skips validation for those expressions, preventing false positives:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// sql.join - content determined at runtime
|
|
160
|
+
sql.unsafe`
|
|
161
|
+
SELECT * FROM users WHERE ${sql.join([
|
|
162
|
+
sql.fragment`name = ${name}`,
|
|
163
|
+
sql.fragment`age > ${age}`,
|
|
164
|
+
], sql.fragment` AND `)}
|
|
165
|
+
`;
|
|
166
|
+
// → Plugin skips validation for the join expression
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
## Type Override Reference
|
|
170
|
+
|
|
171
|
+
When using Slonik, you'll want to map PostgreSQL types to Slonik's token types:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
overrides: {
|
|
175
|
+
types: {
|
|
176
|
+
// Date/Time types
|
|
177
|
+
date: "DateSqlToken",
|
|
178
|
+
timestamp: "TimestampSqlToken",
|
|
179
|
+
timestamptz: "TimestampSqlToken",
|
|
180
|
+
interval: "IntervalSqlToken",
|
|
181
|
+
|
|
182
|
+
// JSON types
|
|
183
|
+
json: "JsonSqlToken",
|
|
184
|
+
jsonb: "JsonBinarySqlToken",
|
|
185
|
+
|
|
186
|
+
// UUID
|
|
187
|
+
uuid: "UuidSqlToken",
|
|
188
|
+
|
|
189
|
+
// Array types (use ArraySqlToken<"element_type">)
|
|
190
|
+
"int4[]": 'ArraySqlToken<"int4">',
|
|
191
|
+
"int8[]": 'ArraySqlToken<"int8">',
|
|
192
|
+
"text[]": 'ArraySqlToken<"text">',
|
|
193
|
+
"uuid[]": 'ArraySqlToken<"uuid">',
|
|
194
|
+
"numeric[]": 'ArraySqlToken<"numeric">',
|
|
195
|
+
"bool[]": 'ArraySqlToken<"bool">',
|
|
196
|
+
|
|
197
|
+
// Vector types (for pgvector)
|
|
198
|
+
"real[]": "VectorSqlToken",
|
|
199
|
+
vector: "VectorSqlToken",
|
|
200
|
+
},
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
## Target Pattern Reference
|
|
205
|
+
|
|
206
|
+
The `tag` option uses regex to match Slonik's query methods:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
targets: [
|
|
210
|
+
{
|
|
211
|
+
// Matches: sql.type(...)``, sql.typeAlias(...)``, sql.unsafe``
|
|
212
|
+
tag: "sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)",
|
|
213
|
+
skipTypeAnnotations: true,
|
|
214
|
+
},
|
|
215
|
+
]
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Example Project Setup
|
|
219
|
+
|
|
220
|
+
### 1. Install dependencies
|
|
221
|
+
|
|
222
|
+
```bash
|
|
223
|
+
pnpm add slonik zod
|
|
224
|
+
pnpm add -D eslint-plugin-slonik @typescript-eslint/utils libpg-query
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### 2. Create your SQL tag with type aliases
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
// src/db/sql.ts
|
|
231
|
+
import { createSqlTag } from "slonik";
|
|
232
|
+
import { z } from "zod";
|
|
233
|
+
|
|
234
|
+
export const sql = createSqlTag({
|
|
235
|
+
typeAliases: {
|
|
236
|
+
id: z.object({ id: z.number() }),
|
|
237
|
+
void: z.object({}).strict(),
|
|
238
|
+
},
|
|
239
|
+
});
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### 3. Configure ESLint
|
|
243
|
+
|
|
244
|
+
```js
|
|
245
|
+
// eslint.config.js
|
|
246
|
+
import slonik from "eslint-plugin-slonik/config";
|
|
247
|
+
import tseslint from "typescript-eslint";
|
|
248
|
+
|
|
249
|
+
export default tseslint.config(
|
|
250
|
+
...tseslint.configs.recommended,
|
|
251
|
+
slonik.configs.connections({
|
|
252
|
+
databaseUrl: process.env.DATABASE_URL,
|
|
253
|
+
overrides: {
|
|
254
|
+
types: {
|
|
255
|
+
date: "DateSqlToken",
|
|
256
|
+
timestamp: "TimestampSqlToken",
|
|
257
|
+
json: "JsonSqlToken",
|
|
258
|
+
jsonb: "JsonBinarySqlToken",
|
|
259
|
+
uuid: "UuidSqlToken",
|
|
260
|
+
"int4[]": 'ArraySqlToken<"int4">',
|
|
261
|
+
"text[]": 'ArraySqlToken<"text">',
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
targets: [
|
|
265
|
+
{
|
|
266
|
+
tag: "sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)",
|
|
267
|
+
skipTypeAnnotations: true,
|
|
268
|
+
},
|
|
269
|
+
],
|
|
270
|
+
})
|
|
271
|
+
);
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### 4. Write validated queries
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { sql } from "./db/sql";
|
|
278
|
+
import { pool } from "./db/pool";
|
|
279
|
+
|
|
280
|
+
// ✅ Valid - query matches schema
|
|
281
|
+
const users = await pool.many(
|
|
282
|
+
sql.type(z.object({ id: z.number(), name: z.string() }))`
|
|
283
|
+
SELECT id, name FROM users WHERE active = true
|
|
284
|
+
`
|
|
285
|
+
);
|
|
286
|
+
|
|
287
|
+
// ✅ Valid - using sql.array with type hint
|
|
288
|
+
const usersByIds = await pool.many(
|
|
289
|
+
sql.type(z.object({ id: z.number(), name: z.string() }))`
|
|
290
|
+
SELECT id, name FROM users WHERE id = ANY(${sql.array(ids, 'int4')})
|
|
291
|
+
`
|
|
292
|
+
);
|
|
293
|
+
|
|
294
|
+
// ✅ Valid - using sql.fragment for composition
|
|
295
|
+
const orderBy = sql.fragment`ORDER BY created_at DESC`;
|
|
296
|
+
const recentUsers = await pool.many(
|
|
297
|
+
sql.type(z.object({ id: z.number(), name: z.string() }))`
|
|
298
|
+
SELECT id, name FROM users ${orderBy}
|
|
299
|
+
`
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
// ❌ Error - column 'naem' does not exist
|
|
303
|
+
const typo = await pool.many(
|
|
304
|
+
sql.type(z.object({ id: z.number(), name: z.string() }))`
|
|
305
|
+
SELECT id, naem FROM users
|
|
306
|
+
`
|
|
307
|
+
);
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
## Differences from @ts-safeql/eslint-plugin
|
|
311
|
+
|
|
312
|
+
This plugin is specifically designed for Slonik and includes:
|
|
313
|
+
|
|
314
|
+
1. **Native Slonik token recognition** — Recognizes all Slonik SQL token types (`ArraySqlToken`, `FragmentSqlToken`, etc.)
|
|
315
|
+
2. **Type hint extraction** — Extracts PostgreSQL types from `sql.array()` and `sql.unnest()` calls
|
|
316
|
+
3. **Fragment embedding** — Properly embeds `sql.fragment` content into the query for validation
|
|
317
|
+
4. **Identifier support** — Converts `sql.identifier()` to quoted identifiers
|
|
318
|
+
5. **Graceful degradation** — Skips validation for runtime-dependent constructs instead of erroring
|
|
319
|
+
|
|
320
|
+
## Development
|
|
321
|
+
|
|
322
|
+
### Prerequisites
|
|
323
|
+
|
|
324
|
+
- Node.js 24+
|
|
325
|
+
- pnpm 10+
|
|
326
|
+
- PostgreSQL 18
|
|
327
|
+
|
|
328
|
+
### Setup
|
|
329
|
+
|
|
330
|
+
```bash
|
|
331
|
+
# Install dependencies
|
|
332
|
+
pnpm install
|
|
333
|
+
|
|
334
|
+
# Start PostgreSQL (e.g., using Docker)
|
|
335
|
+
docker run -d --name postgres -p 5432:5432 \
|
|
336
|
+
-e POSTGRES_USER=postgres \
|
|
337
|
+
-e POSTGRES_PASSWORD=postgres \
|
|
338
|
+
-e POSTGRES_DB=postgres \
|
|
339
|
+
postgres:18
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Running Tests
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
# Set PostgreSQL environment variables
|
|
346
|
+
export PGUSER=postgres
|
|
347
|
+
export PGPASSWORD=postgres
|
|
348
|
+
export PGHOST=localhost
|
|
349
|
+
export PGDATABASE=postgres
|
|
350
|
+
|
|
351
|
+
# Run tests
|
|
352
|
+
pnpm run test:vitest
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### Linting
|
|
356
|
+
|
|
357
|
+
```bash
|
|
358
|
+
pnpm run lint:eslint # ESLint
|
|
359
|
+
pnpm run lint:tsc # TypeScript type checking
|
|
360
|
+
pnpm run lint:cspell # Spell checking
|
|
361
|
+
pnpm run lint:knip # Unused code detection
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
### Building
|
|
365
|
+
|
|
366
|
+
```bash
|
|
367
|
+
pnpm run build
|
|
368
|
+
```
|
package/dist/config.cjs
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const index = require('./shared/eslint-plugin-slonik.BxexVlk1.cjs');
|
|
4
|
+
require('./shared/eslint-plugin-slonik.rlOTrCdf.cjs');
|
|
5
|
+
require('path');
|
|
6
|
+
require('postgres');
|
|
7
|
+
require('crypto');
|
|
8
|
+
require('fs');
|
|
9
|
+
require('ts-pattern');
|
|
10
|
+
require('@typescript-eslint/utils');
|
|
11
|
+
require('fp-ts/lib/Either.js');
|
|
12
|
+
require('fp-ts/lib/function.js');
|
|
13
|
+
require('fp-ts/lib/Option.js');
|
|
14
|
+
require('fp-ts/lib/TaskEither.js');
|
|
15
|
+
require('fp-ts/lib/Json.js');
|
|
16
|
+
require('pg-connection-string');
|
|
17
|
+
require('typescript');
|
|
18
|
+
require('synckit');
|
|
19
|
+
require('node:url');
|
|
20
|
+
require('zod');
|
|
21
|
+
require('module');
|
|
22
|
+
|
|
23
|
+
const version = "1.0.0";
|
|
24
|
+
|
|
25
|
+
const slonikPlugin = {
|
|
26
|
+
rules: index.rules,
|
|
27
|
+
meta: {
|
|
28
|
+
name: "eslint-plugin-slonik",
|
|
29
|
+
version
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const config = {
|
|
34
|
+
configs: {
|
|
35
|
+
/**
|
|
36
|
+
* If you prefer configuring via a config file (slonik.config.ts), use this config.
|
|
37
|
+
*/
|
|
38
|
+
useConfigFile: {
|
|
39
|
+
plugins: {
|
|
40
|
+
slonik: slonikPlugin
|
|
41
|
+
},
|
|
42
|
+
rules: {
|
|
43
|
+
"slonik/check-sql": ["error", { useConfigFile: true }]
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
/**
|
|
47
|
+
* If you prefer configuring via a flat config, use this config.
|
|
48
|
+
*/
|
|
49
|
+
connections: (connections) => ({
|
|
50
|
+
plugins: {
|
|
51
|
+
slonik: slonikPlugin
|
|
52
|
+
},
|
|
53
|
+
rules: {
|
|
54
|
+
"slonik/check-sql": ["error", { connections }]
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
module.exports = config;
|
|
61
|
+
//# sourceMappingURL=config.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.cjs","sources":["../src/plugin.ts","../src/config.ts"],"sourcesContent":["import { FlatConfig } from \"@typescript-eslint/utils/ts-eslint\";\nimport rules from \"./rules\";\nimport { version } from \"../package.json\";\n\nexport default {\n rules: rules,\n meta: {\n name: \"eslint-plugin-slonik\",\n version: version,\n },\n} satisfies FlatConfig.Plugin;\n","import { FlatConfig } from \"@typescript-eslint/utils/ts-eslint\";\nimport { Config } from \"./rules/RuleOptions\";\nimport slonikPlugin from \"./plugin\";\n\nexport default {\n configs: {\n /**\n * If you prefer configuring via a config file (slonik.config.ts), use this config.\n */\n useConfigFile: {\n plugins: {\n slonik: slonikPlugin,\n },\n rules: {\n \"slonik/check-sql\": [\"error\", { useConfigFile: true }],\n },\n } satisfies FlatConfig.Config,\n\n /**\n * If you prefer configuring via a flat config, use this config.\n */\n connections: (connections: Config[\"connections\"]): FlatConfig.Config => ({\n plugins: {\n slonik: slonikPlugin,\n },\n rules: {\n \"slonik/check-sql\": [\"error\", { connections }],\n },\n }),\n },\n};\n"],"names":["rules"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;AAIA,qBAAe;AAAA,SACbA,WAAA;AAAA,EACA,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,sBAAA;AAAA,IACN;AAAA;AAEJ,CAAA;;ACNA,eAAe;AAAA,EACb,OAAA,EAAS;AAAA;AAAA;AAAA;AAAA,IAIP,aAAA,EAAe;AAAA,MACb,OAAA,EAAS;AAAA,QACP,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,KAAA,EAAO;AAAA,QACL,oBAAoB,CAAC,OAAA,EAAS,EAAE,aAAA,EAAe,MAAM;AAAA;AACvD,KACF;AAAA;AAAA;AAAA;AAAA,IAKA,WAAA,EAAa,CAAC,WAAA,MAA2D;AAAA,MACvE,OAAA,EAAS;AAAA,QACP,MAAA,EAAQ;AAAA,OACV;AAAA,MACA,KAAA,EAAO;AAAA,QACL,kBAAA,EAAoB,CAAC,OAAA,EAAS,EAAE,aAAa;AAAA;AAC/C,KACF;AAAA;AAEJ,CAAA;;;;"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import * as _typescript_eslint_utils_ts_eslint from '@typescript-eslint/utils/ts-eslint';
|
|
2
|
+
import { FlatConfig } from '@typescript-eslint/utils/ts-eslint';
|
|
3
|
+
import { C as Config } from './shared/eslint-plugin-slonik.1m1xlVmw.cjs';
|
|
4
|
+
import 'zod';
|
|
5
|
+
|
|
6
|
+
declare const _default: {
|
|
7
|
+
configs: {
|
|
8
|
+
/**
|
|
9
|
+
* If you prefer configuring via a config file (slonik.config.ts), use this config.
|
|
10
|
+
*/
|
|
11
|
+
useConfigFile: {
|
|
12
|
+
plugins: {
|
|
13
|
+
slonik: {
|
|
14
|
+
rules: {
|
|
15
|
+
"check-sql": _typescript_eslint_utils_ts_eslint.RuleModule<"error" | "typeInferenceFailed" | "invalidQuery" | "missingTypeAnnotations" | "incorrectTypeAnnotations" | "invalidTypeAnnotations", ({
|
|
16
|
+
connections: {
|
|
17
|
+
targets: ({
|
|
18
|
+
wrapper: string | {
|
|
19
|
+
regex: string;
|
|
20
|
+
};
|
|
21
|
+
maxDepth?: number | undefined;
|
|
22
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
23
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
24
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
25
|
+
} | {
|
|
26
|
+
tag: string | {
|
|
27
|
+
regex: string;
|
|
28
|
+
};
|
|
29
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
30
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
31
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
32
|
+
})[];
|
|
33
|
+
migrationsDir: string;
|
|
34
|
+
keepAlive?: boolean | undefined;
|
|
35
|
+
overrides?: {
|
|
36
|
+
types?: Record<"bigint" | "boolean" | "int" | "null" | "void" | "date" | "int2" | "int4" | "int8" | "smallint" | "real" | "float4" | "float" | "float8" | "numeric" | "decimal" | "smallserial" | "serial" | "bigserial" | "uuid" | "text" | "varchar" | "char" | "bpchar" | "citext" | "bit" | "bool" | "timestamp" | "timestamptz" | "time" | "timetz" | "interval" | "inet" | "cidr" | "macaddr" | "macaddr8" | "money" | "json" | "jsonb" | "bytea", string | {
|
|
37
|
+
parameter: string | {
|
|
38
|
+
regex: string;
|
|
39
|
+
};
|
|
40
|
+
return: string;
|
|
41
|
+
}> | Record<string, string | {
|
|
42
|
+
parameter: string | {
|
|
43
|
+
regex: string;
|
|
44
|
+
};
|
|
45
|
+
return: string;
|
|
46
|
+
}> | undefined;
|
|
47
|
+
columns?: Record<string, string> | undefined;
|
|
48
|
+
} | undefined;
|
|
49
|
+
nullAsUndefined?: boolean | undefined;
|
|
50
|
+
nullAsOptional?: boolean | undefined;
|
|
51
|
+
inferLiterals?: boolean | ("string" | "number" | "boolean")[] | undefined;
|
|
52
|
+
connectionUrl?: string | undefined;
|
|
53
|
+
databaseName?: string | undefined;
|
|
54
|
+
watchMode?: boolean | undefined;
|
|
55
|
+
} | {
|
|
56
|
+
targets: ({
|
|
57
|
+
wrapper: string | {
|
|
58
|
+
regex: string;
|
|
59
|
+
};
|
|
60
|
+
maxDepth?: number | undefined;
|
|
61
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
62
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
63
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
64
|
+
} | {
|
|
65
|
+
tag: string | {
|
|
66
|
+
regex: string;
|
|
67
|
+
};
|
|
68
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
69
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
70
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
71
|
+
})[];
|
|
72
|
+
databaseUrl: string;
|
|
73
|
+
keepAlive?: boolean | undefined;
|
|
74
|
+
overrides?: {
|
|
75
|
+
types?: Record<"bigint" | "boolean" | "int" | "null" | "void" | "date" | "int2" | "int4" | "int8" | "smallint" | "real" | "float4" | "float" | "float8" | "numeric" | "decimal" | "smallserial" | "serial" | "bigserial" | "uuid" | "text" | "varchar" | "char" | "bpchar" | "citext" | "bit" | "bool" | "timestamp" | "timestamptz" | "time" | "timetz" | "interval" | "inet" | "cidr" | "macaddr" | "macaddr8" | "money" | "json" | "jsonb" | "bytea", string | {
|
|
76
|
+
parameter: string | {
|
|
77
|
+
regex: string;
|
|
78
|
+
};
|
|
79
|
+
return: string;
|
|
80
|
+
}> | Record<string, string | {
|
|
81
|
+
parameter: string | {
|
|
82
|
+
regex: string;
|
|
83
|
+
};
|
|
84
|
+
return: string;
|
|
85
|
+
}> | undefined;
|
|
86
|
+
columns?: Record<string, string> | undefined;
|
|
87
|
+
} | undefined;
|
|
88
|
+
nullAsUndefined?: boolean | undefined;
|
|
89
|
+
nullAsOptional?: boolean | undefined;
|
|
90
|
+
inferLiterals?: boolean | ("string" | "number" | "boolean")[] | undefined;
|
|
91
|
+
} | ({
|
|
92
|
+
targets: ({
|
|
93
|
+
wrapper: string | {
|
|
94
|
+
regex: string;
|
|
95
|
+
};
|
|
96
|
+
maxDepth?: number | undefined;
|
|
97
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
98
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
99
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
100
|
+
} | {
|
|
101
|
+
tag: string | {
|
|
102
|
+
regex: string;
|
|
103
|
+
};
|
|
104
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
105
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
106
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
107
|
+
})[];
|
|
108
|
+
migrationsDir: string;
|
|
109
|
+
keepAlive?: boolean | undefined;
|
|
110
|
+
overrides?: {
|
|
111
|
+
types?: Record<"bigint" | "boolean" | "int" | "null" | "void" | "date" | "int2" | "int4" | "int8" | "smallint" | "real" | "float4" | "float" | "float8" | "numeric" | "decimal" | "smallserial" | "serial" | "bigserial" | "uuid" | "text" | "varchar" | "char" | "bpchar" | "citext" | "bit" | "bool" | "timestamp" | "timestamptz" | "time" | "timetz" | "interval" | "inet" | "cidr" | "macaddr" | "macaddr8" | "money" | "json" | "jsonb" | "bytea", string | {
|
|
112
|
+
parameter: string | {
|
|
113
|
+
regex: string;
|
|
114
|
+
};
|
|
115
|
+
return: string;
|
|
116
|
+
}> | Record<string, string | {
|
|
117
|
+
parameter: string | {
|
|
118
|
+
regex: string;
|
|
119
|
+
};
|
|
120
|
+
return: string;
|
|
121
|
+
}> | undefined;
|
|
122
|
+
columns?: Record<string, string> | undefined;
|
|
123
|
+
} | undefined;
|
|
124
|
+
nullAsUndefined?: boolean | undefined;
|
|
125
|
+
nullAsOptional?: boolean | undefined;
|
|
126
|
+
inferLiterals?: boolean | ("string" | "number" | "boolean")[] | undefined;
|
|
127
|
+
connectionUrl?: string | undefined;
|
|
128
|
+
databaseName?: string | undefined;
|
|
129
|
+
watchMode?: boolean | undefined;
|
|
130
|
+
} | {
|
|
131
|
+
targets: ({
|
|
132
|
+
wrapper: string | {
|
|
133
|
+
regex: string;
|
|
134
|
+
};
|
|
135
|
+
maxDepth?: number | undefined;
|
|
136
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
137
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
138
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
139
|
+
} | {
|
|
140
|
+
tag: string | {
|
|
141
|
+
regex: string;
|
|
142
|
+
};
|
|
143
|
+
transform?: string | (string | [string, string])[] | undefined;
|
|
144
|
+
fieldTransform?: "snake" | "pascal" | "camel" | "screaming snake" | undefined;
|
|
145
|
+
skipTypeAnnotations?: boolean | undefined;
|
|
146
|
+
})[];
|
|
147
|
+
databaseUrl: string;
|
|
148
|
+
keepAlive?: boolean | undefined;
|
|
149
|
+
overrides?: {
|
|
150
|
+
types?: Record<"bigint" | "boolean" | "int" | "null" | "void" | "date" | "int2" | "int4" | "int8" | "smallint" | "real" | "float4" | "float" | "float8" | "numeric" | "decimal" | "smallserial" | "serial" | "bigserial" | "uuid" | "text" | "varchar" | "char" | "bpchar" | "citext" | "bit" | "bool" | "timestamp" | "timestamptz" | "time" | "timetz" | "interval" | "inet" | "cidr" | "macaddr" | "macaddr8" | "money" | "json" | "jsonb" | "bytea", string | {
|
|
151
|
+
parameter: string | {
|
|
152
|
+
regex: string;
|
|
153
|
+
};
|
|
154
|
+
return: string;
|
|
155
|
+
}> | Record<string, string | {
|
|
156
|
+
parameter: string | {
|
|
157
|
+
regex: string;
|
|
158
|
+
};
|
|
159
|
+
return: string;
|
|
160
|
+
}> | undefined;
|
|
161
|
+
columns?: Record<string, string> | undefined;
|
|
162
|
+
} | undefined;
|
|
163
|
+
nullAsUndefined?: boolean | undefined;
|
|
164
|
+
nullAsOptional?: boolean | undefined;
|
|
165
|
+
inferLiterals?: boolean | ("string" | "number" | "boolean")[] | undefined;
|
|
166
|
+
})[];
|
|
167
|
+
} | {
|
|
168
|
+
useConfigFile: boolean;
|
|
169
|
+
})[], unknown, _typescript_eslint_utils_ts_eslint.RuleListener> & {
|
|
170
|
+
name: string;
|
|
171
|
+
};
|
|
172
|
+
};
|
|
173
|
+
meta: {
|
|
174
|
+
name: string;
|
|
175
|
+
version: string;
|
|
176
|
+
};
|
|
177
|
+
};
|
|
178
|
+
};
|
|
179
|
+
rules: {
|
|
180
|
+
"slonik/check-sql": ["error", {
|
|
181
|
+
useConfigFile: boolean;
|
|
182
|
+
}];
|
|
183
|
+
};
|
|
184
|
+
};
|
|
185
|
+
/**
|
|
186
|
+
* If you prefer configuring via a flat config, use this config.
|
|
187
|
+
*/
|
|
188
|
+
connections: (connections: Config["connections"]) => FlatConfig.Config;
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
export = _default;
|