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 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}`);