@nubase/backend 0.1.35 → 0.1.38
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.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,
|