@procedures/client 0.14.0 → 0.15.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.
- package/CHANGELOG.md +69 -0
- package/README.md +12 -3
- package/dist/.tsbuildinfo +1 -1
- package/dist/adapters/http/client/transport.d.ts.map +1 -1
- package/dist/adapters/http/client/transport.js +4 -0
- package/dist/adapters/http/client/transport.js.map +1 -1
- package/dist/adapters/http/server/body.d.ts +17 -0
- package/dist/adapters/http/server/body.d.ts.map +1 -0
- package/dist/adapters/http/server/body.js +44 -0
- package/dist/adapters/http/server/body.js.map +1 -0
- package/dist/adapters/http/server/cors.d.ts +24 -1
- package/dist/adapters/http/server/cors.d.ts.map +1 -1
- package/dist/adapters/http/server/cors.js +33 -1
- package/dist/adapters/http/server/cors.js.map +1 -1
- package/dist/adapters/http/server/handler.d.ts +3 -1
- package/dist/adapters/http/server/handler.d.ts.map +1 -1
- package/dist/adapters/http/server/handler.js +7 -34
- package/dist/adapters/http/server/handler.js.map +1 -1
- package/dist/adapters/local/client/transport.d.ts.map +1 -1
- package/dist/adapters/local/client/transport.js +5 -0
- package/dist/adapters/local/client/transport.js.map +1 -1
- package/dist/browser.d.ts +4 -3
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +6 -1
- package/dist/browser.js.map +1 -1
- package/dist/client/client.js +1 -1
- package/dist/client/client.js.map +1 -1
- package/dist/client/errors/registry.d.ts.map +1 -1
- package/dist/client/errors/registry.js +1 -0
- package/dist/client/errors/registry.js.map +1 -1
- package/dist/client/types.d.ts +6 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client/types.js +7 -1
- package/dist/client/types.js.map +1 -1
- package/dist/events/publisher.d.ts.map +1 -1
- package/dist/events/publisher.js +10 -5
- package/dist/events/publisher.js.map +1 -1
- package/dist/events/rows.d.ts +12 -4
- package/dist/events/rows.d.ts.map +1 -1
- package/dist/events/rows.js +13 -16
- package/dist/events/rows.js.map +1 -1
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +8 -2
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +10 -1
- package/dist/node.d.ts.map +1 -1
- package/dist/node.js +11 -0
- package/dist/node.js.map +1 -1
- package/dist/procedures/collection/admission.d.ts +77 -0
- package/dist/procedures/collection/admission.d.ts.map +1 -0
- package/dist/procedures/collection/admission.js +204 -0
- package/dist/procedures/collection/admission.js.map +1 -0
- package/dist/procedures/collection/changes.d.ts +3 -1
- package/dist/procedures/collection/changes.d.ts.map +1 -1
- package/dist/procedures/collection/changes.js +10 -6
- package/dist/procedures/collection/changes.js.map +1 -1
- package/dist/procedures/collection/index.d.ts +5 -1
- package/dist/procedures/collection/index.d.ts.map +1 -1
- package/dist/procedures/collection/index.js +4 -0
- package/dist/procedures/collection/index.js.map +1 -1
- package/dist/procedures/collection/patch-in-parts.d.ts +15 -0
- package/dist/procedures/collection/patch-in-parts.d.ts.map +1 -0
- package/dist/procedures/collection/patch-in-parts.js +47 -0
- package/dist/procedures/collection/patch-in-parts.js.map +1 -0
- package/dist/procedures/collection/precondition.d.ts +111 -0
- package/dist/procedures/collection/precondition.d.ts.map +1 -0
- package/dist/procedures/collection/precondition.js +153 -0
- package/dist/procedures/collection/precondition.js.map +1 -0
- package/dist/procedures/collection/procedures.d.ts +8 -0
- package/dist/procedures/collection/procedures.d.ts.map +1 -1
- package/dist/procedures/collection/procedures.js +66 -76
- package/dist/procedures/collection/procedures.js.map +1 -1
- package/dist/procedures/collection/query.d.ts +2 -0
- package/dist/procedures/collection/query.d.ts.map +1 -1
- package/dist/procedures/collection/query.js +3 -0
- package/dist/procedures/collection/query.js.map +1 -1
- package/dist/procedures/collection/schemas.d.ts +16 -0
- package/dist/procedures/collection/schemas.d.ts.map +1 -1
- package/dist/procedures/collection/schemas.js +26 -5
- package/dist/procedures/collection/schemas.js.map +1 -1
- package/dist/procedures/collection/storage.d.ts +34 -6
- package/dist/procedures/collection/storage.d.ts.map +1 -1
- package/dist/procedures/collection/storage.js +3 -0
- package/dist/procedures/collection/storage.js.map +1 -1
- package/dist/procedures/index.d.ts +2 -2
- package/dist/procedures/index.d.ts.map +1 -1
- package/dist/procedures/index.js +3 -1
- package/dist/procedures/index.js.map +1 -1
- package/dist/procedures/invoke.d.ts +5 -2
- package/dist/procedures/invoke.d.ts.map +1 -1
- package/dist/procedures/invoke.js +5 -1
- package/dist/procedures/invoke.js.map +1 -1
- package/dist/procedures/types.d.ts +7 -0
- package/dist/procedures/types.d.ts.map +1 -1
- package/dist/procedures/types.js.map +1 -1
- package/dist/protocol.d.ts +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +1 -0
- package/dist/protocol.js.map +1 -1
- package/dist/server/index.d.ts +70 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +26 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/procedure-server.d.ts.map +1 -1
- package/dist/server/procedure-server.js +5 -0
- package/dist/server/procedure-server.js.map +1 -1
- package/package.json +12 -1
- package/src/adapters/http/client/transport.ts +3 -0
- package/src/adapters/http/server/body.ts +43 -0
- package/src/adapters/http/server/cors.ts +42 -1
- package/src/adapters/http/server/handler.ts +10 -32
- package/src/adapters/local/client/transport.ts +4 -0
- package/src/browser.ts +25 -1
- package/src/client/client.ts +1 -1
- package/src/client/errors/registry.ts +1 -0
- package/src/client/types.ts +8 -0
- package/src/events/publisher.ts +8 -5
- package/src/events/rows.ts +19 -6
- package/src/index.ts +9 -6
- package/src/node.ts +13 -1
- package/src/procedures/collection/admission.ts +243 -0
- package/src/procedures/collection/changes.ts +11 -5
- package/src/procedures/collection/index.ts +21 -0
- package/src/procedures/collection/patch-in-parts.ts +56 -0
- package/src/procedures/collection/precondition.ts +180 -0
- package/src/procedures/collection/procedures.ts +69 -78
- package/src/procedures/collection/query.ts +16 -0
- package/src/procedures/collection/schemas.ts +43 -6
- package/src/procedures/collection/storage.ts +36 -6
- package/src/procedures/index.ts +22 -0
- package/src/procedures/invoke.ts +7 -2
- package/src/procedures/types.ts +8 -0
- package/src/protocol.ts +3 -1
- package/src/server/index.ts +94 -17
- package/src/server/procedure-server.ts +4 -0
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
|
|
27
27
|
import type { EventBus } from "../../events/types.js";
|
|
28
28
|
import { withRows } from "../../events/rows.js";
|
|
29
|
+
import type { AdmitItem } from "../invoke.js";
|
|
29
30
|
import type { StorageChanges } from "./storage.js";
|
|
30
31
|
|
|
31
32
|
/**
|
|
@@ -137,13 +138,16 @@ function hint(version: string | undefined, origin: string | undefined): Collecti
|
|
|
137
138
|
|
|
138
139
|
/**
|
|
139
140
|
* The event of a delta with the values of the changed items. Each row `{ id, value }` passes the
|
|
140
|
-
* output filter of each subscriber
|
|
141
|
+
* output filter of each subscriber and the rule of the rows of the collection, and a refused row
|
|
142
|
+
* goes from `values` to `withheld`.
|
|
141
143
|
*/
|
|
142
|
-
function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ readonly id: string; readonly value: unknown }
|
|
144
|
+
function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ readonly id: string; readonly value: unknown }>, admit: AdmitItem | undefined): CollectionChangeEvent {
|
|
143
145
|
return withRows(
|
|
144
146
|
{ ...event, values: entries },
|
|
145
147
|
{
|
|
146
148
|
rows: entries,
|
|
149
|
+
// The rule of the rows of the collection (roadmap CLI-41): each subscriber applies it too
|
|
150
|
+
...(admit && { admit }),
|
|
147
151
|
keep: (admitted) => {
|
|
148
152
|
const withheld = entries.filter((_, index) => !admitted[index]).map(({ id }) => id);
|
|
149
153
|
return {
|
|
@@ -157,7 +161,7 @@ function withValues(event: CollectionChangeEvent, entries: ReadonlyArray<{ reado
|
|
|
157
161
|
}
|
|
158
162
|
|
|
159
163
|
/** One publish: the event of the writes that ended since the last publish. */
|
|
160
|
-
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean): Promise<void> {
|
|
164
|
+
async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, topic: string, values: boolean, admit: AdmitItem | undefined): Promise<void> {
|
|
161
165
|
const writes = chain.ended.splice(0);
|
|
162
166
|
// An earlier publish told these writes
|
|
163
167
|
if (writes.length === 0) return;
|
|
@@ -194,7 +198,7 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
194
198
|
deleted: changes.deleted,
|
|
195
199
|
...(origin !== undefined && { origin }),
|
|
196
200
|
};
|
|
197
|
-
bus.emit(topic, values ? withValues(event, changes.changed.map(([id, value]) => ({ id, value }))) : event);
|
|
201
|
+
bus.emit(topic, values ? withValues(event, changes.changed.map(([id, value]) => ({ id, value })), admit) : event);
|
|
198
202
|
}
|
|
199
203
|
|
|
200
204
|
/**
|
|
@@ -210,6 +214,7 @@ async function publish(chain: ChangeChain, storage: ChangeLog, bus: EventBus, to
|
|
|
210
214
|
* @param origin - The origin of the write
|
|
211
215
|
* @param write - The write
|
|
212
216
|
* @param values - True: the events carry the values, for the output filter of each subscriber
|
|
217
|
+
* @param admit - The rule of the rows of the collection: each subscriber applies it to the values (roadmap CLI-41)
|
|
213
218
|
*/
|
|
214
219
|
export async function writeWithChangeEvent<R>(
|
|
215
220
|
bus: EventBus | undefined,
|
|
@@ -219,6 +224,7 @@ export async function writeWithChangeEvent<R>(
|
|
|
219
224
|
origin: string | undefined,
|
|
220
225
|
write: () => Promise<R>,
|
|
221
226
|
values = false,
|
|
227
|
+
admit?: AdmitItem,
|
|
222
228
|
): Promise<R> {
|
|
223
229
|
if (bus === undefined || topic === undefined) return write();
|
|
224
230
|
const chain = chainOf(storage, topic);
|
|
@@ -227,7 +233,7 @@ export async function writeWithChangeEvent<R>(
|
|
|
227
233
|
return await write();
|
|
228
234
|
} finally {
|
|
229
235
|
chain.ended.push(started);
|
|
230
|
-
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values));
|
|
236
|
+
const run = chain.tail.then(() => publish(chain, storage, bus, topic, values, admit));
|
|
231
237
|
chain.tail = run.catch(() => undefined);
|
|
232
238
|
await chain.tail;
|
|
233
239
|
}
|
|
@@ -74,12 +74,18 @@ export type { CollectionChangeEvent } from "./changes.js";
|
|
|
74
74
|
export { PARTITION_PATTERN, collectionTopicPartition, collectionTopicTarget, partitionOfCall, storageOfCall, withPartition } from "./partition.js";
|
|
75
75
|
export type { CollectionPartitions, PartitionInput, PartitionResolver } from "./partition.js";
|
|
76
76
|
|
|
77
|
+
// The rules of the rows of a collection: a filter of the reads and a guard of the writes (roadmap CLI-41)
|
|
78
|
+
export { admittedStorage, admitsRow } from "./admission.js";
|
|
79
|
+
export type { CollectionAdmission, CollectionRow, CollectionWrite, CollectionWriteGuard } from "./admission.js";
|
|
80
|
+
|
|
77
81
|
// The storage interface of the core (the shape of client-collections' CollectionStorage)
|
|
78
82
|
export type {
|
|
79
83
|
CollectionStorage,
|
|
80
84
|
StorageChanges,
|
|
81
85
|
StorageMetadata,
|
|
82
86
|
StorageWriteOptions,
|
|
87
|
+
StorageItemWriteOptions,
|
|
88
|
+
StorageBatchWriteOptions,
|
|
83
89
|
StorageSetChange,
|
|
84
90
|
StorageDeleteChange,
|
|
85
91
|
StorageClearChange,
|
|
@@ -88,6 +94,21 @@ export type {
|
|
|
88
94
|
StorageObserver,
|
|
89
95
|
} from "./storage.js";
|
|
90
96
|
|
|
97
|
+
// The precondition of the writes: the expected version and VERSION_CONFLICT (roadmap CLI-16)
|
|
98
|
+
export {
|
|
99
|
+
MAX_EXPECTED_VERSION_LENGTH,
|
|
100
|
+
PRECONDITION_NOT_SUPPORTED,
|
|
101
|
+
VERSION_CONFLICTS_METADATA_KEY,
|
|
102
|
+
VersionConflictError,
|
|
103
|
+
asVersionConflictError,
|
|
104
|
+
isExpectedVersion,
|
|
105
|
+
preconditionNotSupported,
|
|
106
|
+
versionConflict,
|
|
107
|
+
versionConflicts,
|
|
108
|
+
versionConflictsOf,
|
|
109
|
+
} from "./precondition.js";
|
|
110
|
+
export type { CurrentItem, VersionConflict, VersionOf } from "./precondition.js";
|
|
111
|
+
|
|
91
112
|
// The query language of the storages: the same JSON on the wire and in query() (roadmap CLI-34:
|
|
92
113
|
// the one copy of the language)
|
|
93
114
|
export {
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The patch of an item of a storage (roadmap CLI-14): `patch()` of the storage, or a read, a merge
|
|
3
|
+
* and a write for a storage without it.
|
|
4
|
+
*/
|
|
5
|
+
import { applyPatch, type StoragePatch } from "./query.js";
|
|
6
|
+
import type { CollectionStorage, StorageWriteOptions } from "./storage.js";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The patches of a storage without `patch()` that run, for each id. A patch of an id waits for the
|
|
10
|
+
* patch before it, so two patches of one process keep each field. A write of another writer
|
|
11
|
+
* between the read and the write of a patch is lost: give the storage a `patch()` for one step.
|
|
12
|
+
*/
|
|
13
|
+
const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* This function patches an item of a storage without `patch()`: a read, a merge and a write. The
|
|
17
|
+
* check gets the stored item and the merged item before the write. An error of the check stops
|
|
18
|
+
* the patch, and the storage does not change.
|
|
19
|
+
*/
|
|
20
|
+
export function patchInParts(
|
|
21
|
+
storage: CollectionStorage<unknown>,
|
|
22
|
+
id: string,
|
|
23
|
+
fields: StoragePatch,
|
|
24
|
+
options?: StorageWriteOptions,
|
|
25
|
+
check?: (prev: unknown, next: unknown) => Promise<void>,
|
|
26
|
+
): Promise<unknown> {
|
|
27
|
+
let running = PATCHES.get(storage);
|
|
28
|
+
if (!running) {
|
|
29
|
+
running = new Map();
|
|
30
|
+
PATCHES.set(storage, running);
|
|
31
|
+
}
|
|
32
|
+
const queue = running;
|
|
33
|
+
const before = queue.get(id) ?? Promise.resolve();
|
|
34
|
+
const patch = before.then(async () => {
|
|
35
|
+
const current = await storage.get(id);
|
|
36
|
+
if (current === undefined) return undefined;
|
|
37
|
+
const next = applyPatch(current, fields);
|
|
38
|
+
if (check) await check(current, next);
|
|
39
|
+
await storage.set(id, next, options);
|
|
40
|
+
return next;
|
|
41
|
+
});
|
|
42
|
+
const settled = patch.then(
|
|
43
|
+
() => undefined,
|
|
44
|
+
() => undefined,
|
|
45
|
+
);
|
|
46
|
+
queue.set(id, settled);
|
|
47
|
+
void settled.then(() => {
|
|
48
|
+
if (queue.get(id) === settled) queue.delete(id);
|
|
49
|
+
});
|
|
50
|
+
return patch;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** This function patches an item: with `patch()` of the storage, or in parts. It gives the new item, or undefined for a missing id. */
|
|
54
|
+
export function patchItem(storage: CollectionStorage<unknown>, id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<unknown> {
|
|
55
|
+
return storage.patch ? storage.patch(id, fields, options) : patchInParts(storage, id, fields, options);
|
|
56
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The precondition of a collection write (roadmap CLI-16, decision D11).
|
|
3
|
+
*
|
|
4
|
+
* - `set` and `delete` take `expectedVersion`. `setBatch` takes `expectedVersions`, with one
|
|
5
|
+
* expected version for each id. The value is the version of the current item that the write
|
|
6
|
+
* expects. `null` expects no item.
|
|
7
|
+
* - The version of an item is the value that the function `versionOf` of the storage gives for
|
|
8
|
+
* the item, for example a revision field. `HybridStorage` reads the same versions with its own
|
|
9
|
+
* `versionOf`, so its queue can send the base version of a write as the expected version.
|
|
10
|
+
* - The storage tests the precondition and writes in one step. When the version of the current
|
|
11
|
+
* item is not the expected version, the write changes nothing, and it rejects with
|
|
12
|
+
* `VersionConflictError`.
|
|
13
|
+
* - The error has the code `VERSION_CONFLICT` (HTTP 409). It has the current item and its version
|
|
14
|
+
* for each conflict. On the wire, the conflicts go in the metadata key `conflicts`.
|
|
15
|
+
* - A storage without `versionOf` cannot test a precondition. It rejects such a write with a
|
|
16
|
+
* `TypeError`, and changes nothing. It never ignores a precondition.
|
|
17
|
+
* - A write without a precondition is the same as before: the last write wins.
|
|
18
|
+
*
|
|
19
|
+
* This module has no imports: `@procedures/client/query` exports it for the storage packages.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One conflict of a write with a precondition (roadmap CLI-16). The data is JSON. */
|
|
23
|
+
export interface VersionConflict {
|
|
24
|
+
/** The id of the item. */
|
|
25
|
+
readonly id: string;
|
|
26
|
+
/** The version that the write expected. `null`: the write expected no item. */
|
|
27
|
+
readonly expected: string | null;
|
|
28
|
+
/** True when the id has an item now. */
|
|
29
|
+
readonly found: boolean;
|
|
30
|
+
/** The current item, when `found` is true. */
|
|
31
|
+
readonly value?: unknown;
|
|
32
|
+
/**
|
|
33
|
+
* The version of the current item: `null` when the id has no item. When the current item has no
|
|
34
|
+
* version, the field is absent.
|
|
35
|
+
*/
|
|
36
|
+
readonly version?: string | null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The function that gives the version of an item, or undefined for an item without a version. */
|
|
40
|
+
export type VersionOf<T> = (item: T) => string | undefined;
|
|
41
|
+
|
|
42
|
+
/** The current state of an id: no item, or an item. */
|
|
43
|
+
export type CurrentItem<T> = { readonly found: false } | { readonly found: true; readonly value: T };
|
|
44
|
+
|
|
45
|
+
/** The metadata key of the conflicts of a `VERSION_CONFLICT` error on the wire. */
|
|
46
|
+
export const VERSION_CONFLICTS_METADATA_KEY = "conflicts";
|
|
47
|
+
|
|
48
|
+
/** The longest expected version, in characters. */
|
|
49
|
+
export const MAX_EXPECTED_VERSION_LENGTH = 1024;
|
|
50
|
+
|
|
51
|
+
/** The message of an error with some conflicts. */
|
|
52
|
+
function conflictMessage(conflicts: readonly VersionConflict[]): string {
|
|
53
|
+
const ids = conflicts.slice(0, 10).map((conflict) => JSON.stringify(conflict.id)).join(", ");
|
|
54
|
+
const more = conflicts.length > 10 ? ` and ${conflicts.length - 10} more` : "";
|
|
55
|
+
return `The version of the current item is not the expected version: ${ids}${more}`;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A write with a precondition found an item with another version (roadmap CLI-16). The write
|
|
60
|
+
* changed nothing. `conflicts` has the current item and its version for each id of a conflict.
|
|
61
|
+
*/
|
|
62
|
+
export class VersionConflictError extends Error {
|
|
63
|
+
override readonly name = "VersionConflictError";
|
|
64
|
+
/** The error code of the protocol: HTTP 409, not retryable. */
|
|
65
|
+
readonly code = "VERSION_CONFLICT";
|
|
66
|
+
/** A retry of the same write fails again: the caller reads the current item first. */
|
|
67
|
+
readonly retryable = false;
|
|
68
|
+
/** The conflicts, one for each id whose current item does not have the expected version. */
|
|
69
|
+
readonly conflicts: readonly VersionConflict[];
|
|
70
|
+
|
|
71
|
+
constructor(conflicts: readonly VersionConflict[], message?: string) {
|
|
72
|
+
super(message ?? conflictMessage(conflicts));
|
|
73
|
+
this.conflicts = conflicts;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* This function gives the conflict of one id, or undefined when the current item has the expected
|
|
79
|
+
* version. `null` expects no item. An item without a version never has an expected string version.
|
|
80
|
+
*
|
|
81
|
+
* @param id - The id of the item
|
|
82
|
+
* @param expected - The expected version, or `null` for no item
|
|
83
|
+
* @param current - The current state of the id
|
|
84
|
+
* @param versionOf - The version of an item
|
|
85
|
+
*/
|
|
86
|
+
export function versionConflict<T>(id: string, expected: string | null, current: CurrentItem<T>, versionOf: VersionOf<T>): VersionConflict | undefined {
|
|
87
|
+
if (!current.found) return expected === null ? undefined : { id, expected, found: false, version: null };
|
|
88
|
+
const version = versionOf(current.value);
|
|
89
|
+
if (expected !== null && version === expected) return undefined;
|
|
90
|
+
return version === undefined ? { id, expected, found: true, value: current.value } : { id, expected, found: true, value: current.value, version };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* This function tests the preconditions of a write against the current items, and gives the
|
|
95
|
+
* conflicts. The list is empty when each precondition holds.
|
|
96
|
+
*
|
|
97
|
+
* @param expected - The expected version of each id
|
|
98
|
+
* @param currentOf - The current state of an id, before the write
|
|
99
|
+
* @param versionOf - The version of an item
|
|
100
|
+
*/
|
|
101
|
+
export function versionConflicts<T>(
|
|
102
|
+
expected: Iterable<readonly [string, string | null]>,
|
|
103
|
+
currentOf: (id: string) => CurrentItem<T>,
|
|
104
|
+
versionOf: VersionOf<T>,
|
|
105
|
+
): VersionConflict[] {
|
|
106
|
+
const conflicts: VersionConflict[] = [];
|
|
107
|
+
for (const [id, version] of expected) {
|
|
108
|
+
const conflict = versionConflict(id, version, currentOf(id), versionOf);
|
|
109
|
+
if (conflict) conflicts.push(conflict);
|
|
110
|
+
}
|
|
111
|
+
return conflicts;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** The code of the error of a write with a precondition on a storage that cannot test it. */
|
|
115
|
+
export const PRECONDITION_NOT_SUPPORTED = "PRECONDITION_NOT_SUPPORTED";
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The error of a write with a precondition on a storage that cannot test it: a storage without
|
|
119
|
+
* `versionOf`. The storage changes nothing. The error is a `TypeError` with the code
|
|
120
|
+
* `PRECONDITION_NOT_SUPPORTED`, so a sync does not take it for a network failure.
|
|
121
|
+
*
|
|
122
|
+
* @param storage - The name of the storage, for the message
|
|
123
|
+
* @param remedy - What makes the test possible
|
|
124
|
+
*/
|
|
125
|
+
export function preconditionNotSupported(storage: string, remedy = "give it the option versionOf"): TypeError & { readonly code: string; readonly retryable: false } {
|
|
126
|
+
return Object.assign(new TypeError(`${storage} cannot test expectedVersion: ${remedy}`), { code: PRECONDITION_NOT_SUPPORTED, retryable: false as const });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** True for a value that a precondition can expect: a string of at most 1024 characters, or `null`. */
|
|
130
|
+
export function isExpectedVersion(value: unknown): value is string | null {
|
|
131
|
+
return value === null || (typeof value === "string" && value.length <= MAX_EXPECTED_VERSION_LENGTH);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The conflicts of untrusted data from the wire, or undefined when the data is not a list of conflicts. */
|
|
135
|
+
function conflictsOfData(data: unknown): VersionConflict[] | undefined {
|
|
136
|
+
if (!Array.isArray(data)) return undefined;
|
|
137
|
+
const conflicts: VersionConflict[] = [];
|
|
138
|
+
for (const entry of data) {
|
|
139
|
+
if (typeof entry !== "object" || entry === null) return undefined;
|
|
140
|
+
const record = entry as Record<string, unknown>;
|
|
141
|
+
const { id, expected, found, version } = record;
|
|
142
|
+
if (typeof id !== "string" || typeof found !== "boolean" || !(expected === null || typeof expected === "string")) return undefined;
|
|
143
|
+
if (!(version === undefined || version === null || typeof version === "string")) return undefined;
|
|
144
|
+
conflicts.push({
|
|
145
|
+
id,
|
|
146
|
+
expected,
|
|
147
|
+
found,
|
|
148
|
+
...(found && Object.prototype.hasOwnProperty.call(record, "value") && { value: record["value"] }),
|
|
149
|
+
...(version !== undefined && { version }),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return conflicts;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* This function gives the conflicts of an error with the code `VERSION_CONFLICT`: the `conflicts`
|
|
157
|
+
* of a `VersionConflictError`, or the metadata key `conflicts` of an error from the wire (a
|
|
158
|
+
* `ClientError`). It gives undefined for another error. A `VERSION_CONFLICT` error without
|
|
159
|
+
* readable conflicts gives an empty list.
|
|
160
|
+
*/
|
|
161
|
+
export function versionConflictsOf(error: unknown): readonly VersionConflict[] | undefined {
|
|
162
|
+
if (typeof error !== "object" || error === null) return undefined;
|
|
163
|
+
if (error instanceof VersionConflictError) return error.conflicts;
|
|
164
|
+
const { code, conflicts, metadata } = error as { code?: unknown; conflicts?: unknown; metadata?: unknown };
|
|
165
|
+
if (code !== "VERSION_CONFLICT") return undefined;
|
|
166
|
+
const fromMetadata = typeof metadata === "object" && metadata !== null ? (metadata as Record<string, unknown>)[VERSION_CONFLICTS_METADATA_KEY] : undefined;
|
|
167
|
+
return conflictsOfData(conflicts) ?? conflictsOfData(fromMetadata) ?? [];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* This function gives a `VersionConflictError` for an error with the code `VERSION_CONFLICT`, with
|
|
172
|
+
* its conflicts and its message. It gives undefined for another error.
|
|
173
|
+
*/
|
|
174
|
+
export function asVersionConflictError(error: unknown): VersionConflictError | undefined {
|
|
175
|
+
if (error instanceof VersionConflictError) return error;
|
|
176
|
+
const conflicts = versionConflictsOf(error);
|
|
177
|
+
if (conflicts === undefined) return undefined;
|
|
178
|
+
const message = error instanceof Error ? error.message : undefined;
|
|
179
|
+
return new VersionConflictError(conflicts, message);
|
|
180
|
+
}
|
|
@@ -56,10 +56,12 @@ import {
|
|
|
56
56
|
type SetBatchInput,
|
|
57
57
|
type DeleteBatchInput,
|
|
58
58
|
} from "./schemas.js";
|
|
59
|
-
import { DEFAULT_FIND_LIMIT,
|
|
60
|
-
import type { CollectionStorage, StorageWriteOptions } from "./storage.js";
|
|
59
|
+
import { DEFAULT_FIND_LIMIT, queryEntries, type StorageQueryOptions, type StorageQueryPolicy } from "./query.js";
|
|
60
|
+
import type { CollectionStorage, StorageBatchWriteOptions, StorageItemWriteOptions, StorageWriteOptions } from "./storage.js";
|
|
61
61
|
import { collectionChangeTopic, startChangeChain, writeWithChangeEvent } from "./changes.js";
|
|
62
62
|
import { partitionOfCall, storageOfCall, withPartition, type PartitionInput } from "./partition.js";
|
|
63
|
+
import { admitsRow, admittedStorage, type CollectionAdmission } from "./admission.js";
|
|
64
|
+
import { patchItem } from "./patch-in-parts.js";
|
|
63
65
|
|
|
64
66
|
// =============================================================================
|
|
65
67
|
// Handler Factory
|
|
@@ -122,6 +124,13 @@ export interface CollectionProceduresOptions {
|
|
|
122
124
|
* @default true, with the topic `collections.<name>` and no values
|
|
123
125
|
*/
|
|
124
126
|
changeEvents?: boolean | CollectionChangeEventsOptions | undefined;
|
|
127
|
+
/**
|
|
128
|
+
* The rules of the rows (roadmap CLI-41). `admit` examines each row of each read and of each
|
|
129
|
+
* change event with values. `guard` examines each write before the storage changes, and a
|
|
130
|
+
* refused write gets `FORBIDDEN` (reason `write-not-allowed`). Without it, the procedures check
|
|
131
|
+
* only the partition.
|
|
132
|
+
*/
|
|
133
|
+
admission?: CollectionAdmission | undefined;
|
|
125
134
|
}
|
|
126
135
|
|
|
127
136
|
/** The write options of an input with an origin. */
|
|
@@ -129,38 +138,17 @@ function writeOptionsOf(input: { origin?: string }): StorageWriteOptions | undef
|
|
|
129
138
|
return input.origin === undefined ? undefined : { origin: input.origin };
|
|
130
139
|
}
|
|
131
140
|
|
|
132
|
-
/**
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
|
|
141
|
+
/** The write options of `set` and `delete`: the origin, and the expected version (roadmap CLI-16). */
|
|
142
|
+
function itemWriteOptionsOf(input: { origin?: string; expectedVersion?: string | null }): StorageItemWriteOptions | undefined {
|
|
143
|
+
if (input.expectedVersion === undefined) return writeOptionsOf(input);
|
|
144
|
+
return { ...writeOptionsOf(input), expectedVersion: input.expectedVersion };
|
|
145
|
+
}
|
|
138
146
|
|
|
139
|
-
/**
|
|
140
|
-
function
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
PATCHES.set(storage, running);
|
|
145
|
-
}
|
|
146
|
-
const queue = running;
|
|
147
|
-
const before = queue.get(id) ?? Promise.resolve();
|
|
148
|
-
const patch = before.then(async () => {
|
|
149
|
-
const current = await storage.get(id);
|
|
150
|
-
if (current === undefined) return undefined;
|
|
151
|
-
const next = applyPatch(current, fields);
|
|
152
|
-
await storage.set(id, next, options);
|
|
153
|
-
return next;
|
|
154
|
-
});
|
|
155
|
-
const settled = patch.then(
|
|
156
|
-
() => undefined,
|
|
157
|
-
() => undefined,
|
|
158
|
-
);
|
|
159
|
-
queue.set(id, settled);
|
|
160
|
-
void settled.then(() => {
|
|
161
|
-
if (queue.get(id) === settled) queue.delete(id);
|
|
162
|
-
});
|
|
163
|
-
return patch;
|
|
147
|
+
/** The write options of `setBatch`: the origin, and the expected version of each item that has one (roadmap CLI-16). */
|
|
148
|
+
function batchWriteOptionsOf(input: SetBatchInput): StorageBatchWriteOptions | undefined {
|
|
149
|
+
const expectedVersions = input.items.flatMap((item): Array<[string, string | null]> => (item.expectedVersion === undefined ? [] : [[item.id, item.expectedVersion]]));
|
|
150
|
+
if (expectedVersions.length === 0) return writeOptionsOf(input);
|
|
151
|
+
return { ...writeOptionsOf(input), expectedVersions };
|
|
164
152
|
}
|
|
165
153
|
|
|
166
154
|
// =============================================================================
|
|
@@ -192,17 +180,20 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
192
180
|
events === false ? undefined : (events !== true && events.topic) || ((name: string, _ctx: ProcedureContext, partition: string | undefined) => collectionChangeTopic(name, partition));
|
|
193
181
|
const topicOf = (ctx: ProcedureContext, partition: string | undefined): string | undefined => topicRule?.(collectionName, ctx, partition);
|
|
194
182
|
const withValues = events !== false && events !== true && events.values === true;
|
|
195
|
-
|
|
183
|
+
const admission = options.admission;
|
|
184
|
+
/** A write, and then its change event on the bus of the context. The chain of the events belongs to the storage, not to its view. */
|
|
196
185
|
const changing = <R>(ctx: ProcedureContext, partition: string | undefined, storage: CollectionStorage<unknown>, ids: readonly string[] | "*", origin: string | undefined, write: () => Promise<R>): Promise<R> =>
|
|
197
|
-
writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx, partition) : undefined, ids, origin, write, withValues);
|
|
186
|
+
writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx, partition) : undefined, ids, origin, write, withValues, admission?.admit);
|
|
198
187
|
/**
|
|
199
188
|
* The partition and the storage of a call (roadmap CLI-17). The repository of the call has the
|
|
200
|
-
* partitions of the collection and their resolver: the tenant comes from the principal.
|
|
189
|
+
* partitions of the collection and their resolver: the tenant comes from the principal. Each read
|
|
190
|
+
* and each write goes through the view, with the rules of the rows (roadmap CLI-41).
|
|
201
191
|
*/
|
|
202
|
-
const storageOf = async (input: PartitionInput, ctx: ProcedureContext): Promise<{ storage: CollectionStorage<unknown>; partition: string | undefined }> => {
|
|
192
|
+
const storageOf = async (input: PartitionInput, ctx: ProcedureContext): Promise<{ storage: CollectionStorage<unknown>; view: CollectionStorage<unknown>; partition: string | undefined }> => {
|
|
203
193
|
const collection = getCollectionFromPath(ctx.path);
|
|
204
194
|
const partition = await partitionOfCall(ctx, collection, input.partition);
|
|
205
|
-
|
|
195
|
+
const storage = storageOfCall<unknown>(ctx, collection, partition);
|
|
196
|
+
return { storage, view: admittedStorage(storage, admission, ctx), partition };
|
|
206
197
|
};
|
|
207
198
|
|
|
208
199
|
return [
|
|
@@ -217,8 +208,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
217
208
|
tags: ["collections", collectionName, "read"],
|
|
218
209
|
},
|
|
219
210
|
handler: async (input: GetInput & PartitionInput, ctx: ProcedureContext) => {
|
|
220
|
-
const {
|
|
221
|
-
const value = await
|
|
211
|
+
const { view } = await storageOf(input, ctx);
|
|
212
|
+
const value = await view.get(input.id);
|
|
222
213
|
if (input.found !== true) return value;
|
|
223
214
|
// The output with `found` keeps a stored null apart from a missing id, in one call
|
|
224
215
|
const output: GetFoundOutput = value === undefined ? { found: false } : { found: true, value };
|
|
@@ -237,8 +228,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
237
228
|
tags: ["collections", collectionName, "write"],
|
|
238
229
|
},
|
|
239
230
|
handler: async (input: SetInput & PartitionInput, ctx: ProcedureContext) => {
|
|
240
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
241
|
-
await changing(ctx, partition, storage, [input.id], input.origin, () =>
|
|
231
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
232
|
+
await changing(ctx, partition, storage, [input.id], input.origin, () => view.set(input.id, input.value, itemWriteOptionsOf(input)));
|
|
242
233
|
},
|
|
243
234
|
}),
|
|
244
235
|
|
|
@@ -253,8 +244,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
253
244
|
tags: ["collections", collectionName, "write"],
|
|
254
245
|
},
|
|
255
246
|
handler: async (input: DeleteInput & PartitionInput, ctx: ProcedureContext) => {
|
|
256
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
257
|
-
return changing(ctx, partition, storage, [input.id], input.origin, () =>
|
|
247
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
248
|
+
return changing(ctx, partition, storage, [input.id], input.origin, () => view.delete(input.id, itemWriteOptionsOf(input)));
|
|
258
249
|
},
|
|
259
250
|
}),
|
|
260
251
|
|
|
@@ -269,8 +260,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
269
260
|
tags: ["collections", collectionName, "read"],
|
|
270
261
|
},
|
|
271
262
|
handler: async (input: HasInput & PartitionInput, ctx: ProcedureContext) => {
|
|
272
|
-
const {
|
|
273
|
-
return
|
|
263
|
+
const { view } = await storageOf(input, ctx);
|
|
264
|
+
return view.has(input.id);
|
|
274
265
|
},
|
|
275
266
|
}),
|
|
276
267
|
|
|
@@ -285,8 +276,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
285
276
|
tags: ["collections", collectionName, "read"],
|
|
286
277
|
},
|
|
287
278
|
handler: async (input: GetAllInput & PartitionInput, ctx: ProcedureContext) => {
|
|
288
|
-
const {
|
|
289
|
-
return
|
|
279
|
+
const { view } = await storageOf(input, ctx);
|
|
280
|
+
return view.getAll();
|
|
290
281
|
},
|
|
291
282
|
}),
|
|
292
283
|
|
|
@@ -301,11 +292,11 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
301
292
|
tags: ["collections", collectionName, "read"],
|
|
302
293
|
},
|
|
303
294
|
handler: async (input: EntriesInput & PartitionInput, ctx: ProcedureContext) => {
|
|
304
|
-
const {
|
|
305
|
-
if (!
|
|
295
|
+
const { view } = await storageOf(input, ctx);
|
|
296
|
+
if (!view.getEntries) {
|
|
306
297
|
throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
|
|
307
298
|
}
|
|
308
|
-
return (await
|
|
299
|
+
return (await view.getEntries()).map(([id, value]) => ({ id, value }));
|
|
309
300
|
},
|
|
310
301
|
}),
|
|
311
302
|
|
|
@@ -320,10 +311,10 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
320
311
|
tags: ["collections", collectionName, "read"],
|
|
321
312
|
},
|
|
322
313
|
handler: async (input: IdsInput & PartitionInput, ctx: ProcedureContext) => {
|
|
323
|
-
const {
|
|
324
|
-
if (
|
|
314
|
+
const { view } = await storageOf(input, ctx);
|
|
315
|
+
if (view.getIds) return view.getIds();
|
|
325
316
|
// A storage without getIds() gives its entries: the values stay on the server
|
|
326
|
-
if (
|
|
317
|
+
if (view.getEntries) return (await view.getEntries()).map(([id]) => id);
|
|
327
318
|
throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
|
|
328
319
|
},
|
|
329
320
|
}),
|
|
@@ -339,9 +330,9 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
339
330
|
tags: ["collections", collectionName, "read"],
|
|
340
331
|
},
|
|
341
332
|
handler: async (input: ChangesInput & PartitionInput, ctx: ProcedureContext): Promise<ChangesOutput> => {
|
|
342
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
343
|
-
if (
|
|
344
|
-
const changes = await
|
|
333
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
334
|
+
if (view.getChangesSince) {
|
|
335
|
+
const changes = await view.getChangesSince(input.since);
|
|
345
336
|
// The chain of the change events starts at this version: the caller then applies the
|
|
346
337
|
// next event without a read (roadmap CLI-15)
|
|
347
338
|
if (topicRule) startChangeChain(ctx.bus, storage, topicOf(ctx, partition), changes.version);
|
|
@@ -354,8 +345,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
354
345
|
}
|
|
355
346
|
// A storage without a change log gives all its items. The empty version asks for all
|
|
356
347
|
// items again at the next call.
|
|
357
|
-
if (
|
|
358
|
-
const entries = await
|
|
348
|
+
if (view.getEntries) {
|
|
349
|
+
const entries = await view.getEntries();
|
|
359
350
|
return { version: "", full: true, changed: entries.map(([id, value]) => ({ id, value })), deleted: [] };
|
|
360
351
|
}
|
|
361
352
|
throw new Error(`The storage of ${collectionName} cannot give its changes or the ids of its items`);
|
|
@@ -373,16 +364,16 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
373
364
|
tags: ["collections", collectionName, "read", "query"],
|
|
374
365
|
},
|
|
375
366
|
handler: async (input: FindInput & PartitionInput, ctx: ProcedureContext) => {
|
|
376
|
-
const {
|
|
367
|
+
const { view } = await storageOf(input, ctx);
|
|
377
368
|
const selector = input.selector ?? {};
|
|
378
369
|
const query = queryOptionsOf(input, queryPolicy);
|
|
379
370
|
// The storage runs the selector itself when it can: a database does not read each item. It
|
|
380
371
|
// gets the policy of the host, so it permits what the procedure permits.
|
|
381
|
-
if (
|
|
382
|
-
if (!
|
|
372
|
+
if (view.query) return (await view.query(selector, query, queryPolicy)).map(([id, value]) => ({ id, value }));
|
|
373
|
+
if (!view.getEntries) {
|
|
383
374
|
throw new Error(`The storage of ${collectionName} cannot run a selector or give the ids of its items`);
|
|
384
375
|
}
|
|
385
|
-
return queryEntries(await
|
|
376
|
+
return queryEntries(await view.getEntries(), selector, query, queryPolicy).map(([id, value]) => ({ id, value }));
|
|
386
377
|
},
|
|
387
378
|
}),
|
|
388
379
|
|
|
@@ -397,12 +388,12 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
397
388
|
tags: ["collections", collectionName, "write"],
|
|
398
389
|
},
|
|
399
390
|
handler: async (input: PatchInput & PartitionInput, ctx: ProcedureContext) => {
|
|
400
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
391
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
401
392
|
const write = writeOptionsOf(input);
|
|
402
|
-
const value = await changing(ctx, partition, storage, [input.id], input.origin, () =>
|
|
403
|
-
|
|
404
|
-
)
|
|
405
|
-
const output: GetFoundOutput =
|
|
393
|
+
const value = await changing(ctx, partition, storage, [input.id], input.origin, () => patchItem(view, input.id, input.fields, write));
|
|
394
|
+
if (value === undefined) return { found: false } satisfies GetFoundOutput;
|
|
395
|
+
// The new item is a row too: a caller gets it only when the rule of the rows admits it (roadmap CLI-41)
|
|
396
|
+
const output: GetFoundOutput = (await admitsRow(admission, ctx, input.id, value)) ? { found: true, value } : { found: true };
|
|
406
397
|
return output;
|
|
407
398
|
},
|
|
408
399
|
}),
|
|
@@ -418,8 +409,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
418
409
|
tags: ["collections", collectionName, "read"],
|
|
419
410
|
},
|
|
420
411
|
handler: async (input: SizeInput & PartitionInput, ctx: ProcedureContext) => {
|
|
421
|
-
const {
|
|
422
|
-
return
|
|
412
|
+
const { view } = await storageOf(input, ctx);
|
|
413
|
+
return view.size();
|
|
423
414
|
},
|
|
424
415
|
}),
|
|
425
416
|
|
|
@@ -434,8 +425,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
434
425
|
tags: ["collections", collectionName, "write"],
|
|
435
426
|
},
|
|
436
427
|
handler: async (input: ClearInput & PartitionInput, ctx: ProcedureContext) => {
|
|
437
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
438
|
-
await changing(ctx, partition, storage, "*", input.origin, () =>
|
|
428
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
429
|
+
await changing(ctx, partition, storage, "*", input.origin, () => view.clear(writeOptionsOf(input)));
|
|
439
430
|
},
|
|
440
431
|
}),
|
|
441
432
|
|
|
@@ -450,8 +441,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
450
441
|
tags: ["collections", collectionName, "read", "batch"],
|
|
451
442
|
},
|
|
452
443
|
handler: async (input: GetBatchInput & PartitionInput, ctx: ProcedureContext) => {
|
|
453
|
-
const {
|
|
454
|
-
const result = await
|
|
444
|
+
const { view } = await storageOf(input, ctx);
|
|
445
|
+
const result = await view.getBatch(input.ids);
|
|
455
446
|
// Convert Map to plain object
|
|
456
447
|
return Object.fromEntries(result);
|
|
457
448
|
},
|
|
@@ -468,12 +459,12 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
468
459
|
tags: ["collections", collectionName, "write", "batch"],
|
|
469
460
|
},
|
|
470
461
|
handler: async (input: SetBatchInput & PartitionInput, ctx: ProcedureContext) => {
|
|
471
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
462
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
472
463
|
const items: Array<[string, unknown]> = input.items.map((item) => [
|
|
473
464
|
item.id,
|
|
474
465
|
item.value,
|
|
475
466
|
]);
|
|
476
|
-
await changing(ctx, partition, storage, items.map(([id]) => id), input.origin, () =>
|
|
467
|
+
await changing(ctx, partition, storage, items.map(([id]) => id), input.origin, () => view.setBatch(items, batchWriteOptionsOf(input)));
|
|
477
468
|
},
|
|
478
469
|
}),
|
|
479
470
|
|
|
@@ -488,8 +479,8 @@ export function createCollectionProcedures(collectionName: string, options: Coll
|
|
|
488
479
|
tags: ["collections", collectionName, "write", "batch"],
|
|
489
480
|
},
|
|
490
481
|
handler: async (input: DeleteBatchInput & PartitionInput, ctx: ProcedureContext) => {
|
|
491
|
-
const { storage, partition } = await storageOf(input, ctx);
|
|
492
|
-
return changing(ctx, partition, storage, input.ids, input.origin, () =>
|
|
482
|
+
const { storage, view, partition } = await storageOf(input, ctx);
|
|
483
|
+
return changing(ctx, partition, storage, input.ids, input.origin, () => view.deleteBatch(input.ids, writeOptionsOf(input)));
|
|
493
484
|
},
|
|
494
485
|
}),
|
|
495
486
|
];
|