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