@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.d.mts +245 -3
- package/dist/index.d.ts +245 -3
- package/dist/index.js +848 -2
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +848 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -5
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,
|