eslint-plugin-slonik 1.6.1 → 1.8.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/README.md +37 -28
- package/dist/index.cjs +16 -401
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -10
- package/dist/index.d.mts +2 -10
- package/dist/index.d.ts +2 -10
- package/dist/index.mjs +13 -398
- package/dist/index.mjs.map +1 -1
- package/dist/shared/{eslint-plugin-slonik.DwYTOoDn.mjs → eslint-plugin-slonik.CHihwTq0.mjs} +3 -176
- package/dist/shared/eslint-plugin-slonik.CHihwTq0.mjs.map +1 -0
- package/dist/shared/{eslint-plugin-slonik.uSZXmGoY.d.cts → eslint-plugin-slonik.ZtKj5lnb.d.cts} +0 -14
- package/dist/shared/{eslint-plugin-slonik.uSZXmGoY.d.mts → eslint-plugin-slonik.ZtKj5lnb.d.mts} +0 -14
- package/dist/shared/{eslint-plugin-slonik.uSZXmGoY.d.ts → eslint-plugin-slonik.ZtKj5lnb.d.ts} +0 -14
- package/dist/shared/{eslint-plugin-slonik.rlOTrCdf.cjs → eslint-plugin-slonik.kniz1QWh.cjs} +2 -184
- package/dist/shared/eslint-plugin-slonik.kniz1QWh.cjs.map +1 -0
- package/dist/workers/check-sql.worker.cjs +134 -39
- package/dist/workers/check-sql.worker.cjs.map +1 -1
- package/dist/workers/check-sql.worker.d.cts +11 -4
- package/dist/workers/check-sql.worker.d.mts +11 -4
- package/dist/workers/check-sql.worker.d.ts +11 -4
- package/dist/workers/check-sql.worker.mjs +133 -39
- package/dist/workers/check-sql.worker.mjs.map +1 -1
- package/package.json +1 -1
- package/dist/shared/eslint-plugin-slonik.DwYTOoDn.mjs.map +0 -1
- package/dist/shared/eslint-plugin-slonik.rlOTrCdf.cjs.map +0 -1
package/README.md
CHANGED
|
@@ -42,24 +42,23 @@ export default [
|
|
|
42
42
|
overrides: {
|
|
43
43
|
types: {
|
|
44
44
|
// Map PostgreSQL types to Slonik token types
|
|
45
|
-
date:
|
|
46
|
-
timestamp:
|
|
47
|
-
interval:
|
|
48
|
-
json:
|
|
49
|
-
jsonb:
|
|
50
|
-
uuid:
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
56
|
},
|
|
57
57
|
},
|
|
58
58
|
targets: [
|
|
59
59
|
{
|
|
60
60
|
// Match Slonik's typed query methods
|
|
61
|
-
tag:
|
|
62
|
-
skipTypeAnnotations: true,
|
|
61
|
+
tag: 'sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)',
|
|
63
62
|
},
|
|
64
63
|
],
|
|
65
64
|
}),
|
|
@@ -131,14 +130,14 @@ When using Slonik, you'll want to map PostgreSQL types to Slonik's token types:
|
|
|
131
130
|
overrides: {
|
|
132
131
|
types: {
|
|
133
132
|
// Date/Time types
|
|
134
|
-
date:
|
|
135
|
-
timestamp:
|
|
133
|
+
date: 'DateSqlToken',
|
|
134
|
+
timestamp: 'TimestampSqlToken',
|
|
136
135
|
timestamptz: "TimestampSqlToken",
|
|
137
|
-
interval:
|
|
136
|
+
interval: 'IntervalSqlToken',
|
|
138
137
|
|
|
139
138
|
// JSON types
|
|
140
|
-
json:
|
|
141
|
-
jsonb:
|
|
139
|
+
json: 'JsonSqlToken',
|
|
140
|
+
jsonb: 'JsonBinarySqlToken',
|
|
142
141
|
|
|
143
142
|
// UUID
|
|
144
143
|
uuid: "UuidSqlToken",
|
|
@@ -167,7 +166,6 @@ targets: [
|
|
|
167
166
|
{
|
|
168
167
|
// Matches: sql.type(...)``, sql.typeAlias(...)``, sql.unsafe``
|
|
169
168
|
tag: "sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)",
|
|
170
|
-
skipTypeAnnotations: true,
|
|
171
169
|
},
|
|
172
170
|
]
|
|
173
171
|
```
|
|
@@ -209,19 +207,18 @@ export default tseslint.config(
|
|
|
209
207
|
databaseUrl: process.env.DATABASE_URL,
|
|
210
208
|
overrides: {
|
|
211
209
|
types: {
|
|
212
|
-
date:
|
|
213
|
-
timestamp:
|
|
214
|
-
json:
|
|
215
|
-
jsonb:
|
|
216
|
-
uuid:
|
|
217
|
-
|
|
218
|
-
|
|
210
|
+
date: 'DateSqlToken',
|
|
211
|
+
timestamp: 'TimestampSqlToken',
|
|
212
|
+
json: 'JsonSqlToken',
|
|
213
|
+
jsonb: 'JsonBinarySqlToken',
|
|
214
|
+
uuid: 'UuidSqlToken',
|
|
215
|
+
'int4[]': 'ArraySqlToken<"int4">',
|
|
216
|
+
'text[]': 'ArraySqlToken<"text">',
|
|
219
217
|
},
|
|
220
218
|
},
|
|
221
219
|
targets: [
|
|
222
220
|
{
|
|
223
|
-
tag:
|
|
224
|
-
skipTypeAnnotations: true,
|
|
221
|
+
tag: 'sql.+(type\\(*\\)|typeAlias\\(*\\)|unsafe)',
|
|
225
222
|
},
|
|
226
223
|
],
|
|
227
224
|
})
|
|
@@ -273,6 +270,18 @@ This plugin is specifically designed for Slonik and includes:
|
|
|
273
270
|
4. **Identifier support** — Converts `sql.identifier()` to quoted identifiers
|
|
274
271
|
5. **Graceful degradation** — Skips validation for runtime-dependent constructs instead of erroring
|
|
275
272
|
|
|
273
|
+
## How It Works
|
|
274
|
+
|
|
275
|
+
ESLint rules must be synchronous, but SQL validation requires async operations like database connections. This plugin solves this using [`synckit`](https://github.com/un-ts/synckit), which enables synchronous calls to async worker threads.
|
|
276
|
+
|
|
277
|
+
The architecture:
|
|
278
|
+
|
|
279
|
+
1. **Worker Thread** — Runs all async operations (database connections, migrations, type generation) in a separate thread
|
|
280
|
+
2. **Synchronous Bridge** — Uses `synckit` to block the main thread until the worker completes, making async operations appear synchronous to ESLint
|
|
281
|
+
3. **Connection Pooling** — Reuses database connections across lint runs for performance
|
|
282
|
+
|
|
283
|
+
Under the hood, `synckit` uses Node.js Worker Threads with `Atomics.wait()` to block the main thread until the worker signals completion via `Atomics.notify()`.
|
|
284
|
+
|
|
276
285
|
## Development
|
|
277
286
|
|
|
278
287
|
### Prerequisites
|
package/dist/index.cjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const checkSql_utils = require('./shared/eslint-plugin-slonik.
|
|
3
|
+
const checkSql_utils = require('./shared/eslint-plugin-slonik.kniz1QWh.cjs');
|
|
4
4
|
const utils = require('@typescript-eslint/utils');
|
|
5
5
|
const tsPattern = require('ts-pattern');
|
|
6
6
|
const E = require('fp-ts/lib/Either.js');
|
|
@@ -8,9 +8,9 @@ const function_js = require('fp-ts/lib/function.js');
|
|
|
8
8
|
require('fp-ts/lib/Option.js');
|
|
9
9
|
require('fp-ts/lib/TaskEither.js');
|
|
10
10
|
const J = require('fp-ts/lib/Json.js');
|
|
11
|
-
const ts = require('typescript');
|
|
12
11
|
const path = require('path');
|
|
13
12
|
const fs = require('fs');
|
|
13
|
+
const ts = require('typescript');
|
|
14
14
|
const synckit = require('synckit');
|
|
15
15
|
const node_url = require('node:url');
|
|
16
16
|
const z = require('zod');
|
|
@@ -35,226 +35,16 @@ function _interopNamespaceCompat(e) {
|
|
|
35
35
|
|
|
36
36
|
const E__namespace = /*#__PURE__*/_interopNamespaceCompat(E);
|
|
37
37
|
const J__namespace = /*#__PURE__*/_interopNamespaceCompat(J);
|
|
38
|
-
const ts__default = /*#__PURE__*/_interopDefaultCompat(ts);
|
|
39
38
|
const path__default = /*#__PURE__*/_interopDefaultCompat(path);
|
|
40
39
|
const fs__default = /*#__PURE__*/_interopDefaultCompat(fs);
|
|
40
|
+
const ts__default = /*#__PURE__*/_interopDefaultCompat(ts);
|
|
41
41
|
const z__default = /*#__PURE__*/_interopDefaultCompat(z);
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
number: "number",
|
|
46
|
-
boolean: "boolean",
|
|
47
|
-
false: "false",
|
|
48
|
-
true: "true",
|
|
49
|
-
null: "null",
|
|
50
|
-
undefined: "undefined",
|
|
51
|
-
any: "any"
|
|
52
|
-
};
|
|
53
|
-
function getResolvedTargetByTypeNode(params) {
|
|
54
|
-
if (!params.typeNode) {
|
|
55
|
-
console.error("[slonik/check-sql] DEBUG: typeNode is undefined in getResolvedTargetByTypeNode");
|
|
56
|
-
throw new Error("typeNode is undefined");
|
|
57
|
-
}
|
|
58
|
-
const tsNode = params.parser.esTreeNodeToTSNodeMap.get(params.typeNode);
|
|
59
|
-
if (!tsNode) {
|
|
60
|
-
console.error("[slonik/check-sql] DEBUG: Could not map ESTree node to TS node. typeNode.type:", params.typeNode.type);
|
|
61
|
-
throw new Error(`Could not map ESTree node (type: ${params.typeNode.type}) to TS node`);
|
|
62
|
-
}
|
|
63
|
-
const typeText = tsNode.getText();
|
|
64
|
-
if (isReservedType(typeText, params.reservedTypes)) {
|
|
65
|
-
return { kind: "type", value: typeText };
|
|
66
|
-
}
|
|
67
|
-
switch (params.typeNode.type) {
|
|
68
|
-
case utils.TSESTree.AST_NODE_TYPES.TSLiteralType:
|
|
69
|
-
return handleLiteralType(params.typeNode);
|
|
70
|
-
case utils.TSESTree.AST_NODE_TYPES.TSUnionType:
|
|
71
|
-
return {
|
|
72
|
-
kind: "union",
|
|
73
|
-
value: params.typeNode.types.map(
|
|
74
|
-
(type) => getResolvedTargetByTypeNode({ ...params, typeNode: type })
|
|
75
|
-
)
|
|
76
|
-
};
|
|
77
|
-
case utils.TSESTree.AST_NODE_TYPES.TSNullKeyword:
|
|
78
|
-
return { kind: "type", value: "null" };
|
|
79
|
-
case utils.TSESTree.AST_NODE_TYPES.TSUndefinedKeyword:
|
|
80
|
-
return { kind: "type", value: "undefined" };
|
|
81
|
-
case utils.TSESTree.AST_NODE_TYPES.TSTypeLiteral:
|
|
82
|
-
return handleTypeLiteral(params.typeNode, params);
|
|
83
|
-
case utils.TSESTree.AST_NODE_TYPES.TSTypeReference:
|
|
84
|
-
return handleTypeReference(params.typeNode, params);
|
|
85
|
-
case utils.TSESTree.AST_NODE_TYPES.TSIntersectionType:
|
|
86
|
-
return handleIntersectionType(params.typeNode, params);
|
|
87
|
-
case utils.TSESTree.AST_NODE_TYPES.TSArrayType:
|
|
88
|
-
return {
|
|
89
|
-
kind: "array",
|
|
90
|
-
value: getResolvedTargetByTypeNode({
|
|
91
|
-
...params,
|
|
92
|
-
typeNode: params.typeNode.elementType
|
|
93
|
-
})
|
|
94
|
-
};
|
|
95
|
-
default:
|
|
96
|
-
return { kind: "type", value: typeText };
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
function isReservedType(typeText, reservedTypes) {
|
|
100
|
-
return reservedTypes.has(typeText) || reservedTypes.has(`${typeText}[]`);
|
|
101
|
-
}
|
|
102
|
-
function handleLiteralType(typeNode) {
|
|
103
|
-
return typeNode.literal.type === utils.TSESTree.AST_NODE_TYPES.Literal ? { kind: "type", value: `'${typeNode.literal.value}'` } : { kind: "type", value: "unknown" };
|
|
104
|
-
}
|
|
105
|
-
function handleTypeLiteral(typeNode, params) {
|
|
106
|
-
const properties = typeNode.members.flatMap((member) => {
|
|
107
|
-
if (member.type !== utils.TSESTree.AST_NODE_TYPES.TSPropertySignature || !member.typeAnnotation) {
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const key = extractPropertyKey(member.key);
|
|
111
|
-
if (!key) return [];
|
|
112
|
-
const propertyName = member.optional ? `${key}?` : key;
|
|
113
|
-
const propertyType = getResolvedTargetByTypeNode({
|
|
114
|
-
...params,
|
|
115
|
-
typeNode: member.typeAnnotation.typeAnnotation
|
|
116
|
-
});
|
|
117
|
-
return [[propertyName, propertyType]];
|
|
118
|
-
});
|
|
119
|
-
return { kind: "object", value: properties };
|
|
120
|
-
}
|
|
121
|
-
function extractPropertyKey(key) {
|
|
122
|
-
switch (key.type) {
|
|
123
|
-
case utils.TSESTree.AST_NODE_TYPES.Identifier:
|
|
124
|
-
return key.name;
|
|
125
|
-
case utils.TSESTree.AST_NODE_TYPES.Literal:
|
|
126
|
-
return String(key.value);
|
|
127
|
-
default:
|
|
128
|
-
return void 0;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
function handleTypeReference(typeNode, params) {
|
|
132
|
-
if (typeNode.typeName.type !== utils.TSESTree.AST_NODE_TYPES.Identifier && typeNode.typeName.type !== utils.TSESTree.AST_NODE_TYPES.TSQualifiedName) {
|
|
133
|
-
return { kind: "type", value: "unknown" };
|
|
134
|
-
}
|
|
135
|
-
const typeNameText = params.parser.esTreeNodeToTSNodeMap.get(typeNode.typeName).getText();
|
|
136
|
-
if (params.reservedTypes.has(typeNameText)) {
|
|
137
|
-
return { kind: "type", value: typeNameText };
|
|
138
|
-
}
|
|
139
|
-
if (typeNameText === "Array" && typeNode.typeArguments?.params[0]) {
|
|
140
|
-
return {
|
|
141
|
-
kind: "array",
|
|
142
|
-
syntax: "type-reference",
|
|
143
|
-
value: getResolvedTargetByTypeNode({
|
|
144
|
-
...params,
|
|
145
|
-
typeNode: typeNode.typeArguments.params[0]
|
|
146
|
-
})
|
|
147
|
-
};
|
|
148
|
-
}
|
|
149
|
-
const type = params.checker.getTypeFromTypeNode(
|
|
150
|
-
params.parser.esTreeNodeToTSNodeMap.get(typeNode)
|
|
151
|
-
);
|
|
152
|
-
return resolveType(type, { ...params, typeNode });
|
|
153
|
-
}
|
|
154
|
-
function handleIntersectionType(typeNode, params) {
|
|
155
|
-
const allProperties = typeNode.types.flatMap((type) => {
|
|
156
|
-
const resolved = getResolvedTargetByTypeNode({ ...params, typeNode: type });
|
|
157
|
-
return resolved.kind === "object" ? resolved.value : [];
|
|
158
|
-
});
|
|
159
|
-
return { kind: "object", value: Array.from(new Map(allProperties).entries()) };
|
|
160
|
-
}
|
|
161
|
-
function resolveType(type, params) {
|
|
162
|
-
const typeAsString = params.checker.typeToString(type);
|
|
163
|
-
if (params.reservedTypes.has(typeAsString)) {
|
|
164
|
-
return { kind: "type", value: typeAsString };
|
|
165
|
-
}
|
|
166
|
-
if (params.reservedTypes.has(`${typeAsString}[]`)) {
|
|
167
|
-
return { kind: "array", value: { kind: "type", value: typeAsString.replace("[]", "") } };
|
|
168
|
-
}
|
|
169
|
-
const primitive = getPrimitiveType(type, typeAsString);
|
|
170
|
-
if (primitive) return primitive;
|
|
171
|
-
if (type.isLiteral()) {
|
|
172
|
-
return { kind: "type", value: `'${type.value}'` };
|
|
173
|
-
}
|
|
174
|
-
if (type.isUnion()) {
|
|
175
|
-
return handleUnionTypeReference(type, params);
|
|
176
|
-
}
|
|
177
|
-
if (type.isIntersection()) {
|
|
178
|
-
return handleIntersectionTypeReference(type, params);
|
|
179
|
-
}
|
|
180
|
-
if (params.checker.isArrayType(type)) {
|
|
181
|
-
return handleArrayTypeReferenceFromType(type, params);
|
|
182
|
-
}
|
|
183
|
-
return handleObjectType(type, params);
|
|
184
|
-
}
|
|
185
|
-
function getPrimitiveType(type, typeAsString) {
|
|
186
|
-
if (PRIMITIVES[typeAsString]) {
|
|
187
|
-
return { kind: "type", value: PRIMITIVES[typeAsString] };
|
|
188
|
-
}
|
|
189
|
-
const flagMap = {
|
|
190
|
-
[ts__default.TypeFlags.String]: "string",
|
|
191
|
-
[ts__default.TypeFlags.Number]: "number",
|
|
192
|
-
[ts__default.TypeFlags.Boolean]: "boolean",
|
|
193
|
-
[ts__default.TypeFlags.Null]: "null",
|
|
194
|
-
[ts__default.TypeFlags.Undefined]: "undefined",
|
|
195
|
-
[ts__default.TypeFlags.Any]: "any"
|
|
196
|
-
};
|
|
197
|
-
return flagMap[type.flags] ? { kind: "type", value: flagMap[type.flags] } : null;
|
|
198
|
-
}
|
|
199
|
-
function handleUnionTypeReference(type, params) {
|
|
200
|
-
const types = type.types.map((t) => resolveType(t, params));
|
|
201
|
-
const isBooleanUnionWithNull = types.length === 3 && types.some((t) => t.value === "false") && types.some((t) => t.value === "true") && types.some((t) => t.value === "null");
|
|
202
|
-
if (isBooleanUnionWithNull) {
|
|
203
|
-
return {
|
|
204
|
-
kind: "union",
|
|
205
|
-
value: [
|
|
206
|
-
{ kind: "type", value: "boolean" },
|
|
207
|
-
{ kind: "type", value: "null" }
|
|
208
|
-
]
|
|
209
|
-
};
|
|
210
|
-
}
|
|
211
|
-
return { kind: "union", value: types };
|
|
212
|
-
}
|
|
213
|
-
function handleIntersectionTypeReference(type, params) {
|
|
214
|
-
const properties = type.types.flatMap((t) => {
|
|
215
|
-
const resolved = resolveType(t, params);
|
|
216
|
-
return resolved.kind === "object" ? resolved.value : [];
|
|
217
|
-
});
|
|
218
|
-
return { kind: "object", value: properties };
|
|
43
|
+
function isIdentifier(node) {
|
|
44
|
+
return node?.type === utils.TSESTree.AST_NODE_TYPES.Identifier;
|
|
219
45
|
}
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
const firstArgument = typeArguments?.[0];
|
|
223
|
-
if (firstArgument) {
|
|
224
|
-
const elementType = resolveType(firstArgument, params);
|
|
225
|
-
return { kind: "array", value: elementType };
|
|
226
|
-
}
|
|
227
|
-
return { kind: "array", value: { kind: "type", value: "unknown" } };
|
|
228
|
-
}
|
|
229
|
-
function handleObjectType(type, params) {
|
|
230
|
-
if (!type.symbol) {
|
|
231
|
-
return { kind: "type", value: type.aliasSymbol?.escapedName.toString() ?? "unknown" };
|
|
232
|
-
}
|
|
233
|
-
if (type.symbol.valueDeclaration) {
|
|
234
|
-
const declaration = type.symbol.valueDeclaration;
|
|
235
|
-
const sourceFile = declaration.getSourceFile();
|
|
236
|
-
const filePath = sourceFile.fileName;
|
|
237
|
-
if (!filePath.includes("node_modules")) {
|
|
238
|
-
return extractObjectProperties(type, params);
|
|
239
|
-
}
|
|
240
|
-
return { kind: "type", value: type.symbol.name };
|
|
241
|
-
}
|
|
242
|
-
if (type.flags === ts__default.TypeFlags.Object) {
|
|
243
|
-
return extractObjectProperties(type, params);
|
|
244
|
-
}
|
|
245
|
-
return { kind: "object", value: [] };
|
|
246
|
-
}
|
|
247
|
-
function extractObjectProperties(type, params) {
|
|
248
|
-
const properties = type.getProperties().map((property) => {
|
|
249
|
-
const key = property.escapedName.toString();
|
|
250
|
-
const propType = params.checker.getTypeOfSymbolAtLocation(
|
|
251
|
-
property,
|
|
252
|
-
params.parser.esTreeNodeToTSNodeMap.get(params.typeNode)
|
|
253
|
-
);
|
|
254
|
-
const resolvedType = resolveType(propType, params);
|
|
255
|
-
return [key, resolvedType];
|
|
256
|
-
});
|
|
257
|
-
return { kind: "object", value: properties };
|
|
46
|
+
function isMemberExpression(node) {
|
|
47
|
+
return node?.type === utils.TSESTree.AST_NODE_TYPES.MemberExpression;
|
|
258
48
|
}
|
|
259
49
|
|
|
260
50
|
function isInEditorEnv() {
|
|
@@ -1108,11 +898,7 @@ const zBaseTarget = z__default.object({
|
|
|
1108
898
|
* - `"pascal"` - `user_id` → `UserId`
|
|
1109
899
|
* - `"screaming snake"` - `user_id` → `USER_ID`
|
|
1110
900
|
*/
|
|
1111
|
-
fieldTransform: z__default.enum(["snake", "pascal", "camel", "screaming snake"]).optional()
|
|
1112
|
-
/**
|
|
1113
|
-
* Whether or not to skip type annotation.
|
|
1114
|
-
*/
|
|
1115
|
-
skipTypeAnnotations: z__default.boolean().optional()
|
|
901
|
+
fieldTransform: z__default.enum(["snake", "pascal", "camel", "screaming snake"]).optional()
|
|
1116
902
|
});
|
|
1117
903
|
const zWrapperTarget = z__default.object({ wrapper: zStringOrRegex, maxDepth: z__default.number().optional() }).merge(zBaseTarget);
|
|
1118
904
|
const zTagTarget = z__default.object({ tag: zStringOrRegex }).merge(zBaseTarget);
|
|
@@ -1193,7 +979,6 @@ const zConfig = z__default.object({
|
|
|
1193
979
|
connections: z__default.union([z__default.array(zRuleOptionConnection), zRuleOptionConnection])
|
|
1194
980
|
});
|
|
1195
981
|
const RuleOptions = z__default.array(zConfig).min(1).max(1);
|
|
1196
|
-
const defaultInferLiteralOptions = ["string"];
|
|
1197
982
|
|
|
1198
983
|
function getConfigFromFileWithContext(params) {
|
|
1199
984
|
return params.context.options[0];
|
|
@@ -1202,12 +987,7 @@ function getConfigFromFileWithContext(params) {
|
|
|
1202
987
|
const messages = {
|
|
1203
988
|
typeInferenceFailed: "Type inference failed {{error}}",
|
|
1204
989
|
error: "{{error}}",
|
|
1205
|
-
invalidQuery: "Invalid Query: {{error}}"
|
|
1206
|
-
missingTypeAnnotations: "Query is missing type annotation\n Fix with: {{fix}}",
|
|
1207
|
-
incorrectTypeAnnotations: `Query has incorrect type annotation.
|
|
1208
|
-
Expected: {{expected}}
|
|
1209
|
-
Actual: {{actual}}`,
|
|
1210
|
-
invalidTypeAnnotations: `Query has invalid type annotation (SafeQL does not support it. If you think it should, please open an issue)`
|
|
990
|
+
invalidQuery: "Invalid Query: {{error}}"
|
|
1211
991
|
};
|
|
1212
992
|
function check(params) {
|
|
1213
993
|
const connections = Array.isArray(params.config.connections) ? params.config.connections : [params.config.connections];
|
|
@@ -1218,10 +998,10 @@ function check(params) {
|
|
|
1218
998
|
}
|
|
1219
999
|
}
|
|
1220
1000
|
function isTagMemberValid(expr) {
|
|
1221
|
-
if (
|
|
1001
|
+
if (isIdentifier(expr.tag)) {
|
|
1222
1002
|
return true;
|
|
1223
1003
|
}
|
|
1224
|
-
if (
|
|
1004
|
+
if (isMemberExpression(expr.tag) && isIdentifier(expr.tag.property)) {
|
|
1225
1005
|
return true;
|
|
1226
1006
|
}
|
|
1227
1007
|
return false;
|
|
@@ -1243,13 +1023,11 @@ const generateSyncE = function_js.flow(
|
|
|
1243
1023
|
);
|
|
1244
1024
|
let fatalError;
|
|
1245
1025
|
function reportCheck(params) {
|
|
1246
|
-
const { context, tag, connection, target, projectDir
|
|
1026
|
+
const { context, tag, connection, target, projectDir } = params;
|
|
1247
1027
|
if (fatalError !== void 0) {
|
|
1248
1028
|
const hint = isInEditorEnv() ? "If you think this is a bug, please open an issue. If not, please try to fix the error and restart ESLint." : "If you think this is a bug, please open an issue.";
|
|
1249
1029
|
return checkSql_utils.reportBaseError({ context, error: fatalError, tag, hint });
|
|
1250
1030
|
}
|
|
1251
|
-
const nullAsOptional = connection.nullAsOptional ?? false;
|
|
1252
|
-
const nullAsUndefined = connection.nullAsUndefined ?? false;
|
|
1253
1031
|
return function_js.pipe(
|
|
1254
1032
|
E__namespace.Do,
|
|
1255
1033
|
E__namespace.bind("parser", () => {
|
|
@@ -1309,89 +1087,11 @@ function reportCheck(params) {
|
|
|
1309
1087
|
If you believe this query should be supported, please open an issue at https://github.com/gajus/eslint-plugin-slonik/issues`
|
|
1310
1088
|
);
|
|
1311
1089
|
return;
|
|
1090
|
+
}).with({ _tag: "ConnectionFailedError" }, () => {
|
|
1091
|
+
return;
|
|
1312
1092
|
}).exhaustive();
|
|
1313
1093
|
},
|
|
1314
|
-
(
|
|
1315
|
-
if (result === null) {
|
|
1316
|
-
return;
|
|
1317
|
-
}
|
|
1318
|
-
const shouldSkipTypeAnnotations = target.skipTypeAnnotations === true;
|
|
1319
|
-
if (shouldSkipTypeAnnotations) {
|
|
1320
|
-
return;
|
|
1321
|
-
}
|
|
1322
|
-
const isMissingTypeAnnotations = typeParameter === void 0;
|
|
1323
|
-
if (isMissingTypeAnnotations) {
|
|
1324
|
-
if (result.output === null) {
|
|
1325
|
-
return;
|
|
1326
|
-
}
|
|
1327
|
-
return checkSql_utils.reportMissingTypeAnnotations({
|
|
1328
|
-
tag,
|
|
1329
|
-
context,
|
|
1330
|
-
baseNode,
|
|
1331
|
-
actual: checkSql_utils.getFinalResolvedTargetString({
|
|
1332
|
-
target: result.output,
|
|
1333
|
-
nullAsOptional: nullAsOptional ?? false,
|
|
1334
|
-
nullAsUndefined: nullAsUndefined ?? false,
|
|
1335
|
-
transform: target.transform,
|
|
1336
|
-
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1337
|
-
})
|
|
1338
|
-
});
|
|
1339
|
-
}
|
|
1340
|
-
const reservedTypes = memoize({
|
|
1341
|
-
key: `reserved-types:${JSON.stringify(connection.overrides)}`,
|
|
1342
|
-
value: () => {
|
|
1343
|
-
const types = /* @__PURE__ */ new Set();
|
|
1344
|
-
for (const value of Object.values(connection.overrides?.types ?? {})) {
|
|
1345
|
-
types.add(typeof value === "string" ? value : value.return);
|
|
1346
|
-
}
|
|
1347
|
-
for (const columnType of Object.values(connection.overrides?.columns ?? {})) {
|
|
1348
|
-
types.add(columnType);
|
|
1349
|
-
}
|
|
1350
|
-
return types;
|
|
1351
|
-
}
|
|
1352
|
-
});
|
|
1353
|
-
const typeAnnotationState = getTypeAnnotationState({
|
|
1354
|
-
generated: result.output,
|
|
1355
|
-
typeParameter,
|
|
1356
|
-
transform: target.transform,
|
|
1357
|
-
checker,
|
|
1358
|
-
parser,
|
|
1359
|
-
reservedTypes,
|
|
1360
|
-
nullAsOptional,
|
|
1361
|
-
nullAsUndefined,
|
|
1362
|
-
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1363
|
-
});
|
|
1364
|
-
if (typeAnnotationState === "INVALID") {
|
|
1365
|
-
return checkSql_utils.reportInvalidTypeAnnotations({
|
|
1366
|
-
context,
|
|
1367
|
-
typeParameter
|
|
1368
|
-
});
|
|
1369
|
-
}
|
|
1370
|
-
if (!typeAnnotationState.isEqual) {
|
|
1371
|
-
return checkSql_utils.reportIncorrectTypeAnnotations({
|
|
1372
|
-
context,
|
|
1373
|
-
typeParameter,
|
|
1374
|
-
expected: checkSql_utils.fmap(
|
|
1375
|
-
typeAnnotationState.expected,
|
|
1376
|
-
(expected) => checkSql_utils.getResolvedTargetString({
|
|
1377
|
-
target: expected,
|
|
1378
|
-
nullAsOptional: false,
|
|
1379
|
-
nullAsUndefined: false,
|
|
1380
|
-
inferLiterals: params.connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1381
|
-
})
|
|
1382
|
-
),
|
|
1383
|
-
actual: checkSql_utils.fmap(
|
|
1384
|
-
result.output,
|
|
1385
|
-
(output) => checkSql_utils.getFinalResolvedTargetString({
|
|
1386
|
-
target: output,
|
|
1387
|
-
nullAsOptional: connection.nullAsOptional ?? false,
|
|
1388
|
-
nullAsUndefined: connection.nullAsUndefined ?? false,
|
|
1389
|
-
transform: target.transform,
|
|
1390
|
-
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1391
|
-
})
|
|
1392
|
-
)
|
|
1393
|
-
});
|
|
1394
|
-
}
|
|
1094
|
+
() => {
|
|
1395
1095
|
}
|
|
1396
1096
|
)
|
|
1397
1097
|
);
|
|
@@ -1445,98 +1145,13 @@ function checkConnectionByWrapperExpression(params) {
|
|
|
1445
1145
|
});
|
|
1446
1146
|
}
|
|
1447
1147
|
}
|
|
1448
|
-
function getTypeAnnotationState({
|
|
1449
|
-
generated,
|
|
1450
|
-
typeParameter,
|
|
1451
|
-
transform,
|
|
1452
|
-
parser,
|
|
1453
|
-
checker,
|
|
1454
|
-
reservedTypes,
|
|
1455
|
-
nullAsOptional,
|
|
1456
|
-
nullAsUndefined,
|
|
1457
|
-
inferLiterals
|
|
1458
|
-
}) {
|
|
1459
|
-
if (typeParameter.params.length !== 1) {
|
|
1460
|
-
return "INVALID";
|
|
1461
|
-
}
|
|
1462
|
-
const typeNode = typeParameter.params[0];
|
|
1463
|
-
let expected;
|
|
1464
|
-
try {
|
|
1465
|
-
expected = getResolvedTargetByTypeNode({
|
|
1466
|
-
checker,
|
|
1467
|
-
parser,
|
|
1468
|
-
typeNode,
|
|
1469
|
-
reservedTypes
|
|
1470
|
-
});
|
|
1471
|
-
} catch (error) {
|
|
1472
|
-
console.error("[slonik/check-sql] DEBUG: Error in getResolvedTargetByTypeNode:", error);
|
|
1473
|
-
console.error("[slonik/check-sql] DEBUG: typeNode:", typeNode);
|
|
1474
|
-
console.error("[slonik/check-sql] DEBUG: typeNode.type:", typeNode?.type);
|
|
1475
|
-
throw error;
|
|
1476
|
-
}
|
|
1477
|
-
return getResolvedTargetsEquality({
|
|
1478
|
-
expected,
|
|
1479
|
-
generated,
|
|
1480
|
-
nullAsOptional,
|
|
1481
|
-
nullAsUndefined,
|
|
1482
|
-
inferLiterals,
|
|
1483
|
-
transform
|
|
1484
|
-
});
|
|
1485
|
-
}
|
|
1486
|
-
function getResolvedTargetsEquality(params) {
|
|
1487
|
-
if (params.expected === null && params.generated === null) {
|
|
1488
|
-
return {
|
|
1489
|
-
isEqual: true,
|
|
1490
|
-
expected: params.expected,
|
|
1491
|
-
generated: params.generated
|
|
1492
|
-
};
|
|
1493
|
-
}
|
|
1494
|
-
if (params.expected === null || params.generated === null) {
|
|
1495
|
-
return {
|
|
1496
|
-
isEqual: false,
|
|
1497
|
-
expected: params.expected,
|
|
1498
|
-
generated: params.generated
|
|
1499
|
-
};
|
|
1500
|
-
}
|
|
1501
|
-
let expectedString = checkSql_utils.getResolvedTargetComparableString({
|
|
1502
|
-
target: params.expected,
|
|
1503
|
-
nullAsOptional: false,
|
|
1504
|
-
nullAsUndefined: false,
|
|
1505
|
-
inferLiterals: params.inferLiterals
|
|
1506
|
-
});
|
|
1507
|
-
let generatedString = checkSql_utils.getResolvedTargetComparableString({
|
|
1508
|
-
target: params.generated,
|
|
1509
|
-
nullAsOptional: params.nullAsOptional,
|
|
1510
|
-
nullAsUndefined: params.nullAsUndefined,
|
|
1511
|
-
inferLiterals: params.inferLiterals
|
|
1512
|
-
});
|
|
1513
|
-
if (expectedString === null || generatedString === null) {
|
|
1514
|
-
return {
|
|
1515
|
-
isEqual: false,
|
|
1516
|
-
expected: params.expected,
|
|
1517
|
-
generated: params.generated
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
expectedString = expectedString.replace(/'/g, '"');
|
|
1521
|
-
generatedString = generatedString.replace(/'/g, '"');
|
|
1522
|
-
expectedString = expectedString.split(", ").sort().join(", ");
|
|
1523
|
-
generatedString = generatedString.split(", ").sort().join(", ");
|
|
1524
|
-
if (params.transform !== void 0) {
|
|
1525
|
-
generatedString = checkSql_utils.transformTypes(generatedString, params.transform);
|
|
1526
|
-
}
|
|
1527
|
-
return {
|
|
1528
|
-
isEqual: expectedString === generatedString,
|
|
1529
|
-
expected: params.expected,
|
|
1530
|
-
generated: params.generated
|
|
1531
|
-
};
|
|
1532
|
-
}
|
|
1533
1148
|
const createRule = utils.ESLintUtils.RuleCreator(() => `https://github.com/gajus/eslint-plugin-slonik`);
|
|
1534
1149
|
const checkSql = createRule({
|
|
1535
1150
|
name: "check-sql",
|
|
1536
1151
|
meta: {
|
|
1537
1152
|
fixable: "code",
|
|
1538
1153
|
docs: {
|
|
1539
|
-
description: "
|
|
1154
|
+
description: "Validate SQL queries against the database schema"
|
|
1540
1155
|
},
|
|
1541
1156
|
messages,
|
|
1542
1157
|
type: "problem",
|