@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.
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,116 @@
1
+ import { type ReadonlySignal } from "@preact/signals-core";
2
+ import { type ChangeSet } from "../changes/changeset.ts";
3
+ import type { Collection } from "../collection/collection.ts";
4
+ import type { DataContext } from "../context.ts";
5
+ import type { Entity, EntityType } from "../schema/model.ts";
6
+ import { type KeyText } from "../store/keys.ts";
7
+ import { EditModel, type EditDraft, type EditOptions } from "./edit-model.ts";
8
+ export interface ListRow {
9
+ /** The record's key, or `new:<n>` until a new row is saved. */
10
+ readonly id: string;
11
+ readonly edit: EditModel;
12
+ readonly isNew: boolean;
13
+ /** Marked for deletion, to happen on save. */
14
+ readonly removing: boolean;
15
+ /** Why the last attempt to delete it failed. */
16
+ readonly removeError?: string;
17
+ /** A new row's fields that take another new row's key on save, by field: that row's id. */
18
+ readonly links?: Readonly<Record<string, string>>;
19
+ }
20
+ export interface ListEditOptions extends EditOptions {
21
+ /** Values for a new row, such as the parent's key. */
22
+ readonly defaults?: () => Entity;
23
+ /**
24
+ * A field holding each row's place: rows are shown in its order, `move`
25
+ * renumbers it from 1, and a new row goes last.
26
+ */
27
+ readonly positionField?: string;
28
+ }
29
+ export interface ListSaveResult {
30
+ readonly saved: readonly ChangeSet[];
31
+ /** Rows whose write failed; they stay pending with the error on them. */
32
+ readonly failed: readonly string[];
33
+ }
34
+ /** A list edit's working state, enough to resume it after a reload. Plain data. */
35
+ export interface ListDraft {
36
+ readonly added: readonly {
37
+ readonly id: string;
38
+ readonly edit: EditDraft;
39
+ readonly links: Readonly<Record<string, string>>;
40
+ }[];
41
+ readonly removing: readonly string[];
42
+ readonly edits: Readonly<Record<string, EditDraft>>;
43
+ readonly counter: number;
44
+ }
45
+ interface Existing {
46
+ readonly key: KeyText;
47
+ readonly entity: Entity;
48
+ }
49
+ /**
50
+ * Edits the rows of a collection together: changes to existing rows, new
51
+ * rows and rows marked for deletion are all pending until `save()`.
52
+ *
53
+ * Invariants: an existing row keeps one edit model while it's in the
54
+ * collection; removing a new row discards it, removing an existing one only
55
+ * marks it; a new row shows as pending in every writable field. A new row
56
+ * can link a field to another new row (a parent added in the same edit):
57
+ * rows are created in the order added, and the link takes the other row's
58
+ * key once it has one.
59
+ */
60
+ export declare class ListEdit {
61
+ #private;
62
+ readonly type: EntityType;
63
+ readonly set: string;
64
+ /** Undefined for a replay. */
65
+ readonly collection: Collection | undefined;
66
+ /** Existing rows in the collection's order, then new rows in the order added; by position when there is one. */
67
+ readonly rows: ReadonlySignal<readonly ListRow[]>;
68
+ readonly dirty: ReadonlySignal<boolean>;
69
+ readonly valid: ReadonlySignal<boolean>;
70
+ readonly busy: ReadonlySignal<boolean>;
71
+ /** Rows have a position field: `move` reorders them. */
72
+ get ordered(): boolean;
73
+ constructor(ctx: DataContext, collection: Collection, options?: ListEditOptions);
74
+ /** @internal */
75
+ constructor(ctx: DataContext, source: Collection | {
76
+ set: string;
77
+ rows: readonly Existing[];
78
+ }, options?: ListEditOptions);
79
+ /**
80
+ * A past change to a list, shown over its old rows: updated rows pending in
81
+ * their changed fields, removed rows struck through, added rows pending in
82
+ * full. Read-only; nothing is saved.
83
+ */
84
+ static replay(ctx: DataContext, set: string, before: readonly Entity[], changes: readonly ChangeSet[], options?: ListEditOptions): ListEdit;
85
+ /** Adds a new row, pending in full. `links` names, by field, the new row whose key that field takes on save. */
86
+ add(values?: Entity, links?: Readonly<Record<string, string>>): ListRow;
87
+ /** Discards a new row; marks an existing row for deletion. */
88
+ remove(id: string): void;
89
+ /** Takes back a pending deletion. */
90
+ restore(id: string): void;
91
+ /** Puts a row at `index` among the rows not being removed, renumbering the position field from 1. */
92
+ move(id: string, index: number): void;
93
+ /** Every pending change, as change sets in the order `save()` applies them: deletions, updates, creations. */
94
+ changes(): ChangeSet[];
95
+ /** Throws away every pending change. */
96
+ revert(): void;
97
+ /**
98
+ * Applies the changes one write at a time: deletions, then updates, then
99
+ * creations. A failed write leaves its row pending with the error on its
100
+ * edit model, and the rest still go. Not atomic: that needs `$batch`.
101
+ */
102
+ save(): Promise<ListSaveResult>;
103
+ /**
104
+ * Takes the current state as saved elsewhere (a deep update of the
105
+ * parent): changed rows keep their values as their new baseline, new
106
+ * rows and deletions are dropped, as the collection's refresh brings
107
+ * the saved rows.
108
+ */
109
+ accept(): void;
110
+ /** The working state: new rows, pending deletions and edited rows. */
111
+ draft(): ListDraft;
112
+ /** Takes up a draft. Rows it names that are no longer in the list are dropped. */
113
+ resume(draft: ListDraft): void;
114
+ dispose(): void;
115
+ }
116
+ export {};
@@ -0,0 +1,332 @@
1
+ import { batch, computed, signal, untracked } from "@preact/signals-core";
2
+ import { apply } from "../changes/changeset.js";
3
+ import { ODataError } from "../odata/errors.js";
4
+ import { keyFromText, keyText, keyValues } from "../store/keys.js";
5
+ import { EditModel } from "./edit-model.js";
6
+ /**
7
+ * Edits the rows of a collection together: changes to existing rows, new
8
+ * rows and rows marked for deletion are all pending until `save()`.
9
+ *
10
+ * Invariants: an existing row keeps one edit model while it's in the
11
+ * collection; removing a new row discards it, removing an existing one only
12
+ * marks it; a new row shows as pending in every writable field. A new row
13
+ * can link a field to another new row (a parent added in the same edit):
14
+ * rows are created in the order added, and the link takes the other row's
15
+ * key once it has one.
16
+ */
17
+ export class ListEdit {
18
+ type;
19
+ set;
20
+ /** Undefined for a replay. */
21
+ collection;
22
+ #ctx;
23
+ #options;
24
+ #existing;
25
+ #edits = new Map();
26
+ #new = signal([]);
27
+ #removing = signal(new Set());
28
+ #removeErrors = signal(new Map());
29
+ #busy = signal(false);
30
+ #replay = false;
31
+ #counter = 0;
32
+ /** Existing rows in the collection's order, then new rows in the order added; by position when there is one. */
33
+ rows;
34
+ dirty;
35
+ valid;
36
+ busy = this.#busy;
37
+ /** Rows have a position field: `move` reorders them. */
38
+ get ordered() {
39
+ return this.#options.positionField !== undefined && !this.#replay;
40
+ }
41
+ constructor(ctx, source, options = {}) {
42
+ this.#ctx = ctx;
43
+ this.#options = options;
44
+ if ("rows" in source) {
45
+ this.set = source.set;
46
+ this.collection = undefined;
47
+ this.#existing = signal(source.rows);
48
+ }
49
+ else {
50
+ const collection = source;
51
+ this.set = collection.query.set;
52
+ this.collection = collection;
53
+ this.#existing = computed(() => {
54
+ const items = collection.items.value;
55
+ return collection.keys.value.flatMap((key, i) => (key === undefined || items[i] === undefined ? [] : [{ key, entity: items[i] }]));
56
+ });
57
+ }
58
+ this.type = ctx.schema.set(this.set).type;
59
+ this.rows = computed(() => {
60
+ const removing = this.#removing.value;
61
+ const errors = this.#removeErrors.value;
62
+ const existing = this.#existing.value.map(({ key }) => ({
63
+ id: key,
64
+ edit: untracked(() => this.#editFor(key)),
65
+ isNew: false,
66
+ removing: removing.has(key),
67
+ ...(errors.has(key) ? { removeError: errors.get(key) } : {}),
68
+ }));
69
+ const added = this.#new.value.map(({ id, edit, links }) => ({ id, edit, isNew: true, removing: false, links }));
70
+ const rows = [...existing, ...added];
71
+ const at = options.positionField;
72
+ if (at === undefined)
73
+ return rows;
74
+ const place = (r) => Number(r.edit.values.value[at] ?? Number.MAX_SAFE_INTEGER);
75
+ return rows.map((r, i) => [r, i]).sort(([a, i], [b, j]) => place(a) - place(b) || i - j).map(([r]) => r);
76
+ });
77
+ this.dirty = computed(() => this.#new.value.length > 0 || this.#removing.value.size > 0 || this.rows.value.some((r) => r.edit.dirty.value));
78
+ this.valid = computed(() => this.rows.value.every((r) => r.removing || r.edit.valid.value));
79
+ }
80
+ /**
81
+ * A past change to a list, shown over its old rows: updated rows pending in
82
+ * their changed fields, removed rows struck through, added rows pending in
83
+ * full. Read-only; nothing is saved.
84
+ */
85
+ static replay(ctx, set, before, changes, options = {}) {
86
+ const type = ctx.schema.set(set).type;
87
+ const rows = before.map((entity) => ({ key: keyText(keyValues(type, entity)), entity }));
88
+ const list = new ListEdit(ctx, { set, rows }, options);
89
+ list.#replay = true;
90
+ const removing = new Set();
91
+ const added = [];
92
+ for (const c of changes) {
93
+ switch (c.kind) {
94
+ case "update": {
95
+ const key = keyText(c.key);
96
+ const old = rows.find((r) => r.key === key)?.entity ?? {};
97
+ list.#edits.set(key, EditModel.replay(ctx, set, old, apply(old, c.changes)));
98
+ break;
99
+ }
100
+ case "delete":
101
+ removing.add(keyText(c.key));
102
+ break;
103
+ case "create": {
104
+ const forced = type.fields.filter((f) => !f.computed).map((f) => f.name);
105
+ added.push({ id: `new:${++list.#counter}`, edit: EditModel.replay(ctx, set, {}, c.values, forced), links: {} });
106
+ break;
107
+ }
108
+ }
109
+ }
110
+ batch(() => {
111
+ list.#removing.value = removing;
112
+ list.#new.value = added;
113
+ });
114
+ return list;
115
+ }
116
+ /** Adds a new row, pending in full. `links` names, by field, the new row whose key that field takes on save. */
117
+ add(values = {}, links = {}) {
118
+ if (this.#replay)
119
+ throw new Error("a replay can't be edited");
120
+ const at = this.#options.positionField;
121
+ const position = at === undefined ? {} : { [at]: Math.max(0, ...this.rows.peek().map((r) => Number(r.edit.values.peek()[at] ?? 0))) + 1 };
122
+ const edit = this.#newEdit({ ...this.#options.defaults?.(), ...position, ...values });
123
+ const id = `new:${++this.#counter}`;
124
+ this.#new.value = [...this.#new.peek(), { id, edit, links }];
125
+ return { id, edit, isNew: true, removing: false, links };
126
+ }
127
+ /** Discards a new row; marks an existing row for deletion. */
128
+ remove(id) {
129
+ if (this.#replay)
130
+ return;
131
+ const added = this.#new.peek().find((r) => r.id === id);
132
+ if (added) {
133
+ added.edit.dispose();
134
+ this.#new.value = this.#new.peek().filter((r) => r.id !== id);
135
+ return;
136
+ }
137
+ this.#removing.value = new Set(this.#removing.peek()).add(id);
138
+ }
139
+ /** Takes back a pending deletion. */
140
+ restore(id) {
141
+ if (this.#replay)
142
+ return;
143
+ const next = new Set(this.#removing.peek());
144
+ next.delete(id);
145
+ this.#removing.value = next;
146
+ this.#setRemoveError(id, undefined);
147
+ }
148
+ /** Puts a row at `index` among the rows not being removed, renumbering the position field from 1. */
149
+ move(id, index) {
150
+ const at = this.#options.positionField;
151
+ if (this.#replay || at === undefined)
152
+ return;
153
+ const live = this.rows.peek().filter((r) => !r.removing);
154
+ const row = live.find((r) => r.id === id);
155
+ if (!row)
156
+ return;
157
+ const order = live.filter((r) => r !== row);
158
+ order.splice(Math.max(0, Math.min(order.length, index)), 0, row);
159
+ batch(() => order.forEach((r, i) => r.edit.values.peek()[at] !== i + 1 && r.edit.set(at, i + 1)));
160
+ }
161
+ /** Every pending change, as change sets in the order `save()` applies them: deletions, updates, creations. */
162
+ changes() {
163
+ const rows = this.rows.peek();
164
+ return [
165
+ ...rows.filter((r) => r.removing).map((r) => ({
166
+ kind: "delete",
167
+ set: this.set,
168
+ key: keyFromText(r.id),
169
+ values: known(r.edit.baseline.peek(), r.edit.hidden.peek()),
170
+ })),
171
+ ...rows.filter((r) => !r.removing && !r.isNew).flatMap((r) => r.edit.changes() ?? []),
172
+ ...this.#new.peek().flatMap(({ edit }) => edit.changes() ?? []),
173
+ ];
174
+ }
175
+ /** Throws away every pending change. */
176
+ revert() {
177
+ batch(() => {
178
+ for (const { edit } of this.#new.peek())
179
+ edit.dispose();
180
+ this.#new.value = [];
181
+ this.#removing.value = new Set();
182
+ for (const edit of this.#edits.values())
183
+ edit.revert();
184
+ });
185
+ }
186
+ /**
187
+ * Applies the changes one write at a time: deletions, then updates, then
188
+ * creations. A failed write leaves its row pending with the error on its
189
+ * edit model, and the rest still go. Not atomic: that needs `$batch`.
190
+ */
191
+ async save() {
192
+ if (this.#replay || this.#busy.peek())
193
+ return { saved: [], failed: [] };
194
+ this.#busy.value = true;
195
+ const saved = [];
196
+ const failed = [];
197
+ try {
198
+ for (const id of [...this.#removing.peek()]) {
199
+ try {
200
+ saved.push(await this.#ctx.remove(this.set, id));
201
+ this.restore(id);
202
+ }
203
+ catch (e) {
204
+ if (!(e instanceof ODataError))
205
+ throw e;
206
+ failed.push(id);
207
+ this.#setRemoveError(id, `Couldn’t delete it: ${e.message}`);
208
+ }
209
+ }
210
+ for (const row of this.rows.peek()) {
211
+ if (row.removing || !row.edit.dirty.peek() || row.isNew)
212
+ continue;
213
+ const result = await row.edit.save();
214
+ if (result)
215
+ saved.push(result.change);
216
+ else
217
+ failed.push(row.id);
218
+ }
219
+ const created = new Map();
220
+ for (const { id, edit, links } of [...this.#new.peek()]) {
221
+ for (const [field, other] of Object.entries(links)) {
222
+ const key = created.get(other);
223
+ if (key !== undefined)
224
+ edit.set(field, key);
225
+ }
226
+ const result = await edit.save();
227
+ if (!result) {
228
+ failed.push(id);
229
+ continue;
230
+ }
231
+ saved.push(result.change);
232
+ created.set(id, keyFromText(result.key)[0]);
233
+ this.#edits.set(result.key, edit);
234
+ this.#new.value = this.#new.peek().filter((r) => r.id !== id);
235
+ }
236
+ }
237
+ finally {
238
+ this.#busy.value = false;
239
+ }
240
+ return { saved, failed };
241
+ }
242
+ /**
243
+ * Takes the current state as saved elsewhere (a deep update of the
244
+ * parent): changed rows keep their values as their new baseline, new
245
+ * rows and deletions are dropped, as the collection's refresh brings
246
+ * the saved rows.
247
+ */
248
+ accept() {
249
+ batch(() => {
250
+ for (const { edit } of this.#new.peek())
251
+ edit.dispose();
252
+ this.#new.value = [];
253
+ this.#removing.value = new Set();
254
+ this.#removeErrors.value = new Map();
255
+ for (const edit of this.#edits.values())
256
+ if (edit.dirty.peek())
257
+ edit.accept();
258
+ });
259
+ }
260
+ /** The working state: new rows, pending deletions and edited rows. */
261
+ draft() {
262
+ const edits = {};
263
+ for (const [key, edit] of this.#edits)
264
+ if (edit.dirty.peek())
265
+ edits[key] = edit.draft();
266
+ return {
267
+ added: this.#new.peek().map(({ id, edit, links }) => ({ id, edit: edit.draft(), links })),
268
+ removing: [...this.#removing.peek()],
269
+ edits,
270
+ counter: this.#counter,
271
+ };
272
+ }
273
+ /** Takes up a draft. Rows it names that are no longer in the list are dropped. */
274
+ resume(draft) {
275
+ batch(() => {
276
+ this.revert();
277
+ this.#counter = Math.max(this.#counter, draft.counter);
278
+ this.#new.value = draft.added.map(({ id, edit, links }) => {
279
+ const m = this.#newEdit(edit.values);
280
+ m.resume(edit);
281
+ return { id, edit: m, links };
282
+ });
283
+ const present = new Set(this.#existing.peek().map((r) => r.key));
284
+ this.#removing.value = new Set(draft.removing.filter((k) => present.has(k)));
285
+ for (const [key, edit] of Object.entries(draft.edits))
286
+ if (present.has(key))
287
+ this.#editFor(key).resume(edit);
288
+ });
289
+ }
290
+ dispose() {
291
+ for (const edit of this.#edits.values())
292
+ edit.dispose();
293
+ for (const { edit } of this.#new.peek())
294
+ edit.dispose();
295
+ this.#edits.clear();
296
+ }
297
+ #newEdit(values) {
298
+ const edit = new EditModel(this.#ctx, this.set, undefined, values, this.#options);
299
+ for (const field of edit.type.fields)
300
+ if (!field.computed)
301
+ edit.forcePending(field.name, true);
302
+ return edit;
303
+ }
304
+ #setRemoveError(id, message) {
305
+ const next = new Map(this.#removeErrors.peek());
306
+ if (message === undefined)
307
+ next.delete(id);
308
+ else
309
+ next.set(id, message);
310
+ this.#removeErrors.value = next;
311
+ }
312
+ #editFor(key) {
313
+ let edit = this.#edits.get(key);
314
+ if (!edit) {
315
+ if (this.#replay) {
316
+ const entity = this.#existing.peek().find((r) => r.key === key)?.entity ?? {};
317
+ edit = EditModel.replay(this.#ctx, this.set, entity, entity);
318
+ }
319
+ else {
320
+ edit = new EditModel(this.#ctx, this.set, key, {}, this.#options);
321
+ }
322
+ this.#edits.set(key, edit);
323
+ }
324
+ return edit;
325
+ }
326
+ }
327
+ /** A deleted record's values for undo, without the hidden ones: their `null` isn't what was stored. */
328
+ function known(entity, hidden) {
329
+ if (hidden.size === 0)
330
+ return entity;
331
+ return Object.fromEntries(Object.entries(entity).filter(([name]) => !hidden.has(name)));
332
+ }
@@ -0,0 +1,93 @@
1
+ import { type ReadonlySignal } from "@preact/signals-core";
2
+ import { type ChangeSet } from "../changes/changeset.ts";
3
+ import type { DataContext } from "../context.ts";
4
+ import type { Entity } from "../schema/model.ts";
5
+ import { type KeyText } from "../store/keys.ts";
6
+ import { EditModel, type EditDraft, type EditOptions } from "./edit-model.ts";
7
+ import { ListEdit, type ListDraft, type ListEditOptions } from "./list-edit.ts";
8
+ /** A record and its owned rows as they were: what a replay shows a change over. Plain data. */
9
+ export interface RecordSnapshot {
10
+ readonly record: Entity;
11
+ readonly owned: Readonly<Record<string, readonly Entity[]>>;
12
+ }
13
+ /** A record edit's working state, enough to resume it after a reload. Plain data. */
14
+ export interface RecordDraft {
15
+ readonly edit: EditDraft;
16
+ readonly owned: Readonly<Record<string, ListDraft>>;
17
+ }
18
+ export interface RecordEditOptions extends EditOptions {
19
+ /** Options for each owned collection's list, by navigation name. */
20
+ readonly owned?: Readonly<Record<string, ListEditOptions>>;
21
+ }
22
+ /** What one save did: `change` is what was sent, with no server-assigned keys. A save is all or nothing. */
23
+ export interface RecordSave {
24
+ readonly ok: boolean;
25
+ readonly change: ChangeSet;
26
+ /** The record and its owned rows before the save. */
27
+ readonly before: RecordSnapshot;
28
+ /** `["record"]` when the save was refused, and nothing was saved; otherwise empty. */
29
+ readonly failed: readonly string[];
30
+ }
31
+ /** What fails validation: the record's fields, and each owned list's rows. */
32
+ export interface RecordProblems {
33
+ readonly record: readonly string[];
34
+ readonly owned: Readonly<Record<string, readonly {
35
+ readonly id: string;
36
+ readonly fields: readonly string[];
37
+ }[]>>;
38
+ }
39
+ /**
40
+ * Edits an existing record together with the collections it owns
41
+ * (`AsterData.Owned`): its fields in one edit model, each owned collection
42
+ * in a list edit whose new rows point at the record. Everything is one
43
+ * change set, one validation and one save; nothing is written per record
44
+ * type.
45
+ *
46
+ * Saving is one deep update (`<navigation>@delta`): the record and every
47
+ * owned change in one request, all or nothing.
48
+ */
49
+ export declare class RecordEdit {
50
+ #private;
51
+ readonly set: string;
52
+ readonly key: KeyText;
53
+ readonly edit: EditModel;
54
+ readonly owned: Readonly<Record<string, ListEdit>>;
55
+ readonly dirty: ReadonlySignal<boolean>;
56
+ readonly valid: ReadonlySignal<boolean>;
57
+ /** Settles when every owned collection has loaded. */
58
+ readonly ready: Promise<void>;
59
+ constructor(ctx: DataContext, set: string, key: KeyText, options?: RecordEditOptions);
60
+ /** @internal */
61
+ constructor(ctx: DataContext, set: string, key: KeyText, options: RecordEditOptions, replay: {
62
+ edit: EditModel;
63
+ owned: Record<string, ListEdit>;
64
+ });
65
+ /**
66
+ * A past change shown over what the record and its owned rows were: the
67
+ * record's fields and each list as replays, read-only, with pending
68
+ * marking exactly what the change did.
69
+ */
70
+ static replay(ctx: DataContext, before: RecordSnapshot, change: ChangeSet): RecordEdit;
71
+ /** The record and its owned rows as they were when editing began. */
72
+ snapshot(): RecordSnapshot;
73
+ /** The change a save would make, or `undefined` when nothing changed. */
74
+ changes(): ChangeSet | undefined;
75
+ /** Validates every field, shown or not, and says what fails. */
76
+ problems(): RecordProblems;
77
+ /**
78
+ * Validates, then writes the record and every owned change in one deep
79
+ * update (`<navigation>@delta`), all or nothing: new children of a new
80
+ * child go inside its entry. Returns `undefined` when nothing changed or
81
+ * something is invalid (see `problems()`). On a 412 the newer record is
82
+ * taken in: with no overlap the edit is sent once more, otherwise
83
+ * `edit.conflict` says so. Server errors land on the fields and rows they
84
+ * name.
85
+ */
86
+ save(): Promise<RecordSave | undefined>;
87
+ readonly failure: ReadonlySignal<string | undefined>;
88
+ revert(): void;
89
+ draft(): RecordDraft;
90
+ /** Takes up a draft once the owned rows are loaded. */
91
+ resume(draft: RecordDraft): Promise<void>;
92
+ dispose(): void;
93
+ }