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.
Files changed (35) hide show
  1. package/LICENSE +48 -0
  2. package/README.md +368 -0
  3. package/dist/config.cjs +61 -0
  4. package/dist/config.cjs.map +1 -0
  5. package/dist/config.d.cts +192 -0
  6. package/dist/config.d.mts +192 -0
  7. package/dist/config.d.ts +192 -0
  8. package/dist/config.mjs +59 -0
  9. package/dist/config.mjs.map +1 -0
  10. package/dist/index.cjs +27 -0
  11. package/dist/index.cjs.map +1 -0
  12. package/dist/index.d.cts +319 -0
  13. package/dist/index.d.mts +319 -0
  14. package/dist/index.d.ts +319 -0
  15. package/dist/index.mjs +20 -0
  16. package/dist/index.mjs.map +1 -0
  17. package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.cts +611 -0
  18. package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.mts +611 -0
  19. package/dist/shared/eslint-plugin-slonik.1m1xlVmw.d.ts +611 -0
  20. package/dist/shared/eslint-plugin-slonik.BxexVlk1.cjs +1539 -0
  21. package/dist/shared/eslint-plugin-slonik.BxexVlk1.cjs.map +1 -0
  22. package/dist/shared/eslint-plugin-slonik.C0xTyWZ2.mjs +2866 -0
  23. package/dist/shared/eslint-plugin-slonik.C0xTyWZ2.mjs.map +1 -0
  24. package/dist/shared/eslint-plugin-slonik.DbzoLz5_.mjs +1514 -0
  25. package/dist/shared/eslint-plugin-slonik.DbzoLz5_.mjs.map +1 -0
  26. package/dist/shared/eslint-plugin-slonik.rlOTrCdf.cjs +2929 -0
  27. package/dist/shared/eslint-plugin-slonik.rlOTrCdf.cjs.map +1 -0
  28. package/dist/workers/check-sql.worker.cjs +2436 -0
  29. package/dist/workers/check-sql.worker.cjs.map +1 -0
  30. package/dist/workers/check-sql.worker.d.cts +171 -0
  31. package/dist/workers/check-sql.worker.d.mts +171 -0
  32. package/dist/workers/check-sql.worker.d.ts +171 -0
  33. package/dist/workers/check-sql.worker.mjs +2412 -0
  34. package/dist/workers/check-sql.worker.mjs.map +1 -0
  35. 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