@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,283 @@
|
|
|
1
|
+
/** A criteria problem: unknown field, a value that doesn't fit its field, or a bad stored value. */
|
|
2
|
+
export class CriteriaError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = "CriteriaError";
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const INT_RANGES = {
|
|
9
|
+
"Edm.Byte": [0, 255],
|
|
10
|
+
"Edm.SByte": [-128, 127],
|
|
11
|
+
"Edm.Int16": [-32768, 32767],
|
|
12
|
+
"Edm.Int32": [-2147483648, 2147483647],
|
|
13
|
+
};
|
|
14
|
+
const RE_INT64 = /^-?\d+$/;
|
|
15
|
+
const RE_DECIMAL = /^-?\d+(\.\d+)?([eE][+-]?\d+)?$/;
|
|
16
|
+
const RE_DATE = /^(-?\d{4,})-(\d{2})-(\d{2})$/;
|
|
17
|
+
const RE_TIME = /^(\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,12}))?)?$/;
|
|
18
|
+
const RE_DTO = /^(-?\d{4,})-(\d{2})-(\d{2})[Tt](\d{2}):(\d{2})(?::(\d{2})(?:\.(\d{1,12}))?)?(?:[Zz]|([+-])(\d{2}):(\d{2}))$/;
|
|
19
|
+
const RE_GUID = /^[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}$/;
|
|
20
|
+
const RE_DURATION = /^(-)?P(?:(\d+)D)?(?:T(?:(\d+)H)?(?:(\d+)M)?(?:(\d+)(?:\.(\d{1,12}))?S)?)?$/;
|
|
21
|
+
export function isFloatType(field) {
|
|
22
|
+
return field.type === "Edm.Single" || field.type === "Edm.Double";
|
|
23
|
+
}
|
|
24
|
+
/** Throws `CriteriaError` unless `value` is a legal JS value for `field`'s Edm type. */
|
|
25
|
+
export function assertLiteral(value, field) {
|
|
26
|
+
if (value === null)
|
|
27
|
+
return;
|
|
28
|
+
const bad = (why) => {
|
|
29
|
+
throw new CriteriaError(`${field.name}: ${why} (${field.type}), got ${JSON.stringify(value)}`);
|
|
30
|
+
};
|
|
31
|
+
const t = field.type;
|
|
32
|
+
switch (t) {
|
|
33
|
+
case "Edm.String":
|
|
34
|
+
if (typeof value !== "string")
|
|
35
|
+
bad("expected a string");
|
|
36
|
+
return;
|
|
37
|
+
case "Edm.Boolean":
|
|
38
|
+
if (typeof value !== "boolean")
|
|
39
|
+
bad("expected a boolean");
|
|
40
|
+
return;
|
|
41
|
+
case "Edm.Byte":
|
|
42
|
+
case "Edm.SByte":
|
|
43
|
+
case "Edm.Int16":
|
|
44
|
+
case "Edm.Int32": {
|
|
45
|
+
const [lo, hi] = INT_RANGES[t];
|
|
46
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < lo || value > hi) {
|
|
47
|
+
bad("expected an integer in range");
|
|
48
|
+
}
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
case "Edm.Single":
|
|
52
|
+
case "Edm.Double":
|
|
53
|
+
if (typeof value !== "number")
|
|
54
|
+
bad("expected a number");
|
|
55
|
+
return;
|
|
56
|
+
case "Edm.Int64":
|
|
57
|
+
if (typeof value !== "string" || !RE_INT64.test(value))
|
|
58
|
+
bad("expected an integer string");
|
|
59
|
+
return;
|
|
60
|
+
case "Edm.Decimal":
|
|
61
|
+
if (typeof value !== "string" || !RE_DECIMAL.test(value))
|
|
62
|
+
bad("expected a decimal string");
|
|
63
|
+
return;
|
|
64
|
+
case "Edm.Date":
|
|
65
|
+
if (typeof value !== "string" || !RE_DATE.test(value))
|
|
66
|
+
bad("expected YYYY-MM-DD");
|
|
67
|
+
return;
|
|
68
|
+
case "Edm.TimeOfDay":
|
|
69
|
+
if (typeof value !== "string" || !RE_TIME.test(value))
|
|
70
|
+
bad("expected hh:mm[:ss[.f]]");
|
|
71
|
+
return;
|
|
72
|
+
case "Edm.DateTimeOffset":
|
|
73
|
+
if (typeof value !== "string" || !RE_DTO.test(value))
|
|
74
|
+
bad("expected an RFC 3339 timestamp with offset");
|
|
75
|
+
return;
|
|
76
|
+
case "Edm.Guid":
|
|
77
|
+
if (typeof value !== "string" || !RE_GUID.test(value))
|
|
78
|
+
bad("expected a GUID");
|
|
79
|
+
return;
|
|
80
|
+
case "Edm.Duration":
|
|
81
|
+
if (typeof value !== "string" || !RE_DURATION.test(value) || value === "P" || value === "-P") {
|
|
82
|
+
bad("expected an ISO 8601 day-time duration");
|
|
83
|
+
}
|
|
84
|
+
return;
|
|
85
|
+
case "Edm.Binary":
|
|
86
|
+
throw new CriteriaError(`${field.name}: Edm.Binary values are not supported in criteria`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const RE_NUM = /^([+-]?)(\d*)(?:\.(\d*))?(?:[eE]([+-]?\d+))?$/;
|
|
90
|
+
function parseDec(v, field) {
|
|
91
|
+
const s = typeof v === "number" ? String(v) : v;
|
|
92
|
+
const m = RE_NUM.exec(s);
|
|
93
|
+
if (!m || (m[2] === "" && (m[3] ?? "") === "")) {
|
|
94
|
+
throw new CriteriaError(`${field.name}: not a number: ${JSON.stringify(v)}`);
|
|
95
|
+
}
|
|
96
|
+
const frac = m[3] ?? "";
|
|
97
|
+
let mant = BigInt(`${m[2] || "0"}${frac}`);
|
|
98
|
+
if (m[1] === "-")
|
|
99
|
+
mant = -mant;
|
|
100
|
+
const exp = Number(m[4] ?? 0) - frac.length;
|
|
101
|
+
return exp <= 0 ? { m: mant, e: -exp } : { m: mant * 10n ** BigInt(exp), e: 0 };
|
|
102
|
+
}
|
|
103
|
+
function cmpDec(a, b) {
|
|
104
|
+
const e = Math.max(a.e, b.e);
|
|
105
|
+
const x = a.m * 10n ** BigInt(e - a.e);
|
|
106
|
+
const y = b.m * 10n ** BigInt(e - b.e);
|
|
107
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
108
|
+
}
|
|
109
|
+
function cmpNum(a, b) {
|
|
110
|
+
if (Number.isNaN(a) || Number.isNaN(b))
|
|
111
|
+
return undefined;
|
|
112
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
113
|
+
}
|
|
114
|
+
function toNumber(v, field) {
|
|
115
|
+
if (typeof v === "number")
|
|
116
|
+
return v;
|
|
117
|
+
if (typeof v === "string" && RE_NUM.test(v))
|
|
118
|
+
return Number(v);
|
|
119
|
+
throw new CriteriaError(`${field.name}: expected a number, got ${JSON.stringify(v)}`);
|
|
120
|
+
}
|
|
121
|
+
// ---- temporal parsing, memoised: sorting parses each value many times ----
|
|
122
|
+
const memo = new Map();
|
|
123
|
+
function cached(kind, s, parse) {
|
|
124
|
+
const k = `${kind}\0${s}`;
|
|
125
|
+
const hit = memo.get(k);
|
|
126
|
+
if (hit !== undefined)
|
|
127
|
+
return hit;
|
|
128
|
+
const v = parse();
|
|
129
|
+
if (memo.size >= 20000)
|
|
130
|
+
memo.clear();
|
|
131
|
+
memo.set(k, v);
|
|
132
|
+
return v;
|
|
133
|
+
}
|
|
134
|
+
function daysFromCivil(y, m, d) {
|
|
135
|
+
const yy = m <= 2 ? y - 1 : y;
|
|
136
|
+
const era = Math.floor(yy / 400);
|
|
137
|
+
const yoe = yy - era * 400;
|
|
138
|
+
const doy = Math.floor((153 * (m + (m > 2 ? -3 : 9)) + 2) / 5) + d - 1;
|
|
139
|
+
const doe = yoe * 365 + Math.floor(yoe / 4) - Math.floor(yoe / 100) + doy;
|
|
140
|
+
return era * 146097 + doe - 719468;
|
|
141
|
+
}
|
|
142
|
+
function nanos(frac) {
|
|
143
|
+
return frac === undefined ? 0 : Number(frac.padEnd(12, "0").slice(0, 9));
|
|
144
|
+
}
|
|
145
|
+
function bad(field, v) {
|
|
146
|
+
throw new CriteriaError(`${field.name}: invalid ${field.type} value ${JSON.stringify(v)}`);
|
|
147
|
+
}
|
|
148
|
+
/** `[days since epoch]` for a Date. */
|
|
149
|
+
function dateKey(v, field) {
|
|
150
|
+
if (typeof v !== "string")
|
|
151
|
+
return bad(field, v);
|
|
152
|
+
return cached("d", v, () => {
|
|
153
|
+
const m = RE_DATE.exec(v);
|
|
154
|
+
if (!m)
|
|
155
|
+
return bad(field, v);
|
|
156
|
+
return daysFromCivil(Number(m[1]), Number(m[2]), Number(m[3]));
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function timeKey(v, field) {
|
|
160
|
+
if (typeof v !== "string")
|
|
161
|
+
return bad(field, v);
|
|
162
|
+
return cached("t", v, () => {
|
|
163
|
+
const m = RE_TIME.exec(v);
|
|
164
|
+
if (!m)
|
|
165
|
+
return bad(field, v);
|
|
166
|
+
// A leap second (:60) sorts after :59 and its fraction, as in the engine.
|
|
167
|
+
return (Number(m[1]) * 3600 + Number(m[2]) * 60 + Number(m[3] ?? 0)) * 1e9 + nanos(m[4]);
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
/** `[UTC seconds, nanoseconds]`: orders by instant, ignoring the offset. */
|
|
171
|
+
function instantKey(v, field) {
|
|
172
|
+
if (typeof v !== "string")
|
|
173
|
+
return bad(field, v);
|
|
174
|
+
return cached("i", v, () => {
|
|
175
|
+
const m = RE_DTO.exec(v);
|
|
176
|
+
if (!m)
|
|
177
|
+
return bad(field, v);
|
|
178
|
+
const offset = m[8] === undefined ? 0 : (m[8] === "-" ? -1 : 1) * (Number(m[9]) * 3600 + Number(m[10]) * 60);
|
|
179
|
+
const secs = daysFromCivil(Number(m[1]), Number(m[2]), Number(m[3])) * 86400 +
|
|
180
|
+
Number(m[4]) * 3600 +
|
|
181
|
+
Number(m[5]) * 60 +
|
|
182
|
+
Number(m[6] ?? 0) -
|
|
183
|
+
offset;
|
|
184
|
+
return [secs, nanos(m[7])];
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
function durationKey(v, field) {
|
|
188
|
+
if (typeof v !== "string")
|
|
189
|
+
return bad(field, v);
|
|
190
|
+
return cached("u", v, () => {
|
|
191
|
+
const m = RE_DURATION.exec(v);
|
|
192
|
+
if (!m || v === "P" || v === "-P")
|
|
193
|
+
return bad(field, v);
|
|
194
|
+
let ns = BigInt(m[2] ?? 0) * 86400000000000n +
|
|
195
|
+
BigInt(m[3] ?? 0) * 3600000000000n +
|
|
196
|
+
BigInt(m[4] ?? 0) * 60000000000n +
|
|
197
|
+
BigInt(m[5] ?? 0) * 1000000000n +
|
|
198
|
+
BigInt(nanos(m[6]));
|
|
199
|
+
if (m[1] === "-")
|
|
200
|
+
ns = -ns;
|
|
201
|
+
return ns;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
/** Unicode code point order: what the engine's byte-wise UTF-8 comparison gives. */
|
|
205
|
+
export function compareCodePoints(a, b) {
|
|
206
|
+
const n = Math.min(a.length, b.length);
|
|
207
|
+
for (let i = 0; i < n; i++) {
|
|
208
|
+
const x = a.charCodeAt(i);
|
|
209
|
+
const y = b.charCodeAt(i);
|
|
210
|
+
if (x === y)
|
|
211
|
+
continue;
|
|
212
|
+
// UTF-16 order differs from code point order only when a surrogate meets a
|
|
213
|
+
// BMP unit at or above U+E000: surrogates stand for code points >= U+10000.
|
|
214
|
+
const xs = x >= 0xd800 && x <= 0xdfff;
|
|
215
|
+
const ys = y >= 0xd800 && y <= 0xdfff;
|
|
216
|
+
if (xs !== ys)
|
|
217
|
+
return xs ? 1 : -1;
|
|
218
|
+
return x < y ? -1 : 1;
|
|
219
|
+
}
|
|
220
|
+
return a.length === b.length ? 0 : a.length < b.length ? -1 : 1;
|
|
221
|
+
}
|
|
222
|
+
function sign(x) {
|
|
223
|
+
return x < 0 ? -1 : x > 0 ? 1 : 0;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Orders two non-null values of `field`'s type; `undefined` when unordered
|
|
227
|
+
* (a NaN is involved).
|
|
228
|
+
*/
|
|
229
|
+
export function ordering(a, b, field) {
|
|
230
|
+
switch (field.type) {
|
|
231
|
+
case "Edm.String":
|
|
232
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
233
|
+
return bad(field, typeof a !== "string" ? a : b);
|
|
234
|
+
return compareCodePoints(a, b);
|
|
235
|
+
case "Edm.Boolean":
|
|
236
|
+
if (typeof a !== "boolean" || typeof b !== "boolean")
|
|
237
|
+
return bad(field, typeof a !== "boolean" ? a : b);
|
|
238
|
+
return a === b ? 0 : a ? 1 : -1;
|
|
239
|
+
case "Edm.Byte":
|
|
240
|
+
case "Edm.SByte":
|
|
241
|
+
case "Edm.Int16":
|
|
242
|
+
case "Edm.Int32":
|
|
243
|
+
case "Edm.Single":
|
|
244
|
+
case "Edm.Double":
|
|
245
|
+
return cmpNum(toNumber(a, field), toNumber(b, field));
|
|
246
|
+
case "Edm.Int64":
|
|
247
|
+
case "Edm.Decimal": {
|
|
248
|
+
// A non-integer JS number is a double, which makes the comparison floating point.
|
|
249
|
+
const fa = typeof a === "number" && !Number.isInteger(a);
|
|
250
|
+
const fb = typeof b === "number" && !Number.isInteger(b);
|
|
251
|
+
if (fa || fb)
|
|
252
|
+
return cmpNum(toNumber(a, field), toNumber(b, field));
|
|
253
|
+
return cmpDec(parseDec(a, field), parseDec(b, field));
|
|
254
|
+
}
|
|
255
|
+
case "Edm.Date":
|
|
256
|
+
return sign(dateKey(a, field) - dateKey(b, field));
|
|
257
|
+
case "Edm.TimeOfDay":
|
|
258
|
+
return sign(timeKey(a, field) - timeKey(b, field));
|
|
259
|
+
case "Edm.DateTimeOffset": {
|
|
260
|
+
const [s1, n1] = instantKey(a, field);
|
|
261
|
+
const [s2, n2] = instantKey(b, field);
|
|
262
|
+
return s1 !== s2 ? sign(s1 - s2) : sign(n1 - n2);
|
|
263
|
+
}
|
|
264
|
+
case "Edm.Duration":
|
|
265
|
+
return sign(durationKey(a, field) - durationKey(b, field));
|
|
266
|
+
case "Edm.Guid":
|
|
267
|
+
if (typeof a !== "string" || typeof b !== "string")
|
|
268
|
+
return bad(field, typeof a !== "string" ? a : b);
|
|
269
|
+
return compareCodePoints(a.toLowerCase(), b.toLowerCase());
|
|
270
|
+
case "Edm.Binary":
|
|
271
|
+
throw new CriteriaError(`${field.name}: Edm.Binary values are not supported in criteria`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
/** Whether a number value is NaN (sorts after everything, equal to itself). */
|
|
275
|
+
export function isNaNValue(v) {
|
|
276
|
+
return typeof v === "number" && Number.isNaN(v);
|
|
277
|
+
}
|
|
278
|
+
/** A comparison's value, which must be plain by now: relative dates are resolved before compiling or evaluating. */
|
|
279
|
+
export function plain(v) {
|
|
280
|
+
if (typeof v === "object" && v !== null)
|
|
281
|
+
throw new CriteriaError("a relative date reached a query unresolved; resolve it at fetch time (resolveRelative)");
|
|
282
|
+
return v;
|
|
283
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { type ReadonlySignal } from "@preact/signals-core";
|
|
2
|
+
import { type ChangeSet } from "../changes/changeset.ts";
|
|
3
|
+
import type { DataContext, Saved } from "../context.ts";
|
|
4
|
+
import type { StoredRecord } from "../store/store.ts";
|
|
5
|
+
import type { Entity, EntityType, Value } from "../schema/model.ts";
|
|
6
|
+
import { type Case, type Explanation, type FieldState, type PageScope, type StateRules } from "../scope/scope.ts";
|
|
7
|
+
import { type KeyText } from "../store/keys.ts";
|
|
8
|
+
import { type FieldErrors } from "../validate/validate.ts";
|
|
9
|
+
/**
|
|
10
|
+
* An application rule: errors by field for the current values. A rule's
|
|
11
|
+
* message wins over the metadata's for the same field, so it can say what
|
|
12
|
+
* the metadata can't ("already a member") or say it better for the screen.
|
|
13
|
+
*/
|
|
14
|
+
export type Rule = (values: Entity) => Readonly<Record<string, string | undefined>>;
|
|
15
|
+
export interface EditOptions {
|
|
16
|
+
readonly rules?: readonly Rule[];
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* A field's page rules. With `clearWhenHidden`, hiding a shown field clears
|
|
20
|
+
* its value. `derived` gives the field a value from the page: cases for
|
|
21
|
+
* `scope.select`, or a signal computed in code, in the field's JSON form.
|
|
22
|
+
* On a read-only field it always replaces the value; on an editable one it's
|
|
23
|
+
* a suggestion that stops once the person changes the field.
|
|
24
|
+
*/
|
|
25
|
+
export interface FieldRules extends StateRules {
|
|
26
|
+
readonly clearWhenHidden?: boolean;
|
|
27
|
+
readonly derived?: readonly Case[] | ReadonlySignal<unknown>;
|
|
28
|
+
}
|
|
29
|
+
/** An edit's working state, enough to resume it after a reload. Plain data. */
|
|
30
|
+
export interface EditDraft {
|
|
31
|
+
readonly key?: KeyText;
|
|
32
|
+
readonly baseline: Entity;
|
|
33
|
+
readonly values: Entity;
|
|
34
|
+
readonly etag?: string;
|
|
35
|
+
readonly forced: readonly string[];
|
|
36
|
+
/** Fields with a derived value that the person changed, so the suggestion no longer applies. */
|
|
37
|
+
readonly typed?: readonly string[];
|
|
38
|
+
}
|
|
39
|
+
export interface Conflict {
|
|
40
|
+
/** The record as it now is on the server. */
|
|
41
|
+
readonly theirs: Entity;
|
|
42
|
+
/** Fields both sides changed since editing began. */
|
|
43
|
+
readonly fields: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Uncommitted values for one record, apart from the stored record.
|
|
47
|
+
*
|
|
48
|
+
* Invariant: `baseline` is the stored record the edit started from (or the
|
|
49
|
+
* defaults for a new one); a field is pending when its value differs from
|
|
50
|
+
* the baseline, or when pending is forced on.
|
|
51
|
+
*
|
|
52
|
+
* A field hidden on the stored record (`StoredRecord.hidden`) is read-only,
|
|
53
|
+
* never pending and never sent: its `null` isn't a value the person has, so
|
|
54
|
+
* neither a change, a check nor a save may act on it. Every diff below goes
|
|
55
|
+
* through `#diff`, which leaves hidden fields out.
|
|
56
|
+
*/
|
|
57
|
+
export declare class EditModel {
|
|
58
|
+
#private;
|
|
59
|
+
readonly entitySet: string;
|
|
60
|
+
readonly type: EntityType;
|
|
61
|
+
readonly key: ReadonlySignal<KeyText | undefined>;
|
|
62
|
+
readonly baseline: ReadonlySignal<Entity>;
|
|
63
|
+
readonly values: ReadonlySignal<Entity>;
|
|
64
|
+
/** Fields the server hides from this caller on the record; their value is unknown, not empty. */
|
|
65
|
+
readonly hidden: ReadonlySignal<ReadonlySet<string>>;
|
|
66
|
+
readonly errors: ReadonlySignal<FieldErrors>;
|
|
67
|
+
/** A failure that isn't about one field. */
|
|
68
|
+
readonly formError: ReadonlySignal<string | undefined>;
|
|
69
|
+
readonly busy: ReadonlySignal<boolean>;
|
|
70
|
+
readonly conflict: ReadonlySignal<Conflict | undefined>;
|
|
71
|
+
/** The record was deleted, by someone else or elsewhere in this client. Saving is refused. */
|
|
72
|
+
readonly deleted: ReadonlySignal<boolean>;
|
|
73
|
+
readonly isNew: ReadonlySignal<boolean>;
|
|
74
|
+
readonly dirty: ReadonlySignal<boolean>;
|
|
75
|
+
/** Whether the current values would pass `validate()`, without showing errors. */
|
|
76
|
+
readonly valid: ReadonlySignal<boolean>;
|
|
77
|
+
/** Edits an existing record when `key` is given, otherwise a new one starting from `defaults`. */
|
|
78
|
+
constructor(ctx: DataContext, set: string, key?: KeyText, defaults?: Entity, options?: EditOptions);
|
|
79
|
+
/**
|
|
80
|
+
* A change shown as it was made: `before` is the baseline and `after` the
|
|
81
|
+
* values, so pending marks exactly what changed. Every field is read-only
|
|
82
|
+
* and nothing is saved.
|
|
83
|
+
*/
|
|
84
|
+
static replay(ctx: DataContext, set: string, before: Entity, after: Entity, forced?: readonly string[]): EditModel;
|
|
85
|
+
/** A past change shown over its old values (`replay`), not an edit. */
|
|
86
|
+
get replaying(): boolean;
|
|
87
|
+
/** The ETag of the version the edit started from. */
|
|
88
|
+
get etag(): string | undefined;
|
|
89
|
+
/**
|
|
90
|
+
* Takes what was saved elsewhere (a deep update of a parent) as the new
|
|
91
|
+
* starting point: the saved record if given, otherwise the current values.
|
|
92
|
+
*/
|
|
93
|
+
accept(saved?: StoredRecord): void;
|
|
94
|
+
/** Shows errors the server gave for these fields, such as a deep update's errors on a child. */
|
|
95
|
+
showErrors(errors: FieldErrors): void;
|
|
96
|
+
/** The working state, for keeping across a reload. */
|
|
97
|
+
draft(): EditDraft;
|
|
98
|
+
/**
|
|
99
|
+
* Takes up a draft of this record. If the stored record has moved on since,
|
|
100
|
+
* that's handled as any external change: followed where it touches nothing
|
|
101
|
+
* edited, otherwise a conflict.
|
|
102
|
+
*/
|
|
103
|
+
resume(draft: EditDraft): void;
|
|
104
|
+
get(name: string): Value | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Sets a value. A field that's read-only keeps its value and shows the
|
|
107
|
+
* reason as its error. Once a field shows an error, the error follows the
|
|
108
|
+
* value: it changes with it and clears as soon as the value is valid.
|
|
109
|
+
* Empty text is null on a field marked `emptyAsNull`.
|
|
110
|
+
*/
|
|
111
|
+
set(name: string, entered: Value): void;
|
|
112
|
+
/**
|
|
113
|
+
* Page rules for some fields, named `<target>.<field>` in the scope's
|
|
114
|
+
* `explain`. Call once, before the fields are shown.
|
|
115
|
+
*/
|
|
116
|
+
applyRules(scope: PageScope, target: string, rules: Readonly<Record<string, FieldRules>>): void;
|
|
117
|
+
/**
|
|
118
|
+
* Whether the field holds its derived value: suggested or replaced, not
|
|
119
|
+
* typed over. False for a field without one.
|
|
120
|
+
*/
|
|
121
|
+
derived(name: string): ReadonlySignal<boolean>;
|
|
122
|
+
/** The page rules behind a field's state, with what they read now. */
|
|
123
|
+
explain(name: string): Explanation[];
|
|
124
|
+
/**
|
|
125
|
+
* A field's state: its page rules, then what the metadata and the record
|
|
126
|
+
* say. Computed fields are read-only, immutable ones once saved, and every
|
|
127
|
+
* field of a deleted record; a non-nullable field is required.
|
|
128
|
+
*/
|
|
129
|
+
state(name: string): ReadonlySignal<FieldState>;
|
|
130
|
+
pending(name: string): ReadonlySignal<boolean>;
|
|
131
|
+
forcePending(name: string, on: boolean): void;
|
|
132
|
+
/** Puts the starting value back; a derived field takes suggestions again from the next change. */
|
|
133
|
+
revert(name?: string): void;
|
|
134
|
+
/** Validates every field and shows the errors; returns whether the values can be saved. */
|
|
135
|
+
validate(): boolean;
|
|
136
|
+
/** The change a save would make, or `undefined` when nothing changed. */
|
|
137
|
+
changes(): ChangeSet | undefined;
|
|
138
|
+
/**
|
|
139
|
+
* Validates, then creates or updates. Field errors from the server land on
|
|
140
|
+
* their fields. On a 412 the newer record is loaded: when the other change
|
|
141
|
+
* touched none of the fields edited here, the edit is rebased onto it and
|
|
142
|
+
* sent once more; otherwise `conflict` is set. Returns `undefined` when
|
|
143
|
+
* nothing was saved, and then `errors`, `formError`, `conflict` or
|
|
144
|
+
* `deleted` says why.
|
|
145
|
+
*/
|
|
146
|
+
save(): Promise<Saved | undefined>;
|
|
147
|
+
/** Takes the server's record as the new baseline and keeps local changes on top. */
|
|
148
|
+
rebase(): void;
|
|
149
|
+
/** Drops local changes in favour of the server's record. */
|
|
150
|
+
discard(): void;
|
|
151
|
+
dispose(): void;
|
|
152
|
+
}
|