@procedures/client 0.8.0 → 0.8.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 (188) hide show
  1. package/CHANGELOG.md +58 -0
  2. package/README.md +95 -8
  3. package/dist/.tsbuildinfo +1 -1
  4. package/dist/adapters/http/client/transport.d.ts +4 -0
  5. package/dist/adapters/http/client/transport.d.ts.map +1 -1
  6. package/dist/adapters/http/client/transport.js +11 -1
  7. package/dist/adapters/http/client/transport.js.map +1 -1
  8. package/dist/adapters/http/client/types.d.ts +19 -0
  9. package/dist/adapters/http/client/types.d.ts.map +1 -1
  10. package/dist/adapters/http/server/cors.d.ts +64 -0
  11. package/dist/adapters/http/server/cors.d.ts.map +1 -0
  12. package/dist/adapters/http/server/cors.js +113 -0
  13. package/dist/adapters/http/server/cors.js.map +1 -0
  14. package/dist/adapters/http/server/handler.d.ts +136 -0
  15. package/dist/adapters/http/server/handler.d.ts.map +1 -0
  16. package/dist/adapters/http/server/handler.js +495 -0
  17. package/dist/adapters/http/server/handler.js.map +1 -0
  18. package/dist/adapters/http/server/strategies.d.ts +14 -5
  19. package/dist/adapters/http/server/strategies.d.ts.map +1 -1
  20. package/dist/adapters/http/server/strategies.js.map +1 -1
  21. package/dist/adapters/http/server/transport.d.ts +30 -46
  22. package/dist/adapters/http/server/transport.d.ts.map +1 -1
  23. package/dist/adapters/http/server/transport.js +48 -432
  24. package/dist/adapters/http/server/transport.js.map +1 -1
  25. package/dist/adapters/http/server/types.d.ts +13 -0
  26. package/dist/adapters/http/server/types.d.ts.map +1 -1
  27. package/dist/adapters/websocket/server/transport.d.ts +46 -1
  28. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  29. package/dist/adapters/websocket/server/transport.js +152 -15
  30. package/dist/adapters/websocket/server/transport.js.map +1 -1
  31. package/dist/adapters/websocket/server/types.d.ts +10 -3
  32. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  33. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  34. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  35. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  36. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  37. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  38. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  39. package/dist/adapters/websocket/server/upgrade.js +49 -0
  40. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  41. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  42. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  43. package/dist/adapters/websocket/shared/rpc.js +8 -2
  44. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  45. package/dist/browser.d.ts +12 -4
  46. package/dist/browser.d.ts.map +1 -1
  47. package/dist/browser.js +11 -2
  48. package/dist/browser.js.map +1 -1
  49. package/dist/client/client.d.ts +43 -0
  50. package/dist/client/client.d.ts.map +1 -1
  51. package/dist/client/client.js +69 -1
  52. package/dist/client/client.js.map +1 -1
  53. package/dist/events/publisher.d.ts +14 -0
  54. package/dist/events/publisher.d.ts.map +1 -1
  55. package/dist/events/publisher.js +42 -4
  56. package/dist/events/publisher.js.map +1 -1
  57. package/dist/events/rows.d.ts +31 -0
  58. package/dist/events/rows.d.ts.map +1 -0
  59. package/dist/events/rows.js +39 -0
  60. package/dist/events/rows.js.map +1 -0
  61. package/dist/index.d.ts +1 -1
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +1 -1
  64. package/dist/index.js.map +1 -1
  65. package/dist/node.d.ts +14 -0
  66. package/dist/node.d.ts.map +1 -1
  67. package/dist/node.js +9 -0
  68. package/dist/node.js.map +1 -1
  69. package/dist/procedures/capability.d.ts +6 -0
  70. package/dist/procedures/capability.d.ts.map +1 -1
  71. package/dist/procedures/capability.js +25 -0
  72. package/dist/procedures/capability.js.map +1 -1
  73. package/dist/procedures/collection/changes.d.ts +94 -0
  74. package/dist/procedures/collection/changes.d.ts.map +1 -0
  75. package/dist/procedures/collection/changes.js +182 -0
  76. package/dist/procedures/collection/changes.js.map +1 -0
  77. package/dist/procedures/collection/index.d.ts +8 -3
  78. package/dist/procedures/collection/index.d.ts.map +1 -1
  79. package/dist/procedures/collection/index.js +6 -1
  80. package/dist/procedures/collection/index.js.map +1 -1
  81. package/dist/procedures/collection/procedures.d.ts +39 -2
  82. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  83. package/dist/procedures/collection/procedures.js +111 -8
  84. package/dist/procedures/collection/procedures.js.map +1 -1
  85. package/dist/procedures/collection/query.d.ts +246 -0
  86. package/dist/procedures/collection/query.d.ts.map +1 -0
  87. package/dist/procedures/collection/query.js +854 -0
  88. package/dist/procedures/collection/query.js.map +1 -0
  89. package/dist/procedures/collection/schemas.d.ts +50 -0
  90. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  91. package/dist/procedures/collection/schemas.js +144 -6
  92. package/dist/procedures/collection/schemas.js.map +1 -1
  93. package/dist/procedures/collection/storage.d.ts +58 -6
  94. package/dist/procedures/collection/storage.d.ts.map +1 -1
  95. package/dist/procedures/collection/storage.js +23 -0
  96. package/dist/procedures/collection/storage.js.map +1 -1
  97. package/dist/procedures/elicit.d.ts +190 -0
  98. package/dist/procedures/elicit.d.ts.map +1 -0
  99. package/dist/procedures/elicit.js +127 -0
  100. package/dist/procedures/elicit.js.map +1 -0
  101. package/dist/procedures/hints.d.ts +43 -0
  102. package/dist/procedures/hints.d.ts.map +1 -0
  103. package/dist/procedures/hints.js +74 -0
  104. package/dist/procedures/hints.js.map +1 -0
  105. package/dist/procedures/index.d.ts +9 -2
  106. package/dist/procedures/index.d.ts.map +1 -1
  107. package/dist/procedures/index.js +5 -1
  108. package/dist/procedures/index.js.map +1 -1
  109. package/dist/procedures/invoke.d.ts +20 -0
  110. package/dist/procedures/invoke.d.ts.map +1 -1
  111. package/dist/procedures/invoke.js +13 -0
  112. package/dist/procedures/invoke.js.map +1 -1
  113. package/dist/procedures/progress.d.ts +24 -0
  114. package/dist/procedures/progress.d.ts.map +1 -0
  115. package/dist/procedures/progress.js +11 -0
  116. package/dist/procedures/progress.js.map +1 -0
  117. package/dist/procedures/registry.d.ts.map +1 -1
  118. package/dist/procedures/registry.js +5 -0
  119. package/dist/procedures/registry.js.map +1 -1
  120. package/dist/procedures/types.d.ts +42 -0
  121. package/dist/procedures/types.d.ts.map +1 -1
  122. package/dist/procedures/types.js.map +1 -1
  123. package/dist/procedures/untrusted.d.ts +28 -0
  124. package/dist/procedures/untrusted.d.ts.map +1 -0
  125. package/dist/procedures/untrusted.js +32 -0
  126. package/dist/procedures/untrusted.js.map +1 -0
  127. package/dist/server/browser-guard.d.ts +12 -2
  128. package/dist/server/browser-guard.d.ts.map +1 -1
  129. package/dist/server/browser-guard.js +43 -6
  130. package/dist/server/browser-guard.js.map +1 -1
  131. package/dist/server/host-policy.d.ts +10 -1
  132. package/dist/server/host-policy.d.ts.map +1 -1
  133. package/dist/server/host-policy.js +5 -0
  134. package/dist/server/host-policy.js.map +1 -1
  135. package/dist/server/index.d.ts +1 -1
  136. package/dist/server/index.d.ts.map +1 -1
  137. package/dist/server/index.js.map +1 -1
  138. package/dist/server/procedure-server.d.ts +38 -6
  139. package/dist/server/procedure-server.d.ts.map +1 -1
  140. package/dist/server/procedure-server.js +65 -13
  141. package/dist/server/procedure-server.js.map +1 -1
  142. package/dist/server/request-auth.d.ts +51 -4
  143. package/dist/server/request-auth.d.ts.map +1 -1
  144. package/dist/server/request-auth.js +116 -5
  145. package/dist/server/request-auth.js.map +1 -1
  146. package/dist/server/types.d.ts +73 -5
  147. package/dist/server/types.d.ts.map +1 -1
  148. package/dist/server/types.js.map +1 -1
  149. package/package.json +5 -1
  150. package/src/adapters/http/client/transport.ts +11 -1
  151. package/src/adapters/http/client/types.ts +22 -0
  152. package/src/adapters/http/server/cors.ts +144 -0
  153. package/src/adapters/http/server/handler.ts +624 -0
  154. package/src/adapters/http/server/strategies.ts +16 -6
  155. package/src/adapters/http/server/transport.ts +55 -478
  156. package/src/adapters/http/server/types.ts +15 -0
  157. package/src/adapters/websocket/server/transport.ts +166 -16
  158. package/src/adapters/websocket/server/types.ts +11 -3
  159. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  160. package/src/adapters/websocket/server/upgrade.ts +88 -0
  161. package/src/adapters/websocket/shared/rpc.ts +9 -2
  162. package/src/browser.ts +79 -2
  163. package/src/client/client.ts +83 -1
  164. package/src/events/publisher.ts +48 -4
  165. package/src/events/rows.ts +48 -0
  166. package/src/index.ts +1 -1
  167. package/src/node.ts +21 -0
  168. package/src/procedures/capability.ts +21 -0
  169. package/src/procedures/collection/changes.ts +245 -0
  170. package/src/procedures/collection/index.ts +53 -1
  171. package/src/procedures/collection/procedures.ts +163 -7
  172. package/src/procedures/collection/query.ts +1009 -0
  173. package/src/procedures/collection/schemas.ts +196 -7
  174. package/src/procedures/collection/storage.ts +78 -6
  175. package/src/procedures/elicit.ts +320 -0
  176. package/src/procedures/hints.ts +91 -0
  177. package/src/procedures/index.ts +78 -2
  178. package/src/procedures/invoke.ts +29 -0
  179. package/src/procedures/progress.ts +25 -0
  180. package/src/procedures/registry.ts +4 -0
  181. package/src/procedures/types.ts +49 -0
  182. package/src/procedures/untrusted.ts +50 -0
  183. package/src/server/browser-guard.ts +48 -6
  184. package/src/server/host-policy.ts +13 -1
  185. package/src/server/index.ts +3 -0
  186. package/src/server/procedure-server.ts +82 -14
  187. package/src/server/request-auth.ts +128 -6
  188. package/src/server/types.ts +82 -5
@@ -21,6 +21,10 @@ export {
21
21
  getAllOutputSchema,
22
22
  entriesInputSchema,
23
23
  entriesOutputSchema,
24
+ findInputSchema,
25
+ findOutputSchema,
26
+ patchInputSchema,
27
+ patchOutputSchema,
24
28
  idsInputSchema,
25
29
  idsOutputSchema,
26
30
  changesInputSchema,
@@ -46,6 +50,8 @@ export type {
46
50
  GetAllInput,
47
51
  EntriesInput,
48
52
  CollectionEntry,
53
+ FindInput,
54
+ PatchInput,
49
55
  IdsInput,
50
56
  ChangesInput,
51
57
  ChangesOutput,
@@ -58,6 +64,52 @@ export type {
58
64
 
59
65
  // Procedures
60
66
  export { createCollectionProcedures } from "./procedures.js";
67
+ export type { CollectionProceduresOptions, CollectionChangeEventsOptions } from "./procedures.js";
68
+
69
+ // The change events of the writes (roadmap CLI-15)
70
+ export { collectionChangeTopic, collectionTopicReads } from "./changes.js";
71
+ export type { CollectionChangeEvent } from "./changes.js";
61
72
 
62
73
  // The storage interface of the core (the shape of client-collections' CollectionStorage)
63
- export type { CollectionStorage, StorageChanges, StorageMetadata } from "./storage.js";
74
+ export type {
75
+ CollectionStorage,
76
+ StorageChanges,
77
+ StorageMetadata,
78
+ StorageWriteOptions,
79
+ StorageSetChange,
80
+ StorageDeleteChange,
81
+ StorageClearChange,
82
+ StorageChangeEvent,
83
+ StorageChangeBatch,
84
+ StorageObserver,
85
+ } from "./storage.js";
86
+
87
+ // The query language of the storages: the same JSON on the wire and in query() (roadmap CLI-34:
88
+ // the one copy of the language)
89
+ export {
90
+ DEFAULT_FIND_LIMIT,
91
+ DEFAULT_QUERY_OPERATORS,
92
+ DEFAULT_QUERY_POLICY,
93
+ QUERY_OPERATORS,
94
+ applyPatch,
95
+ compareStorageValues,
96
+ compileSelector,
97
+ isPatchOperations,
98
+ matchesSelector,
99
+ mergePatch,
100
+ patchProblems,
101
+ queryEntries,
102
+ queryOptionsProblems,
103
+ selectorProblems,
104
+ } from "./query.js";
105
+ export type {
106
+ StorageFieldOperators,
107
+ StoragePatch,
108
+ StoragePatchOperations,
109
+ StorageQueryOperator,
110
+ StorageQueryOptions,
111
+ StorageQueryPolicy,
112
+ StorageSelector,
113
+ StorageSort,
114
+ StorageSortDirection,
115
+ } from "./query.js";
@@ -34,6 +34,12 @@ import {
34
34
  setBatchOutputSchema,
35
35
  deleteBatchInputSchema,
36
36
  deleteBatchOutputSchema,
37
+ findInputSchemaOf,
38
+ findOutputSchema,
39
+ patchInputSchema,
40
+ patchOutputSchema,
41
+ type FindInput,
42
+ type PatchInput,
37
43
  type GetInput,
38
44
  type GetFoundOutput,
39
45
  type SetInput,
@@ -50,6 +56,9 @@ import {
50
56
  type SetBatchInput,
51
57
  type DeleteBatchInput,
52
58
  } from "./schemas.js";
59
+ import { DEFAULT_FIND_LIMIT, applyPatch, queryEntries, type StoragePatch, type StorageQueryOptions, type StorageQueryPolicy } from "./query.js";
60
+ import type { CollectionStorage, StorageWriteOptions } from "./storage.js";
61
+ import { collectionChangeTopic, startChangeChain, writeWithChangeEvent } from "./changes.js";
53
62
 
54
63
  // =============================================================================
55
64
  // Handler Factory
@@ -77,6 +86,93 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
77
86
  return ctx.repository.getStorage<T>(collection);
78
87
  }
79
88
 
89
+ /** The options of a query from the input of `find`. */
90
+ function queryOptionsOf(input: FindInput, policy: StorageQueryPolicy): StorageQueryOptions {
91
+ // A find without a limit gets the maxLimit of the policy (roadmap CLI-14)
92
+ const limit = input.limit ?? (Number.isFinite(policy.maxLimit) ? policy.maxLimit : undefined);
93
+ return {
94
+ ...(input.sort !== undefined && { sort: input.sort }),
95
+ ...(input.skip !== undefined && { skip: input.skip }),
96
+ ...(limit !== undefined && { limit }),
97
+ };
98
+ }
99
+
100
+ /** The options of the change events of the collection procedures (roadmap CLI-15). */
101
+ export interface CollectionChangeEventsOptions {
102
+ /**
103
+ * The topic of the change events of a call, or undefined for no event. The partition of a
104
+ * collection (roadmap CLI-17) goes into the topic here, for example from the scope of the call.
105
+ * @default the topic `collections.<name>` of `collectionChangeTopic()`
106
+ */
107
+ topic?: ((collection: string, ctx: ProcedureContext) => string | undefined) | undefined;
108
+ /**
109
+ * True: the events also carry the values of the changed items. Each row `{ id, value }` passes
110
+ * the output filter of the host (`admitItem`) for each subscriber, and a refused row stays out of
111
+ * that event: its id goes to `withheld`. Without it, the events carry only the ids and the
112
+ * versions, and a subscriber reads the values with `changes`.
113
+ * @default false
114
+ */
115
+ values?: boolean | undefined;
116
+ }
117
+
118
+ /** The options of the collection procedures. */
119
+ export interface CollectionProceduresOptions {
120
+ /**
121
+ * The query policy of `find`: the operators, and the limits of a selector and of the result. A
122
+ * selector or a limit that the policy does not permit is a `VALIDATION_ERROR`. The policy also
123
+ * goes to `query()` of the storage. `$regex` is off unless the policy has it in `operators`.
124
+ * @default DEFAULT_QUERY_POLICY, with `maxLimit` DEFAULT_FIND_LIMIT (1000)
125
+ */
126
+ queryPolicy?: StorageQueryPolicy | undefined;
127
+ /**
128
+ * The change events of the writes (roadmap CLI-15). Each write publishes a
129
+ * `CollectionChangeEvent` on the bus of its context, while a subscriber listens to the topic.
130
+ * The event has the ids and the versions, and the values only with `values`. False: the
131
+ * procedures publish no event.
132
+ * @default true, with the topic `collections.<name>` and no values
133
+ */
134
+ changeEvents?: boolean | CollectionChangeEventsOptions | undefined;
135
+ }
136
+
137
+ /** The write options of an input with an origin. */
138
+ function writeOptionsOf(input: { origin?: string }): StorageWriteOptions | undefined {
139
+ return input.origin === undefined ? undefined : { origin: input.origin };
140
+ }
141
+
142
+ /**
143
+ * The patches of a storage without `patch()` that run, for each id. A patch of an id waits for the
144
+ * patch before it, so two patches of one process keep each field. A write of another writer
145
+ * between the read and the write of a patch is lost: give the storage a `patch()` for one step.
146
+ */
147
+ const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
148
+
149
+ /** This function patches an item of a storage without `patch()`: a read, a merge and a write. */
150
+ function patchInParts(storage: CollectionStorage<unknown>, id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<unknown> {
151
+ let running = PATCHES.get(storage);
152
+ if (!running) {
153
+ running = new Map();
154
+ PATCHES.set(storage, running);
155
+ }
156
+ const queue = running;
157
+ const before = queue.get(id) ?? Promise.resolve();
158
+ const patch = before.then(async () => {
159
+ const current = await storage.get(id);
160
+ if (current === undefined) return undefined;
161
+ const next = applyPatch(current, fields);
162
+ await storage.set(id, next, options);
163
+ return next;
164
+ });
165
+ const settled = patch.then(
166
+ () => undefined,
167
+ () => undefined,
168
+ );
169
+ queue.set(id, settled);
170
+ void settled.then(() => {
171
+ if (queue.get(id) === settled) queue.delete(id);
172
+ });
173
+ return patch;
174
+ }
175
+
80
176
  // =============================================================================
81
177
  // Collection Procedure Definitions
82
178
  // =============================================================================
@@ -85,6 +181,7 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
85
181
  * Create collection procedures for a specific collection name.
86
182
  *
87
183
  * @param collectionName - Name of the collection
184
+ * @param options - The query policy of `find`
88
185
  * @returns Array of procedure definitions
89
186
  *
90
187
  * @example
@@ -93,10 +190,21 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
93
190
  * registry.registerAll(userProcedures);
94
191
  * ```
95
192
  */
96
- export function createCollectionProcedures(collectionName: string): AnyProcedure[] {
193
+ export function createCollectionProcedures(collectionName: string, options: CollectionProceduresOptions = {}): AnyProcedure[] {
97
194
  const basePath = ["collections", collectionName];
195
+ // A find of a caller gets a limit: DEFAULT_FIND_LIMIT, unless the policy gives its own maxLimit
196
+ const queryPolicy: StorageQueryPolicy = { ...options.queryPolicy, maxLimit: options.queryPolicy?.maxLimit ?? DEFAULT_FIND_LIMIT };
98
197
  // Each collection is a resource of its own (CAPABILITIES-2026-10): a capability can permit one
99
198
  const resource = `collection:${collectionName}`;
199
+ // The topic of the change events of a call (roadmap CLI-15). CLI-17 adds the partition with the option topic.
200
+ const events = options.changeEvents ?? true;
201
+ const topicRule: ((collection: string, ctx: ProcedureContext) => string | undefined) | undefined =
202
+ events === false ? undefined : (events !== true && events.topic) || ((name: string) => collectionChangeTopic(name));
203
+ const topicOf = (ctx: ProcedureContext): string | undefined => topicRule?.(collectionName, ctx);
204
+ const withValues = events !== false && events !== true && events.values === true;
205
+ /** A write, and then its change event on the bus of the context. */
206
+ const changing = <R>(ctx: ProcedureContext, storage: CollectionStorage<unknown>, ids: readonly string[] | "*", origin: string | undefined, write: () => Promise<R>): Promise<R> =>
207
+ writeWithChangeEvent(ctx.bus, storage, topicRule ? topicOf(ctx) : undefined, ids, origin, write, withValues);
100
208
 
101
209
  return [
102
210
  // Get
@@ -131,7 +239,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
131
239
  },
132
240
  handler: async (input: SetInput, ctx: ProcedureContext) => {
133
241
  const storage = getStorage(ctx);
134
- await storage.set(input.id, input.value);
242
+ await changing(ctx, storage, [input.id], input.origin, () => storage.set(input.id, input.value, writeOptionsOf(input)));
135
243
  },
136
244
  }),
137
245
 
@@ -147,7 +255,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
147
255
  },
148
256
  handler: async (input: DeleteInput, ctx: ProcedureContext) => {
149
257
  const storage = getStorage(ctx);
150
- return storage.delete(input.id);
258
+ return changing(ctx, storage, [input.id], input.origin, () => storage.delete(input.id, writeOptionsOf(input)));
151
259
  },
152
260
  }),
153
261
 
@@ -235,6 +343,9 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
235
343
  const storage = getStorage(ctx);
236
344
  if (storage.getChangesSince) {
237
345
  const changes = await storage.getChangesSince(input.since);
346
+ // The chain of the change events starts at this version: the caller then applies the
347
+ // next event without a read (roadmap CLI-15)
348
+ if (topicRule) startChangeChain(ctx.bus, storage, topicOf(ctx), changes.version);
238
349
  return {
239
350
  version: changes.version,
240
351
  full: changes.full,
@@ -252,6 +363,51 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
252
363
  },
253
364
  }),
254
365
 
366
+ // Find: the items that match a JSON selector, with an order, a skip and a limit (roadmap CLI-14)
367
+ defineProcedure({
368
+ path: [...basePath, "find"],
369
+ input: findInputSchemaOf(queryPolicy),
370
+ output: findOutputSchema,
371
+ metadata: {
372
+ effects: { reads: [resource], writes: [] },
373
+ description: `Find the items of ${collectionName} that match a selector, with their ids, an order, a skip and a limit`,
374
+ tags: ["collections", collectionName, "read", "query"],
375
+ },
376
+ handler: async (input: FindInput, ctx: ProcedureContext) => {
377
+ const storage = getStorage(ctx);
378
+ const selector = input.selector ?? {};
379
+ const query = queryOptionsOf(input, queryPolicy);
380
+ // The storage runs the selector itself when it can: a database does not read each item. It
381
+ // gets the policy of the host, so it permits what the procedure permits.
382
+ if (storage.query) return (await storage.query(selector, query, queryPolicy)).map(([id, value]) => ({ id, value }));
383
+ if (!storage.getEntries) {
384
+ throw new Error(`The storage of ${collectionName} cannot run a selector or give the ids of its items`);
385
+ }
386
+ return queryEntries(await storage.getEntries(), selector, query, queryPolicy).map(([id, value]) => ({ id, value }));
387
+ },
388
+ }),
389
+
390
+ // Patch: merge fields into one item, a JSON Merge Patch (roadmap CLI-14)
391
+ defineProcedure({
392
+ path: [...basePath, "patch"],
393
+ input: patchInputSchema,
394
+ output: patchOutputSchema,
395
+ metadata: {
396
+ effects: { reads: [resource], writes: [resource], idempotent: true },
397
+ description: `Merge fields into one item of ${collectionName}: a JSON Merge Patch, or the operations $set and $unset`,
398
+ tags: ["collections", collectionName, "write"],
399
+ },
400
+ handler: async (input: PatchInput, ctx: ProcedureContext) => {
401
+ const storage = getStorage(ctx);
402
+ const write = writeOptionsOf(input);
403
+ const value = await changing(ctx, storage, [input.id], input.origin, () =>
404
+ storage.patch ? storage.patch(input.id, input.fields, write) : patchInParts(storage, input.id, input.fields, write),
405
+ );
406
+ const output: GetFoundOutput = value === undefined ? { found: false } : { found: true, value };
407
+ return output;
408
+ },
409
+ }),
410
+
255
411
  // Size
256
412
  defineProcedure({
257
413
  path: [...basePath, "size"],
@@ -278,9 +434,9 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
278
434
  description: `Clear all items from ${collectionName}`,
279
435
  tags: ["collections", collectionName, "write"],
280
436
  },
281
- handler: async (_input: ClearInput, ctx: ProcedureContext) => {
437
+ handler: async (input: ClearInput, ctx: ProcedureContext) => {
282
438
  const storage = getStorage(ctx);
283
- await storage.clear();
439
+ await changing(ctx, storage, "*", input.origin, () => storage.clear(writeOptionsOf(input)));
284
440
  },
285
441
  }),
286
442
 
@@ -318,7 +474,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
318
474
  item.id,
319
475
  item.value,
320
476
  ]);
321
- await storage.setBatch(items);
477
+ await changing(ctx, storage, items.map(([id]) => id), input.origin, () => storage.setBatch(items, writeOptionsOf(input)));
322
478
  },
323
479
  }),
324
480
 
@@ -334,7 +490,7 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
334
490
  },
335
491
  handler: async (input: DeleteBatchInput, ctx: ProcedureContext) => {
336
492
  const storage = getStorage(ctx);
337
- return storage.deleteBatch(input.ids);
493
+ return changing(ctx, storage, input.ids, input.origin, () => storage.deleteBatch(input.ids, writeOptionsOf(input)));
338
494
  },
339
495
  }),
340
496
  ];