eslint-plugin-slonik 1.8.0 → 1.10.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 +50 -8
- package/dist/index.cjs +256 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.mjs +256 -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` |
|
|
84
|
+
| `sql.binary(buffer)` | ✅ Full | Extracts type → `$1::bytea` |
|
|
77
85
|
| `sql.join([...], glue)` | ✅ Skip | Skipped (runtime content) |
|
|
78
|
-
| `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,48 @@ 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
|
|
146
|
+
|
|
147
|
+
// sql.binary for binary data
|
|
148
|
+
sql.type(z.object({ id: z.number() }))`
|
|
149
|
+
UPDATE files SET content = ${sql.binary(buffer)} WHERE id = ${id}
|
|
150
|
+
`;
|
|
151
|
+
// → Validates: UPDATE files SET content = $1::bytea WHERE id = $2
|
|
110
152
|
```
|
|
111
153
|
|
|
112
154
|
**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,118 @@ 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
|
+
}
|
|
480
|
+
function isSlonikBinaryCall(expression) {
|
|
481
|
+
if (expression.type !== "CallExpression") {
|
|
482
|
+
return false;
|
|
483
|
+
}
|
|
484
|
+
const callee = expression.callee;
|
|
485
|
+
if (callee.type !== "MemberExpression") {
|
|
486
|
+
return false;
|
|
487
|
+
}
|
|
488
|
+
if (callee.property.type !== "Identifier" || callee.property.name !== "binary") {
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
const objectName = getMemberExpressionObjectName(callee.object);
|
|
492
|
+
return objectName === "sql";
|
|
493
|
+
}
|
|
382
494
|
function extractSlonikFragment(expression) {
|
|
383
495
|
if (expression.type !== "TaggedTemplateExpression") {
|
|
384
496
|
return null;
|
|
@@ -500,6 +612,150 @@ function mapTemplateLiteralToQueryText(quasi, parser, checker, options, sourceCo
|
|
|
500
612
|
if (isSlonikJoinCall(expression)) {
|
|
501
613
|
return E__namespace.right(null);
|
|
502
614
|
}
|
|
615
|
+
if (isSlonikDateCall(expression)) {
|
|
616
|
+
const placeholder2 = `$${++$idx}::date`;
|
|
617
|
+
$queryText += placeholder2;
|
|
618
|
+
sourcemaps.push({
|
|
619
|
+
original: {
|
|
620
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
621
|
+
end: expression.range[1] - quasi.range[0],
|
|
622
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
623
|
+
},
|
|
624
|
+
generated: {
|
|
625
|
+
start: position,
|
|
626
|
+
end: position + placeholder2.length,
|
|
627
|
+
text: placeholder2
|
|
628
|
+
},
|
|
629
|
+
offset: 0
|
|
630
|
+
});
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (isSlonikTimestampCall(expression)) {
|
|
634
|
+
const placeholder2 = `$${++$idx}::timestamptz`;
|
|
635
|
+
$queryText += placeholder2;
|
|
636
|
+
sourcemaps.push({
|
|
637
|
+
original: {
|
|
638
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
639
|
+
end: expression.range[1] - quasi.range[0],
|
|
640
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
641
|
+
},
|
|
642
|
+
generated: {
|
|
643
|
+
start: position,
|
|
644
|
+
end: position + placeholder2.length,
|
|
645
|
+
text: placeholder2
|
|
646
|
+
},
|
|
647
|
+
offset: 0
|
|
648
|
+
});
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (isSlonikIntervalCall(expression)) {
|
|
652
|
+
const placeholder2 = `$${++$idx}::interval`;
|
|
653
|
+
$queryText += placeholder2;
|
|
654
|
+
sourcemaps.push({
|
|
655
|
+
original: {
|
|
656
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
657
|
+
end: expression.range[1] - quasi.range[0],
|
|
658
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
659
|
+
},
|
|
660
|
+
generated: {
|
|
661
|
+
start: position,
|
|
662
|
+
end: position + placeholder2.length,
|
|
663
|
+
text: placeholder2
|
|
664
|
+
},
|
|
665
|
+
offset: 0
|
|
666
|
+
});
|
|
667
|
+
continue;
|
|
668
|
+
}
|
|
669
|
+
if (isSlonikJsonCall(expression)) {
|
|
670
|
+
const placeholder2 = `$${++$idx}::json`;
|
|
671
|
+
$queryText += placeholder2;
|
|
672
|
+
sourcemaps.push({
|
|
673
|
+
original: {
|
|
674
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
675
|
+
end: expression.range[1] - quasi.range[0],
|
|
676
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
677
|
+
},
|
|
678
|
+
generated: {
|
|
679
|
+
start: position,
|
|
680
|
+
end: position + placeholder2.length,
|
|
681
|
+
text: placeholder2
|
|
682
|
+
},
|
|
683
|
+
offset: 0
|
|
684
|
+
});
|
|
685
|
+
continue;
|
|
686
|
+
}
|
|
687
|
+
if (isSlonikJsonbCall(expression)) {
|
|
688
|
+
const placeholder2 = `$${++$idx}::jsonb`;
|
|
689
|
+
$queryText += placeholder2;
|
|
690
|
+
sourcemaps.push({
|
|
691
|
+
original: {
|
|
692
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
693
|
+
end: expression.range[1] - quasi.range[0],
|
|
694
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
695
|
+
},
|
|
696
|
+
generated: {
|
|
697
|
+
start: position,
|
|
698
|
+
end: position + placeholder2.length,
|
|
699
|
+
text: placeholder2
|
|
700
|
+
},
|
|
701
|
+
offset: 0
|
|
702
|
+
});
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
705
|
+
if (isSlonikLiteralValueCall(expression)) {
|
|
706
|
+
const placeholder2 = `$${++$idx}`;
|
|
707
|
+
$queryText += placeholder2;
|
|
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 + placeholder2.length,
|
|
717
|
+
text: placeholder2
|
|
718
|
+
},
|
|
719
|
+
offset: 0
|
|
720
|
+
});
|
|
721
|
+
continue;
|
|
722
|
+
}
|
|
723
|
+
if (isSlonikUuidCall(expression)) {
|
|
724
|
+
const placeholder2 = `$${++$idx}::uuid`;
|
|
725
|
+
$queryText += placeholder2;
|
|
726
|
+
sourcemaps.push({
|
|
727
|
+
original: {
|
|
728
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
729
|
+
end: expression.range[1] - quasi.range[0],
|
|
730
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
731
|
+
},
|
|
732
|
+
generated: {
|
|
733
|
+
start: position,
|
|
734
|
+
end: position + placeholder2.length,
|
|
735
|
+
text: placeholder2
|
|
736
|
+
},
|
|
737
|
+
offset: 0
|
|
738
|
+
});
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
741
|
+
if (isSlonikBinaryCall(expression)) {
|
|
742
|
+
const placeholder2 = `$${++$idx}::bytea`;
|
|
743
|
+
$queryText += placeholder2;
|
|
744
|
+
sourcemaps.push({
|
|
745
|
+
original: {
|
|
746
|
+
start: expression.range[0] - quasi.range[0] - 2,
|
|
747
|
+
end: expression.range[1] - quasi.range[0],
|
|
748
|
+
text: sourceCode.text.slice(expression.range[0] - 2, expression.range[1] + 1)
|
|
749
|
+
},
|
|
750
|
+
generated: {
|
|
751
|
+
start: position,
|
|
752
|
+
end: position + placeholder2.length,
|
|
753
|
+
text: placeholder2
|
|
754
|
+
},
|
|
755
|
+
offset: 0
|
|
756
|
+
});
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
503
759
|
const slonikUnnestTypes = extractSlonikUnnestTypes(expression);
|
|
504
760
|
if (slonikUnnestTypes !== null) {
|
|
505
761
|
const placeholders = slonikUnnestTypes.map((type) => `$${++$idx}::${type}`);
|