@getaster/data 0.0.0-stage → 0.1.0-preview.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.
- package/LICENSE +202 -0
- package/NOTICE +10 -0
- package/README.md +6 -2
- package/dist/authentication.d.ts +42 -0
- package/dist/authentication.js +121 -0
- package/dist/changes/changeset.d.ts +47 -0
- package/dist/changes/changeset.js +75 -0
- package/dist/collection/aggregate.d.ts +22 -0
- package/dist/collection/aggregate.js +94 -0
- package/dist/collection/collection.d.ts +96 -0
- package/dist/collection/collection.js +588 -0
- package/dist/collection/grouping.d.ts +75 -0
- package/dist/collection/grouping.js +190 -0
- package/dist/collection/groups.d.ts +31 -0
- package/dist/collection/groups.js +117 -0
- package/dist/collection/summary.d.ts +17 -0
- package/dist/collection/summary.js +103 -0
- package/dist/composition/activity.d.ts +36 -0
- package/dist/composition/activity.js +60 -0
- package/dist/composition/command.d.ts +22 -0
- package/dist/composition/command.js +57 -0
- package/dist/composition/lifetime.d.ts +11 -0
- package/dist/composition/lifetime.js +33 -0
- package/dist/composition/sessions.d.ts +64 -0
- package/dist/composition/sessions.js +209 -0
- package/dist/context.d.ts +76 -0
- package/dist/context.js +211 -0
- package/dist/criteria/ast.d.ts +79 -0
- package/dist/criteria/ast.js +1 -0
- package/dist/criteria/compile.d.ts +37 -0
- package/dist/criteria/compile.js +233 -0
- package/dist/criteria/evaluate.d.ts +20 -0
- package/dist/criteria/evaluate.js +171 -0
- package/dist/criteria/filter-draft.d.ts +79 -0
- package/dist/criteria/filter-draft.js +320 -0
- package/dist/criteria/relative.d.ts +13 -0
- package/dist/criteria/relative.js +82 -0
- package/dist/criteria/values.d.ts +20 -0
- package/dist/criteria/values.js +283 -0
- package/dist/edit/edit-model.d.ts +152 -0
- package/dist/edit/edit-model.js +620 -0
- package/dist/edit/list-edit.d.ts +116 -0
- package/dist/edit/list-edit.js +332 -0
- package/dist/edit/record-edit.d.ts +93 -0
- package/dist/edit/record-edit.js +317 -0
- package/dist/export/csv.d.ts +25 -0
- package/dist/export/csv.js +75 -0
- package/dist/index.d.ts +38 -0
- package/dist/index.js +35 -0
- package/dist/odata/errors.d.ts +25 -0
- package/dist/odata/errors.js +56 -0
- package/dist/odata/source.d.ts +65 -0
- package/dist/odata/source.js +193 -0
- package/dist/progress/status.d.ts +29 -0
- package/dist/progress/status.js +105 -0
- package/dist/schema/csdl.d.ts +13 -0
- package/dist/schema/csdl.js +531 -0
- package/dist/schema/model.d.ts +132 -0
- package/dist/schema/model.js +1 -0
- package/dist/scope/filters.d.ts +130 -0
- package/dist/scope/filters.js +349 -0
- package/dist/scope/publications.d.ts +27 -0
- package/dist/scope/publications.js +74 -0
- package/dist/scope/related-aggregate.d.ts +9 -0
- package/dist/scope/related-aggregate.js +67 -0
- package/dist/scope/scope.d.ts +181 -0
- package/dist/scope/scope.js +574 -0
- package/dist/selection/selection.d.ts +56 -0
- package/dist/selection/selection.js +137 -0
- package/dist/store/keys.d.ts +10 -0
- package/dist/store/keys.js +21 -0
- package/dist/store/store.d.ts +72 -0
- package/dist/store/store.js +143 -0
- package/dist/template/template.d.ts +19 -0
- package/dist/template/template.js +186 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +20 -0
- package/dist/tree/tree.d.ts +117 -0
- package/dist/tree/tree.js +435 -0
- package/dist/validate/criteria-rule.d.ts +28 -0
- package/dist/validate/criteria-rule.js +22 -0
- package/dist/validate/validate.d.ts +13 -0
- package/dist/validate/validate.js +123 -0
- package/package.json +32 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import { batch, computed, signal } from "@preact/signals-core";
|
|
2
|
+
import { toFilter } from "./compile.js";
|
|
3
|
+
import { relatedOf, resolveRelative } from "./relative.js";
|
|
4
|
+
/** Typed operators offered by both metadata-driven query controls and filter drafts. */
|
|
5
|
+
export function filterOperators(field) {
|
|
6
|
+
if (!field || field.type === "Edm.Binary")
|
|
7
|
+
return [];
|
|
8
|
+
const equality = ["eq", "ne", "in", "isnull", "notnull"];
|
|
9
|
+
if (field.allowedValues)
|
|
10
|
+
return equality;
|
|
11
|
+
switch (field.type) {
|
|
12
|
+
case "Edm.Boolean":
|
|
13
|
+
case "Edm.Guid": return equality;
|
|
14
|
+
case "Edm.String": return [...equality, "contains", "startswith", "endswith"];
|
|
15
|
+
default: return [...equality, "gt", "ge", "lt", "le"];
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** A query draft independent of database clause records. Only apply changes its committed criteria. */
|
|
19
|
+
export class FilterDraft {
|
|
20
|
+
schema;
|
|
21
|
+
set;
|
|
22
|
+
#root = signal({ kind: "group", id: "", op: "and", children: [], synthetic: true });
|
|
23
|
+
#applied = signal(undefined);
|
|
24
|
+
#baseline = signal("");
|
|
25
|
+
#sequence = 0;
|
|
26
|
+
root = this.#root;
|
|
27
|
+
applied = this.#applied;
|
|
28
|
+
dirty = computed(() => JSON.stringify(this.#root.value) !== this.#baseline.value);
|
|
29
|
+
result;
|
|
30
|
+
constructor(schema, set, criteria) {
|
|
31
|
+
this.schema = schema;
|
|
32
|
+
this.set = set;
|
|
33
|
+
schema.set(set);
|
|
34
|
+
this.result = computed(() => {
|
|
35
|
+
const problems = [];
|
|
36
|
+
const root = this.#root.value;
|
|
37
|
+
const criteria = root.children.length === 0 && root.synthetic ? undefined : this.#compile(root, schema.set(set).type, problems);
|
|
38
|
+
if (criteria && !problems.length) {
|
|
39
|
+
try {
|
|
40
|
+
toFilter(resolveRelative(criteria, schema.set(set).type, Date.now(), relatedOf(schema, schema.set(set).type)), schema.set(set).type, schema.set(set), schema);
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
problems.push({ id: root.id, message: error instanceof Error ? error.message : String(error) });
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return { valid: problems.length === 0, criteria: problems.length ? undefined : criteria, problems };
|
|
47
|
+
});
|
|
48
|
+
this.reset(criteria);
|
|
49
|
+
}
|
|
50
|
+
/** Replaces the applied value and draft together; no query or record write is performed. */
|
|
51
|
+
reset(criteria) {
|
|
52
|
+
const snapshot = criteria === undefined ? undefined : structuredClone(criteria);
|
|
53
|
+
const parsed = snapshot ? this.#from(snapshot) : undefined;
|
|
54
|
+
const root = parsed?.kind === "group" ? parsed : { kind: "group", id: this.#id(), op: "and", synthetic: true, children: parsed ? [parsed] : [] };
|
|
55
|
+
batch(() => { this.#applied.value = snapshot; this.#root.value = root; this.#baseline.value = JSON.stringify(root); });
|
|
56
|
+
}
|
|
57
|
+
/** Discards draft edits and restores the applied expression. */
|
|
58
|
+
cancel() { this.reset(this.#applied.peek()); }
|
|
59
|
+
/** Clears only the draft. Call apply to commit the cleared expression. */
|
|
60
|
+
clear() { this.#root.value = { kind: "group", id: this.#id(), op: "and", children: [], synthetic: true }; }
|
|
61
|
+
/** Commits a complete valid draft atomically; returns false for any invalid clause. */
|
|
62
|
+
apply() {
|
|
63
|
+
const result = this.result.peek();
|
|
64
|
+
if (!result.valid)
|
|
65
|
+
return false;
|
|
66
|
+
batch(() => { this.#applied.value = result.criteria; this.#baseline.value = JSON.stringify(this.#root.peek()); });
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/** Changes a clause without dropping incompatible values or unsupported fields. */
|
|
70
|
+
change(id, change) {
|
|
71
|
+
const scope = this.typeFor(id);
|
|
72
|
+
this.#map(id, (node) => {
|
|
73
|
+
switch (node.kind) {
|
|
74
|
+
case "condition": {
|
|
75
|
+
let next = { ...node, ...change };
|
|
76
|
+
if (change.op !== undefined && change.op !== node.op) {
|
|
77
|
+
if (["in", "isnull", "notnull", "contains", "startswith", "endswith"].includes(change.op))
|
|
78
|
+
next = { ...next, mode: "value" };
|
|
79
|
+
const field = scope?.field(node.field);
|
|
80
|
+
if (change.op === "in" && node.mode === "value" && field) {
|
|
81
|
+
let value = node.text;
|
|
82
|
+
try {
|
|
83
|
+
value = literal(node.text, field);
|
|
84
|
+
}
|
|
85
|
+
catch { /* Keep invalid input available for repair. */ }
|
|
86
|
+
next = { ...next, text: JSON.stringify(node.text === "" ? [] : [value]) };
|
|
87
|
+
}
|
|
88
|
+
else if (node.op === "in") {
|
|
89
|
+
try {
|
|
90
|
+
const values = JSON.parse(node.text);
|
|
91
|
+
if (Array.isArray(values) && values.length === 1 && values[0] !== null)
|
|
92
|
+
next = { ...next, text: String(values[0]) };
|
|
93
|
+
}
|
|
94
|
+
catch { /* Keep invalid input available for repair. */ }
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return next;
|
|
98
|
+
}
|
|
99
|
+
case "related": return { ...node, ...(change.field === undefined ? {} : { field: change.field }), ...(change.op === undefined ? {} : { op: change.op }) };
|
|
100
|
+
case "group": return { ...node, ...(change.op === undefined ? {} : { op: change.op, synthetic: false }) };
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
/** Adds a condition, boolean group or related-record group. Returns its stable draft ID. */
|
|
105
|
+
add(parent, kind = "condition") {
|
|
106
|
+
const id = this.#id();
|
|
107
|
+
let node;
|
|
108
|
+
switch (kind) {
|
|
109
|
+
case "condition":
|
|
110
|
+
node = { kind, id, field: "", op: "eq", mode: "value", text: "", other: "", caseInsensitive: false };
|
|
111
|
+
break;
|
|
112
|
+
case "group":
|
|
113
|
+
node = { kind, id, op: "or", children: [] };
|
|
114
|
+
break;
|
|
115
|
+
case "related":
|
|
116
|
+
node = { kind, id, field: "", op: "any", children: [] };
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
this.#map(parent, (p) => p.kind === "condition" ? p : { ...p, children: [...p.children, node] });
|
|
120
|
+
return id;
|
|
121
|
+
}
|
|
122
|
+
/** Removes a clause and its descendants. The root remains available for adding conditions. */
|
|
123
|
+
remove(id) {
|
|
124
|
+
const walk = (node) => node.kind === "condition" ? node : { ...node, children: node.children.filter((c) => c.id !== id).map(walk) };
|
|
125
|
+
this.#root.value = walk(this.#root.peek());
|
|
126
|
+
}
|
|
127
|
+
/** Entity type in scope at a clause; undefined while an ancestor navigation is invalid. */
|
|
128
|
+
typeFor(id) {
|
|
129
|
+
const walk = (node, type) => {
|
|
130
|
+
if (node.id === id)
|
|
131
|
+
return type;
|
|
132
|
+
if (node.kind === "condition")
|
|
133
|
+
return undefined;
|
|
134
|
+
const nav = node.kind === "related" ? type?.navigation(node.field) : undefined;
|
|
135
|
+
const childType = node.kind === "related" ? nav ? this.schema.set(nav.target).type : undefined : type;
|
|
136
|
+
for (const child of node.children) {
|
|
137
|
+
const found = walk(child, childType);
|
|
138
|
+
if (found)
|
|
139
|
+
return found;
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
return walk(this.#root.value, this.schema.set(this.set).type);
|
|
144
|
+
}
|
|
145
|
+
#id() { return `c${++this.#sequence}`; }
|
|
146
|
+
#map(id, update) {
|
|
147
|
+
const walk = (n) => n.id === id ? update(n) : n.kind === "condition" ? n : { ...n, children: n.children.map(walk) };
|
|
148
|
+
this.#root.value = walk(this.#root.peek());
|
|
149
|
+
}
|
|
150
|
+
#from(c) {
|
|
151
|
+
const id = this.#id();
|
|
152
|
+
switch (c.op) {
|
|
153
|
+
case "and":
|
|
154
|
+
case "or": return { kind: "group", id, op: c.op, children: c.args.map((a) => this.#from(a)) };
|
|
155
|
+
case "not": return { kind: "group", id, op: "not", children: [this.#from(c.arg)] };
|
|
156
|
+
case "any":
|
|
157
|
+
case "all":
|
|
158
|
+
case "via": return { kind: "related", id, op: c.op, field: c.field, children: [this.#from(c.where)] };
|
|
159
|
+
default: {
|
|
160
|
+
const other = "other" in c ? c.other : "";
|
|
161
|
+
const value = "value" in c ? c.value : undefined;
|
|
162
|
+
const relative = typeof value === "object" && value !== null;
|
|
163
|
+
const op = value === null && (c.op === "eq" || c.op === "ne") ? c.op === "eq" ? "isnull" : "notnull" : c.op;
|
|
164
|
+
return { kind: "condition", id, op, field: c.field, mode: other ? "field" : relative ? "relative" : "value", other,
|
|
165
|
+
text: c.op === "in" ? JSON.stringify(c.values) : relative ? JSON.stringify(value) : value === undefined || value === null ? "" : String(value),
|
|
166
|
+
caseInsensitive: "caseInsensitive" in c && c.caseInsensitive === true };
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
#compile(node, type, problems) {
|
|
171
|
+
try {
|
|
172
|
+
let result;
|
|
173
|
+
switch (node.kind) {
|
|
174
|
+
case "group": {
|
|
175
|
+
if (!node.children.length)
|
|
176
|
+
throw new Error("Add a condition or remove this empty group.");
|
|
177
|
+
const children = node.children.map((n) => this.#compile(n, type, problems)).filter((c) => c !== undefined);
|
|
178
|
+
switch (node.op) {
|
|
179
|
+
case "and":
|
|
180
|
+
case "or":
|
|
181
|
+
result = node.synthetic && children.length === 1 ? children[0] : { op: node.op, args: children };
|
|
182
|
+
break;
|
|
183
|
+
case "not":
|
|
184
|
+
result = { op: "not", arg: children.length === 1 ? children[0] : { op: "and", args: children } };
|
|
185
|
+
break;
|
|
186
|
+
default: throw new Error(`Unknown group operator ${node.op}`);
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case "related": {
|
|
191
|
+
const nav = type.navigation(node.field);
|
|
192
|
+
if (!nav)
|
|
193
|
+
throw new Error(`Choose a related field; ${node.field || "(none)"} is unavailable.`);
|
|
194
|
+
if (nav.collection ? node.op !== "any" && node.op !== "all" : node.op !== "via")
|
|
195
|
+
throw new Error("Choose an operator matching the relationship.");
|
|
196
|
+
if (!node.children.length)
|
|
197
|
+
throw new Error("Add a related-record condition.");
|
|
198
|
+
const children = node.children.map((c) => this.#compile(c, this.schema.set(nav.target).type, problems)).filter((c) => c !== undefined);
|
|
199
|
+
result = { op: node.op, field: node.field, where: children.length === 1 ? children[0] : { op: "and", args: children } };
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case "condition": {
|
|
203
|
+
const field = type.field(node.field);
|
|
204
|
+
if (!field)
|
|
205
|
+
throw new Error(`Choose a field; ${node.field || "(none)"} is unavailable. Use a related group for related fields.`);
|
|
206
|
+
if (!filterOperators(field).includes(node.op))
|
|
207
|
+
throw new Error(`Choose an operator supported by ${field.title ?? field.name}.`);
|
|
208
|
+
const fieldName = node.field;
|
|
209
|
+
switch (node.op) {
|
|
210
|
+
case "isnull":
|
|
211
|
+
case "notnull":
|
|
212
|
+
result = { op: node.op === "isnull" ? "eq" : "ne", field: fieldName, value: null };
|
|
213
|
+
break;
|
|
214
|
+
case "contains":
|
|
215
|
+
case "startswith":
|
|
216
|
+
case "endswith":
|
|
217
|
+
if (node.mode !== "value")
|
|
218
|
+
throw new Error("Text matching requires a literal value.");
|
|
219
|
+
result = { op: node.op, field: fieldName, value: node.text, ...(node.caseInsensitive ? { caseInsensitive: true } : {}) };
|
|
220
|
+
break;
|
|
221
|
+
case "in": {
|
|
222
|
+
if (node.mode !== "value")
|
|
223
|
+
throw new Error("A list requires literal values.");
|
|
224
|
+
const values = JSON.parse(node.text);
|
|
225
|
+
if (!Array.isArray(values) || values.some((v) => v !== null && !["string", "number", "boolean"].includes(typeof v)))
|
|
226
|
+
throw new Error("Enter a JSON array of typed values.");
|
|
227
|
+
if (field.allowedValues && (!values.length || values.some((v) => v !== null && !field.allowedValues.some((o) => o.value === v))))
|
|
228
|
+
throw new Error("Choose one or more of the offered values.");
|
|
229
|
+
result = { op: "in", field: fieldName, values: values };
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
default: {
|
|
233
|
+
const op = node.op;
|
|
234
|
+
if (node.mode === "field") {
|
|
235
|
+
if (!node.other)
|
|
236
|
+
throw new Error("Choose a comparison field.");
|
|
237
|
+
result = { op, field: fieldName, other: node.other };
|
|
238
|
+
}
|
|
239
|
+
else if (node.mode === "relative") {
|
|
240
|
+
const value = JSON.parse(node.text);
|
|
241
|
+
if (!value || !["today", "now"].includes(value.relative) || Object.keys(value).some((k) => !["relative", "days", "weeks", "months", "startOf"].includes(k)) || [value.days, value.weeks, value.months].some((v) => v !== undefined && !Number.isSafeInteger(v)) || (value.startOf !== undefined && value.startOf !== "week" && value.startOf !== "month"))
|
|
242
|
+
throw new Error("Enter a valid relative date.");
|
|
243
|
+
result = { op, field: fieldName, value };
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const value = literal(node.text, field);
|
|
247
|
+
if (field.allowedValues && !field.allowedValues.some((o) => o.value === value))
|
|
248
|
+
throw new Error("Choose one of the offered values.");
|
|
249
|
+
result = { op, field: fieldName, value };
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
// Validate each subtree in its entity scope without resolving stored relative dates.
|
|
257
|
+
const resolved = resolveRelative(result, type, Date.now(), relatedOf(this.schema, type));
|
|
258
|
+
toFilter(resolved, type, undefined, this.schema);
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
problems.push({ id: node.id, message: error instanceof Error ? error.message : String(error) });
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function literal(text, field) {
|
|
268
|
+
switch (field.type) {
|
|
269
|
+
case "Edm.String": return text;
|
|
270
|
+
case "Edm.Boolean":
|
|
271
|
+
if (text !== "true" && text !== "false")
|
|
272
|
+
throw new Error("Choose Yes or No.");
|
|
273
|
+
return text === "true";
|
|
274
|
+
case "Edm.Byte":
|
|
275
|
+
case "Edm.SByte":
|
|
276
|
+
case "Edm.Int16":
|
|
277
|
+
case "Edm.Int32":
|
|
278
|
+
case "Edm.Single":
|
|
279
|
+
case "Edm.Double":
|
|
280
|
+
if (!text.trim() || !Number.isFinite(Number(text)))
|
|
281
|
+
throw new Error("Enter a number.");
|
|
282
|
+
return Number(text);
|
|
283
|
+
default:
|
|
284
|
+
if (!text.trim())
|
|
285
|
+
throw new Error("Enter a value.");
|
|
286
|
+
return text.trim();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/** A readable bracketed expression using metadata titles; this is not executable text. */
|
|
290
|
+
export function describeCriteria(criteria, type) {
|
|
291
|
+
if (!criteria)
|
|
292
|
+
return "No advanced filter";
|
|
293
|
+
const title = (name) => type.field(name)?.title ?? name;
|
|
294
|
+
switch (criteria.op) {
|
|
295
|
+
case "and":
|
|
296
|
+
case "or": return `(${criteria.args.map((c) => describeCriteria(c, type)).join(criteria.op === "and" ? " AND " : " OR ")})`;
|
|
297
|
+
case "not": return `NOT (${describeCriteria(criteria.arg, type)})`;
|
|
298
|
+
case "any":
|
|
299
|
+
case "all":
|
|
300
|
+
case "via": return `${criteria.op === "any" ? "Any" : criteria.op === "all" ? "Every" : "Related"} ${criteria.field}: (${describeCriteria(criteria.where, type)})`;
|
|
301
|
+
default: {
|
|
302
|
+
const labels = { eq: "equals", ne: "does not equal", gt: "is greater than", ge: "is at least", lt: "is less than", le: "is at most", in: "is one of", contains: "contains", startswith: "starts with", endswith: "ends with" };
|
|
303
|
+
const value = "other" in criteria ? title(criteria.other) : "values" in criteria ? criteria.values.map(describeValue).join(", ") || "(no values)" : describeValue(criteria.value);
|
|
304
|
+
return `${title(criteria.field)} ${labels[criteria.op]} ${value}${"caseInsensitive" in criteria && criteria.caseInsensitive ? " (ignoring case)" : ""}`;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
function describeValue(value) {
|
|
309
|
+
if (value === null)
|
|
310
|
+
return "empty (null)";
|
|
311
|
+
if (typeof value !== "object")
|
|
312
|
+
return JSON.stringify(value);
|
|
313
|
+
const parts = [value.relative === "today" ? "today (UTC)" : "now"];
|
|
314
|
+
if (value.startOf)
|
|
315
|
+
parts.push(`aligned to start of ${value.startOf}`);
|
|
316
|
+
for (const unit of ["months", "weeks", "days"])
|
|
317
|
+
if (value[unit])
|
|
318
|
+
parts.push(`${value[unit] > 0 ? "+" : ""}${value[unit]} ${unit}`);
|
|
319
|
+
return parts.join(", ");
|
|
320
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityType, Field, Schema } from "../schema/model.ts";
|
|
2
|
+
import type { Criteria, RelativeDate } from "./ast.ts";
|
|
3
|
+
export declare function isRelative(v: unknown): v is RelativeDate;
|
|
4
|
+
export declare function hasRelative(c: Criteria | undefined): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* The type a navigation leads to, for criteria inside `any`, `all` and `via`:
|
|
7
|
+
* from `from` when given (a nested lambda), otherwise from `type`.
|
|
8
|
+
*/
|
|
9
|
+
export declare function relatedOf(schema: Schema, type: EntityType): (navigation: string, from?: EntityType) => EntityType | undefined;
|
|
10
|
+
/** `criteria` with each relative date replaced by its value at `now`, typed by its field (UTC). */
|
|
11
|
+
export declare function resolveRelative(criteria: Criteria, type: EntityType, now: number, related?: (navigation: string, from: EntityType) => EntityType | undefined): Criteria;
|
|
12
|
+
/** A relative date as a literal for `field`: a date for `Edm.Date`, an instant for `Edm.DateTimeOffset`. */
|
|
13
|
+
export declare function relativeValue(r: RelativeDate, field: Field, now: number): string;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { CriteriaError } from "./values.js";
|
|
2
|
+
const DAY = 86_400_000;
|
|
3
|
+
export function isRelative(v) {
|
|
4
|
+
return typeof v === "object" && v !== null && "relative" in v;
|
|
5
|
+
}
|
|
6
|
+
export function hasRelative(c) {
|
|
7
|
+
if (!c)
|
|
8
|
+
return false;
|
|
9
|
+
switch (c.op) {
|
|
10
|
+
case "and":
|
|
11
|
+
case "or":
|
|
12
|
+
return c.args.some(hasRelative);
|
|
13
|
+
case "not":
|
|
14
|
+
return hasRelative(c.arg);
|
|
15
|
+
case "any":
|
|
16
|
+
case "all":
|
|
17
|
+
case "via":
|
|
18
|
+
return hasRelative(c.where);
|
|
19
|
+
default:
|
|
20
|
+
return "value" in c && isRelative(c.value);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The type a navigation leads to, for criteria inside `any`, `all` and `via`:
|
|
25
|
+
* from `from` when given (a nested lambda), otherwise from `type`.
|
|
26
|
+
*/
|
|
27
|
+
export function relatedOf(schema, type) {
|
|
28
|
+
return (navigation, from = type) => {
|
|
29
|
+
const nav = from.navigation(navigation);
|
|
30
|
+
return nav ? schema.set(nav.target).type : undefined;
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
/** `criteria` with each relative date replaced by its value at `now`, typed by its field (UTC). */
|
|
34
|
+
export function resolveRelative(criteria, type, now, related) {
|
|
35
|
+
if (!hasRelative(criteria))
|
|
36
|
+
return criteria;
|
|
37
|
+
switch (criteria.op) {
|
|
38
|
+
case "and":
|
|
39
|
+
case "or":
|
|
40
|
+
return { ...criteria, args: criteria.args.map((a) => resolveRelative(a, type, now, related)) };
|
|
41
|
+
case "not":
|
|
42
|
+
return { ...criteria, arg: resolveRelative(criteria.arg, type, now, related) };
|
|
43
|
+
case "any":
|
|
44
|
+
case "all":
|
|
45
|
+
case "via": {
|
|
46
|
+
const target = related?.(criteria.field, type);
|
|
47
|
+
if (!target)
|
|
48
|
+
throw new CriteriaError(`a relative date inside ${criteria.op} over '${criteria.field}' needs the related type`);
|
|
49
|
+
return { ...criteria, where: resolveRelative(criteria.where, target, now, related) };
|
|
50
|
+
}
|
|
51
|
+
default: {
|
|
52
|
+
if (!("value" in criteria) || !isRelative(criteria.value))
|
|
53
|
+
return criteria;
|
|
54
|
+
const field = type.field(criteria.field);
|
|
55
|
+
if (!field)
|
|
56
|
+
throw new CriteriaError(`unknown field '${criteria.field}' on ${type.name}`);
|
|
57
|
+
return { ...criteria, value: relativeValue(criteria.value, field, now) };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** A relative date as a literal for `field`: a date for `Edm.Date`, an instant for `Edm.DateTimeOffset`. */
|
|
62
|
+
export function relativeValue(r, field, now) {
|
|
63
|
+
let d = new Date(r.relative === "today" ? Math.floor(now / DAY) * DAY : now);
|
|
64
|
+
if (r.startOf === "week")
|
|
65
|
+
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate() - ((d.getUTCDay() + 6) % 7)));
|
|
66
|
+
if (r.startOf === "month")
|
|
67
|
+
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), 1));
|
|
68
|
+
if (r.months) {
|
|
69
|
+
// The same day of the target month, or its last day when it's shorter (31 Jan + 1 month is 28 or 29 Feb).
|
|
70
|
+
const last = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + r.months + 1, 0)).getUTCDate();
|
|
71
|
+
d = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + r.months, Math.min(d.getUTCDate(), last), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds()));
|
|
72
|
+
}
|
|
73
|
+
d = new Date(d.getTime() + ((r.days ?? 0) + 7 * (r.weeks ?? 0)) * DAY);
|
|
74
|
+
switch (field.type) {
|
|
75
|
+
case "Edm.Date":
|
|
76
|
+
return d.toISOString().slice(0, 10);
|
|
77
|
+
case "Edm.DateTimeOffset":
|
|
78
|
+
return d.toISOString().replace(".000Z", "Z");
|
|
79
|
+
default:
|
|
80
|
+
throw new CriteriaError(`${field.name}: a relative date needs a date field, not ${field.type}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { RelativeDate } from "./ast.ts";
|
|
2
|
+
import type { Field, Value } from "../schema/model.ts";
|
|
3
|
+
/** A criteria problem: unknown field, a value that doesn't fit its field, or a bad stored value. */
|
|
4
|
+
export declare class CriteriaError extends Error {
|
|
5
|
+
constructor(message: string);
|
|
6
|
+
}
|
|
7
|
+
export declare function isFloatType(field: Field): boolean;
|
|
8
|
+
/** Throws `CriteriaError` unless `value` is a legal JS value for `field`'s Edm type. */
|
|
9
|
+
export declare function assertLiteral(value: Value, field: Field): void;
|
|
10
|
+
/** Unicode code point order: what the engine's byte-wise UTF-8 comparison gives. */
|
|
11
|
+
export declare function compareCodePoints(a: string, b: string): number;
|
|
12
|
+
/**
|
|
13
|
+
* Orders two non-null values of `field`'s type; `undefined` when unordered
|
|
14
|
+
* (a NaN is involved).
|
|
15
|
+
*/
|
|
16
|
+
export declare function ordering(a: Value, b: Value, field: Field): number | undefined;
|
|
17
|
+
/** Whether a number value is NaN (sorts after everything, equal to itself). */
|
|
18
|
+
export declare function isNaNValue(v: Value): boolean;
|
|
19
|
+
/** A comparison's value, which must be plain by now: relative dates are resolved before compiling or evaluating. */
|
|
20
|
+
export declare function plain(v: Value | RelativeDate): Value;
|