@protobase/schema 0.0.0-stage → 0.1.1

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.
Files changed (91) hide show
  1. package/LICENSE +202 -0
  2. package/dist/access/capability.d.ts +10 -0
  3. package/dist/access/capability.js +30 -0
  4. package/dist/access/combine.js +39 -0
  5. package/dist/access/index.d.ts +8 -0
  6. package/dist/access/pick-view.d.ts +8 -0
  7. package/dist/access/pick-view.js +11 -0
  8. package/dist/access/resolve.d.ts +61 -0
  9. package/dist/access/resolve.js +150 -0
  10. package/dist/access/restrict.d.ts +16 -0
  11. package/dist/access/restrict.js +37 -0
  12. package/dist/access/roles.d.ts +30 -0
  13. package/dist/access/roles.js +55 -0
  14. package/dist/access/rule.d.ts +11 -0
  15. package/dist/access/rule.js +9 -0
  16. package/dist/access/types.d.ts +37 -0
  17. package/dist/enum-label.d.ts +4 -0
  18. package/dist/enum-label.js +5 -0
  19. package/dist/field.d.ts +69 -0
  20. package/dist/field.js +85 -0
  21. package/dist/fields/bigint.js +12 -0
  22. package/dist/fields/boolean.js +9 -0
  23. package/dist/fields/country.js +9 -0
  24. package/dist/fields/currency.js +9 -0
  25. package/dist/fields/date.js +9 -0
  26. package/dist/fields/decimal.js +22 -0
  27. package/dist/fields/enum.js +12 -0
  28. package/dist/fields/integer.js +14 -0
  29. package/dist/fields/json.js +9 -0
  30. package/dist/fields/relation.js +9 -0
  31. package/dist/fields/text.js +19 -0
  32. package/dist/fields/timestamp.js +9 -0
  33. package/dist/fields/uuid.js +9 -0
  34. package/dist/fields.d.ts +23 -0
  35. package/dist/fields.js +49 -0
  36. package/dist/filter/check-field.js +25 -0
  37. package/dist/filter/check-model.js +123 -0
  38. package/dist/filter/check-value.js +64 -0
  39. package/dist/filter/check.d.ts +16 -0
  40. package/dist/filter/check.js +22 -0
  41. package/dist/filter/datetime.js +33 -0
  42. package/dist/filter/duration.js +16 -0
  43. package/dist/filter/errors.js +29 -0
  44. package/dist/filter/evaluate.d.ts +12 -0
  45. package/dist/filter/evaluate.js +33 -0
  46. package/dist/filter/index.d.ts +7 -0
  47. package/dist/filter/lift.js +107 -0
  48. package/dist/filter/lower.js +170 -0
  49. package/dist/filter/operators.js +26 -0
  50. package/dist/filter/order-by.d.ts +23 -0
  51. package/dist/filter/order-by.js +51 -0
  52. package/dist/filter/parse.d.ts +20 -0
  53. package/dist/filter/parse.js +28 -0
  54. package/dist/filter/print.d.ts +5 -0
  55. package/dist/filter/print.js +7 -0
  56. package/dist/filter/profile.js +69 -0
  57. package/dist/filter/where.d.ts +58 -0
  58. package/dist/filter/where.js +121 -0
  59. package/dist/index.d.ts +28 -0
  60. package/dist/index.js +21 -0
  61. package/dist/keys.d.ts +12 -0
  62. package/dist/keys.js +57 -0
  63. package/dist/layout.d.ts +12 -0
  64. package/dist/layout.js +21 -0
  65. package/dist/model.d.ts +333 -0
  66. package/dist/naming.js +4 -0
  67. package/dist/nav-recent.d.ts +8 -0
  68. package/dist/nav-recent.js +37 -0
  69. package/dist/refs.d.ts +11 -0
  70. package/dist/refs.js +8 -0
  71. package/dist/relation-field.d.ts +6 -0
  72. package/dist/relation-field.js +12 -0
  73. package/dist/resource-model.d.ts +15 -0
  74. package/dist/resource-model.js +71 -0
  75. package/dist/resource-types.d.ts +21 -0
  76. package/dist/resource.d.ts +47 -0
  77. package/dist/resource.js +97 -0
  78. package/dist/search-match.d.ts +29 -0
  79. package/dist/search-match.js +50 -0
  80. package/dist/user-menu.d.ts +25 -0
  81. package/dist/user-menu.js +32 -0
  82. package/dist/view-actions.d.ts +24 -0
  83. package/dist/view-actions.js +35 -0
  84. package/dist/view-field.d.ts +16 -0
  85. package/dist/view-field.js +39 -0
  86. package/dist/view-widgets.d.ts +16 -0
  87. package/dist/view-widgets.js +24 -0
  88. package/dist/view.d.ts +69 -0
  89. package/dist/view.js +109 -0
  90. package/package.json +31 -3
  91. package/README.md +0 -3
@@ -0,0 +1,123 @@
1
+ import { filterError } from "./errors.js";
2
+ import { checkValue } from "./check-value.js";
3
+ import { resolveField } from "./check-field.js";
4
+ import { compareOps, isOpaque } from "./operators.js";
5
+ //#region src/filter/check-model.ts
6
+ const value = (ctx, field, node) => {
7
+ const problem = checkValue(field, node);
8
+ if (problem) ctx.errors.push(filterError("invalid-value", problem.message, problem.hint, node.span));
9
+ return !problem;
10
+ };
11
+ const operatorError = (ctx, message, hint, span) => {
12
+ ctx.errors.push(filterError("operator-not-allowed", message, hint, span));
13
+ };
14
+ const isWildcard = (op, node) => (op === "=" || op === "!=") && node.kind === "string" && node.value.includes("*");
15
+ const checkCompare = (ctx, node) => {
16
+ const field = resolveField(ctx, node.field, "filterable");
17
+ if (!field) return void 0;
18
+ const allowed = compareOps(field.type);
19
+ if (!allowed.includes(node.op)) {
20
+ operatorError(ctx, `Operator "${node.op}" is not supported for ${field.type} fields`, allowed.length ? `Supported: ${allowed.join(" ")}` : "Use field:value on json fields, or isNull(field)", node.span);
21
+ return;
22
+ }
23
+ if (isWildcard(node.op, node.value) && field.type !== "text") {
24
+ ctx.errors.push(filterError("wildcard-not-allowed", `Wildcards are only supported on text fields, not ${field.type}`, "Remove the \"*\" or filter a text field", node.value.span));
25
+ return;
26
+ }
27
+ if (!isWildcard(node.op, node.value) && !value(ctx, field, node.value)) return void 0;
28
+ return {
29
+ ...node,
30
+ field
31
+ };
32
+ };
33
+ const checkHas = (ctx, node) => {
34
+ const field = resolveField(ctx, node.field, "filterable");
35
+ if (!field) return void 0;
36
+ if (field.type !== "json") {
37
+ operatorError(ctx, `":" is only supported on json fields, not ${field.type}`, `Use = on ${field.type} fields, or field:* to test for a value`, node.span);
38
+ return;
39
+ }
40
+ return {
41
+ ...node,
42
+ field
43
+ };
44
+ };
45
+ const checkIn = (ctx, node) => {
46
+ const field = resolveField(ctx, node.field, "filterable");
47
+ if (!field) return void 0;
48
+ if (isOpaque(field.type)) {
49
+ operatorError(ctx, `in() is not supported for ${field.type} fields`, "Use field:value on json fields", node.span);
50
+ return;
51
+ }
52
+ return node.values.map((literal) => value(ctx, field, literal)).every(Boolean) ? {
53
+ ...node,
54
+ field
55
+ } : void 0;
56
+ };
57
+ const checkText = (ctx, name, path) => {
58
+ const field = resolveField(ctx, path, "filterable");
59
+ if (field && field.type !== "text") {
60
+ operatorError(ctx, `${name}() only works on text fields, not ${field.type}`, `Field "${field.name}" is ${field.type}`, path.span);
61
+ return;
62
+ }
63
+ return field;
64
+ };
65
+ const checkNode = (ctx, node) => {
66
+ switch (node.kind) {
67
+ case "and":
68
+ case "or": {
69
+ const args = node.args.map((arg) => checkNode(ctx, arg));
70
+ return args.every((arg) => arg) ? {
71
+ ...node,
72
+ args
73
+ } : void 0;
74
+ }
75
+ case "not": {
76
+ const arg = checkNode(ctx, node.arg);
77
+ return arg && {
78
+ ...node,
79
+ arg
80
+ };
81
+ }
82
+ case "compare": return checkCompare(ctx, node);
83
+ case "has": return checkHas(ctx, node);
84
+ case "in": return checkIn(ctx, node);
85
+ case "search":
86
+ if (ctx.model.search?.length) return node;
87
+ ctx.errors.push(filterError("no-search-fields", "This resource has no search fields", "Declare them with .search((r) => [r.title]) in data.ts", node.span));
88
+ return;
89
+ case "present":
90
+ case "isNull": {
91
+ const field = resolveField(ctx, node.field, "filterable");
92
+ return field && {
93
+ ...node,
94
+ field
95
+ };
96
+ }
97
+ case "similar":
98
+ case "regex": {
99
+ const field = checkText(ctx, node.kind, node.field);
100
+ return field && {
101
+ ...node,
102
+ field
103
+ };
104
+ }
105
+ }
106
+ };
107
+ const checkAgainstModel = (model, ast) => {
108
+ const ctx = {
109
+ model,
110
+ errors: []
111
+ };
112
+ const filter = checkNode(ctx, ast);
113
+ if (!filter || ctx.errors.length > 0) return {
114
+ ok: false,
115
+ errors: [...ctx.errors].sort((a, b) => a.span.start - b.span.start)
116
+ };
117
+ return {
118
+ ok: true,
119
+ filter
120
+ };
121
+ };
122
+ //#endregion
123
+ export { checkAgainstModel };
@@ -0,0 +1,64 @@
1
+ import { isValidDate, isValidTimestamp } from "./datetime.js";
2
+ //#region src/filter/check-value.ts
3
+ const problem = (message, hint) => ({
4
+ message,
5
+ hint
6
+ });
7
+ const int64Min = -(2n ** 63n);
8
+ const int64Max = 2n ** 63n - 1n;
9
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
10
+ const numericText = (value) => value.kind === "number" ? value.raw : value.kind === "string" ? value.value : void 0;
11
+ const expected = (what, hint = `Write ${what} values like the examples in the docs`) => problem(`Expected ${what}`, hint);
12
+ const checkInteger = (value) => {
13
+ const text = numericText(value);
14
+ const number = text === void 0 ? NaN : Number(text);
15
+ if (text === void 0 || text.trim() === "" || !Number.isSafeInteger(number)) return expected("a whole number", "Use an integer such as 42, or a bigint field for larger values");
16
+ };
17
+ const checkBigint = (value) => {
18
+ const text = numericText(value);
19
+ if (text === void 0 || !/^-?\d+$/.test(text) || BigInt(text) < int64Min || BigInt(text) > int64Max) return expected("a 64-bit integer", "Use plain digits such as 9007199254740993 (no exponent or fraction)");
20
+ };
21
+ const checkDecimal = (value) => {
22
+ const text = numericText(value);
23
+ if (text === void 0 || !/^-?\d+(\.\d+)?$/.test(text)) return expected("a decimal number", "Use plain digits such as 100.50, or quote it: \"100.50\" (no exponent)");
24
+ };
25
+ const checkValue = (field, value) => {
26
+ if (value.kind === "now") {
27
+ if (field.type === "date" || field.type === "timestamp") return void 0;
28
+ return problem("now() only works with date and timestamp fields", `Field "${field.name}" is ${field.type}`);
29
+ }
30
+ switch (field.type) {
31
+ case "text":
32
+ if (value.kind !== "string") return expected("a quoted string", "Wrap the text in double quotes");
33
+ return;
34
+ case "integer": return checkInteger(value);
35
+ case "bigint": return checkBigint(value);
36
+ case "decimal": return checkDecimal(value);
37
+ case "boolean": return value.kind === "boolean" ? void 0 : expected("true or false");
38
+ case "date":
39
+ if (value.kind === "string" && isValidDate(value.value)) return void 0;
40
+ return expected("a date", "Quote a calendar date: \"2012-04-21\"");
41
+ case "timestamp":
42
+ if (value.kind === "timestamp" || value.kind === "string" && isValidTimestamp(value.value)) return void 0;
43
+ return expected("an RFC-3339 timestamp", "Use 2012-04-21T11:30:00Z, or now() - 7d");
44
+ case "uuid":
45
+ if (value.kind === "string" && uuid.test(value.value)) return void 0;
46
+ return expected("a uuid", "Quote a uuid like \"0b5a4f0e-6f2a-4a3c-9d4e-1f2a3b4c5d6e\"");
47
+ case "enum":
48
+ if (value.kind === "string" && field.enumValues?.includes(value.value)) return void 0;
49
+ return problem("Invalid enum value", `Allowed values: ${(field.enumValues ?? []).join(", ")}`);
50
+ case "currency":
51
+ if (value.kind === "string" && /^[A-Z]{3}$/.test(value.value)) return void 0;
52
+ return expected("an ISO 4217 currency code", "Use three capital letters, for example \"EUR\"");
53
+ case "country":
54
+ if (value.kind === "string" && /^[A-Z]{2}$/.test(value.value)) return void 0;
55
+ return expected("an ISO 3166-1 country code", "Use two capital letters, for example \"NL\"");
56
+ case "relation":
57
+ if (value.kind === "string" || value.kind === "number" && Number.isSafeInteger(value.value)) return void 0;
58
+ return expected("a record key", "Use the related record key: 42 or \"abc\"");
59
+ case "json":
60
+ case "file": return problem(`${field.type} fields cannot be compared`, "Use field:value on json fields, or isNull(field)");
61
+ }
62
+ };
63
+ //#endregion
64
+ export { checkValue };
@@ -0,0 +1,16 @@
1
+ import { CheckedFilter, FilterError, FilterExpr, ResourceModel } from "../model.js";
2
+ //#region src/filter/check.d.ts
3
+ export type CheckResult = {
4
+ ok: true;
5
+ filter: CheckedFilter;
6
+ } | {
7
+ ok: false;
8
+ errors: FilterError[];
9
+ };
10
+ /**
11
+ * Validates against the resource: the library checks fields, aliases, filterability, enum
12
+ * values, operators and function signatures; the model pass then checks the value formats the
13
+ * library cannot know (decimal, bigint, uuid, date, ...), traversal and search fields.
14
+ */
15
+ export declare const checkFilter: (model: ResourceModel, ast: FilterExpr) => CheckResult;
16
+ //#endregion
@@ -0,0 +1,22 @@
1
+ import { schemaOf } from "./profile.js";
2
+ import { checkAgainstModel } from "./check-model.js";
3
+ import { lift } from "./lift.js";
4
+ import { checkFilter } from "aip-parsers/filter";
5
+ //#region src/filter/check.ts
6
+ /**
7
+ * Validates against the resource: the library checks fields, aliases, filterability, enum
8
+ * values, operators and function signatures; the model pass then checks the value formats the
9
+ * library cannot know (decimal, bigint, uuid, date, ...), traversal and search fields.
10
+ */
11
+ const checkFilter$1 = (model, ast) => {
12
+ const traversal = checkAgainstModel(model, ast);
13
+ if (!traversal.ok && traversal.errors.some((e) => e.code === "unsupported-traversal")) return traversal;
14
+ const generic = checkFilter(lift(ast), schemaOf(model));
15
+ if (!generic.ok) return {
16
+ ok: false,
17
+ errors: generic.errors
18
+ };
19
+ return checkAgainstModel(model, ast);
20
+ };
21
+ //#endregion
22
+ export { checkFilter$1 as checkFilter };
@@ -0,0 +1,33 @@
1
+ //#region src/filter/datetime.ts
2
+ const daysIn = (year, month) => {
3
+ if (month === 2) return year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0) ? 29 : 28;
4
+ return [
5
+ 4,
6
+ 6,
7
+ 9,
8
+ 11
9
+ ].includes(month) ? 30 : 31;
10
+ };
11
+ const isValidDate = (text) => {
12
+ const match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(text);
13
+ if (!match) return false;
14
+ const [year, month, day] = [
15
+ Number(match[1]),
16
+ Number(match[2]),
17
+ Number(match[3])
18
+ ];
19
+ return month >= 1 && month <= 12 && day >= 1 && day <= daysIn(year, month);
20
+ };
21
+ const isValidTimestamp = (text) => {
22
+ const match = /^(\d{4}-\d{2}-\d{2})[Tt](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:[Zz]|[+-](\d{2}):(\d{2}))$/.exec(text);
23
+ if (!match) return false;
24
+ const [hour, minute, second] = [
25
+ Number(match[2]),
26
+ Number(match[3]),
27
+ Number(match[4])
28
+ ];
29
+ const [offsetHour, offsetMinute] = [Number(match[5] ?? 0), Number(match[6] ?? 0)];
30
+ return isValidDate(match[1]) && hour <= 23 && minute <= 59 && second <= 59 && offsetHour <= 23 && offsetMinute <= 59;
31
+ };
32
+ //#endregion
33
+ export { isValidDate, isValidTimestamp };
@@ -0,0 +1,16 @@
1
+ //#region src/filter/duration.ts
2
+ const parseDurationText = (text) => {
3
+ const match = /^(\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)([smhdw])$/.exec(text);
4
+ if (!match) throw new Error(`Invalid duration "${text}", expected for example 30d or 1.5h`);
5
+ return {
6
+ kind: "duration",
7
+ amount: Number(match[1]),
8
+ unit: match[2],
9
+ span: {
10
+ start: 0,
11
+ end: 0
12
+ }
13
+ };
14
+ };
15
+ //#endregion
16
+ export { parseDurationText };
@@ -0,0 +1,29 @@
1
+ //#region src/filter/errors.ts
2
+ const filterError = (code, message, hint, span) => ({
3
+ code,
4
+ message,
5
+ hint,
6
+ span
7
+ });
8
+ const distance = (a, b) => {
9
+ let row = Array.from({ length: b.length + 1 }, (_, i) => i);
10
+ for (let i = 1; i <= a.length; i++) {
11
+ const next = [i];
12
+ for (let j = 1; j <= b.length; j++) {
13
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
14
+ next[j] = Math.min(row[j] + 1, next[j - 1] + 1, row[j - 1] + cost);
15
+ }
16
+ row = next;
17
+ }
18
+ return row[b.length];
19
+ };
20
+ const suggest = (name, candidates) => {
21
+ const lower = name.toLowerCase();
22
+ const scored = candidates.map((candidate) => ({
23
+ candidate,
24
+ score: distance(lower, candidate.toLowerCase())
25
+ })).sort((a, b) => a.score - b.score)[0];
26
+ return scored && scored.score <= Math.max(2, Math.floor(name.length / 3)) ? scored.candidate : void 0;
27
+ };
28
+ //#endregion
29
+ export { filterError, suggest };
@@ -0,0 +1,12 @@
1
+ import { CheckedFilter } from "../model.js";
2
+ //#region src/filter/evaluate.d.ts
3
+ export type EvaluateOptions = {
4
+ /** Clock for `now()`; defaults to the current time. */
5
+ now?: () => Date;
6
+ };
7
+ /**
8
+ * Evaluates a checked filter against a plain record keyed by field name, for rules such as
9
+ * access filters on create. Throws `Not implemented` for `search` and `similar`.
10
+ */
11
+ export declare const evaluateFilter: (checked: CheckedFilter, record: unknown, options?: EvaluateOptions) => boolean;
12
+ //#endregion
@@ -0,0 +1,33 @@
1
+ import { functions } from "./profile.js";
2
+ import { lift } from "./lift.js";
3
+ import { evaluateFilter } from "aip-parsers/filter";
4
+ //#region src/filter/evaluate.ts
5
+ const needsDatabase = (name) => () => {
6
+ throw new Error(`Not implemented: ${name}() needs the database (the resource's search fields, trigram matching), so it cannot be evaluated against a record`);
7
+ };
8
+ const implementations = (now) => ({
9
+ ...functions,
10
+ search: {
11
+ ...functions.search,
12
+ evaluate: needsDatabase("search")
13
+ },
14
+ similar: {
15
+ ...functions.similar,
16
+ evaluate: needsDatabase("similar")
17
+ },
18
+ regex: {
19
+ ...functions.regex,
20
+ evaluate: (values, pattern) => values.some((value) => typeof value === "string" && new RegExp(pattern).test(value))
21
+ },
22
+ now: {
23
+ ...functions.now,
24
+ evaluate: now
25
+ }
26
+ });
27
+ /**
28
+ * Evaluates a checked filter against a plain record keyed by field name, for rules such as
29
+ * access filters on create. Throws `Not implemented` for `search` and `similar`.
30
+ */
31
+ const evaluateFilter$1 = (checked, record, options = {}) => evaluateFilter(lift(checked, { expand: true }), record, { functions: implementations(options.now ?? (() => /* @__PURE__ */ new Date())) });
32
+ //#endregion
33
+ export { evaluateFilter$1 as evaluateFilter };
@@ -0,0 +1,7 @@
1
+ import { FilterLimits, FilterParse, parseFilter } from "./parse.js";
2
+ import { CheckResult, checkFilter } from "./check.js";
3
+ import { EvaluateOptions, evaluateFilter } from "./evaluate.js";
4
+ import { printFilter } from "./print.js";
5
+ import { OrderByCheck, OrderByParse, checkOrderBy, parseOrderBy, printOrderBy } from "./order-by.js";
6
+ import { createWhere, where } from "./where.js";
7
+ export { type CheckResult, type EvaluateOptions, type FilterLimits, type FilterParse, type OrderByCheck, type OrderByParse, checkFilter, checkOrderBy, createWhere, evaluateFilter, parseFilter, parseOrderBy, printFilter, printOrderBy, where };
@@ -0,0 +1,107 @@
1
+ import { libraryType } from "./profile.js";
2
+ //#region src/filter/lift.ts
3
+ const member = (field, span) => "path" in field ? {
4
+ kind: "member",
5
+ path: field.path,
6
+ span: field.span
7
+ } : {
8
+ kind: "member",
9
+ path: [field.name],
10
+ field: {
11
+ name: field.name,
12
+ spec: { type: libraryType(field) }
13
+ },
14
+ span
15
+ };
16
+ const call = (name, args, span) => ({
17
+ kind: "call",
18
+ name,
19
+ args,
20
+ span
21
+ });
22
+ const text = (value, span) => ({
23
+ kind: "string",
24
+ value,
25
+ span
26
+ });
27
+ const value = (node) => {
28
+ if (node.kind !== "now") return node;
29
+ const now = {
30
+ kind: "call",
31
+ name: "now",
32
+ args: [],
33
+ span: node.span
34
+ };
35
+ if (!node.offset) return now;
36
+ return {
37
+ kind: "binary",
38
+ op: node.offset.sign,
39
+ left: now,
40
+ right: node.offset.duration,
41
+ span: node.span
42
+ };
43
+ };
44
+ /** Protobase's filter shape as a generic AIP-160 expression, for checking, printing and evaluating. */
45
+ const lift = (node, options = {}) => {
46
+ const next = (child) => lift(child, options);
47
+ switch (node.kind) {
48
+ case "and":
49
+ case "or": return {
50
+ kind: node.kind,
51
+ args: node.args.map(next),
52
+ span: node.span
53
+ };
54
+ case "not": return {
55
+ kind: "not",
56
+ arg: next(node.arg),
57
+ span: node.span
58
+ };
59
+ case "compare": return {
60
+ kind: "compare",
61
+ op: node.op,
62
+ left: member(node.field, node.span),
63
+ right: value(node.value),
64
+ span: node.span
65
+ };
66
+ case "has": return {
67
+ kind: "has",
68
+ left: member(node.field, node.span),
69
+ right: node.value,
70
+ span: node.span
71
+ };
72
+ case "present": return {
73
+ kind: "present",
74
+ left: member(node.field, node.span),
75
+ span: node.span
76
+ };
77
+ case "in":
78
+ if (options.expand) return {
79
+ kind: "or",
80
+ args: node.values.map((literal) => ({
81
+ kind: "compare",
82
+ op: "=",
83
+ left: member(node.field, node.span),
84
+ right: literal,
85
+ span: node.span
86
+ })),
87
+ span: node.span
88
+ };
89
+ return call("in", [member(node.field, node.span), ...node.values], node.span);
90
+ case "search": return call("search", [text(node.text, node.span)], node.span);
91
+ case "similar": return call("similar", [member(node.field, node.span), text(node.text, node.span)], node.span);
92
+ case "regex": return call("regex", [member(node.field, node.span), text(node.pattern, node.span)], node.span);
93
+ case "isNull":
94
+ if (options.expand) return {
95
+ kind: "not",
96
+ arg: {
97
+ kind: "present",
98
+ left: member(node.field, node.span),
99
+ span: node.span
100
+ },
101
+ span: node.span
102
+ };
103
+ return call("isNull", [member(node.field, node.span)], node.span);
104
+ }
105
+ };
106
+ //#endregion
107
+ export { lift };
@@ -0,0 +1,170 @@
1
+ import { filterError } from "./errors.js";
2
+ import { functions } from "./profile.js";
3
+ //#region src/filter/lower.ts
4
+ const unsupported = (l, message, hint, span) => {
5
+ l.errors.push(filterError("unsupported", message, hint, span));
6
+ };
7
+ const fieldOf = (l, node) => node.kind === "member" ? {
8
+ kind: "field",
9
+ path: node.path,
10
+ span: node.span
11
+ } : unsupported(l, "Only fields can be compared", "Put a field name on the left, for example total > 100", node.span);
12
+ const valueOf = (l, node) => {
13
+ switch (node.kind) {
14
+ case "member": return {
15
+ kind: "string",
16
+ value: node.path.join("."),
17
+ span: node.span
18
+ };
19
+ case "call":
20
+ if (node.name === "now" && node.args.length === 0) return {
21
+ kind: "now",
22
+ span: node.span
23
+ };
24
+ return unsupported(l, `${node.name}() cannot be used as a value`, "Only now() is a value function", node.span);
25
+ case "binary": {
26
+ const left = node.left;
27
+ if (left.kind === "call" && left.name === "now" && left.args.length === 0 && node.right.kind === "duration") return {
28
+ kind: "now",
29
+ offset: {
30
+ sign: node.op,
31
+ duration: node.right
32
+ },
33
+ span: node.span
34
+ };
35
+ return unsupported(l, "Arithmetic is only supported as now() +/- duration", "Write now() - 30d", node.span);
36
+ }
37
+ default: return node;
38
+ }
39
+ };
40
+ const literalOf = (l, node) => {
41
+ const value = "kind" in node && [
42
+ "member",
43
+ "call",
44
+ "binary"
45
+ ].includes(node.kind) ? void 0 : node;
46
+ if (value) return value;
47
+ if (node.kind === "member") return {
48
+ kind: "string",
49
+ value: node.path.join("."),
50
+ span: node.span
51
+ };
52
+ return unsupported(l, "Expected a literal value", "Use strings, numbers, true/false, durations or timestamps", node.span);
53
+ };
54
+ const wordOf = (node) => {
55
+ switch (node.kind) {
56
+ case "member": return node.path.join(".");
57
+ case "string":
58
+ case "timestamp": return node.kind === "string" ? node.value : node.value;
59
+ case "number": return node.raw;
60
+ case "boolean": return String(node.value);
61
+ case "duration": return `${node.amount}${node.unit}`;
62
+ default: return "";
63
+ }
64
+ };
65
+ const lowerCall = (l, node) => {
66
+ const [first, second, ...rest] = node.args;
67
+ const field = first?.kind === "member" ? fieldOf(l, first) : void 0;
68
+ const string = (arg) => arg?.kind === "string" ? arg.value : void 0;
69
+ const bad = (usage) => {
70
+ l.errors.push(filterError("invalid-arguments", `Invalid arguments to ${node.name}()`, `Usage: ${usage}`, node.span));
71
+ };
72
+ switch (node.name) {
73
+ case "in": {
74
+ const values = node.args.slice(1).map((arg) => literalOf(l, arg));
75
+ if (!field || values.length === 0) return bad("in(field, value1, value2, ...)");
76
+ return values.every((v) => v) ? {
77
+ kind: "in",
78
+ field,
79
+ values,
80
+ span: node.span
81
+ } : void 0;
82
+ }
83
+ case "search": return string(first) !== void 0 && !second ? {
84
+ kind: "search",
85
+ text: string(first),
86
+ span: node.span
87
+ } : bad("search(\"text\")");
88
+ case "similar": return field && string(second) !== void 0 && rest.length === 0 ? {
89
+ kind: "similar",
90
+ field,
91
+ text: string(second),
92
+ span: node.span
93
+ } : bad("similar(field, \"text\")");
94
+ case "regex": return field && string(second) !== void 0 && rest.length === 0 ? {
95
+ kind: "regex",
96
+ field,
97
+ pattern: string(second),
98
+ span: node.span
99
+ } : bad("regex(field, \"pattern\")");
100
+ case "isNull": return field && !second ? {
101
+ kind: "isNull",
102
+ field,
103
+ span: node.span
104
+ } : bad("isNull(field)");
105
+ default:
106
+ l.errors.push(filterError("unknown-function", `Unknown function "${node.name}"`, `Available functions: ${Object.keys(functions).join(", ")}`, node.span));
107
+ return;
108
+ }
109
+ };
110
+ const all = (items) => items.every((item) => item) ? items : void 0;
111
+ /** Lowers a generic AIP-160 expression to Protobase's filter shape; unsupported constructs become errors. */
112
+ const lower = (l, node) => {
113
+ switch (node.kind) {
114
+ case "sequence":
115
+ case "and":
116
+ case "or": {
117
+ const args = all(node.args.map((arg) => lower(l, arg)));
118
+ return args && {
119
+ kind: node.kind === "or" ? "or" : "and",
120
+ args,
121
+ span: node.span
122
+ };
123
+ }
124
+ case "not": {
125
+ const arg = lower(l, node.arg);
126
+ return arg && {
127
+ kind: "not",
128
+ arg,
129
+ span: node.span
130
+ };
131
+ }
132
+ case "compare": {
133
+ const field = fieldOf(l, node.left);
134
+ const value = valueOf(l, node.right);
135
+ return field && value && {
136
+ kind: "compare",
137
+ op: node.op,
138
+ field,
139
+ value,
140
+ span: node.span
141
+ };
142
+ }
143
+ case "has": {
144
+ const field = fieldOf(l, node.left);
145
+ const value = literalOf(l, node.right);
146
+ return field && value && {
147
+ kind: "has",
148
+ field,
149
+ value,
150
+ span: node.span
151
+ };
152
+ }
153
+ case "present": {
154
+ const field = fieldOf(l, node.left);
155
+ return field && {
156
+ kind: "present",
157
+ field,
158
+ span: node.span
159
+ };
160
+ }
161
+ case "global": return {
162
+ kind: "search",
163
+ text: wordOf(node.value),
164
+ span: node.span
165
+ };
166
+ case "call": return lowerCall(l, node);
167
+ }
168
+ };
169
+ //#endregion
170
+ export { lower };
@@ -0,0 +1,26 @@
1
+ //#region src/filter/operators.ts
2
+ const equality = ["=", "!="];
3
+ const ordered = [
4
+ "=",
5
+ "!=",
6
+ "<",
7
+ "<=",
8
+ ">",
9
+ ">="
10
+ ];
11
+ const compareOps = (type) => {
12
+ switch (type) {
13
+ case "text":
14
+ case "integer":
15
+ case "bigint":
16
+ case "decimal":
17
+ case "date":
18
+ case "timestamp": return ordered;
19
+ case "json":
20
+ case "file": return [];
21
+ default: return equality;
22
+ }
23
+ };
24
+ const isOpaque = (type) => type === "json" || type === "file";
25
+ //#endregion
26
+ export { compareOps, isOpaque };