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
|
@@ -0,0 +1,1514 @@
|
|
|
1
|
+
import { o as InvalidQueryError, q as defaultTypeMapping, s as doesMatchPattern, n as normalizeIndent, u as objectKeysNonEmpty, v as InvalidConfigError, w as shouldLintFile, x as reportInvalidConfig, y as reportDuplicateColumns, z as reportPostgresError, A as reportInvalidQueryError, B as reportBaseError, C as reportMissingTypeAnnotations, E as getFinalResolvedTargetString, F as reportInvalidTypeAnnotations, G as reportIncorrectTypeAnnotations, f as fmap, H as isIdentifier, J as isMemberExpression, K as getResolvedTargetComparableString, L as transformTypes, M as getResolvedTargetString } from './eslint-plugin-slonik.C0xTyWZ2.mjs';
|
|
2
|
+
import { TSESTree, ESLintUtils } from '@typescript-eslint/utils';
|
|
3
|
+
import { match } from 'ts-pattern';
|
|
4
|
+
import * as E from 'fp-ts/lib/Either.js';
|
|
5
|
+
import { pipe, flow } from 'fp-ts/lib/function.js';
|
|
6
|
+
import 'fp-ts/lib/Option.js';
|
|
7
|
+
import 'fp-ts/lib/TaskEither.js';
|
|
8
|
+
import * as J from 'fp-ts/lib/Json.js';
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import fs from 'fs';
|
|
12
|
+
import { createSyncFn } from 'synckit';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import z from 'zod';
|
|
15
|
+
import { createRequire } from 'module';
|
|
16
|
+
|
|
17
|
+
const PRIMITIVES = {
|
|
18
|
+
string: "string",
|
|
19
|
+
number: "number",
|
|
20
|
+
boolean: "boolean",
|
|
21
|
+
false: "false",
|
|
22
|
+
true: "true",
|
|
23
|
+
null: "null",
|
|
24
|
+
undefined: "undefined",
|
|
25
|
+
any: "any"
|
|
26
|
+
};
|
|
27
|
+
function getResolvedTargetByTypeNode(params) {
|
|
28
|
+
const typeText = params.parser.esTreeNodeToTSNodeMap.get(params.typeNode).getText();
|
|
29
|
+
if (isReservedType(typeText, params.reservedTypes)) {
|
|
30
|
+
return { kind: "type", value: typeText };
|
|
31
|
+
}
|
|
32
|
+
switch (params.typeNode.type) {
|
|
33
|
+
case TSESTree.AST_NODE_TYPES.TSLiteralType:
|
|
34
|
+
return handleLiteralType(params.typeNode);
|
|
35
|
+
case TSESTree.AST_NODE_TYPES.TSUnionType:
|
|
36
|
+
return {
|
|
37
|
+
kind: "union",
|
|
38
|
+
value: params.typeNode.types.map(
|
|
39
|
+
(type) => getResolvedTargetByTypeNode({ ...params, typeNode: type })
|
|
40
|
+
)
|
|
41
|
+
};
|
|
42
|
+
case TSESTree.AST_NODE_TYPES.TSNullKeyword:
|
|
43
|
+
return { kind: "type", value: "null" };
|
|
44
|
+
case TSESTree.AST_NODE_TYPES.TSUndefinedKeyword:
|
|
45
|
+
return { kind: "type", value: "undefined" };
|
|
46
|
+
case TSESTree.AST_NODE_TYPES.TSTypeLiteral:
|
|
47
|
+
return handleTypeLiteral(params.typeNode, params);
|
|
48
|
+
case TSESTree.AST_NODE_TYPES.TSTypeReference:
|
|
49
|
+
return handleTypeReference(params.typeNode, params);
|
|
50
|
+
case TSESTree.AST_NODE_TYPES.TSIntersectionType:
|
|
51
|
+
return handleIntersectionType(params.typeNode, params);
|
|
52
|
+
case TSESTree.AST_NODE_TYPES.TSArrayType:
|
|
53
|
+
return {
|
|
54
|
+
kind: "array",
|
|
55
|
+
value: getResolvedTargetByTypeNode({
|
|
56
|
+
...params,
|
|
57
|
+
typeNode: params.typeNode.elementType
|
|
58
|
+
})
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return { kind: "type", value: typeText };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function isReservedType(typeText, reservedTypes) {
|
|
65
|
+
return reservedTypes.has(typeText) || reservedTypes.has(`${typeText}[]`);
|
|
66
|
+
}
|
|
67
|
+
function handleLiteralType(typeNode) {
|
|
68
|
+
return typeNode.literal.type === TSESTree.AST_NODE_TYPES.Literal ? { kind: "type", value: `'${typeNode.literal.value}'` } : { kind: "type", value: "unknown" };
|
|
69
|
+
}
|
|
70
|
+
function handleTypeLiteral(typeNode, params) {
|
|
71
|
+
const properties = typeNode.members.flatMap((member) => {
|
|
72
|
+
if (member.type !== TSESTree.AST_NODE_TYPES.TSPropertySignature || !member.typeAnnotation) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
const key = extractPropertyKey(member.key);
|
|
76
|
+
if (!key) return [];
|
|
77
|
+
const propertyName = member.optional ? `${key}?` : key;
|
|
78
|
+
const propertyType = getResolvedTargetByTypeNode({
|
|
79
|
+
...params,
|
|
80
|
+
typeNode: member.typeAnnotation.typeAnnotation
|
|
81
|
+
});
|
|
82
|
+
return [[propertyName, propertyType]];
|
|
83
|
+
});
|
|
84
|
+
return { kind: "object", value: properties };
|
|
85
|
+
}
|
|
86
|
+
function extractPropertyKey(key) {
|
|
87
|
+
switch (key.type) {
|
|
88
|
+
case TSESTree.AST_NODE_TYPES.Identifier:
|
|
89
|
+
return key.name;
|
|
90
|
+
case TSESTree.AST_NODE_TYPES.Literal:
|
|
91
|
+
return String(key.value);
|
|
92
|
+
default:
|
|
93
|
+
return void 0;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
function handleTypeReference(typeNode, params) {
|
|
97
|
+
if (typeNode.typeName.type !== TSESTree.AST_NODE_TYPES.Identifier && typeNode.typeName.type !== TSESTree.AST_NODE_TYPES.TSQualifiedName) {
|
|
98
|
+
return { kind: "type", value: "unknown" };
|
|
99
|
+
}
|
|
100
|
+
const typeNameText = params.parser.esTreeNodeToTSNodeMap.get(typeNode.typeName).getText();
|
|
101
|
+
if (params.reservedTypes.has(typeNameText)) {
|
|
102
|
+
return { kind: "type", value: typeNameText };
|
|
103
|
+
}
|
|
104
|
+
if (typeNameText === "Array" && typeNode.typeArguments?.params[0]) {
|
|
105
|
+
return {
|
|
106
|
+
kind: "array",
|
|
107
|
+
syntax: "type-reference",
|
|
108
|
+
value: getResolvedTargetByTypeNode({
|
|
109
|
+
...params,
|
|
110
|
+
typeNode: typeNode.typeArguments.params[0]
|
|
111
|
+
})
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const type = params.checker.getTypeFromTypeNode(
|
|
115
|
+
params.parser.esTreeNodeToTSNodeMap.get(typeNode)
|
|
116
|
+
);
|
|
117
|
+
return resolveType(type, { ...params, typeNode });
|
|
118
|
+
}
|
|
119
|
+
function handleIntersectionType(typeNode, params) {
|
|
120
|
+
const allProperties = typeNode.types.flatMap((type) => {
|
|
121
|
+
const resolved = getResolvedTargetByTypeNode({ ...params, typeNode: type });
|
|
122
|
+
return resolved.kind === "object" ? resolved.value : [];
|
|
123
|
+
});
|
|
124
|
+
return { kind: "object", value: Array.from(new Map(allProperties).entries()) };
|
|
125
|
+
}
|
|
126
|
+
function resolveType(type, params) {
|
|
127
|
+
const typeAsString = params.checker.typeToString(type);
|
|
128
|
+
if (params.reservedTypes.has(typeAsString)) {
|
|
129
|
+
return { kind: "type", value: typeAsString };
|
|
130
|
+
}
|
|
131
|
+
if (params.reservedTypes.has(`${typeAsString}[]`)) {
|
|
132
|
+
return { kind: "array", value: { kind: "type", value: typeAsString.replace("[]", "") } };
|
|
133
|
+
}
|
|
134
|
+
const primitive = getPrimitiveType(type, typeAsString);
|
|
135
|
+
if (primitive) return primitive;
|
|
136
|
+
if (type.isLiteral()) {
|
|
137
|
+
return { kind: "type", value: `'${type.value}'` };
|
|
138
|
+
}
|
|
139
|
+
if (type.isUnion()) {
|
|
140
|
+
return handleUnionTypeReference(type, params);
|
|
141
|
+
}
|
|
142
|
+
if (type.isIntersection()) {
|
|
143
|
+
return handleIntersectionTypeReference(type, params);
|
|
144
|
+
}
|
|
145
|
+
if (params.checker.isArrayType(type)) {
|
|
146
|
+
return handleArrayTypeReferenceFromType(type, params);
|
|
147
|
+
}
|
|
148
|
+
return handleObjectType(type, params);
|
|
149
|
+
}
|
|
150
|
+
function getPrimitiveType(type, typeAsString) {
|
|
151
|
+
if (PRIMITIVES[typeAsString]) {
|
|
152
|
+
return { kind: "type", value: PRIMITIVES[typeAsString] };
|
|
153
|
+
}
|
|
154
|
+
const flagMap = {
|
|
155
|
+
[ts.TypeFlags.String]: "string",
|
|
156
|
+
[ts.TypeFlags.Number]: "number",
|
|
157
|
+
[ts.TypeFlags.Boolean]: "boolean",
|
|
158
|
+
[ts.TypeFlags.Null]: "null",
|
|
159
|
+
[ts.TypeFlags.Undefined]: "undefined",
|
|
160
|
+
[ts.TypeFlags.Any]: "any"
|
|
161
|
+
};
|
|
162
|
+
return flagMap[type.flags] ? { kind: "type", value: flagMap[type.flags] } : null;
|
|
163
|
+
}
|
|
164
|
+
function handleUnionTypeReference(type, params) {
|
|
165
|
+
const types = type.types.map((t) => resolveType(t, params));
|
|
166
|
+
const isBooleanUnionWithNull = types.length === 3 && types.some((t) => t.value === "false") && types.some((t) => t.value === "true") && types.some((t) => t.value === "null");
|
|
167
|
+
if (isBooleanUnionWithNull) {
|
|
168
|
+
return {
|
|
169
|
+
kind: "union",
|
|
170
|
+
value: [
|
|
171
|
+
{ kind: "type", value: "boolean" },
|
|
172
|
+
{ kind: "type", value: "null" }
|
|
173
|
+
]
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { kind: "union", value: types };
|
|
177
|
+
}
|
|
178
|
+
function handleIntersectionTypeReference(type, params) {
|
|
179
|
+
const properties = type.types.flatMap((t) => {
|
|
180
|
+
const resolved = resolveType(t, params);
|
|
181
|
+
return resolved.kind === "object" ? resolved.value : [];
|
|
182
|
+
});
|
|
183
|
+
return { kind: "object", value: properties };
|
|
184
|
+
}
|
|
185
|
+
function handleArrayTypeReferenceFromType(type, params) {
|
|
186
|
+
const typeArguments = type.typeArguments;
|
|
187
|
+
const firstArgument = typeArguments?.[0];
|
|
188
|
+
if (firstArgument) {
|
|
189
|
+
const elementType = resolveType(firstArgument, params);
|
|
190
|
+
return { kind: "array", value: elementType };
|
|
191
|
+
}
|
|
192
|
+
return { kind: "array", value: { kind: "type", value: "unknown" } };
|
|
193
|
+
}
|
|
194
|
+
function handleObjectType(type, params) {
|
|
195
|
+
if (!type.symbol) {
|
|
196
|
+
return { kind: "type", value: type.aliasSymbol?.escapedName.toString() ?? "unknown" };
|
|
197
|
+
}
|
|
198
|
+
if (type.symbol.valueDeclaration) {
|
|
199
|
+
const declaration = type.symbol.valueDeclaration;
|
|
200
|
+
const sourceFile = declaration.getSourceFile();
|
|
201
|
+
const filePath = sourceFile.fileName;
|
|
202
|
+
if (!filePath.includes("node_modules")) {
|
|
203
|
+
return extractObjectProperties(type, params);
|
|
204
|
+
}
|
|
205
|
+
return { kind: "type", value: type.symbol.name };
|
|
206
|
+
}
|
|
207
|
+
if (type.flags === ts.TypeFlags.Object) {
|
|
208
|
+
return extractObjectProperties(type, params);
|
|
209
|
+
}
|
|
210
|
+
return { kind: "object", value: [] };
|
|
211
|
+
}
|
|
212
|
+
function extractObjectProperties(type, params) {
|
|
213
|
+
const properties = type.getProperties().map((property) => {
|
|
214
|
+
const key = property.escapedName.toString();
|
|
215
|
+
const propType = params.checker.getTypeOfSymbolAtLocation(
|
|
216
|
+
property,
|
|
217
|
+
params.parser.esTreeNodeToTSNodeMap.get(params.typeNode)
|
|
218
|
+
);
|
|
219
|
+
const resolvedType = resolveType(propType, params);
|
|
220
|
+
return [key, resolvedType];
|
|
221
|
+
});
|
|
222
|
+
return { kind: "object", value: properties };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isInEditorEnv() {
|
|
226
|
+
if (process.env.CI) return false;
|
|
227
|
+
if (isInGitHooksOrLintStaged()) return false;
|
|
228
|
+
return !!(process.env.VSCODE_PID || process.env.VSCODE_CWD || process.env.JETBRAINS_IDE || process.env.VIM || process.env.NVIM);
|
|
229
|
+
}
|
|
230
|
+
function isInGitHooksOrLintStaged() {
|
|
231
|
+
return !!(process.env.GIT_PARAMS || process.env.VSCODE_GIT_COMMAND || process.env.npm_lifecycle_script?.startsWith("lint-staged"));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const memoized = /* @__PURE__ */ new Map();
|
|
235
|
+
function memoize(params) {
|
|
236
|
+
const { key, value } = params;
|
|
237
|
+
if (memoized.has(key)) {
|
|
238
|
+
return memoized.get(key);
|
|
239
|
+
}
|
|
240
|
+
const result = value();
|
|
241
|
+
memoized.set(key, result);
|
|
242
|
+
return result;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function locateNearestPackageJsonDir(filePath) {
|
|
246
|
+
const dir = path.dirname(filePath);
|
|
247
|
+
const packageJsonFile = path.join(dir, "package.json");
|
|
248
|
+
if (fs.existsSync(packageJsonFile)) {
|
|
249
|
+
return dir;
|
|
250
|
+
}
|
|
251
|
+
return locateNearestPackageJsonDir(dir);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const TSUtils = {
|
|
255
|
+
isTypeUnion: (typeNode) => {
|
|
256
|
+
return typeNode?.kind === ts.SyntaxKind.UnionType;
|
|
257
|
+
},
|
|
258
|
+
isTsUnionType(type) {
|
|
259
|
+
return type.flags === ts.TypeFlags.Union;
|
|
260
|
+
},
|
|
261
|
+
isTsTypeReference(type) {
|
|
262
|
+
return TSUtils.isTsObjectType(type) && type.objectFlags === ts.ObjectFlags.Reference;
|
|
263
|
+
},
|
|
264
|
+
isTsArrayUnionType(checker, type) {
|
|
265
|
+
if (!TSUtils.isTsTypeReference(type)) {
|
|
266
|
+
return false;
|
|
267
|
+
}
|
|
268
|
+
const firstArgument = checker.getTypeArguments(type)[0];
|
|
269
|
+
if (firstArgument === void 0) {
|
|
270
|
+
return false;
|
|
271
|
+
}
|
|
272
|
+
return TSUtils.isTsUnionType(firstArgument);
|
|
273
|
+
},
|
|
274
|
+
isTsObjectType(type) {
|
|
275
|
+
return type.flags === ts.TypeFlags.Object;
|
|
276
|
+
},
|
|
277
|
+
getEnumKind(type) {
|
|
278
|
+
const symbol = type.getSymbol();
|
|
279
|
+
if (!symbol || !(symbol.flags & ts.SymbolFlags.Enum)) {
|
|
280
|
+
return void 0;
|
|
281
|
+
}
|
|
282
|
+
const declarations = symbol.getDeclarations();
|
|
283
|
+
if (!declarations) {
|
|
284
|
+
return void 0;
|
|
285
|
+
}
|
|
286
|
+
let hasString = false;
|
|
287
|
+
let hasNumeric = false;
|
|
288
|
+
const stringValues = [];
|
|
289
|
+
for (const declaration of declarations) {
|
|
290
|
+
if (ts.isEnumDeclaration(declaration)) {
|
|
291
|
+
for (const member of declaration.members) {
|
|
292
|
+
const initializer = member.initializer;
|
|
293
|
+
if (initializer) {
|
|
294
|
+
if (ts.isStringLiteralLike(initializer)) {
|
|
295
|
+
hasString = true;
|
|
296
|
+
stringValues.push(initializer.text);
|
|
297
|
+
}
|
|
298
|
+
if (initializer.kind === ts.SyntaxKind.NumericLiteral) {
|
|
299
|
+
hasNumeric = true;
|
|
300
|
+
}
|
|
301
|
+
} else {
|
|
302
|
+
hasNumeric = true;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (symbol.flags & ts.SymbolFlags.ConstEnum) {
|
|
308
|
+
return { kind: "Const" };
|
|
309
|
+
}
|
|
310
|
+
if (hasString && hasNumeric) {
|
|
311
|
+
return { kind: "Heterogeneous" };
|
|
312
|
+
}
|
|
313
|
+
if (hasString) {
|
|
314
|
+
return { kind: "String", values: stringValues };
|
|
315
|
+
}
|
|
316
|
+
return { kind: "Numeric" };
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const keywords = [
|
|
321
|
+
"WITH",
|
|
322
|
+
"SELECT",
|
|
323
|
+
"FROM",
|
|
324
|
+
"WHERE",
|
|
325
|
+
"GROUP BY",
|
|
326
|
+
"HAVING",
|
|
327
|
+
"WINDOW",
|
|
328
|
+
"ORDER BY",
|
|
329
|
+
"PARTITION BY",
|
|
330
|
+
"LIMIT",
|
|
331
|
+
"OFFSET",
|
|
332
|
+
"INSERT INTO",
|
|
333
|
+
"VALUES",
|
|
334
|
+
"UPDATE",
|
|
335
|
+
"SET",
|
|
336
|
+
"RETURNING",
|
|
337
|
+
"ON",
|
|
338
|
+
"JOIN",
|
|
339
|
+
"INNER JOIN",
|
|
340
|
+
"LEFT JOIN",
|
|
341
|
+
"RIGHT JOIN",
|
|
342
|
+
"FULL JOIN",
|
|
343
|
+
"FULL OUTER JOIN",
|
|
344
|
+
"CROSS JOIN",
|
|
345
|
+
"WHEN",
|
|
346
|
+
"USING",
|
|
347
|
+
"UNION",
|
|
348
|
+
"UNION ALL",
|
|
349
|
+
"INTERSECT",
|
|
350
|
+
"EXCEPT"
|
|
351
|
+
];
|
|
352
|
+
const keywordSet = new Set(keywords);
|
|
353
|
+
function isLastQueryContextOneOf(queryText, keywords2) {
|
|
354
|
+
const contextKeywords = getLastQueryContext(queryText);
|
|
355
|
+
const lastKeyword = contextKeywords[contextKeywords.length - 1];
|
|
356
|
+
return keywords2.some((keyword) => keyword === lastKeyword);
|
|
357
|
+
}
|
|
358
|
+
function getLastQueryContext(queryText) {
|
|
359
|
+
const context = getQueryContext(queryText);
|
|
360
|
+
const iterate = (ctx) => {
|
|
361
|
+
const last = ctx[ctx.length - 1];
|
|
362
|
+
if (Array.isArray(last)) {
|
|
363
|
+
return iterate(last);
|
|
364
|
+
}
|
|
365
|
+
return ctx;
|
|
366
|
+
};
|
|
367
|
+
return iterate(context);
|
|
368
|
+
}
|
|
369
|
+
function getQueryContext(queryText) {
|
|
370
|
+
const tokens = removePgComments(queryText).split(/(\s+|\(|\))/).filter((token) => token.trim() !== "");
|
|
371
|
+
let index = 0;
|
|
372
|
+
function parseQuery() {
|
|
373
|
+
const context = [];
|
|
374
|
+
while (index < tokens.length) {
|
|
375
|
+
const token = tokens[index++].toUpperCase();
|
|
376
|
+
if (token === ")") {
|
|
377
|
+
return context;
|
|
378
|
+
}
|
|
379
|
+
if (token === "(") {
|
|
380
|
+
const subquery = parseQuery();
|
|
381
|
+
if (subquery.length > 0) {
|
|
382
|
+
context.push(subquery);
|
|
383
|
+
}
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
const previousToken = tokens[index - 2]?.toUpperCase();
|
|
387
|
+
const nextToken = tokens[index]?.toUpperCase();
|
|
388
|
+
if (isOneOf(["ORDER", "GROUP", "PARTITION"], token) && nextToken === "BY") {
|
|
389
|
+
index++;
|
|
390
|
+
context.push(`${token} BY`);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
if (token === "JOIN") {
|
|
394
|
+
switch (previousToken) {
|
|
395
|
+
case "INNER":
|
|
396
|
+
case "LEFT":
|
|
397
|
+
case "RIGHT":
|
|
398
|
+
case "FULL":
|
|
399
|
+
case "CROSS":
|
|
400
|
+
context.push(`${previousToken} JOIN`);
|
|
401
|
+
break;
|
|
402
|
+
case "OUTER":
|
|
403
|
+
context.push("FULL OUTER JOIN");
|
|
404
|
+
break;
|
|
405
|
+
}
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
if (keywordSet.has(token)) {
|
|
409
|
+
context.push(token);
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return context;
|
|
414
|
+
}
|
|
415
|
+
return parseQuery();
|
|
416
|
+
}
|
|
417
|
+
function removePgComments(query) {
|
|
418
|
+
return query.replace(/--.*(\r?\n|$)|\/\*[\s\S]*?\*\//g, "").trim();
|
|
419
|
+
}
|
|
420
|
+
function isOneOf(values, value) {
|
|
421
|
+
return values.includes(value);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
const SLONIK_SQL_TOKEN_TYPES = /* @__PURE__ */ new Set([
|
|
425
|
+
// Core SQL tokens from Slonik
|
|
426
|
+
"SqlToken",
|
|
427
|
+
"SqlSqlToken",
|
|
428
|
+
"QuerySqlToken",
|
|
429
|
+
"FragmentSqlToken",
|
|
430
|
+
"SqlFragmentToken",
|
|
431
|
+
"SqlFragment",
|
|
432
|
+
// Return type of sql.fragment
|
|
433
|
+
"ListSqlToken",
|
|
434
|
+
"UnnestSqlToken",
|
|
435
|
+
"IdentifierSqlToken",
|
|
436
|
+
"ArraySqlToken",
|
|
437
|
+
"JsonSqlToken",
|
|
438
|
+
"JsonBinarySqlToken",
|
|
439
|
+
"BinarySqlToken",
|
|
440
|
+
"DateSqlToken",
|
|
441
|
+
"TimestampSqlToken",
|
|
442
|
+
"IntervalSqlToken",
|
|
443
|
+
"UuidSqlToken",
|
|
444
|
+
// Return type of sql.uuid
|
|
445
|
+
// Generic/union types
|
|
446
|
+
"PrimitiveValueExpression",
|
|
447
|
+
"ValueExpression",
|
|
448
|
+
"SqlTokenType"
|
|
449
|
+
]);
|
|
450
|
+
function isSlonikSqlTokenType(typeStr) {
|
|
451
|
+
if (SLONIK_SQL_TOKEN_TYPES.has(typeStr)) {
|
|
452
|
+
return true;
|
|
453
|
+
}
|
|
454
|
+
for (const tokenType of SLONIK_SQL_TOKEN_TYPES) {
|
|
455
|
+
if (typeStr.includes(tokenType)) {
|
|
456
|
+
return true;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
function extractSlonikArrayType(expression) {
|
|
462
|
+
if (expression.type !== "CallExpression") {
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
const callee = expression.callee;
|
|
466
|
+
if (callee.type !== "MemberExpression") {
|
|
467
|
+
return null;
|
|
468
|
+
}
|
|
469
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "array") {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
473
|
+
if (objectName !== "sql") {
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
const typeArg = expression.arguments[1];
|
|
477
|
+
if (!typeArg) {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
if (typeArg.type === "Literal" && typeof typeArg.value === "string") {
|
|
481
|
+
return `${typeArg.value}[]`;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
function getMemberExpressionObjectName(node) {
|
|
486
|
+
if (node.type === "Identifier") {
|
|
487
|
+
return node.name;
|
|
488
|
+
}
|
|
489
|
+
if (node.type === "MemberExpression" && node.object.type === "ThisExpression" && node.property.type === "Identifier") {
|
|
490
|
+
return node.property.name;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function extractSlonikIdentifier(expression) {
|
|
495
|
+
if (expression.type !== "CallExpression") {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
const callee = expression.callee;
|
|
499
|
+
if (callee.type !== "MemberExpression") {
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "identifier") {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
506
|
+
if (objectName !== "sql") {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
const partsArg = expression.arguments[0];
|
|
510
|
+
if (!partsArg) {
|
|
511
|
+
return null;
|
|
512
|
+
}
|
|
513
|
+
if (partsArg.type === "ArrayExpression") {
|
|
514
|
+
const parts = [];
|
|
515
|
+
for (const element of partsArg.elements) {
|
|
516
|
+
if (element && element.type === "Literal" && typeof element.value === "string") {
|
|
517
|
+
parts.push(element.value);
|
|
518
|
+
} else {
|
|
519
|
+
return null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if (parts.length === 0) {
|
|
523
|
+
return null;
|
|
524
|
+
}
|
|
525
|
+
return parts.map((part) => `"${part}"`).join(".");
|
|
526
|
+
}
|
|
527
|
+
return null;
|
|
528
|
+
}
|
|
529
|
+
function isSlonikJoinCall(expression) {
|
|
530
|
+
if (expression.type !== "CallExpression") {
|
|
531
|
+
return false;
|
|
532
|
+
}
|
|
533
|
+
const callee = expression.callee;
|
|
534
|
+
if (callee.type !== "MemberExpression") {
|
|
535
|
+
return false;
|
|
536
|
+
}
|
|
537
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "join") {
|
|
538
|
+
return false;
|
|
539
|
+
}
|
|
540
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
541
|
+
return objectName === "sql";
|
|
542
|
+
}
|
|
543
|
+
function extractSlonikFragment(expression) {
|
|
544
|
+
if (expression.type !== "TaggedTemplateExpression") {
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const tag = expression.tag;
|
|
548
|
+
if (tag.type !== "MemberExpression") {
|
|
549
|
+
return null;
|
|
550
|
+
}
|
|
551
|
+
if (tag.property.type !== "Identifier" || tag.property.name !== "fragment") {
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
const objectName = getMemberExpressionObjectName(tag.object);
|
|
555
|
+
if (objectName !== "sql") {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const quasi = expression.quasi;
|
|
559
|
+
let sqlText = "";
|
|
560
|
+
const nestedExpressions = [];
|
|
561
|
+
for (const [i, templateElement] of quasi.quasis.entries()) {
|
|
562
|
+
sqlText += templateElement.value.raw;
|
|
563
|
+
if (!templateElement.tail && quasi.expressions[i]) {
|
|
564
|
+
nestedExpressions.push(quasi.expressions[i]);
|
|
565
|
+
sqlText += `\${__FRAGMENT_EXPR_${nestedExpressions.length - 1}__}`;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return { sqlText, expressions: nestedExpressions };
|
|
569
|
+
}
|
|
570
|
+
function extractSlonikUnnestTypes(expression) {
|
|
571
|
+
if (expression.type !== "CallExpression") {
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
const callee = expression.callee;
|
|
575
|
+
if (callee.type !== "MemberExpression") {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "unnest") {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
582
|
+
if (objectName !== "sql") {
|
|
583
|
+
return null;
|
|
584
|
+
}
|
|
585
|
+
const typeArg = expression.arguments[1];
|
|
586
|
+
if (!typeArg) {
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
if (typeArg.type === "ArrayExpression") {
|
|
590
|
+
const types = [];
|
|
591
|
+
for (const element of typeArg.elements) {
|
|
592
|
+
if (element && element.type === "Literal" && typeof element.value === "string") {
|
|
593
|
+
types.push(`${element.value}[]`);
|
|
594
|
+
} else {
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
return types.length > 0 ? types : null;
|
|
599
|
+
}
|
|
600
|
+
return null;
|
|
601
|
+
}
|
|
602
|
+
function mapTemplateLiteralToQueryText(quasi, parser, checker, options, sourceCode) {
|
|
603
|
+
let $idx = 0;
|
|
604
|
+
let $queryText = "";
|
|
605
|
+
const sourcemaps = [];
|
|
606
|
+
for (const [quasiIdx, $quasi] of quasi.quasis.entries()) {
|
|
607
|
+
$queryText += $quasi.value.raw;
|
|
608
|
+
if ($quasi.tail) {
|
|
609
|
+
break;
|
|
610
|
+
}
|
|
611
|
+
const position = $queryText.length;
|
|
612
|
+
const expression = quasi.expressions[quasiIdx];
|
|
613
|
+
const slonikArrayType = extractSlonikArrayType(expression);
|
|
614
|
+
if (slonikArrayType !== null) {
|
|
615
|
+
const placeholder2 = `$${++$idx}::${slonikArrayType}`;
|
|
616
|
+
$queryText += placeholder2;
|
|
617
|
+
sourcemaps.push({
|
|
618
|
+
original: {
|
|
619
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
620
|
+
end: expression.range[1] - quasi.range[0],
|
|
621
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
622
|
+
},
|
|
623
|
+
generated: {
|
|
624
|
+
start: position,
|
|
625
|
+
end: position + placeholder2.length,
|
|
626
|
+
text: placeholder2
|
|
627
|
+
},
|
|
628
|
+
offset: 0
|
|
629
|
+
});
|
|
630
|
+
continue;
|
|
631
|
+
}
|
|
632
|
+
const slonikIdentifier = extractSlonikIdentifier(expression);
|
|
633
|
+
if (slonikIdentifier !== null) {
|
|
634
|
+
$queryText += slonikIdentifier;
|
|
635
|
+
sourcemaps.push({
|
|
636
|
+
original: {
|
|
637
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
638
|
+
end: expression.range[1] - quasi.range[0],
|
|
639
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
640
|
+
},
|
|
641
|
+
generated: {
|
|
642
|
+
start: position,
|
|
643
|
+
end: position + slonikIdentifier.length,
|
|
644
|
+
text: slonikIdentifier
|
|
645
|
+
},
|
|
646
|
+
offset: 0
|
|
647
|
+
});
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (isSlonikJoinCall(expression)) {
|
|
651
|
+
const placeholder2 = `$${++$idx}`;
|
|
652
|
+
$queryText += placeholder2;
|
|
653
|
+
sourcemaps.push({
|
|
654
|
+
original: {
|
|
655
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
656
|
+
end: expression.range[1] - quasi.range[0],
|
|
657
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
658
|
+
},
|
|
659
|
+
generated: {
|
|
660
|
+
start: position,
|
|
661
|
+
end: position + placeholder2.length,
|
|
662
|
+
text: placeholder2
|
|
663
|
+
},
|
|
664
|
+
offset: 0
|
|
665
|
+
});
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
const slonikUnnestTypes = extractSlonikUnnestTypes(expression);
|
|
669
|
+
if (slonikUnnestTypes !== null) {
|
|
670
|
+
const placeholders = slonikUnnestTypes.map((type) => `$${++$idx}::${type}`);
|
|
671
|
+
const placeholder2 = `unnest(${placeholders.join(", ")})`;
|
|
672
|
+
$queryText += placeholder2;
|
|
673
|
+
sourcemaps.push({
|
|
674
|
+
original: {
|
|
675
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
676
|
+
end: expression.range[1] - quasi.range[0],
|
|
677
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
678
|
+
},
|
|
679
|
+
generated: {
|
|
680
|
+
start: position,
|
|
681
|
+
end: position + placeholder2.length,
|
|
682
|
+
text: placeholder2
|
|
683
|
+
},
|
|
684
|
+
offset: 0
|
|
685
|
+
});
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const slonikFragment = extractSlonikFragment(expression);
|
|
689
|
+
if (slonikFragment !== null) {
|
|
690
|
+
let fragmentSql = slonikFragment.sqlText;
|
|
691
|
+
for (let i = 0; i < slonikFragment.expressions.length; i++) {
|
|
692
|
+
const nestedExpr = slonikFragment.expressions[i];
|
|
693
|
+
const nestedPgType = pipe(
|
|
694
|
+
mapExpressionToTsTypeString({ expression: nestedExpr, parser, checker }),
|
|
695
|
+
(params) => getPgTypeFromTsType({ ...params, checker, options })
|
|
696
|
+
);
|
|
697
|
+
let nestedPlaceholder;
|
|
698
|
+
if (E.isLeft(nestedPgType) || nestedPgType.right === null) {
|
|
699
|
+
nestedPlaceholder = `$${++$idx}`;
|
|
700
|
+
} else if (nestedPgType.right.kind === "literal") {
|
|
701
|
+
nestedPlaceholder = nestedPgType.right.value;
|
|
702
|
+
} else {
|
|
703
|
+
nestedPlaceholder = `$${++$idx}::${nestedPgType.right.cast}`;
|
|
704
|
+
}
|
|
705
|
+
fragmentSql = fragmentSql.replace(`\${__FRAGMENT_EXPR_${i}__}`, nestedPlaceholder);
|
|
706
|
+
}
|
|
707
|
+
$queryText += fragmentSql;
|
|
708
|
+
sourcemaps.push({
|
|
709
|
+
original: {
|
|
710
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
711
|
+
end: expression.range[1] - quasi.range[0],
|
|
712
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
713
|
+
},
|
|
714
|
+
generated: {
|
|
715
|
+
start: position,
|
|
716
|
+
end: position + fragmentSql.length,
|
|
717
|
+
text: fragmentSql
|
|
718
|
+
},
|
|
719
|
+
offset: 0
|
|
720
|
+
});
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
const pgType = pipe(
|
|
724
|
+
mapExpressionToTsTypeString({ expression, parser, checker }),
|
|
725
|
+
(params) => getPgTypeFromTsType({ ...params, checker, options })
|
|
726
|
+
);
|
|
727
|
+
if (E.isLeft(pgType)) {
|
|
728
|
+
return E.left(InvalidQueryError.of(pgType.left, expression));
|
|
729
|
+
}
|
|
730
|
+
const pgTypeValue = pgType.right;
|
|
731
|
+
if (pgTypeValue === null) {
|
|
732
|
+
const placeholder2 = `$${++$idx}`;
|
|
733
|
+
$queryText += placeholder2;
|
|
734
|
+
sourcemaps.push({
|
|
735
|
+
original: {
|
|
736
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1),
|
|
737
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
738
|
+
end: expression.range[1] - quasi.range[0] + 1
|
|
739
|
+
},
|
|
740
|
+
generated: {
|
|
741
|
+
text: placeholder2,
|
|
742
|
+
start: position,
|
|
743
|
+
end: position + placeholder2.length
|
|
744
|
+
},
|
|
745
|
+
offset: 0
|
|
746
|
+
});
|
|
747
|
+
continue;
|
|
748
|
+
}
|
|
749
|
+
if (pgTypeValue.kind === "literal") {
|
|
750
|
+
const placeholder2 = pgTypeValue.value;
|
|
751
|
+
$queryText += placeholder2;
|
|
752
|
+
sourcemaps.push({
|
|
753
|
+
original: {
|
|
754
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
755
|
+
end: expression.range[1] - quasi.range[0] + 1,
|
|
756
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
757
|
+
},
|
|
758
|
+
generated: {
|
|
759
|
+
start: position,
|
|
760
|
+
end: position + placeholder2.length,
|
|
761
|
+
text: placeholder2
|
|
762
|
+
},
|
|
763
|
+
offset: 0
|
|
764
|
+
});
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
const escapePgValue = (text) => text.replace(/'/g, "''");
|
|
768
|
+
if (pgTypeValue.kind === "one-of" && $queryText.trimEnd().endsWith("=") && isLastQueryContextOneOf($queryText, ["SELECT", "ON", "WHERE", "WHEN", "HAVING", "RETURNING"])) {
|
|
769
|
+
const textFromEquals = $queryText.slice($queryText.lastIndexOf("="));
|
|
770
|
+
const placeholder2 = `IN (${pgTypeValue.types.map((t) => `'${escapePgValue(t)}'`).join(", ")})`;
|
|
771
|
+
const expressionText = sourceCode.text.slice(
|
|
772
|
+
expression.range[0] - 2,
|
|
773
|
+
expression.range[1] + 1
|
|
774
|
+
);
|
|
775
|
+
$queryText = $queryText.replace(/(=)\s*$/, "");
|
|
776
|
+
$queryText += placeholder2;
|
|
777
|
+
sourcemaps.push({
|
|
778
|
+
original: {
|
|
779
|
+
start: expression.range[0] - quasi.range[0] - 2 - textFromEquals.length,
|
|
780
|
+
end: expression.range[1] - quasi.range[0] + 2 - textFromEquals.length,
|
|
781
|
+
text: `${textFromEquals}${expressionText}`
|
|
782
|
+
},
|
|
783
|
+
generated: {
|
|
784
|
+
start: position - textFromEquals.length + 1,
|
|
785
|
+
end: position + placeholder2.length - textFromEquals.length,
|
|
786
|
+
text: placeholder2
|
|
787
|
+
},
|
|
788
|
+
offset: textFromEquals.length
|
|
789
|
+
});
|
|
790
|
+
continue;
|
|
791
|
+
}
|
|
792
|
+
const placeholder = `$${++$idx}::${pgTypeValue.cast}`;
|
|
793
|
+
$queryText += placeholder;
|
|
794
|
+
sourcemaps.push({
|
|
795
|
+
original: {
|
|
796
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
797
|
+
end: expression.range[1] - quasi.range[0],
|
|
798
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
799
|
+
},
|
|
800
|
+
generated: {
|
|
801
|
+
start: position,
|
|
802
|
+
end: position + placeholder.length,
|
|
803
|
+
text: placeholder
|
|
804
|
+
},
|
|
805
|
+
offset: 0
|
|
806
|
+
});
|
|
807
|
+
}
|
|
808
|
+
return E.right({ text: $queryText, sourcemaps });
|
|
809
|
+
}
|
|
810
|
+
function mapExpressionToTsTypeString(params) {
|
|
811
|
+
const tsNode = params.parser.esTreeNodeToTSNodeMap.get(params.expression);
|
|
812
|
+
const tsType = params.checker.getTypeAtLocation(tsNode);
|
|
813
|
+
return {
|
|
814
|
+
node: tsNode,
|
|
815
|
+
type: tsType
|
|
816
|
+
};
|
|
817
|
+
}
|
|
818
|
+
const tsTypeToPgTypeMap = {
|
|
819
|
+
number: "int",
|
|
820
|
+
string: "text",
|
|
821
|
+
boolean: "boolean",
|
|
822
|
+
bigint: "bigint",
|
|
823
|
+
any: "text",
|
|
824
|
+
unknown: "text"
|
|
825
|
+
};
|
|
826
|
+
const tsFlagToPgTypeMap = {
|
|
827
|
+
[ts.TypeFlags.String]: "text",
|
|
828
|
+
[ts.TypeFlags.Number]: "int",
|
|
829
|
+
[ts.TypeFlags.Boolean]: "boolean",
|
|
830
|
+
[ts.TypeFlags.BigInt]: "bigint",
|
|
831
|
+
[ts.TypeFlags.NumberLiteral]: "int",
|
|
832
|
+
[ts.TypeFlags.StringLiteral]: "text",
|
|
833
|
+
[ts.TypeFlags.BooleanLiteral]: "boolean",
|
|
834
|
+
[ts.TypeFlags.BigIntLiteral]: "bigint"
|
|
835
|
+
};
|
|
836
|
+
function getPgTypeFromTsTypeUnion(params) {
|
|
837
|
+
const { types, checker, options } = params;
|
|
838
|
+
const nonNullTypes = types.filter((t) => (t.flags & ts.TypeFlags.Null) === 0);
|
|
839
|
+
if (nonNullTypes.length === 0) {
|
|
840
|
+
return E.right(null);
|
|
841
|
+
}
|
|
842
|
+
const hasSlonikToken = nonNullTypes.some((t) => {
|
|
843
|
+
const typeStr = checker.typeToString(t);
|
|
844
|
+
return isSlonikSqlTokenType(typeStr);
|
|
845
|
+
});
|
|
846
|
+
if (hasSlonikToken) {
|
|
847
|
+
return E.right(null);
|
|
848
|
+
}
|
|
849
|
+
const isStringLiterals = nonNullTypes.every((t) => t.flags & ts.TypeFlags.StringLiteral);
|
|
850
|
+
if (isStringLiterals) {
|
|
851
|
+
return E.right({
|
|
852
|
+
kind: "one-of",
|
|
853
|
+
types: nonNullTypes.map((t) => t.value),
|
|
854
|
+
cast: "text"
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
const results = nonNullTypes.map((t) => checkType({ checker, type: t, options }));
|
|
858
|
+
const strategies = [];
|
|
859
|
+
for (const result of results) {
|
|
860
|
+
if (E.isLeft(result)) {
|
|
861
|
+
return result;
|
|
862
|
+
}
|
|
863
|
+
if (result.right !== null) {
|
|
864
|
+
strategies.push(result.right);
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
if (strategies.length === 0) {
|
|
868
|
+
const typesStr = nonNullTypes.map((t) => checker.typeToString(t)).join(", ");
|
|
869
|
+
return E.left(`No PostgreSQL type could be inferred for the union members: ${typesStr}`);
|
|
870
|
+
}
|
|
871
|
+
const firstStrategy = strategies[0];
|
|
872
|
+
const mixedTypes = [firstStrategy.cast];
|
|
873
|
+
for (let i = 1; i < strategies.length; i++) {
|
|
874
|
+
const strategy = strategies[i];
|
|
875
|
+
if (strategy.cast !== firstStrategy.cast) {
|
|
876
|
+
mixedTypes.push(strategy.cast);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (mixedTypes.length > 1) {
|
|
880
|
+
return E.left(
|
|
881
|
+
`Union types must result in the same PostgreSQL type (found ${mixedTypes.join(", ")})`
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
return E.right(firstStrategy);
|
|
885
|
+
}
|
|
886
|
+
function getPgTypeFromTsType(params) {
|
|
887
|
+
const { checker, node, type, options } = params;
|
|
888
|
+
const typeStr = checker.typeToString(type);
|
|
889
|
+
if (isSlonikSqlTokenType(typeStr)) {
|
|
890
|
+
return E.right(null);
|
|
891
|
+
}
|
|
892
|
+
if (node.kind === ts.SyntaxKind.ConditionalExpression) {
|
|
893
|
+
const trueType = checker.getTypeAtLocation(node.whenTrue);
|
|
894
|
+
const falseType = checker.getTypeAtLocation(node.whenFalse);
|
|
895
|
+
const trueTypeStr = checker.typeToString(trueType);
|
|
896
|
+
const falseTypeStr = checker.typeToString(falseType);
|
|
897
|
+
if (isSlonikSqlTokenType(trueTypeStr) || isSlonikSqlTokenType(falseTypeStr)) {
|
|
898
|
+
return E.right(null);
|
|
899
|
+
}
|
|
900
|
+
const whenTrue = checkType({
|
|
901
|
+
checker,
|
|
902
|
+
type: trueType,
|
|
903
|
+
options
|
|
904
|
+
});
|
|
905
|
+
const whenFalse = checkType({
|
|
906
|
+
checker,
|
|
907
|
+
type: falseType,
|
|
908
|
+
options
|
|
909
|
+
});
|
|
910
|
+
if (E.isLeft(whenTrue)) {
|
|
911
|
+
return whenTrue;
|
|
912
|
+
}
|
|
913
|
+
if (E.isLeft(whenFalse)) {
|
|
914
|
+
return whenFalse;
|
|
915
|
+
}
|
|
916
|
+
const trueStrategy = whenTrue.right;
|
|
917
|
+
const falseStrategy = whenFalse.right;
|
|
918
|
+
if (trueStrategy === null && falseStrategy === null) {
|
|
919
|
+
return E.right(null);
|
|
920
|
+
}
|
|
921
|
+
if (trueStrategy !== null && falseStrategy !== null && trueStrategy.cast !== falseStrategy.cast) {
|
|
922
|
+
return E.left(
|
|
923
|
+
`Conditional expression must have the same type (true = ${trueStrategy.cast}, false = ${falseStrategy.cast})`
|
|
924
|
+
);
|
|
925
|
+
}
|
|
926
|
+
const strategy = trueStrategy ?? falseStrategy;
|
|
927
|
+
if (strategy === null) {
|
|
928
|
+
return E.right(null);
|
|
929
|
+
}
|
|
930
|
+
return E.right({ kind: "cast", cast: strategy.cast });
|
|
931
|
+
}
|
|
932
|
+
return checkType({ checker, type, options });
|
|
933
|
+
}
|
|
934
|
+
function checkType(params) {
|
|
935
|
+
const { checker, type, options } = params;
|
|
936
|
+
if (type.flags & ts.TypeFlags.Null) {
|
|
937
|
+
return E.right(null);
|
|
938
|
+
}
|
|
939
|
+
const typeStr = checker.typeToString(type);
|
|
940
|
+
if (isSlonikSqlTokenType(typeStr)) {
|
|
941
|
+
return E.right(null);
|
|
942
|
+
}
|
|
943
|
+
const singularType = typeStr.replace(/\[\]$/, "");
|
|
944
|
+
const isArray = typeStr !== singularType;
|
|
945
|
+
const singularPgType = tsTypeToPgTypeMap[singularType];
|
|
946
|
+
if (singularPgType) {
|
|
947
|
+
return E.right({ kind: "cast", cast: isArray ? `${singularPgType}[]` : singularPgType });
|
|
948
|
+
}
|
|
949
|
+
const typesWithOverrides = { ...defaultTypeMapping, ...options.overrides?.types };
|
|
950
|
+
const override = Object.entries(typesWithOverrides).find(
|
|
951
|
+
([, tsType]) => doesMatchPattern({
|
|
952
|
+
pattern: typeof tsType === "string" ? tsType : tsType.parameter,
|
|
953
|
+
text: singularType
|
|
954
|
+
})
|
|
955
|
+
);
|
|
956
|
+
if (override) {
|
|
957
|
+
const [pgType] = override;
|
|
958
|
+
return E.right({ kind: "cast", cast: isArray ? `${pgType}[]` : pgType });
|
|
959
|
+
}
|
|
960
|
+
const enumType = TSUtils.getEnumKind(type);
|
|
961
|
+
if (enumType) {
|
|
962
|
+
switch (enumType.kind) {
|
|
963
|
+
case "Const":
|
|
964
|
+
case "Numeric":
|
|
965
|
+
return E.right({ kind: "cast", cast: "int" });
|
|
966
|
+
case "String":
|
|
967
|
+
return E.right({ kind: "one-of", types: enumType.values, cast: "text" });
|
|
968
|
+
case "Heterogeneous":
|
|
969
|
+
return E.left("Heterogeneous enums are not supported");
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (checker.isArrayType(type)) {
|
|
973
|
+
const elementType = type.typeArguments?.[0];
|
|
974
|
+
if (elementType) {
|
|
975
|
+
return pipe(
|
|
976
|
+
checkType({ checker, type: elementType, options }),
|
|
977
|
+
E.map(
|
|
978
|
+
(pgType) => pgType === null ? null : { kind: "cast", cast: `${pgType.cast}[]` }
|
|
979
|
+
)
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
}
|
|
983
|
+
if (type.isStringLiteral()) {
|
|
984
|
+
return E.right({ kind: "literal", value: `'${type.value}'`, cast: "text" });
|
|
985
|
+
}
|
|
986
|
+
if (type.isNumberLiteral()) {
|
|
987
|
+
return E.right({ kind: "literal", value: `${type.value}`, cast: "int" });
|
|
988
|
+
}
|
|
989
|
+
if (type.isUnion()) {
|
|
990
|
+
return pipe(
|
|
991
|
+
getPgTypeFromTsTypeUnion({ types: type.types, checker, options }),
|
|
992
|
+
E.chain(
|
|
993
|
+
(pgType) => pgType === null ? E.left("Unsupported union type (only null)") : E.right(pgType)
|
|
994
|
+
)
|
|
995
|
+
);
|
|
996
|
+
}
|
|
997
|
+
if (type.flags in tsFlagToPgTypeMap) {
|
|
998
|
+
const pgType = tsFlagToPgTypeMap[type.flags];
|
|
999
|
+
return E.right({ kind: "cast", cast: isArray ? `${pgType}[]` : pgType });
|
|
1000
|
+
}
|
|
1001
|
+
return E.left(normalizeIndent`
|
|
1002
|
+
The type "${typeStr}" has no corresponding PostgreSQL type.
|
|
1003
|
+
Please add it manually using the "overrides.types" option:
|
|
1004
|
+
|
|
1005
|
+
\`\`\`ts
|
|
1006
|
+
{
|
|
1007
|
+
"connections": {
|
|
1008
|
+
...,
|
|
1009
|
+
"overrides": {
|
|
1010
|
+
"types": {
|
|
1011
|
+
"PG TYPE (e.g. 'date')": "${typeStr}"
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
\`\`\`
|
|
1017
|
+
|
|
1018
|
+
Read docs - https://github.com/gajus/eslint-plugin-slonik#type-override-reference
|
|
1019
|
+
`);
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const distDir = fileURLToPath(new URL("../../dist", import.meta.url));
|
|
1023
|
+
function defineWorker(params) {
|
|
1024
|
+
return createSyncFn(path.join(distDir, `./workers/${params.name}.worker.mjs`), {
|
|
1025
|
+
tsRunner: "tsx",
|
|
1026
|
+
timeout: params.timeout
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
const workers = {
|
|
1030
|
+
generateSync: defineWorker({ name: "check-sql", timeout: 1e3 * 60 * 1 })
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const zStringOrRegex = z.union([z.string(), z.object({ regex: z.string() })]);
|
|
1034
|
+
const zBaseTarget = z.object({
|
|
1035
|
+
/**
|
|
1036
|
+
* Transform the end result of the type.
|
|
1037
|
+
*
|
|
1038
|
+
* For example:
|
|
1039
|
+
* - `"{type}[]"` will transform the type to an array
|
|
1040
|
+
* - `["colname", "x_colname"]` will replace `colname` with `x_colname` in the type.
|
|
1041
|
+
* - `["{type}[]", ["colname", x_colname"]]` will do both
|
|
1042
|
+
*/
|
|
1043
|
+
transform: z.union([z.string(), z.array(z.union([z.string(), z.tuple([z.string(), z.string()])]))]).optional(),
|
|
1044
|
+
/**
|
|
1045
|
+
* Transform the (column) field key. Can be one of the following:
|
|
1046
|
+
* - `"snake"` - `userId` → `user_id`
|
|
1047
|
+
* - `"camel"` - `user_id` → `userId`
|
|
1048
|
+
* - `"pascal"` - `user_id` → `UserId`
|
|
1049
|
+
* - `"screaming snake"` - `user_id` → `USER_ID`
|
|
1050
|
+
*/
|
|
1051
|
+
fieldTransform: z.enum(["snake", "pascal", "camel", "screaming snake"]).optional(),
|
|
1052
|
+
/**
|
|
1053
|
+
* Whether or not to skip type annotation.
|
|
1054
|
+
*/
|
|
1055
|
+
skipTypeAnnotations: z.boolean().optional()
|
|
1056
|
+
});
|
|
1057
|
+
const zWrapperTarget = z.object({ wrapper: zStringOrRegex, maxDepth: z.number().optional() }).merge(zBaseTarget);
|
|
1058
|
+
const zTagTarget = z.object({ tag: zStringOrRegex }).merge(zBaseTarget);
|
|
1059
|
+
const zOverrideTypeResolver = z.union([
|
|
1060
|
+
z.string(),
|
|
1061
|
+
z.object({ parameter: zStringOrRegex, return: z.string() })
|
|
1062
|
+
]);
|
|
1063
|
+
const zBaseSchema = z.object({
|
|
1064
|
+
targets: z.union([zWrapperTarget, zTagTarget]).array(),
|
|
1065
|
+
/**
|
|
1066
|
+
* Whether or not keep the connection alive. Change it only if you know what you're doing.
|
|
1067
|
+
*/
|
|
1068
|
+
keepAlive: z.boolean().optional(),
|
|
1069
|
+
/**
|
|
1070
|
+
* Override defaults
|
|
1071
|
+
*/
|
|
1072
|
+
overrides: z.object({
|
|
1073
|
+
types: z.union([
|
|
1074
|
+
z.record(z.enum(objectKeysNonEmpty(defaultTypeMapping)), zOverrideTypeResolver),
|
|
1075
|
+
z.record(z.string(), zOverrideTypeResolver)
|
|
1076
|
+
]),
|
|
1077
|
+
columns: z.record(z.string(), z.string())
|
|
1078
|
+
}).partial().optional(),
|
|
1079
|
+
/**
|
|
1080
|
+
* Use `undefined` instead of `null` when the value is nullable.
|
|
1081
|
+
*/
|
|
1082
|
+
nullAsUndefined: z.boolean().optional(),
|
|
1083
|
+
/**
|
|
1084
|
+
* Mark the property as optional when the value is nullable.
|
|
1085
|
+
*/
|
|
1086
|
+
nullAsOptional: z.boolean().optional(),
|
|
1087
|
+
/**
|
|
1088
|
+
* Specifies whether to infer literals and their types.
|
|
1089
|
+
* Can be a boolean or an array of specific types to infer.
|
|
1090
|
+
*
|
|
1091
|
+
* By default, it will infer all literals.
|
|
1092
|
+
*/
|
|
1093
|
+
inferLiterals: z.union([z.boolean(), z.enum(["number", "string", "boolean"]).array()]).optional()
|
|
1094
|
+
});
|
|
1095
|
+
const zConnectionMigration = z.object({
|
|
1096
|
+
/**
|
|
1097
|
+
* The path where the migration files are located.
|
|
1098
|
+
*/
|
|
1099
|
+
migrationsDir: z.string(),
|
|
1100
|
+
/**
|
|
1101
|
+
* THIS IS NOT THE PRODUCTION DATABASE.
|
|
1102
|
+
*
|
|
1103
|
+
* A connection url to the database.
|
|
1104
|
+
* This is required since in order to run the migrations, a connection to postgres is required.
|
|
1105
|
+
* Will be used only to create and drop the shadow database (see `databaseName`).
|
|
1106
|
+
*/
|
|
1107
|
+
connectionUrl: z.string().optional(),
|
|
1108
|
+
/**
|
|
1109
|
+
* The name of the shadow database that will be created from the migration files.
|
|
1110
|
+
*/
|
|
1111
|
+
databaseName: z.string().optional(),
|
|
1112
|
+
/**
|
|
1113
|
+
* Whether or not should refresh the shadow database when the migration files change.
|
|
1114
|
+
*/
|
|
1115
|
+
watchMode: z.boolean().optional()
|
|
1116
|
+
});
|
|
1117
|
+
const zConnectionUrl = z.object({
|
|
1118
|
+
/**
|
|
1119
|
+
* The connection url to the database
|
|
1120
|
+
*/
|
|
1121
|
+
databaseUrl: z.string()
|
|
1122
|
+
});
|
|
1123
|
+
const zRuleOptionConnection = z.union([
|
|
1124
|
+
zBaseSchema.merge(zConnectionMigration),
|
|
1125
|
+
zBaseSchema.merge(zConnectionUrl)
|
|
1126
|
+
]);
|
|
1127
|
+
const zConfig = z.object({
|
|
1128
|
+
connections: z.union([z.array(zRuleOptionConnection), zRuleOptionConnection])
|
|
1129
|
+
});
|
|
1130
|
+
const UserConfigFile = z.object({
|
|
1131
|
+
useConfigFile: z.boolean()
|
|
1132
|
+
});
|
|
1133
|
+
const Options = z.union([zConfig, UserConfigFile]);
|
|
1134
|
+
const RuleOptions = z.array(Options).min(1).max(1);
|
|
1135
|
+
const defaultInferLiteralOptions = ["string"];
|
|
1136
|
+
|
|
1137
|
+
function getConfigFromFileWithContext(params) {
|
|
1138
|
+
const options = params.context.options[0];
|
|
1139
|
+
if (!isConfigFileRuleOptions(options)) {
|
|
1140
|
+
return options;
|
|
1141
|
+
}
|
|
1142
|
+
return pipe(
|
|
1143
|
+
getConfigFromFile(params.projectDir),
|
|
1144
|
+
E.getOrElseW((message) => {
|
|
1145
|
+
throw new Error(`eslint-plugin-slonik: ${message}`);
|
|
1146
|
+
})
|
|
1147
|
+
);
|
|
1148
|
+
}
|
|
1149
|
+
function getConfigFromFile(projectDir) {
|
|
1150
|
+
try {
|
|
1151
|
+
const configFilePath = path.join(projectDir, "slonik.config.ts");
|
|
1152
|
+
const require = createRequire(import.meta.url);
|
|
1153
|
+
const rawConfig = require(`tsx/cjs/api`).require(configFilePath, configFilePath).default;
|
|
1154
|
+
if (rawConfig === void 0) {
|
|
1155
|
+
throw new InvalidConfigError(`slonik.config.ts must export a default value`);
|
|
1156
|
+
}
|
|
1157
|
+
const config = zConfig.safeParse(rawConfig);
|
|
1158
|
+
if (!config.success) {
|
|
1159
|
+
throw new InvalidConfigError(`slonik.config.ts is invalid: ${config.error.message}`);
|
|
1160
|
+
}
|
|
1161
|
+
return E.right(config.data);
|
|
1162
|
+
} catch (error) {
|
|
1163
|
+
return E.left(`${error}`);
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
function isConfigFileRuleOptions(options) {
|
|
1167
|
+
return "useConfigFile" in options;
|
|
1168
|
+
}
|
|
1169
|
+
function defineConfig(config) {
|
|
1170
|
+
return config;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
const messages = {
|
|
1174
|
+
typeInferenceFailed: "Type inference failed {{error}}",
|
|
1175
|
+
error: "{{error}}",
|
|
1176
|
+
invalidQuery: "Invalid Query: {{error}}",
|
|
1177
|
+
missingTypeAnnotations: "Query is missing type annotation\n Fix with: {{fix}}",
|
|
1178
|
+
incorrectTypeAnnotations: `Query has incorrect type annotation.
|
|
1179
|
+
Expected: {{expected}}
|
|
1180
|
+
Actual: {{actual}}`,
|
|
1181
|
+
invalidTypeAnnotations: `Query has invalid type annotation (SafeQL does not support it. If you think it should, please open an issue)`
|
|
1182
|
+
};
|
|
1183
|
+
function check(params) {
|
|
1184
|
+
const connections = Array.isArray(params.config.connections) ? params.config.connections : [params.config.connections];
|
|
1185
|
+
for (const connection of connections) {
|
|
1186
|
+
for (const target of connection.targets) {
|
|
1187
|
+
checkConnection({ ...params, connection, target });
|
|
1188
|
+
}
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
function isTagMemberValid(expr) {
|
|
1192
|
+
if (isIdentifier(expr.tag)) {
|
|
1193
|
+
return true;
|
|
1194
|
+
}
|
|
1195
|
+
if (isMemberExpression(expr.tag) && isIdentifier(expr.tag.property)) {
|
|
1196
|
+
return true;
|
|
1197
|
+
}
|
|
1198
|
+
return false;
|
|
1199
|
+
}
|
|
1200
|
+
function checkConnection(params) {
|
|
1201
|
+
if ("tag" in params.target) {
|
|
1202
|
+
return checkConnectionByTagExpression({ ...params, target: params.target });
|
|
1203
|
+
}
|
|
1204
|
+
if ("wrapper" in params.target) {
|
|
1205
|
+
return checkConnectionByWrapperExpression({ ...params, target: params.target });
|
|
1206
|
+
}
|
|
1207
|
+
return match(params.target).exhaustive();
|
|
1208
|
+
}
|
|
1209
|
+
const generateSyncE = flow(
|
|
1210
|
+
workers.generateSync,
|
|
1211
|
+
E.chain(J.parse),
|
|
1212
|
+
E.chainW((parsed) => parsed),
|
|
1213
|
+
E.mapLeft((error) => error)
|
|
1214
|
+
);
|
|
1215
|
+
let fatalError;
|
|
1216
|
+
function reportCheck(params) {
|
|
1217
|
+
const { context, tag, connection, target, projectDir, typeParameter, baseNode } = params;
|
|
1218
|
+
if (fatalError !== void 0) {
|
|
1219
|
+
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.";
|
|
1220
|
+
return reportBaseError({ context, error: fatalError, tag, hint });
|
|
1221
|
+
}
|
|
1222
|
+
const nullAsOptional = connection.nullAsOptional ?? false;
|
|
1223
|
+
const nullAsUndefined = connection.nullAsUndefined ?? false;
|
|
1224
|
+
return pipe(
|
|
1225
|
+
E.Do,
|
|
1226
|
+
E.bind("parser", () => {
|
|
1227
|
+
return hasParserServicesWithTypeInformation(context.sourceCode.parserServices) ? E.right(context.sourceCode.parserServices) : E.left(new InvalidConfigError("Parser services are not available"));
|
|
1228
|
+
}),
|
|
1229
|
+
E.bind("checker", ({ parser }) => {
|
|
1230
|
+
return !parser.program ? E.left(new InvalidConfigError("Type checker is not available")) : E.right(parser.program.getTypeChecker());
|
|
1231
|
+
}),
|
|
1232
|
+
E.bindW(
|
|
1233
|
+
"query",
|
|
1234
|
+
({ parser, checker }) => mapTemplateLiteralToQueryText(
|
|
1235
|
+
tag.quasi,
|
|
1236
|
+
parser,
|
|
1237
|
+
checker,
|
|
1238
|
+
params.connection,
|
|
1239
|
+
params.context.sourceCode
|
|
1240
|
+
)
|
|
1241
|
+
),
|
|
1242
|
+
E.bindW("result", ({ query }) => {
|
|
1243
|
+
return generateSyncE({ query, connection, target, projectDir });
|
|
1244
|
+
}),
|
|
1245
|
+
E.fold(
|
|
1246
|
+
(error) => {
|
|
1247
|
+
return match(error).with({ _tag: "InvalidConfigError" }, (error2) => {
|
|
1248
|
+
return reportInvalidConfig({ context, error: error2, tag });
|
|
1249
|
+
}).with({ _tag: "DuplicateColumnsError" }, (error2) => {
|
|
1250
|
+
return reportDuplicateColumns({ context, error: error2, tag });
|
|
1251
|
+
}).with({ _tag: "PostgresError" }, (error2) => {
|
|
1252
|
+
return reportPostgresError({ context, error: error2, tag });
|
|
1253
|
+
}).with({ _tag: "InvalidQueryError" }, (error2) => {
|
|
1254
|
+
return reportInvalidQueryError({ context, error: error2 });
|
|
1255
|
+
}).with(
|
|
1256
|
+
{ _tag: "InvalidMigrationError" },
|
|
1257
|
+
{ _tag: "InvalidMigrationsPathError" },
|
|
1258
|
+
{ _tag: "DatabaseInitializationError" },
|
|
1259
|
+
{ _tag: "InternalError" },
|
|
1260
|
+
(error2) => {
|
|
1261
|
+
if (params.connection.keepAlive === true) {
|
|
1262
|
+
fatalError = error2;
|
|
1263
|
+
}
|
|
1264
|
+
return reportBaseError({ context, error: error2, tag });
|
|
1265
|
+
}
|
|
1266
|
+
).exhaustive();
|
|
1267
|
+
},
|
|
1268
|
+
({ result, checker, parser }) => {
|
|
1269
|
+
const shouldSkipTypeAnnotations = target.skipTypeAnnotations === true;
|
|
1270
|
+
if (shouldSkipTypeAnnotations) {
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const isMissingTypeAnnotations = typeParameter === void 0;
|
|
1274
|
+
if (isMissingTypeAnnotations) {
|
|
1275
|
+
if (result.output === null) {
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
return reportMissingTypeAnnotations({
|
|
1279
|
+
tag,
|
|
1280
|
+
context,
|
|
1281
|
+
baseNode,
|
|
1282
|
+
actual: getFinalResolvedTargetString({
|
|
1283
|
+
target: result.output,
|
|
1284
|
+
nullAsOptional: nullAsOptional ?? false,
|
|
1285
|
+
nullAsUndefined: nullAsUndefined ?? false,
|
|
1286
|
+
transform: target.transform,
|
|
1287
|
+
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1288
|
+
})
|
|
1289
|
+
});
|
|
1290
|
+
}
|
|
1291
|
+
const reservedTypes = memoize({
|
|
1292
|
+
key: `reserved-types:${JSON.stringify(connection.overrides)}`,
|
|
1293
|
+
value: () => {
|
|
1294
|
+
const types = /* @__PURE__ */ new Set();
|
|
1295
|
+
for (const value of Object.values(connection.overrides?.types ?? {})) {
|
|
1296
|
+
types.add(typeof value === "string" ? value : value.return);
|
|
1297
|
+
}
|
|
1298
|
+
for (const columnType of Object.values(connection.overrides?.columns ?? {})) {
|
|
1299
|
+
types.add(columnType);
|
|
1300
|
+
}
|
|
1301
|
+
return types;
|
|
1302
|
+
}
|
|
1303
|
+
});
|
|
1304
|
+
const typeAnnotationState = getTypeAnnotationState({
|
|
1305
|
+
generated: result.output,
|
|
1306
|
+
typeParameter,
|
|
1307
|
+
transform: target.transform,
|
|
1308
|
+
checker,
|
|
1309
|
+
parser,
|
|
1310
|
+
reservedTypes,
|
|
1311
|
+
nullAsOptional,
|
|
1312
|
+
nullAsUndefined,
|
|
1313
|
+
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1314
|
+
});
|
|
1315
|
+
if (typeAnnotationState === "INVALID") {
|
|
1316
|
+
return reportInvalidTypeAnnotations({
|
|
1317
|
+
context,
|
|
1318
|
+
typeParameter
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
if (!typeAnnotationState.isEqual) {
|
|
1322
|
+
return reportIncorrectTypeAnnotations({
|
|
1323
|
+
context,
|
|
1324
|
+
typeParameter,
|
|
1325
|
+
expected: fmap(
|
|
1326
|
+
typeAnnotationState.expected,
|
|
1327
|
+
(expected) => getResolvedTargetString({
|
|
1328
|
+
target: expected,
|
|
1329
|
+
nullAsOptional: false,
|
|
1330
|
+
nullAsUndefined: false,
|
|
1331
|
+
inferLiterals: params.connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1332
|
+
})
|
|
1333
|
+
),
|
|
1334
|
+
actual: fmap(
|
|
1335
|
+
result.output,
|
|
1336
|
+
(output) => getFinalResolvedTargetString({
|
|
1337
|
+
target: output,
|
|
1338
|
+
nullAsOptional: connection.nullAsOptional ?? false,
|
|
1339
|
+
nullAsUndefined: connection.nullAsUndefined ?? false,
|
|
1340
|
+
transform: target.transform,
|
|
1341
|
+
inferLiterals: connection.inferLiterals ?? defaultInferLiteralOptions
|
|
1342
|
+
})
|
|
1343
|
+
)
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
)
|
|
1348
|
+
);
|
|
1349
|
+
}
|
|
1350
|
+
function hasParserServicesWithTypeInformation(parser) {
|
|
1351
|
+
return parser !== void 0 && parser.program !== null;
|
|
1352
|
+
}
|
|
1353
|
+
function checkConnectionByTagExpression(params) {
|
|
1354
|
+
const { context, tag, projectDir, connection, target } = params;
|
|
1355
|
+
const tagAsText = context.sourceCode.getText(tag.tag).replace(/^this\./, "");
|
|
1356
|
+
if (doesMatchPattern({ pattern: target.tag, text: tagAsText })) {
|
|
1357
|
+
return reportCheck({
|
|
1358
|
+
context,
|
|
1359
|
+
tag,
|
|
1360
|
+
connection,
|
|
1361
|
+
target,
|
|
1362
|
+
projectDir,
|
|
1363
|
+
baseNode: tag.tag,
|
|
1364
|
+
typeParameter: tag.typeArguments
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
function getValidParentUntilDepth(node, depth) {
|
|
1369
|
+
if (node.type === "CallExpression" && node.callee.type === "MemberExpression") {
|
|
1370
|
+
return node;
|
|
1371
|
+
}
|
|
1372
|
+
if (depth > 0 && node.parent) {
|
|
1373
|
+
return getValidParentUntilDepth(node.parent, depth - 1);
|
|
1374
|
+
}
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
function checkConnectionByWrapperExpression(params) {
|
|
1378
|
+
const { context, tag, projectDir, connection, target } = params;
|
|
1379
|
+
if (!isTagMemberValid(tag)) {
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
const wrapperNode = getValidParentUntilDepth(tag.parent, target.maxDepth ?? 0);
|
|
1383
|
+
if (wrapperNode === null) {
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
const calleeAsText = context.sourceCode.getText(wrapperNode.callee).replace(/^this\./, "");
|
|
1387
|
+
if (doesMatchPattern({ pattern: target.wrapper, text: calleeAsText })) {
|
|
1388
|
+
return reportCheck({
|
|
1389
|
+
context,
|
|
1390
|
+
tag,
|
|
1391
|
+
connection,
|
|
1392
|
+
target,
|
|
1393
|
+
projectDir,
|
|
1394
|
+
baseNode: wrapperNode.callee,
|
|
1395
|
+
typeParameter: wrapperNode.typeArguments
|
|
1396
|
+
});
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
function getTypeAnnotationState({
|
|
1400
|
+
generated,
|
|
1401
|
+
typeParameter,
|
|
1402
|
+
transform,
|
|
1403
|
+
parser,
|
|
1404
|
+
checker,
|
|
1405
|
+
reservedTypes,
|
|
1406
|
+
nullAsOptional,
|
|
1407
|
+
nullAsUndefined,
|
|
1408
|
+
inferLiterals
|
|
1409
|
+
}) {
|
|
1410
|
+
if (typeParameter.params.length !== 1) {
|
|
1411
|
+
return "INVALID";
|
|
1412
|
+
}
|
|
1413
|
+
const typeNode = typeParameter.params[0];
|
|
1414
|
+
const expected = getResolvedTargetByTypeNode({
|
|
1415
|
+
checker,
|
|
1416
|
+
parser,
|
|
1417
|
+
typeNode,
|
|
1418
|
+
reservedTypes
|
|
1419
|
+
});
|
|
1420
|
+
return getResolvedTargetsEquality({
|
|
1421
|
+
expected,
|
|
1422
|
+
generated,
|
|
1423
|
+
nullAsOptional,
|
|
1424
|
+
nullAsUndefined,
|
|
1425
|
+
inferLiterals,
|
|
1426
|
+
transform
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
function getResolvedTargetsEquality(params) {
|
|
1430
|
+
if (params.expected === null && params.generated === null) {
|
|
1431
|
+
return {
|
|
1432
|
+
isEqual: true,
|
|
1433
|
+
expected: params.expected,
|
|
1434
|
+
generated: params.generated
|
|
1435
|
+
};
|
|
1436
|
+
}
|
|
1437
|
+
if (params.expected === null || params.generated === null) {
|
|
1438
|
+
return {
|
|
1439
|
+
isEqual: false,
|
|
1440
|
+
expected: params.expected,
|
|
1441
|
+
generated: params.generated
|
|
1442
|
+
};
|
|
1443
|
+
}
|
|
1444
|
+
let expectedString = getResolvedTargetComparableString({
|
|
1445
|
+
target: params.expected,
|
|
1446
|
+
nullAsOptional: false,
|
|
1447
|
+
nullAsUndefined: false,
|
|
1448
|
+
inferLiterals: params.inferLiterals
|
|
1449
|
+
});
|
|
1450
|
+
let generatedString = getResolvedTargetComparableString({
|
|
1451
|
+
target: params.generated,
|
|
1452
|
+
nullAsOptional: params.nullAsOptional,
|
|
1453
|
+
nullAsUndefined: params.nullAsUndefined,
|
|
1454
|
+
inferLiterals: params.inferLiterals
|
|
1455
|
+
});
|
|
1456
|
+
if (expectedString === null || generatedString === null) {
|
|
1457
|
+
return {
|
|
1458
|
+
isEqual: false,
|
|
1459
|
+
expected: params.expected,
|
|
1460
|
+
generated: params.generated
|
|
1461
|
+
};
|
|
1462
|
+
}
|
|
1463
|
+
expectedString = expectedString.replace(/'/g, '"');
|
|
1464
|
+
generatedString = generatedString.replace(/'/g, '"');
|
|
1465
|
+
expectedString = expectedString.split(", ").sort().join(", ");
|
|
1466
|
+
generatedString = generatedString.split(", ").sort().join(", ");
|
|
1467
|
+
if (params.transform !== void 0) {
|
|
1468
|
+
generatedString = transformTypes(generatedString, params.transform);
|
|
1469
|
+
}
|
|
1470
|
+
return {
|
|
1471
|
+
isEqual: expectedString === generatedString,
|
|
1472
|
+
expected: params.expected,
|
|
1473
|
+
generated: params.generated
|
|
1474
|
+
};
|
|
1475
|
+
}
|
|
1476
|
+
const createRule = ESLintUtils.RuleCreator(() => `https://github.com/gajus/eslint-plugin-slonik`);
|
|
1477
|
+
const checkSql = createRule({
|
|
1478
|
+
name: "check-sql",
|
|
1479
|
+
meta: {
|
|
1480
|
+
fixable: "code",
|
|
1481
|
+
docs: {
|
|
1482
|
+
description: "Ensure that sql queries have type annotations"
|
|
1483
|
+
},
|
|
1484
|
+
messages,
|
|
1485
|
+
type: "problem",
|
|
1486
|
+
schema: z.toJSONSchema(RuleOptions, { target: "draft-4" })
|
|
1487
|
+
},
|
|
1488
|
+
defaultOptions: [],
|
|
1489
|
+
create(context) {
|
|
1490
|
+
if (!shouldLintFile(context)) {
|
|
1491
|
+
return {};
|
|
1492
|
+
}
|
|
1493
|
+
const projectDir = memoize({
|
|
1494
|
+
key: context.filename,
|
|
1495
|
+
value: () => locateNearestPackageJsonDir(context.filename)
|
|
1496
|
+
});
|
|
1497
|
+
const config = memoize({
|
|
1498
|
+
key: JSON.stringify({ key: "config", options: context.options, projectDir }),
|
|
1499
|
+
value: () => getConfigFromFileWithContext({ context, projectDir })
|
|
1500
|
+
});
|
|
1501
|
+
return {
|
|
1502
|
+
TaggedTemplateExpression(tag) {
|
|
1503
|
+
check({ context, tag, config, projectDir });
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
const rules = {
|
|
1510
|
+
"check-sql": checkSql
|
|
1511
|
+
};
|
|
1512
|
+
|
|
1513
|
+
export { defineConfig as d, rules as r };
|
|
1514
|
+
//# sourceMappingURL=eslint-plugin-slonik.DbzoLz5_.mjs.map
|