eslint-plugin-slonik 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -7
- package/dist/index.cjs +224 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +224 -0
- package/dist/index.mjs.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -74,15 +74,15 @@ export default [
|
|
|
74
74
|
| `sql.unnest([[...]], ['int4','text'])` | ✅ Full | Extracts types → `unnest($1::int4[], $2::text[])` |
|
|
75
75
|
| `sql.identifier(['schema','table'])` | ✅ Full | Embeds → `"schema"."table"` |
|
|
76
76
|
| `` sql.fragment`...` `` | ✅ Full | Embeds SQL content directly |
|
|
77
|
+
| `sql.date(date)` | ✅ Full | Extracts type → `$1::date` |
|
|
78
|
+
| `sql.timestamp(date)` | ✅ Full | Extracts type → `$1::timestamptz` |
|
|
79
|
+
| `sql.interval({...})` | ✅ Full | Extracts type → `$1::interval` |
|
|
80
|
+
| `sql.json(value)` | ✅ Full | Extracts type → `$1::json` |
|
|
81
|
+
| `sql.jsonb(value)` | ✅ Full | Extracts type → `$1::jsonb` |
|
|
82
|
+
| `sql.literalValue(value)` | ✅ Full | Extracts type → `$1` |
|
|
83
|
+
| `sql.uuid(str)` | ✅ Full | Extracts type → `$1::uuid` |
|
|
77
84
|
| `sql.join([...], glue)` | ✅ Skip | Skipped (runtime content) |
|
|
78
85
|
| `sql.binary(buffer)` | ✅ Skip | Skipped |
|
|
79
|
-
| `sql.date(date)` | ✅ Skip | Skipped |
|
|
80
|
-
| `sql.timestamp(date)` | ✅ Skip | Skipped |
|
|
81
|
-
| `sql.interval({...})` | ✅ Skip | Skipped |
|
|
82
|
-
| `sql.json(value)` | ✅ Skip | Skipped |
|
|
83
|
-
| `sql.jsonb(value)` | ✅ Skip | Skipped |
|
|
84
|
-
| `sql.uuid(str)` | ✅ Skip | Skipped |
|
|
85
|
-
| `sql.literalValue(str)` | ✅ Skip | Skipped |
|
|
86
86
|
|
|
87
87
|
### How It Works
|
|
88
88
|
|
|
@@ -107,6 +107,42 @@ sql.type(z.object({ id: z.number() }))`
|
|
|
107
107
|
SELECT id FROM users ${whereClause}
|
|
108
108
|
`;
|
|
109
109
|
// → Validates: SELECT id FROM users WHERE active = true
|
|
110
|
+
|
|
111
|
+
// sql.date for date values
|
|
112
|
+
sql.type(z.object({ id: z.number() }))`
|
|
113
|
+
SELECT id FROM events WHERE event_date = ${sql.date(myDate)}
|
|
114
|
+
`;
|
|
115
|
+
// → Validates: SELECT id FROM events WHERE event_date = $1::date
|
|
116
|
+
|
|
117
|
+
// sql.timestamp for timestamp values
|
|
118
|
+
sql.type(z.object({ id: z.number() }))`
|
|
119
|
+
SELECT id FROM events WHERE created_at = ${sql.timestamp(myTimestamp)}
|
|
120
|
+
`;
|
|
121
|
+
// → Validates: SELECT id FROM events WHERE created_at = $1::timestamptz
|
|
122
|
+
|
|
123
|
+
// sql.interval for interval values
|
|
124
|
+
sql.type(z.object({ id: z.number() }))`
|
|
125
|
+
SELECT id FROM events WHERE created_at > NOW() - ${sql.interval({ days: 7 })}
|
|
126
|
+
`;
|
|
127
|
+
// → Validates: SELECT id FROM events WHERE created_at > NOW() - $1::interval
|
|
128
|
+
|
|
129
|
+
// sql.json and sql.jsonb for JSON values
|
|
130
|
+
sql.type(z.object({ id: z.number() }))`
|
|
131
|
+
INSERT INTO settings (config) VALUES (${sql.jsonb({ theme: 'dark' })})
|
|
132
|
+
`;
|
|
133
|
+
// → Validates: INSERT INTO settings (config) VALUES ($1::jsonb)
|
|
134
|
+
|
|
135
|
+
// sql.literalValue for literal SQL values
|
|
136
|
+
sql.type(z.object({ result: z.string() }))`
|
|
137
|
+
SELECT ${sql.literalValue('hello')} AS result
|
|
138
|
+
`;
|
|
139
|
+
// → Validates: SELECT $1 AS result
|
|
140
|
+
|
|
141
|
+
// sql.uuid for UUID values
|
|
142
|
+
sql.type(z.object({ id: z.number() }))`
|
|
143
|
+
SELECT id FROM users WHERE external_id = ${sql.uuid(externalId)}
|
|
144
|
+
`;
|
|
145
|
+
// → Validates: SELECT id FROM users WHERE external_id = $1::uuid
|
|
110
146
|
```
|
|
111
147
|
|
|
112
148
|
**Graceful Skip** means the plugin recognizes Slonik tokens and skips validation for those expressions, preventing false positives:
|
package/dist/index.cjs
CHANGED
|
@@ -379,6 +379,104 @@ function isSlonikJoinCall(expression) {
|
|
|
379
379
|
const objectName = getMemberExpressionObjectName(callee.object);
|
|
380
380
|
return objectName === "sql";
|
|
381
381
|
}
|
|
382
|
+
function isSlonikDateCall(expression) {
|
|
383
|
+
if (expression.type !== "CallExpression") {
|
|
384
|
+
return false;
|
|
385
|
+
}
|
|
386
|
+
const callee = expression.callee;
|
|
387
|
+
if (callee.type !== "MemberExpression") {
|
|
388
|
+
return false;
|
|
389
|
+
}
|
|
390
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "date") {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
394
|
+
return objectName === "sql";
|
|
395
|
+
}
|
|
396
|
+
function isSlonikTimestampCall(expression) {
|
|
397
|
+
if (expression.type !== "CallExpression") {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
const callee = expression.callee;
|
|
401
|
+
if (callee.type !== "MemberExpression") {
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "timestamp") {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
408
|
+
return objectName === "sql";
|
|
409
|
+
}
|
|
410
|
+
function isSlonikIntervalCall(expression) {
|
|
411
|
+
if (expression.type !== "CallExpression") {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
const callee = expression.callee;
|
|
415
|
+
if (callee.type !== "MemberExpression") {
|
|
416
|
+
return false;
|
|
417
|
+
}
|
|
418
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "interval") {
|
|
419
|
+
return false;
|
|
420
|
+
}
|
|
421
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
422
|
+
return objectName === "sql";
|
|
423
|
+
}
|
|
424
|
+
function isSlonikJsonCall(expression) {
|
|
425
|
+
if (expression.type !== "CallExpression") {
|
|
426
|
+
return false;
|
|
427
|
+
}
|
|
428
|
+
const callee = expression.callee;
|
|
429
|
+
if (callee.type !== "MemberExpression") {
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "json") {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
436
|
+
return objectName === "sql";
|
|
437
|
+
}
|
|
438
|
+
function isSlonikJsonbCall(expression) {
|
|
439
|
+
if (expression.type !== "CallExpression") {
|
|
440
|
+
return false;
|
|
441
|
+
}
|
|
442
|
+
const callee = expression.callee;
|
|
443
|
+
if (callee.type !== "MemberExpression") {
|
|
444
|
+
return false;
|
|
445
|
+
}
|
|
446
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "jsonb") {
|
|
447
|
+
return false;
|
|
448
|
+
}
|
|
449
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
450
|
+
return objectName === "sql";
|
|
451
|
+
}
|
|
452
|
+
function isSlonikLiteralValueCall(expression) {
|
|
453
|
+
if (expression.type !== "CallExpression") {
|
|
454
|
+
return false;
|
|
455
|
+
}
|
|
456
|
+
const callee = expression.callee;
|
|
457
|
+
if (callee.type !== "MemberExpression") {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "literalValue") {
|
|
461
|
+
return false;
|
|
462
|
+
}
|
|
463
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
464
|
+
return objectName === "sql";
|
|
465
|
+
}
|
|
466
|
+
function isSlonikUuidCall(expression) {
|
|
467
|
+
if (expression.type !== "CallExpression") {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
const callee = expression.callee;
|
|
471
|
+
if (callee.type !== "MemberExpression") {
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "uuid") {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
478
|
+
return objectName === "sql";
|
|
479
|
+
}
|
|
382
480
|
function extractSlonikFragment(expression) {
|
|
383
481
|
if (expression.type !== "TaggedTemplateExpression") {
|
|
384
482
|
return null;
|
|
@@ -500,6 +598,132 @@ function mapTemplateLiteralToQueryText(quasi, parser, checker, options, sourceCo
|
|
|
500
598
|
if (isSlonikJoinCall(expression)) {
|
|
501
599
|
return E__namespace.right(null);
|
|
502
600
|
}
|
|
601
|
+
if (isSlonikDateCall(expression)) {
|
|
602
|
+
const placeholder2 = `$${++$idx}::date`;
|
|
603
|
+
$queryText += placeholder2;
|
|
604
|
+
sourcemaps.push({
|
|
605
|
+
original: {
|
|
606
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
607
|
+
end: expression.range[1] - quasi.range[0],
|
|
608
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
609
|
+
},
|
|
610
|
+
generated: {
|
|
611
|
+
start: position,
|
|
612
|
+
end: position + placeholder2.length,
|
|
613
|
+
text: placeholder2
|
|
614
|
+
},
|
|
615
|
+
offset: 0
|
|
616
|
+
});
|
|
617
|
+
continue;
|
|
618
|
+
}
|
|
619
|
+
if (isSlonikTimestampCall(expression)) {
|
|
620
|
+
const placeholder2 = `$${++$idx}::timestamptz`;
|
|
621
|
+
$queryText += placeholder2;
|
|
622
|
+
sourcemaps.push({
|
|
623
|
+
original: {
|
|
624
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
625
|
+
end: expression.range[1] - quasi.range[0],
|
|
626
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
627
|
+
},
|
|
628
|
+
generated: {
|
|
629
|
+
start: position,
|
|
630
|
+
end: position + placeholder2.length,
|
|
631
|
+
text: placeholder2
|
|
632
|
+
},
|
|
633
|
+
offset: 0
|
|
634
|
+
});
|
|
635
|
+
continue;
|
|
636
|
+
}
|
|
637
|
+
if (isSlonikIntervalCall(expression)) {
|
|
638
|
+
const placeholder2 = `$${++$idx}::interval`;
|
|
639
|
+
$queryText += placeholder2;
|
|
640
|
+
sourcemaps.push({
|
|
641
|
+
original: {
|
|
642
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
643
|
+
end: expression.range[1] - quasi.range[0],
|
|
644
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
645
|
+
},
|
|
646
|
+
generated: {
|
|
647
|
+
start: position,
|
|
648
|
+
end: position + placeholder2.length,
|
|
649
|
+
text: placeholder2
|
|
650
|
+
},
|
|
651
|
+
offset: 0
|
|
652
|
+
});
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (isSlonikJsonCall(expression)) {
|
|
656
|
+
const placeholder2 = `$${++$idx}::json`;
|
|
657
|
+
$queryText += placeholder2;
|
|
658
|
+
sourcemaps.push({
|
|
659
|
+
original: {
|
|
660
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
661
|
+
end: expression.range[1] - quasi.range[0],
|
|
662
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
663
|
+
},
|
|
664
|
+
generated: {
|
|
665
|
+
start: position,
|
|
666
|
+
end: position + placeholder2.length,
|
|
667
|
+
text: placeholder2
|
|
668
|
+
},
|
|
669
|
+
offset: 0
|
|
670
|
+
});
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
if (isSlonikJsonbCall(expression)) {
|
|
674
|
+
const placeholder2 = `$${++$idx}::jsonb`;
|
|
675
|
+
$queryText += placeholder2;
|
|
676
|
+
sourcemaps.push({
|
|
677
|
+
original: {
|
|
678
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
679
|
+
end: expression.range[1] - quasi.range[0],
|
|
680
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
681
|
+
},
|
|
682
|
+
generated: {
|
|
683
|
+
start: position,
|
|
684
|
+
end: position + placeholder2.length,
|
|
685
|
+
text: placeholder2
|
|
686
|
+
},
|
|
687
|
+
offset: 0
|
|
688
|
+
});
|
|
689
|
+
continue;
|
|
690
|
+
}
|
|
691
|
+
if (isSlonikLiteralValueCall(expression)) {
|
|
692
|
+
const placeholder2 = `$${++$idx}`;
|
|
693
|
+
$queryText += placeholder2;
|
|
694
|
+
sourcemaps.push({
|
|
695
|
+
original: {
|
|
696
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
697
|
+
end: expression.range[1] - quasi.range[0],
|
|
698
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
699
|
+
},
|
|
700
|
+
generated: {
|
|
701
|
+
start: position,
|
|
702
|
+
end: position + placeholder2.length,
|
|
703
|
+
text: placeholder2
|
|
704
|
+
},
|
|
705
|
+
offset: 0
|
|
706
|
+
});
|
|
707
|
+
continue;
|
|
708
|
+
}
|
|
709
|
+
if (isSlonikUuidCall(expression)) {
|
|
710
|
+
const placeholder2 = `$${++$idx}::uuid`;
|
|
711
|
+
$queryText += placeholder2;
|
|
712
|
+
sourcemaps.push({
|
|
713
|
+
original: {
|
|
714
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
715
|
+
end: expression.range[1] - quasi.range[0],
|
|
716
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
717
|
+
},
|
|
718
|
+
generated: {
|
|
719
|
+
start: position,
|
|
720
|
+
end: position + placeholder2.length,
|
|
721
|
+
text: placeholder2
|
|
722
|
+
},
|
|
723
|
+
offset: 0
|
|
724
|
+
});
|
|
725
|
+
continue;
|
|
726
|
+
}
|
|
503
727
|
const slonikUnnestTypes = extractSlonikUnnestTypes(expression);
|
|
504
728
|
if (slonikUnnestTypes !== null) {
|
|
505
729
|
const placeholders = slonikUnnestTypes.map((type) => `$${++$idx}::${type}`);
|