@nubase/backend 0.1.35 → 0.1.37

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/dist/index.mjs CHANGED
@@ -100,11 +100,14 @@ function createAuthHandlers(options) {
100
100
 
101
101
  // src/typed-handlers.ts
102
102
  var HttpError = class extends Error {
103
- constructor(statusCode, message) {
103
+ constructor(statusCode, message, payload) {
104
104
  super(message);
105
105
  this.statusCode = statusCode;
106
+ this.payload = payload;
106
107
  this.name = "HttpError";
107
108
  }
109
+ statusCode;
110
+ payload;
108
111
  };
109
112
  function createTypedHandler(schemaOrEndpoint, handler) {
110
113
  return createTypedHandlerInternal(schemaOrEndpoint, handler);
@@ -184,7 +187,8 @@ function createTypedHandlerInternal(schema, handler, options) {
184
187
  if (error instanceof HttpError) {
185
188
  return c.json(
186
189
  {
187
- error: error.message
190
+ error: error.message,
191
+ ...error.payload ?? {}
188
192
  },
189
193
  error.statusCode
190
194
  );
@@ -238,6 +242,846 @@ function createHandlerFactory(config) {
238
242
  };
239
243
  }
240
244
 
245
+ // src/nql/bindings.ts
246
+ function createNqlBindings() {
247
+ return (schema, mapping) => {
248
+ for (const key of Object.keys(mapping)) {
249
+ if (!(key in schema._shape)) {
250
+ throw new Error(
251
+ `createNqlBindings: field '${key}' is not in the schema shape`
252
+ );
253
+ }
254
+ }
255
+ return mapping;
256
+ };
257
+ }
258
+
259
+ // src/nql/compiler.ts
260
+ function compileToExpression(node, eb, options) {
261
+ const ctx = {
262
+ eb,
263
+ resolveColumn: (field) => {
264
+ const column = options.fields[field];
265
+ if (column === void 0) {
266
+ throw new Error(
267
+ `NQL compile bug: no binding for schema field '${field}'`
268
+ );
269
+ }
270
+ return column;
271
+ }
272
+ };
273
+ return visit(node, ctx);
274
+ }
275
+ function visit(node, ctx) {
276
+ switch (node.type) {
277
+ case "logical":
278
+ return visitLogical(node, ctx);
279
+ case "comparison":
280
+ return visitComparison(node, ctx);
281
+ case "nullCheck":
282
+ return visitNullCheck(node, ctx);
283
+ case "in":
284
+ return visitIn(node, ctx);
285
+ }
286
+ }
287
+ function visitLogical(node, ctx) {
288
+ const children = node.children.map((c) => visit(c, ctx));
289
+ if (node.op === "AND") return ctx.eb.and(children);
290
+ if (node.op === "OR") return ctx.eb.or(children);
291
+ return ctx.eb.not(children[0]);
292
+ }
293
+ function visitComparison(node, ctx) {
294
+ const col = ctx.resolveColumn(node.field.name);
295
+ const { op } = node;
296
+ const baseType = node.field.baseType;
297
+ const rawValue = literalValue(node.value);
298
+ if (baseType === "string") {
299
+ const str = rawValue;
300
+ switch (op) {
301
+ case "=":
302
+ return ctx.eb(col, "ilike", escapeLike(str));
303
+ case "!=":
304
+ return ctx.eb(col, "not ilike", escapeLike(str));
305
+ case "CONTAINS":
306
+ return ctx.eb(col, "ilike", `%${escapeLike(str)}%`);
307
+ case "STARTS_WITH":
308
+ return ctx.eb(col, "ilike", `${escapeLike(str)}%`);
309
+ case "ENDS_WITH":
310
+ return ctx.eb(col, "ilike", `%${escapeLike(str)}`);
311
+ }
312
+ }
313
+ switch (op) {
314
+ case "=":
315
+ return ctx.eb(col, "=", rawValue);
316
+ case "!=":
317
+ return ctx.eb(col, "!=", rawValue);
318
+ case "<":
319
+ return ctx.eb(col, "<", rawValue);
320
+ case "<=":
321
+ return ctx.eb(col, "<=", rawValue);
322
+ case ">":
323
+ return ctx.eb(col, ">", rawValue);
324
+ case ">=":
325
+ return ctx.eb(col, ">=", rawValue);
326
+ default:
327
+ throw new Error(
328
+ `NQL compile bug: operator '${op}' not expected on ${baseType}`
329
+ );
330
+ }
331
+ }
332
+ function visitNullCheck(node, ctx) {
333
+ const col = ctx.resolveColumn(node.field.name);
334
+ return ctx.eb(col, node.negated ? "is not" : "is", null);
335
+ }
336
+ function visitIn(node, ctx) {
337
+ const col = ctx.resolveColumn(node.field.name);
338
+ const values = node.values.map(literalValue);
339
+ return ctx.eb(col, node.negated ? "not in" : "in", values);
340
+ }
341
+ function literalValue(literal) {
342
+ switch (literal.kind) {
343
+ case "string":
344
+ return literal.value;
345
+ case "number":
346
+ return literal.value;
347
+ case "boolean":
348
+ return literal.value;
349
+ case "null":
350
+ return null;
351
+ }
352
+ }
353
+ function escapeLike(s) {
354
+ return s.replace(/\\/g, "\\\\").replace(/%/g, "\\%").replace(/_/g, "\\_");
355
+ }
356
+
357
+ // src/nql/parser.ts
358
+ var COMPARISON_OP_BY_KIND = {
359
+ Eq: "=",
360
+ NotEq: "!=",
361
+ Lt: "<",
362
+ LtEq: "<=",
363
+ Gt: ">",
364
+ GtEq: ">=",
365
+ Contains: "CONTAINS",
366
+ StartsWith: "STARTS_WITH",
367
+ EndsWith: "ENDS_WITH"
368
+ };
369
+ function parse(tokens) {
370
+ const parser = new Parser(tokens);
371
+ try {
372
+ const node = parser.parseExpression();
373
+ parser.expect("Eof", "Expected end of input");
374
+ return { ok: true, value: node };
375
+ } catch (e) {
376
+ if (e instanceof ParseError) {
377
+ return {
378
+ ok: false,
379
+ error: {
380
+ code: "PARSE",
381
+ message: e.message,
382
+ line: e.span.line,
383
+ column: e.span.column,
384
+ length: e.span.length
385
+ }
386
+ };
387
+ }
388
+ throw e;
389
+ }
390
+ }
391
+ var ParseError = class extends Error {
392
+ constructor(message, span) {
393
+ super(message);
394
+ this.span = span;
395
+ this.name = "ParseError";
396
+ }
397
+ span;
398
+ };
399
+ var Parser = class {
400
+ constructor(tokens) {
401
+ this.tokens = tokens;
402
+ }
403
+ tokens;
404
+ pos = 0;
405
+ parseExpression() {
406
+ return this.parseOr();
407
+ }
408
+ // or_expr := and_expr ( OR and_expr )*
409
+ parseOr() {
410
+ let left = this.parseAnd();
411
+ while (this.peek().kind === "Or") {
412
+ this.advance();
413
+ const right = this.parseAnd();
414
+ left = makeLogical("OR", [left, right]);
415
+ }
416
+ return left;
417
+ }
418
+ // and_expr := not_expr ( AND not_expr )*
419
+ parseAnd() {
420
+ let left = this.parseNot();
421
+ while (this.peek().kind === "And") {
422
+ this.advance();
423
+ const right = this.parseNot();
424
+ left = makeLogical("AND", [left, right]);
425
+ }
426
+ return left;
427
+ }
428
+ // not_expr := NOT not_expr | primary
429
+ parseNot() {
430
+ if (this.peek().kind === "Not") {
431
+ const notTok = this.advance();
432
+ const inner = this.parseNot();
433
+ return {
434
+ type: "logical",
435
+ op: "NOT",
436
+ children: [inner],
437
+ span: mergeSpans(notTok.span, inner.span)
438
+ };
439
+ }
440
+ return this.parsePrimary();
441
+ }
442
+ // primary := "(" expression ")" | comparison
443
+ parsePrimary() {
444
+ const tok = this.peek();
445
+ if (tok.kind === "LParen") {
446
+ this.advance();
447
+ const inner = this.parseExpression();
448
+ this.expect("RParen", "Expected closing ')'");
449
+ return inner;
450
+ }
451
+ return this.parseComparison();
452
+ }
453
+ // comparison := identifier comparison_op literal
454
+ // | identifier (IN | NOT IN) "(" literal ("," literal)* ")"
455
+ // | identifier IS [NOT] NULL
456
+ parseComparison() {
457
+ const identTok = this.peek();
458
+ if (identTok.kind !== "Identifier") {
459
+ throw new ParseError(
460
+ `Expected field name, got ${describeToken(identTok)}`,
461
+ identTok.span
462
+ );
463
+ }
464
+ this.advance();
465
+ const field = { raw: identTok.text, span: identTok.span };
466
+ const opTok = this.peek();
467
+ if (opTok.kind === "Is") {
468
+ this.advance();
469
+ let negated = false;
470
+ if (this.peek().kind === "Not") {
471
+ this.advance();
472
+ negated = true;
473
+ }
474
+ const nullTok = this.peek();
475
+ if (nullTok.kind !== "Null") {
476
+ throw new ParseError(
477
+ `Expected NULL after ${negated ? "IS NOT" : "IS"}, got ${describeToken(nullTok)}`,
478
+ nullTok.span
479
+ );
480
+ }
481
+ this.advance();
482
+ return {
483
+ type: "nullCheck",
484
+ field,
485
+ negated,
486
+ span: mergeSpans(identTok.span, nullTok.span)
487
+ };
488
+ }
489
+ if (opTok.kind === "In") {
490
+ this.advance();
491
+ const { values, endSpan } = this.parseLiteralList();
492
+ return {
493
+ type: "in",
494
+ field,
495
+ negated: false,
496
+ values,
497
+ span: mergeSpans(identTok.span, endSpan)
498
+ };
499
+ }
500
+ if (opTok.kind === "Not") {
501
+ this.advance();
502
+ const inTok = this.peek();
503
+ if (inTok.kind !== "In") {
504
+ throw new ParseError(
505
+ `Expected IN after NOT, got ${describeToken(inTok)}`,
506
+ inTok.span
507
+ );
508
+ }
509
+ this.advance();
510
+ const { values, endSpan } = this.parseLiteralList();
511
+ return {
512
+ type: "in",
513
+ field,
514
+ negated: true,
515
+ values,
516
+ span: mergeSpans(identTok.span, endSpan)
517
+ };
518
+ }
519
+ const op = COMPARISON_OP_BY_KIND[opTok.kind];
520
+ if (!op) {
521
+ throw new ParseError(
522
+ `Expected comparison operator after '${identTok.text}', got ${describeToken(opTok)}`,
523
+ opTok.span
524
+ );
525
+ }
526
+ this.advance();
527
+ const literal = this.parseLiteral();
528
+ return {
529
+ type: "comparison",
530
+ field,
531
+ op,
532
+ value: literal,
533
+ span: mergeSpans(identTok.span, literal.span)
534
+ };
535
+ }
536
+ // "(" literal ("," literal)* ")"
537
+ parseLiteralList() {
538
+ this.expect("LParen", "Expected '(' after IN");
539
+ const values = [];
540
+ if (this.peek().kind === "RParen") {
541
+ throw new ParseError("IN list cannot be empty", this.peek().span);
542
+ }
543
+ values.push(this.parseLiteral());
544
+ while (this.peek().kind === "Comma") {
545
+ this.advance();
546
+ values.push(this.parseLiteral());
547
+ }
548
+ const rparen = this.expect("RParen", "Expected ')' or ',' in IN list");
549
+ return { values, endSpan: rparen.span };
550
+ }
551
+ parseLiteral() {
552
+ const tok = this.peek();
553
+ switch (tok.kind) {
554
+ case "String":
555
+ this.advance();
556
+ return {
557
+ kind: "string",
558
+ value: tok.stringValue ?? "",
559
+ span: tok.span
560
+ };
561
+ case "Number":
562
+ this.advance();
563
+ return {
564
+ kind: "number",
565
+ value: tok.numberValue ?? 0,
566
+ span: tok.span
567
+ };
568
+ case "True":
569
+ this.advance();
570
+ return { kind: "boolean", value: true, span: tok.span };
571
+ case "False":
572
+ this.advance();
573
+ return { kind: "boolean", value: false, span: tok.span };
574
+ case "Null":
575
+ this.advance();
576
+ return { kind: "null", span: tok.span };
577
+ default:
578
+ throw new ParseError(
579
+ `Expected literal value, got ${describeToken(tok)}`,
580
+ tok.span
581
+ );
582
+ }
583
+ }
584
+ peek() {
585
+ return this.tokens[this.pos];
586
+ }
587
+ advance() {
588
+ const tok = this.tokens[this.pos];
589
+ if (tok.kind !== "Eof") this.pos += 1;
590
+ return tok;
591
+ }
592
+ expect(kind, message) {
593
+ const tok = this.peek();
594
+ if (tok.kind !== kind) {
595
+ throw new ParseError(`${message}, got ${describeToken(tok)}`, tok.span);
596
+ }
597
+ return this.advance();
598
+ }
599
+ };
600
+ function makeLogical(op, children) {
601
+ return {
602
+ type: "logical",
603
+ op,
604
+ children,
605
+ span: mergeSpans(children[0].span, children[1].span)
606
+ };
607
+ }
608
+ function mergeSpans(a, b) {
609
+ const start = a.offset <= b.offset ? a : b;
610
+ const endSpan = a.offset + a.length >= b.offset + b.length ? a : b;
611
+ const endOffset = endSpan.offset + endSpan.length;
612
+ return {
613
+ line: start.line,
614
+ column: start.column,
615
+ offset: start.offset,
616
+ length: endOffset - start.offset
617
+ };
618
+ }
619
+ function describeToken(tok) {
620
+ if (tok.kind === "Eof") return "end of input";
621
+ if (tok.text) return `'${tok.text}'`;
622
+ return tok.kind;
623
+ }
624
+
625
+ // src/nql/tokenizer.ts
626
+ var KEYWORDS = {
627
+ AND: "And",
628
+ OR: "Or",
629
+ NOT: "Not",
630
+ IS: "Is",
631
+ IN: "In",
632
+ TRUE: "True",
633
+ FALSE: "False",
634
+ NULL: "Null",
635
+ CONTAINS: "Contains",
636
+ STARTS_WITH: "StartsWith",
637
+ ENDS_WITH: "EndsWith"
638
+ };
639
+ function tokenize(source) {
640
+ const tokens = [];
641
+ let offset = 0;
642
+ let line = 1;
643
+ let column = 1;
644
+ const makeSpan = (startOffset, startLine, startColumn) => ({
645
+ line: startLine,
646
+ column: startColumn,
647
+ offset: startOffset,
648
+ length: offset - startOffset
649
+ });
650
+ const err = (message, atLine, atColumn, length = 1) => ({
651
+ ok: false,
652
+ error: {
653
+ code: "TOKENIZE",
654
+ message,
655
+ line: atLine,
656
+ column: atColumn,
657
+ length
658
+ }
659
+ });
660
+ const advance = (ch) => {
661
+ offset += 1;
662
+ if (ch === "\n") {
663
+ line += 1;
664
+ column = 1;
665
+ } else {
666
+ column += 1;
667
+ }
668
+ };
669
+ while (offset < source.length) {
670
+ const ch = source[offset];
671
+ if (ch === " " || ch === " " || ch === "\r" || ch === "\n") {
672
+ advance(ch);
673
+ continue;
674
+ }
675
+ const startOffset = offset;
676
+ const startLine = line;
677
+ const startColumn = column;
678
+ if (ch === "(") {
679
+ advance(ch);
680
+ tokens.push({
681
+ kind: "LParen",
682
+ span: makeSpan(startOffset, startLine, startColumn),
683
+ text: "("
684
+ });
685
+ continue;
686
+ }
687
+ if (ch === ")") {
688
+ advance(ch);
689
+ tokens.push({
690
+ kind: "RParen",
691
+ span: makeSpan(startOffset, startLine, startColumn),
692
+ text: ")"
693
+ });
694
+ continue;
695
+ }
696
+ if (ch === ",") {
697
+ advance(ch);
698
+ tokens.push({
699
+ kind: "Comma",
700
+ span: makeSpan(startOffset, startLine, startColumn),
701
+ text: ","
702
+ });
703
+ continue;
704
+ }
705
+ if (ch === "=") {
706
+ advance(ch);
707
+ tokens.push({
708
+ kind: "Eq",
709
+ span: makeSpan(startOffset, startLine, startColumn),
710
+ text: "="
711
+ });
712
+ continue;
713
+ }
714
+ if (ch === "!") {
715
+ const next = source[offset + 1];
716
+ if (next === "=") {
717
+ advance(ch);
718
+ advance(next);
719
+ tokens.push({
720
+ kind: "NotEq",
721
+ span: makeSpan(startOffset, startLine, startColumn),
722
+ text: "!="
723
+ });
724
+ continue;
725
+ }
726
+ return err(`Unexpected character '!'`, startLine, startColumn);
727
+ }
728
+ if (ch === "<") {
729
+ const next = source[offset + 1];
730
+ if (next === "=") {
731
+ advance(ch);
732
+ advance(next);
733
+ tokens.push({
734
+ kind: "LtEq",
735
+ span: makeSpan(startOffset, startLine, startColumn),
736
+ text: "<="
737
+ });
738
+ } else {
739
+ advance(ch);
740
+ tokens.push({
741
+ kind: "Lt",
742
+ span: makeSpan(startOffset, startLine, startColumn),
743
+ text: "<"
744
+ });
745
+ }
746
+ continue;
747
+ }
748
+ if (ch === ">") {
749
+ const next = source[offset + 1];
750
+ if (next === "=") {
751
+ advance(ch);
752
+ advance(next);
753
+ tokens.push({
754
+ kind: "GtEq",
755
+ span: makeSpan(startOffset, startLine, startColumn),
756
+ text: ">="
757
+ });
758
+ } else {
759
+ advance(ch);
760
+ tokens.push({
761
+ kind: "Gt",
762
+ span: makeSpan(startOffset, startLine, startColumn),
763
+ text: ">"
764
+ });
765
+ }
766
+ continue;
767
+ }
768
+ if (ch === '"') {
769
+ advance(ch);
770
+ let value = "";
771
+ let closed = false;
772
+ while (offset < source.length) {
773
+ const c = source[offset];
774
+ if (c === "\\") {
775
+ const next = source[offset + 1];
776
+ if (next === '"' || next === "\\") {
777
+ value += next;
778
+ advance(c);
779
+ advance(next);
780
+ continue;
781
+ }
782
+ return err(
783
+ `Invalid escape sequence '\\${next ?? ""}'`,
784
+ line,
785
+ column,
786
+ 2
787
+ );
788
+ }
789
+ if (c === '"') {
790
+ advance(c);
791
+ closed = true;
792
+ break;
793
+ }
794
+ if (c === "\n") {
795
+ return err("Unterminated string literal", startLine, startColumn);
796
+ }
797
+ value += c;
798
+ advance(c);
799
+ }
800
+ if (!closed) {
801
+ return err("Unterminated string literal", startLine, startColumn);
802
+ }
803
+ const span = makeSpan(startOffset, startLine, startColumn);
804
+ tokens.push({
805
+ kind: "String",
806
+ span,
807
+ text: source.slice(startOffset, offset),
808
+ stringValue: value
809
+ });
810
+ continue;
811
+ }
812
+ if (isDigit(ch) || ch === "-" && isDigit(source[offset + 1] ?? "")) {
813
+ if (ch === "-") advance(ch);
814
+ while (offset < source.length && isDigit(source[offset])) {
815
+ advance(source[offset]);
816
+ }
817
+ if (source[offset] === ".") {
818
+ const afterDot = source[offset + 1];
819
+ if (!afterDot || !isDigit(afterDot)) {
820
+ return err("Expected digit after decimal point", line, column);
821
+ }
822
+ advance(".");
823
+ while (offset < source.length && isDigit(source[offset])) {
824
+ advance(source[offset]);
825
+ }
826
+ }
827
+ const text = source.slice(startOffset, offset);
828
+ const parsed = Number(text);
829
+ if (!Number.isFinite(parsed)) {
830
+ return err(
831
+ `Invalid number '${text}'`,
832
+ startLine,
833
+ startColumn,
834
+ text.length
835
+ );
836
+ }
837
+ tokens.push({
838
+ kind: "Number",
839
+ span: makeSpan(startOffset, startLine, startColumn),
840
+ text,
841
+ numberValue: parsed
842
+ });
843
+ continue;
844
+ }
845
+ if (isIdentStart(ch)) {
846
+ while (offset < source.length && isIdentPart(source[offset])) {
847
+ advance(source[offset]);
848
+ }
849
+ const text = source.slice(startOffset, offset);
850
+ const upper = text.toUpperCase();
851
+ const span = makeSpan(startOffset, startLine, startColumn);
852
+ const keywordKind = KEYWORDS[upper];
853
+ if (keywordKind) {
854
+ tokens.push({ kind: keywordKind, span, text });
855
+ } else {
856
+ tokens.push({ kind: "Identifier", span, text });
857
+ }
858
+ continue;
859
+ }
860
+ return err(`Unexpected character '${ch}'`, startLine, startColumn);
861
+ }
862
+ tokens.push({
863
+ kind: "Eof",
864
+ span: { line, column, offset, length: 0 },
865
+ text: ""
866
+ });
867
+ return { ok: true, value: tokens };
868
+ }
869
+ function isDigit(ch) {
870
+ return ch >= "0" && ch <= "9";
871
+ }
872
+ function isIdentStart(ch) {
873
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z" || ch === "_";
874
+ }
875
+ function isIdentPart(ch) {
876
+ return isIdentStart(ch) || isDigit(ch);
877
+ }
878
+
879
+ // src/nql/validator.ts
880
+ import {
881
+ OptionalSchema
882
+ } from "@nubase/core";
883
+ var ALLOWED_OPS_BY_TYPE = {
884
+ string: /* @__PURE__ */ new Set([
885
+ "=",
886
+ "!=",
887
+ "CONTAINS",
888
+ "STARTS_WITH",
889
+ "ENDS_WITH"
890
+ ]),
891
+ number: /* @__PURE__ */ new Set(["=", "!=", "<", "<=", ">", ">="]),
892
+ boolean: /* @__PURE__ */ new Set(["=", "!="])
893
+ };
894
+ function validate(node, schema, options = {}) {
895
+ const fields = buildFieldMap(schema, options.allowFields);
896
+ try {
897
+ return { ok: true, value: visit2(node, fields) };
898
+ } catch (e) {
899
+ if (e instanceof ValidateError) {
900
+ return {
901
+ ok: false,
902
+ error: {
903
+ code: "VALIDATE",
904
+ message: e.message,
905
+ line: e.span.line,
906
+ column: e.span.column,
907
+ length: e.span.length
908
+ }
909
+ };
910
+ }
911
+ throw e;
912
+ }
913
+ }
914
+ var ValidateError = class extends Error {
915
+ constructor(message, span) {
916
+ super(message);
917
+ this.span = span;
918
+ this.name = "ValidateError";
919
+ }
920
+ span;
921
+ };
922
+ function buildFieldMap(schema, allowFields) {
923
+ const map = /* @__PURE__ */ new Map();
924
+ const allowSet = allowFields ? new Set(allowFields.map((f) => f.toLowerCase())) : void 0;
925
+ for (const key of Object.keys(schema._shape)) {
926
+ if (allowSet && !allowSet.has(key.toLowerCase())) continue;
927
+ const fieldSchema = schema._shape[key];
928
+ const optional = fieldSchema instanceof OptionalSchema;
929
+ const baseTypeRaw = optional ? fieldSchema.baseType : fieldSchema.type;
930
+ map.set(key.toLowerCase(), {
931
+ canonical: key,
932
+ baseType: coerceBaseType(baseTypeRaw),
933
+ optional,
934
+ baseTypeRaw
935
+ });
936
+ }
937
+ return map;
938
+ }
939
+ function coerceBaseType(raw) {
940
+ if (raw === "string" || raw === "number" || raw === "boolean") {
941
+ return raw;
942
+ }
943
+ return null;
944
+ }
945
+ function visit2(node, fields) {
946
+ switch (node.type) {
947
+ case "logical":
948
+ return visitLogical2(node, fields);
949
+ case "comparison":
950
+ return visitComparison2(node, fields);
951
+ case "nullCheck":
952
+ return visitNullCheck2(node, fields);
953
+ case "in":
954
+ return visitIn2(node, fields);
955
+ }
956
+ }
957
+ function visitLogical2(node, fields) {
958
+ return {
959
+ type: "logical",
960
+ op: node.op,
961
+ children: node.children.map((c) => visit2(c, fields)),
962
+ span: node.span
963
+ };
964
+ }
965
+ function visitComparison2(node, fields) {
966
+ const field = resolveField(node.field, fields);
967
+ ensureSupportedBaseType(field, node.field.span);
968
+ ensureOperatorAllowed(field.baseType, node.op, node.field.span);
969
+ ensureLiteralMatchesField(
970
+ node.value,
971
+ field.baseType,
972
+ field.canonical,
973
+ /*allowNull*/
974
+ false
975
+ );
976
+ return {
977
+ type: "comparison",
978
+ field: makeFieldRef(field, node.field.span),
979
+ op: node.op,
980
+ value: node.value,
981
+ span: node.span
982
+ };
983
+ }
984
+ function visitNullCheck2(node, fields) {
985
+ const field = resolveField(node.field, fields);
986
+ ensureSupportedBaseType(field, node.field.span);
987
+ if (!field.optional) {
988
+ throw new ValidateError(
989
+ `Field '${field.canonical}' is required and cannot be null`,
990
+ node.span
991
+ );
992
+ }
993
+ return {
994
+ type: "nullCheck",
995
+ field: makeFieldRef(field, node.field.span),
996
+ negated: node.negated,
997
+ span: node.span
998
+ };
999
+ }
1000
+ function visitIn2(node, fields) {
1001
+ const field = resolveField(node.field, fields);
1002
+ ensureSupportedBaseType(field, node.field.span);
1003
+ for (const value of node.values) {
1004
+ ensureLiteralMatchesField(
1005
+ value,
1006
+ field.baseType,
1007
+ field.canonical,
1008
+ /*allowNull*/
1009
+ false
1010
+ );
1011
+ }
1012
+ return {
1013
+ type: "in",
1014
+ field: makeFieldRef(field, node.field.span),
1015
+ negated: node.negated,
1016
+ values: node.values,
1017
+ span: node.span
1018
+ };
1019
+ }
1020
+ function resolveField(ref, fields) {
1021
+ const resolved = fields.get(ref.raw.toLowerCase());
1022
+ if (!resolved) {
1023
+ throw new ValidateError(`Unknown field '${ref.raw}'`, ref.span);
1024
+ }
1025
+ return resolved;
1026
+ }
1027
+ function ensureSupportedBaseType(field, span) {
1028
+ if (field.baseType === null) {
1029
+ throw new ValidateError(
1030
+ `Field '${field.canonical}' of type '${field.baseTypeRaw}' is not queryable`,
1031
+ span
1032
+ );
1033
+ }
1034
+ }
1035
+ function ensureOperatorAllowed(baseType, op, span) {
1036
+ const allowed = ALLOWED_OPS_BY_TYPE[baseType];
1037
+ if (!allowed.has(op)) {
1038
+ throw new ValidateError(
1039
+ `Operator '${op}' is not supported on ${baseType} fields`,
1040
+ span
1041
+ );
1042
+ }
1043
+ }
1044
+ function ensureLiteralMatchesField(literal, baseType, fieldName, allowNull) {
1045
+ if (literal.kind === "null") {
1046
+ if (!allowNull) {
1047
+ throw new ValidateError(
1048
+ `Use 'IS NULL' to check for null values on '${fieldName}'`,
1049
+ literal.span
1050
+ );
1051
+ }
1052
+ return;
1053
+ }
1054
+ if (literal.kind !== baseType) {
1055
+ throw new ValidateError(
1056
+ `Field '${fieldName}' expects a ${baseType} value, got ${literal.kind}`,
1057
+ literal.span
1058
+ );
1059
+ }
1060
+ }
1061
+ function makeFieldRef(field, span) {
1062
+ return {
1063
+ name: field.canonical,
1064
+ baseType: field.baseType,
1065
+ optional: field.optional,
1066
+ span
1067
+ };
1068
+ }
1069
+
1070
+ // src/nql/compile.ts
1071
+ function compileNql(source, schema, options) {
1072
+ const toks = tokenize(source);
1073
+ if (!toks.ok) return { ok: false, error: toks.error };
1074
+ const parsed = parse(toks.value);
1075
+ if (!parsed.ok) return { ok: false, error: parsed.error };
1076
+ const validated = validate(parsed.value, schema, {
1077
+ allowFields: Object.keys(options.fields)
1078
+ });
1079
+ if (!validated.ok) return { ok: false, error: validated.error };
1080
+ const node = validated.value;
1081
+ const applied = (eb) => compileToExpression(node, eb, { fields: options.fields });
1082
+ return { ok: true, value: applied };
1083
+ }
1084
+
241
1085
  // src/utils/cookies.ts
242
1086
  function parseCookies(cookieHeader) {
243
1087
  const cookies = {};
@@ -283,10 +1127,12 @@ export {
283
1127
  AUTH_CONTROLLER_KEY,
284
1128
  AUTH_USER_KEY,
285
1129
  HttpError,
1130
+ compileNql,
286
1131
  createAuthHandlers,
287
1132
  createAuthMiddleware,
288
1133
  createHandlerFactory,
289
1134
  createHttpHandler,
1135
+ createNqlBindings,
290
1136
  createTypedHandler,
291
1137
  createTypedRoutes,
292
1138
  getAuthController,