@getaster/data 0.0.0-stage → 0.1.0-preview.0

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 (84) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +6 -2
  4. package/dist/authentication.d.ts +42 -0
  5. package/dist/authentication.js +121 -0
  6. package/dist/changes/changeset.d.ts +47 -0
  7. package/dist/changes/changeset.js +75 -0
  8. package/dist/collection/aggregate.d.ts +22 -0
  9. package/dist/collection/aggregate.js +94 -0
  10. package/dist/collection/collection.d.ts +96 -0
  11. package/dist/collection/collection.js +588 -0
  12. package/dist/collection/grouping.d.ts +75 -0
  13. package/dist/collection/grouping.js +190 -0
  14. package/dist/collection/groups.d.ts +31 -0
  15. package/dist/collection/groups.js +117 -0
  16. package/dist/collection/summary.d.ts +17 -0
  17. package/dist/collection/summary.js +103 -0
  18. package/dist/composition/activity.d.ts +36 -0
  19. package/dist/composition/activity.js +60 -0
  20. package/dist/composition/command.d.ts +22 -0
  21. package/dist/composition/command.js +57 -0
  22. package/dist/composition/lifetime.d.ts +11 -0
  23. package/dist/composition/lifetime.js +33 -0
  24. package/dist/composition/sessions.d.ts +64 -0
  25. package/dist/composition/sessions.js +209 -0
  26. package/dist/context.d.ts +76 -0
  27. package/dist/context.js +211 -0
  28. package/dist/criteria/ast.d.ts +79 -0
  29. package/dist/criteria/ast.js +1 -0
  30. package/dist/criteria/compile.d.ts +37 -0
  31. package/dist/criteria/compile.js +233 -0
  32. package/dist/criteria/evaluate.d.ts +20 -0
  33. package/dist/criteria/evaluate.js +171 -0
  34. package/dist/criteria/filter-draft.d.ts +79 -0
  35. package/dist/criteria/filter-draft.js +320 -0
  36. package/dist/criteria/relative.d.ts +13 -0
  37. package/dist/criteria/relative.js +82 -0
  38. package/dist/criteria/values.d.ts +20 -0
  39. package/dist/criteria/values.js +283 -0
  40. package/dist/edit/edit-model.d.ts +152 -0
  41. package/dist/edit/edit-model.js +620 -0
  42. package/dist/edit/list-edit.d.ts +116 -0
  43. package/dist/edit/list-edit.js +332 -0
  44. package/dist/edit/record-edit.d.ts +93 -0
  45. package/dist/edit/record-edit.js +317 -0
  46. package/dist/export/csv.d.ts +25 -0
  47. package/dist/export/csv.js +75 -0
  48. package/dist/index.d.ts +38 -0
  49. package/dist/index.js +35 -0
  50. package/dist/odata/errors.d.ts +25 -0
  51. package/dist/odata/errors.js +56 -0
  52. package/dist/odata/source.d.ts +65 -0
  53. package/dist/odata/source.js +193 -0
  54. package/dist/progress/status.d.ts +29 -0
  55. package/dist/progress/status.js +105 -0
  56. package/dist/schema/csdl.d.ts +13 -0
  57. package/dist/schema/csdl.js +531 -0
  58. package/dist/schema/model.d.ts +132 -0
  59. package/dist/schema/model.js +1 -0
  60. package/dist/scope/filters.d.ts +130 -0
  61. package/dist/scope/filters.js +349 -0
  62. package/dist/scope/publications.d.ts +27 -0
  63. package/dist/scope/publications.js +74 -0
  64. package/dist/scope/related-aggregate.d.ts +9 -0
  65. package/dist/scope/related-aggregate.js +67 -0
  66. package/dist/scope/scope.d.ts +181 -0
  67. package/dist/scope/scope.js +574 -0
  68. package/dist/selection/selection.d.ts +56 -0
  69. package/dist/selection/selection.js +137 -0
  70. package/dist/store/keys.d.ts +10 -0
  71. package/dist/store/keys.js +21 -0
  72. package/dist/store/store.d.ts +72 -0
  73. package/dist/store/store.js +143 -0
  74. package/dist/template/template.d.ts +19 -0
  75. package/dist/template/template.js +186 -0
  76. package/dist/transport.d.ts +28 -0
  77. package/dist/transport.js +20 -0
  78. package/dist/tree/tree.d.ts +117 -0
  79. package/dist/tree/tree.js +435 -0
  80. package/dist/validate/criteria-rule.d.ts +28 -0
  81. package/dist/validate/criteria-rule.js +22 -0
  82. package/dist/validate/validate.d.ts +13 -0
  83. package/dist/validate/validate.js +123 -0
  84. package/package.json +32 -4
@@ -0,0 +1,137 @@
1
+ import { computed, signal } from "@preact/signals-core";
2
+ /** Cross-field policy validation shared with declarative configuration contracts. */
3
+ export function selectionOptionsError(options) {
4
+ if (options.ordered && !options.multiple)
5
+ return "ordered selection requires multiple: true";
6
+ if (options.maxOrdered !== undefined && (!options.ordered || !Number.isSafeInteger(options.maxOrdered) || options.maxOrdered < 1))
7
+ return "maxOrdered requires ordered selection and a positive safe integer";
8
+ return undefined;
9
+ }
10
+ const NONE = new Set();
11
+ /**
12
+ * The keys selected in a collection. `keys` is derived, never written back:
13
+ * chosen keys that have left the collection drop out, and the fallback
14
+ * applies, as soon as the collection changes. A key on a page the collection
15
+ * has unloaded stays selected.
16
+ */
17
+ export class Selection {
18
+ #chosen = signal([]);
19
+ #collection;
20
+ multiple;
21
+ ordered;
22
+ maxOrdered;
23
+ keys;
24
+ /** The first selected key. */
25
+ key;
26
+ /** The selected records, live from the store. */
27
+ records;
28
+ /** The first selected record. */
29
+ selected;
30
+ /** The fields hidden on the first selected record (`StoredRecord.hidden`). */
31
+ hidden;
32
+ count;
33
+ any;
34
+ many;
35
+ constructor(store, collection, options = {}) {
36
+ this.#collection = "query" in collection ? signal(collection) : collection;
37
+ this.multiple = options.multiple ?? false;
38
+ this.ordered = options.ordered ?? false;
39
+ this.maxOrdered = options.maxOrdered;
40
+ const policyError = selectionOptionsError(options);
41
+ if (policyError)
42
+ throw new Error(policyError);
43
+ const fallback = options.fallback ?? !this.multiple;
44
+ // Seen keys survive eviction and ordering/projection changes. A changed
45
+ // membership query must show a key again before it can survive eviction.
46
+ let seenFor;
47
+ const seen = new Set();
48
+ this.keys = computed(() => {
49
+ const c = this.#collection.value;
50
+ const chosen = this.#chosen.value;
51
+ if (c.count.value === undefined)
52
+ return chosen;
53
+ if (seenFor !== c.query) {
54
+ const sameMembership = seenFor?.set === c.query.set && JSON.stringify(seenFor.filter) === JSON.stringify(c.query.filter);
55
+ seenFor = c.query;
56
+ if (!sameMembership)
57
+ seen.clear();
58
+ }
59
+ const loaded = new Set(c.keys.value);
60
+ const complete = c.complete.value;
61
+ const kept = chosen.filter((k) => {
62
+ if (loaded.has(k)) {
63
+ seen.add(k);
64
+ return true;
65
+ }
66
+ return !complete && seen.has(k) && store.record(c.query.set, k).value !== undefined && c.has(k) !== false;
67
+ });
68
+ if (kept.length > 0 || !fallback)
69
+ return kept.length === chosen.length ? chosen : kept;
70
+ const first = c.keys.value.find((k) => k !== undefined);
71
+ return first === undefined ? [] : [first];
72
+ });
73
+ this.key = computed(() => this.keys.value[0]);
74
+ this.records = computed(() => {
75
+ const set = this.#collection.value.query.set;
76
+ const out = [];
77
+ for (const k of this.keys.value) {
78
+ const entity = store.record(set, k).value?.entity;
79
+ if (entity)
80
+ out.push(entity);
81
+ }
82
+ return out;
83
+ });
84
+ this.selected = computed(() => this.records.value[0]);
85
+ this.hidden = computed(() => {
86
+ const k = this.key.value;
87
+ return (k === undefined ? undefined : store.record(this.#collection.value.query.set, k).value?.hidden) ?? NONE;
88
+ });
89
+ this.count = computed(() => this.keys.value.length);
90
+ this.any = computed(() => this.count.value > 0);
91
+ this.many = computed(() => this.count.value > 1);
92
+ }
93
+ /** The collection selected in; a signal read, so views follow a selection whose collection changes. */
94
+ get collection() {
95
+ return this.#collection.value;
96
+ }
97
+ /** Selects only `key`, or nothing. */
98
+ select(key) {
99
+ this.#chosen.value = key === undefined ? [] : [key];
100
+ }
101
+ /** Adds or removes `key` (multiple selection), or selects it alone. */
102
+ toggle(key) {
103
+ const chosen = this.keys.peek();
104
+ if (!this.multiple)
105
+ this.#chosen.value = [key];
106
+ else if (chosen.includes(key))
107
+ this.#chosen.value = chosen.filter((k) => k !== key);
108
+ else {
109
+ if (this.maxOrdered !== undefined && chosen.length >= this.maxOrdered)
110
+ return false;
111
+ this.#chosen.value = [...chosen, key];
112
+ }
113
+ return true;
114
+ }
115
+ selectAll(keys) {
116
+ const unique = [...new Set(keys)];
117
+ const next = this.multiple ? unique : unique.slice(0, 1);
118
+ if (this.maxOrdered !== undefined && next.length > this.maxOrdered)
119
+ return false;
120
+ this.#chosen.value = next;
121
+ return true;
122
+ }
123
+ /** Moves a selected key to a zero-based position; never changes membership. */
124
+ move(key, index) {
125
+ const chosen = this.keys.peek();
126
+ if (!this.ordered || !chosen.includes(key) || !Number.isInteger(index) || index < 0 || index >= chosen.length)
127
+ return false;
128
+ const next = chosen.filter((k) => k !== key);
129
+ next.splice(index, 0, key);
130
+ this.#chosen.value = next;
131
+ return true;
132
+ }
133
+ /** Selects nothing; with the fallback, the first row is selected again. */
134
+ clear() {
135
+ this.#chosen.value = [];
136
+ }
137
+ }
@@ -0,0 +1,10 @@
1
+ import type { Entity, EntityType, Value } from "../schema/model.ts";
2
+ /** Key values in key order. */
3
+ export type KeyValues = readonly Value[];
4
+ /** A key as a map key: equal keys always give the same text. */
5
+ export type KeyText = string;
6
+ export declare function keyValues(type: EntityType, entity: Entity): KeyValues;
7
+ export declare function keyText(key: KeyValues): KeyText;
8
+ export declare function keyOf(type: EntityType, entity: Entity): KeyText;
9
+ export declare function hasKey(type: EntityType, entity: Entity): boolean;
10
+ export declare function keyFromText(text: KeyText): KeyValues;
@@ -0,0 +1,21 @@
1
+ export function keyValues(type, entity) {
2
+ return type.key.map((name) => {
3
+ const v = entity[name];
4
+ if (v === undefined || v === null) {
5
+ throw new Error(`${type.name} has no value for key property ${name}`);
6
+ }
7
+ return v;
8
+ });
9
+ }
10
+ export function keyText(key) {
11
+ return JSON.stringify(key);
12
+ }
13
+ export function keyOf(type, entity) {
14
+ return keyText(keyValues(type, entity));
15
+ }
16
+ export function hasKey(type, entity) {
17
+ return type.key.every((name) => entity[name] !== undefined && entity[name] !== null);
18
+ }
19
+ export function keyFromText(text) {
20
+ return JSON.parse(text);
21
+ }
@@ -0,0 +1,72 @@
1
+ import { type ReadonlySignal } from "@preact/signals-core";
2
+ import type { Entity, Schema } from "../schema/model.ts";
3
+ import { type KeyText } from "./keys.ts";
4
+ export interface StoredRecord {
5
+ readonly entity: Entity;
6
+ /** Absent when the set has no `Core.OptimisticConcurrency`. */
7
+ readonly etag?: string;
8
+ /**
9
+ * The fields known to be current, when the record came from a `$select`
10
+ * and other fields may be missing or from an older version. Absent when
11
+ * every field is current.
12
+ */
13
+ readonly known?: ReadonlySet<string>;
14
+ /**
15
+ * Fields the server hides from this caller on this record
16
+ * (`@AsterData.Hidden`). Their value is `null`, which means "not yours to
17
+ * see", not empty.
18
+ *
19
+ * Invariant: a hidden field is never in `known`, and a record with hidden
20
+ * fields always has `known`. So everything that decides on a field only
21
+ * when it's known (a collection placing a record, page rules) treats a
22
+ * hidden one as unknown, and edit models never offer or send it.
23
+ */
24
+ readonly hidden?: ReadonlySet<string>;
25
+ }
26
+ /**
27
+ * Where a change came from. Sync treats them alike except that a collection
28
+ * never re-places a record it fetched itself: the server already put it there.
29
+ */
30
+ export type Origin = {
31
+ readonly kind: "fetch";
32
+ readonly collection?: object;
33
+ } | {
34
+ readonly kind: "save";
35
+ readonly by?: object;
36
+ } | {
37
+ readonly kind: "external";
38
+ };
39
+ export interface StoreChange {
40
+ readonly set: string;
41
+ readonly key: KeyText;
42
+ readonly before: StoredRecord | undefined;
43
+ readonly after: StoredRecord | undefined;
44
+ readonly origin: Origin;
45
+ }
46
+ export type StoreListener = (change: StoreChange) => void;
47
+ /**
48
+ * One normalized copy of every record the client holds, by set and key.
49
+ *
50
+ * Invariant: a record's signal and the listeners hear of a change in one
51
+ * batch, so collections have placed or removed the record before any view
52
+ * reads either.
53
+ */
54
+ export declare class RecordStore {
55
+ #private;
56
+ constructor(schema: Schema);
57
+ get schema(): Schema;
58
+ /** A signal for the record; `undefined` until it's loaded, and after it's removed. */
59
+ record(set: string, key: KeyText): ReadonlySignal<StoredRecord | undefined>;
60
+ get(set: string, key: KeyText): StoredRecord | undefined;
61
+ /**
62
+ * Stores the record. With `fields` (a `$select`), the record holds only
63
+ * those fields and is merged into the stored one, never blanking the rest.
64
+ * No change is reported when values, ETag and known fields are unchanged.
65
+ */
66
+ put(set: string, record: StoredRecord, origin: Origin, fields?: readonly string[]): KeyText;
67
+ /** Stores many records, reporting changes after all are in place. */
68
+ putAll(set: string, records: readonly StoredRecord[], origin: Origin, fields?: readonly string[]): KeyText[];
69
+ remove(set: string, key: KeyText, origin: Origin): void;
70
+ subscribe(listener: StoreListener): () => void;
71
+ }
72
+ export declare function sameEntity(a: Entity, b: Entity): boolean;
@@ -0,0 +1,143 @@
1
+ import { batch, signal } from "@preact/signals-core";
2
+ import { keyOf } from "./keys.js";
3
+ /**
4
+ * One normalized copy of every record the client holds, by set and key.
5
+ *
6
+ * Invariant: a record's signal and the listeners hear of a change in one
7
+ * batch, so collections have placed or removed the record before any view
8
+ * reads either.
9
+ */
10
+ export class RecordStore {
11
+ #schema;
12
+ #sets = new Map();
13
+ #listeners = new Set();
14
+ constructor(schema) {
15
+ this.#schema = schema;
16
+ }
17
+ get schema() {
18
+ return this.#schema;
19
+ }
20
+ /** A signal for the record; `undefined` until it's loaded, and after it's removed. */
21
+ record(set, key) {
22
+ return this.#slot(set, key);
23
+ }
24
+ get(set, key) {
25
+ return this.#sets.get(set)?.get(key)?.peek();
26
+ }
27
+ /**
28
+ * Stores the record. With `fields` (a `$select`), the record holds only
29
+ * those fields and is merged into the stored one, never blanking the rest.
30
+ * No change is reported when values, ETag and known fields are unchanged.
31
+ */
32
+ put(set, record, origin, fields) {
33
+ const type = this.#schema.set(set).type;
34
+ const key = keyOf(type, record.entity);
35
+ const slot = this.#slot(set, key);
36
+ const before = slot.peek();
37
+ const merged = fields ? partial(type, before, record, fields) : whole(type, record);
38
+ if (before &&
39
+ before.etag === merged.etag &&
40
+ sameEntity(before.entity, merged.entity) &&
41
+ sameSet(before.known, merged.known) &&
42
+ sameSet(before.hidden, merged.hidden)) {
43
+ return key;
44
+ }
45
+ const after = Object.freeze({
46
+ entity: Object.freeze({ ...merged.entity }),
47
+ ...etagOf(merged),
48
+ ...(merged.known ? { known: merged.known } : {}),
49
+ ...(merged.hidden ? { hidden: merged.hidden } : {}),
50
+ });
51
+ batch(() => {
52
+ slot.value = after;
53
+ this.#emit({ set, key, before, after, origin });
54
+ });
55
+ return key;
56
+ }
57
+ /** Stores many records, reporting changes after all are in place. */
58
+ putAll(set, records, origin, fields) {
59
+ return batch(() => records.map((r) => this.put(set, r, origin, fields)));
60
+ }
61
+ remove(set, key, origin) {
62
+ const slot = this.#sets.get(set)?.get(key);
63
+ const before = slot?.peek();
64
+ if (!slot || !before)
65
+ return;
66
+ batch(() => {
67
+ slot.value = undefined;
68
+ this.#emit({ set, key, before, after: undefined, origin });
69
+ });
70
+ }
71
+ subscribe(listener) {
72
+ this.#listeners.add(listener);
73
+ return () => this.#listeners.delete(listener);
74
+ }
75
+ #slot(set, key) {
76
+ let records = this.#sets.get(set);
77
+ if (!records) {
78
+ this.#schema.set(set);
79
+ records = new Map();
80
+ this.#sets.set(set, records);
81
+ }
82
+ let slot = records.get(key);
83
+ if (!slot) {
84
+ slot = signal(undefined);
85
+ records.set(key, slot);
86
+ }
87
+ return slot;
88
+ }
89
+ #emit(change) {
90
+ for (const listener of this.#listeners)
91
+ listener(change);
92
+ }
93
+ }
94
+ /** A whole record: every field current except the hidden ones. */
95
+ function whole(type, record) {
96
+ const hidden = record.hidden;
97
+ if (!hidden?.size)
98
+ return { entity: record.entity, ...etagOf(record) };
99
+ const known = new Set(type.fields.map((f) => f.name).filter((f) => !hidden.has(f)));
100
+ return { entity: blanked(record.entity, hidden), ...etagOf(record), known, hidden };
101
+ }
102
+ /**
103
+ * A `$select` result merged over the stored record. The stored fields stay;
104
+ * they're current only when the ETag is the same version. Without ETags
105
+ * there are no versions to tell apart, so stored fields count as current.
106
+ * The fields the result carries say whether they're hidden; the others stay
107
+ * as last heard.
108
+ */
109
+ function partial(type, before, record, fields) {
110
+ const hidden = new Set([...(before?.hidden ?? [])].filter((f) => !fields.includes(f)));
111
+ for (const f of record.hidden ?? [])
112
+ hidden.add(f);
113
+ const entity = blanked({ ...before?.entity, ...record.entity }, hidden);
114
+ const sameVersion = before !== undefined && before.etag === record.etag;
115
+ if (sameVersion && !before.known && hidden.size === 0)
116
+ return { entity, ...etagOf(record) };
117
+ const prior = sameVersion ? (before.known ?? type.fields.map((f) => f.name)) : [];
118
+ const known = new Set([...type.key, ...fields, ...prior]);
119
+ for (const f of hidden)
120
+ known.delete(f);
121
+ return { entity, ...etagOf(record), known, ...(hidden.size ? { hidden } : {}) };
122
+ }
123
+ /** A hidden field holds `null`, whatever an older version held. */
124
+ function blanked(entity, hidden) {
125
+ if (hidden.size === 0)
126
+ return entity;
127
+ const out = { ...entity };
128
+ for (const f of hidden)
129
+ out[f] = null;
130
+ return out;
131
+ }
132
+ const etagOf = (r) => (r.etag !== undefined ? { etag: r.etag } : {});
133
+ function sameSet(a, b) {
134
+ if (!a || !b)
135
+ return a === b;
136
+ return a.size === b.size && [...a].every((f) => b.has(f));
137
+ }
138
+ export function sameEntity(a, b) {
139
+ const ak = Object.keys(a);
140
+ if (ak.length !== Object.keys(b).length)
141
+ return false;
142
+ return ak.every((k) => Object.is(a[k], b[k]));
143
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Display text with values in it: `"{name} ({count == 1 ? 'one task' : count})"`.
3
+ * A template is parsed once, into a tree, and rendered by walking it; it's
4
+ * never compiled to code, so it's safe from untrusted sources and under a
5
+ * strict CSP. Inside braces: paths (`project.name`), string, number and
6
+ * boolean literals, `null`, comparisons (`== != < <= > >=`), `&&`, `||`, `!`,
7
+ * the conditional `a ? b : c` and parentheses. `{{` and `}}` are literal
8
+ * braces. A null or missing value renders as nothing.
9
+ */
10
+ export interface Template {
11
+ readonly source: string;
12
+ /** The paths it reads, for following them. */
13
+ readonly paths: readonly string[];
14
+ render(lookup: (path: string) => unknown): string;
15
+ }
16
+ export declare class TemplateError extends Error {
17
+ constructor(message: string, source: string, at: number);
18
+ }
19
+ export declare function parseTemplate(source: string): Template;
@@ -0,0 +1,186 @@
1
+ export class TemplateError extends Error {
2
+ constructor(message, source, at) {
3
+ super(`${message} at ${at} in ${JSON.stringify(source)}`);
4
+ this.name = "TemplateError";
5
+ }
6
+ }
7
+ const TOKEN = /\s*(?:(\d+(?:\.\d+)?)|('(?:[^'\\]|\\.)*'|"(?:[^"\\]|\\.)*")|([A-Za-z_]\w*(?:\.[A-Za-z_]\w*)*)|(==|!=|<=|>=|&&|\|\||[<>!?:()]))/y;
8
+ export function parseTemplate(source) {
9
+ const parts = [];
10
+ const paths = new Set();
11
+ let text = "";
12
+ for (let i = 0; i < source.length;) {
13
+ const ch = source[i];
14
+ if ((ch === "{" || ch === "}") && source[i + 1] === ch) {
15
+ text += ch;
16
+ i += 2;
17
+ }
18
+ else if (ch === "}") {
19
+ throw new TemplateError("a '}' with no '{'", source, i);
20
+ }
21
+ else if (ch !== "{") {
22
+ text += ch;
23
+ i++;
24
+ }
25
+ else {
26
+ if (text)
27
+ parts.push(text);
28
+ text = "";
29
+ const end = source.indexOf("}", i);
30
+ if (end < 0)
31
+ throw new TemplateError("a '{' with no '}'", source, i);
32
+ parts.push(parseExpr(source, tokenize(source, i + 1, end), end, paths));
33
+ i = end + 1;
34
+ }
35
+ }
36
+ if (text)
37
+ parts.push(text);
38
+ return {
39
+ source,
40
+ paths: [...paths],
41
+ render: (lookup) => parts.map((p) => (typeof p === "string" ? p : show(evaluate(p, lookup)))).join(""),
42
+ };
43
+ }
44
+ function tokenize(source, start, end) {
45
+ const tokens = [];
46
+ TOKEN.lastIndex = start;
47
+ while (TOKEN.lastIndex < end && !/^\s*$/.test(source.slice(TOKEN.lastIndex, end))) {
48
+ const at = TOKEN.lastIndex;
49
+ const m = TOKEN.exec(source);
50
+ if (!m || TOKEN.lastIndex > end)
51
+ throw new TemplateError("unexpected text", source, at);
52
+ if (m[1])
53
+ tokens.push({ t: m[1], kind: "num", at });
54
+ else if (m[2])
55
+ tokens.push({ t: m[2], kind: "str", at });
56
+ else if (m[3])
57
+ tokens.push({ t: m[3], kind: "id", at });
58
+ else
59
+ tokens.push({ t: m[4], kind: "op", at });
60
+ }
61
+ return tokens;
62
+ }
63
+ /** Precedence, loosest first: `?:`, `||`, `&&`, comparison, `!`. */
64
+ function parseExpr(source, tokens, end, paths) {
65
+ let n = 0;
66
+ const peek = () => tokens[n]?.t;
67
+ const take = (t) => {
68
+ const tok = tokens[n];
69
+ if (!tok || (t !== undefined && tok.t !== t))
70
+ throw new TemplateError(t ? `expected '${t}'` : "expected a value", source, tok?.at ?? end);
71
+ n++;
72
+ return tok;
73
+ };
74
+ const conditional = () => {
75
+ const c = or();
76
+ if (peek() !== "?")
77
+ return c;
78
+ take("?");
79
+ const a = conditional();
80
+ take(":");
81
+ return { k: "if", c, a, b: conditional() };
82
+ };
83
+ const or = () => {
84
+ let a = and();
85
+ while (peek() === "||") {
86
+ take();
87
+ a = { k: "bin", op: "||", a, b: and() };
88
+ }
89
+ return a;
90
+ };
91
+ const and = () => {
92
+ let a = compare();
93
+ while (peek() === "&&") {
94
+ take();
95
+ a = { k: "bin", op: "&&", a, b: compare() };
96
+ }
97
+ return a;
98
+ };
99
+ const compare = () => {
100
+ const a = unary();
101
+ const op = peek();
102
+ if (op === undefined || !["==", "!=", "<", "<=", ">", ">="].includes(op))
103
+ return a;
104
+ take();
105
+ return { k: "bin", op, a, b: unary() };
106
+ };
107
+ const unary = () => {
108
+ if (peek() !== "!")
109
+ return primary();
110
+ take();
111
+ return { k: "not", a: unary() };
112
+ };
113
+ const primary = () => {
114
+ const tok = take();
115
+ switch (tok.kind) {
116
+ case "num":
117
+ return { k: "lit", v: Number(tok.t) };
118
+ case "str":
119
+ return { k: "lit", v: tok.t.slice(1, -1).replace(/\\(.)/g, "$1") };
120
+ case "id":
121
+ switch (tok.t) {
122
+ case "true":
123
+ return { k: "lit", v: true };
124
+ case "false":
125
+ return { k: "lit", v: false };
126
+ case "null":
127
+ return { k: "lit", v: null };
128
+ default:
129
+ paths.add(tok.t);
130
+ return { k: "path", p: tok.t };
131
+ }
132
+ case "op": {
133
+ if (tok.t !== "(")
134
+ throw new TemplateError(`unexpected '${tok.t}'`, source, tok.at);
135
+ const inner = conditional();
136
+ take(")");
137
+ return inner;
138
+ }
139
+ }
140
+ };
141
+ const node = conditional();
142
+ if (n < tokens.length)
143
+ throw new TemplateError("unexpected text", source, tokens[n].at);
144
+ return node;
145
+ }
146
+ function evaluate(node, lookup) {
147
+ switch (node.k) {
148
+ case "lit":
149
+ return node.v;
150
+ case "path":
151
+ return lookup(node.p) ?? null;
152
+ case "not":
153
+ return !truthy(evaluate(node.a, lookup));
154
+ case "if":
155
+ return truthy(evaluate(node.c, lookup)) ? evaluate(node.a, lookup) : evaluate(node.b, lookup);
156
+ case "bin":
157
+ return binary(node.op, node.a, node.b, lookup);
158
+ }
159
+ }
160
+ function binary(op, left, right, lookup) {
161
+ switch (op) {
162
+ case "&&":
163
+ return truthy(evaluate(left, lookup)) && truthy(evaluate(right, lookup));
164
+ case "||":
165
+ return truthy(evaluate(left, lookup)) || truthy(evaluate(right, lookup));
166
+ case "==":
167
+ return evaluate(left, lookup) === evaluate(right, lookup);
168
+ case "!=":
169
+ return evaluate(left, lookup) !== evaluate(right, lookup);
170
+ default: {
171
+ // Ordering compares numbers with numbers and text with text; anything else is false.
172
+ const a = evaluate(left, lookup);
173
+ const b = evaluate(right, lookup);
174
+ if (typeof a !== typeof b || (typeof a !== "number" && typeof a !== "string"))
175
+ return false;
176
+ const [x, y] = [a, b];
177
+ return op === "<" ? x < y : op === "<=" ? x <= y : op === ">" ? x > y : x >= y;
178
+ }
179
+ }
180
+ }
181
+ function truthy(v) {
182
+ return v !== null && v !== undefined && v !== false && v !== "" && v !== 0;
183
+ }
184
+ function show(v) {
185
+ return v === null || v === undefined ? "" : String(v);
186
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * How the OData source reaches a service. `fetch` to a real server, or an
3
+ * in-process call into the mock OData service; the source can't tell which.
4
+ */
5
+ export type Method = "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
6
+ export interface ODataRequest {
7
+ readonly method: Method;
8
+ /**
9
+ * Relative to the service root, without a leading slash:
10
+ * `Projects?$filter=state%20eq%20'active'`, `Projects(3)`, `$metadata`.
11
+ * Query values are percent-encoded.
12
+ */
13
+ readonly url: string;
14
+ /** Header names in any case. */
15
+ readonly headers?: Readonly<Record<string, string>>;
16
+ readonly body?: string;
17
+ /** Cancels this request, including any wait for authentication. */
18
+ readonly signal?: AbortSignal;
19
+ }
20
+ export interface ODataResponse {
21
+ readonly status: number;
22
+ /** Header names lower-cased. */
23
+ readonly headers: Readonly<Record<string, string>>;
24
+ readonly body: string;
25
+ }
26
+ export type Transport = (request: ODataRequest) => Promise<ODataResponse>;
27
+ /** A transport over `fetch` to `serviceRoot` (no trailing slash). */
28
+ export declare function fetchTransport(serviceRoot: string, init?: RequestInit): Transport;
@@ -0,0 +1,20 @@
1
+ /** A transport over `fetch` to `serviceRoot` (no trailing slash). */
2
+ export function fetchTransport(serviceRoot, init) {
3
+ return async (request) => {
4
+ const init2 = { ...init, method: request.method };
5
+ const merged = new Headers(init?.headers);
6
+ for (const [name, value] of Object.entries(request.headers ?? {}))
7
+ merged.set(name, value);
8
+ init2.headers = merged;
9
+ if (request.signal)
10
+ init2.signal = init?.signal ? AbortSignal.any([init.signal, request.signal]) : request.signal;
11
+ if (request.body !== undefined)
12
+ init2.body = request.body;
13
+ const res = await fetch(`${serviceRoot}/${request.url}`, init2);
14
+ const headers = {};
15
+ res.headers.forEach((value, name) => {
16
+ headers[name.toLowerCase()] = value;
17
+ });
18
+ return { status: res.status, headers, body: await res.text() };
19
+ };
20
+ }