@procedures/client 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/CHANGELOG.md +78 -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/client/transport.d.ts.map +1 -1
  28. package/dist/adapters/websocket/client/transport.js +4 -12
  29. package/dist/adapters/websocket/client/transport.js.map +1 -1
  30. package/dist/adapters/websocket/server/transport.d.ts +46 -1
  31. package/dist/adapters/websocket/server/transport.d.ts.map +1 -1
  32. package/dist/adapters/websocket/server/transport.js +152 -15
  33. package/dist/adapters/websocket/server/transport.js.map +1 -1
  34. package/dist/adapters/websocket/server/types.d.ts +10 -3
  35. package/dist/adapters/websocket/server/types.d.ts.map +1 -1
  36. package/dist/adapters/websocket/server/upgrade-router.d.ts +28 -0
  37. package/dist/adapters/websocket/server/upgrade-router.d.ts.map +1 -0
  38. package/dist/adapters/websocket/server/upgrade-router.js +62 -0
  39. package/dist/adapters/websocket/server/upgrade-router.js.map +1 -0
  40. package/dist/adapters/websocket/server/upgrade.d.ts +68 -0
  41. package/dist/adapters/websocket/server/upgrade.d.ts.map +1 -0
  42. package/dist/adapters/websocket/server/upgrade.js +49 -0
  43. package/dist/adapters/websocket/server/upgrade.js.map +1 -0
  44. package/dist/adapters/websocket/shared/rpc.d.ts +2 -0
  45. package/dist/adapters/websocket/shared/rpc.d.ts.map +1 -1
  46. package/dist/adapters/websocket/shared/rpc.js +8 -2
  47. package/dist/adapters/websocket/shared/rpc.js.map +1 -1
  48. package/dist/browser.d.ts +12 -4
  49. package/dist/browser.d.ts.map +1 -1
  50. package/dist/browser.js +13 -2
  51. package/dist/browser.js.map +1 -1
  52. package/dist/client/client.d.ts +43 -0
  53. package/dist/client/client.d.ts.map +1 -1
  54. package/dist/client/client.js +69 -1
  55. package/dist/client/client.js.map +1 -1
  56. package/dist/events/publisher.d.ts +25 -1
  57. package/dist/events/publisher.d.ts.map +1 -1
  58. package/dist/events/publisher.js +66 -9
  59. package/dist/events/publisher.js.map +1 -1
  60. package/dist/events/rows.d.ts +31 -0
  61. package/dist/events/rows.d.ts.map +1 -0
  62. package/dist/events/rows.js +39 -0
  63. package/dist/events/rows.js.map +1 -0
  64. package/dist/index.d.ts +1 -1
  65. package/dist/index.d.ts.map +1 -1
  66. package/dist/index.js +1 -1
  67. package/dist/index.js.map +1 -1
  68. package/dist/node.d.ts +14 -0
  69. package/dist/node.d.ts.map +1 -1
  70. package/dist/node.js +9 -0
  71. package/dist/node.js.map +1 -1
  72. package/dist/procedures/capability.d.ts +6 -0
  73. package/dist/procedures/capability.d.ts.map +1 -1
  74. package/dist/procedures/capability.js +25 -0
  75. package/dist/procedures/capability.js.map +1 -1
  76. package/dist/procedures/collection/changes.d.ts +94 -0
  77. package/dist/procedures/collection/changes.d.ts.map +1 -0
  78. package/dist/procedures/collection/changes.js +182 -0
  79. package/dist/procedures/collection/changes.js.map +1 -0
  80. package/dist/procedures/collection/index.d.ts +10 -3
  81. package/dist/procedures/collection/index.d.ts.map +1 -1
  82. package/dist/procedures/collection/index.js +8 -1
  83. package/dist/procedures/collection/index.js.map +1 -1
  84. package/dist/procedures/collection/partition.d.ts +67 -0
  85. package/dist/procedures/collection/partition.d.ts.map +1 -0
  86. package/dist/procedures/collection/partition.js +82 -0
  87. package/dist/procedures/collection/partition.js.map +1 -0
  88. package/dist/procedures/collection/procedures.d.ts +39 -2
  89. package/dist/procedures/collection/procedures.d.ts.map +1 -1
  90. package/dist/procedures/collection/procedures.js +147 -44
  91. package/dist/procedures/collection/procedures.js.map +1 -1
  92. package/dist/procedures/collection/query.d.ts +246 -0
  93. package/dist/procedures/collection/query.d.ts.map +1 -0
  94. package/dist/procedures/collection/query.js +854 -0
  95. package/dist/procedures/collection/query.js.map +1 -0
  96. package/dist/procedures/collection/schemas.d.ts +50 -0
  97. package/dist/procedures/collection/schemas.d.ts.map +1 -1
  98. package/dist/procedures/collection/schemas.js +144 -6
  99. package/dist/procedures/collection/schemas.js.map +1 -1
  100. package/dist/procedures/collection/storage.d.ts +58 -6
  101. package/dist/procedures/collection/storage.d.ts.map +1 -1
  102. package/dist/procedures/collection/storage.js +23 -0
  103. package/dist/procedures/collection/storage.js.map +1 -1
  104. package/dist/procedures/elicit.d.ts +190 -0
  105. package/dist/procedures/elicit.d.ts.map +1 -0
  106. package/dist/procedures/elicit.js +127 -0
  107. package/dist/procedures/elicit.js.map +1 -0
  108. package/dist/procedures/hints.d.ts +43 -0
  109. package/dist/procedures/hints.d.ts.map +1 -0
  110. package/dist/procedures/hints.js +74 -0
  111. package/dist/procedures/hints.js.map +1 -0
  112. package/dist/procedures/index.d.ts +9 -2
  113. package/dist/procedures/index.d.ts.map +1 -1
  114. package/dist/procedures/index.js +5 -1
  115. package/dist/procedures/index.js.map +1 -1
  116. package/dist/procedures/invoke.d.ts +24 -2
  117. package/dist/procedures/invoke.d.ts.map +1 -1
  118. package/dist/procedures/invoke.js +14 -1
  119. package/dist/procedures/invoke.js.map +1 -1
  120. package/dist/procedures/progress.d.ts +24 -0
  121. package/dist/procedures/progress.d.ts.map +1 -0
  122. package/dist/procedures/progress.js +11 -0
  123. package/dist/procedures/progress.js.map +1 -0
  124. package/dist/procedures/registry.d.ts.map +1 -1
  125. package/dist/procedures/registry.js +5 -0
  126. package/dist/procedures/registry.js.map +1 -1
  127. package/dist/procedures/types.d.ts +55 -0
  128. package/dist/procedures/types.d.ts.map +1 -1
  129. package/dist/procedures/types.js.map +1 -1
  130. package/dist/procedures/untrusted.d.ts +28 -0
  131. package/dist/procedures/untrusted.d.ts.map +1 -0
  132. package/dist/procedures/untrusted.js +32 -0
  133. package/dist/procedures/untrusted.js.map +1 -0
  134. package/dist/server/browser-guard.d.ts +12 -2
  135. package/dist/server/browser-guard.d.ts.map +1 -1
  136. package/dist/server/browser-guard.js +43 -6
  137. package/dist/server/browser-guard.js.map +1 -1
  138. package/dist/server/host-policy.d.ts +10 -1
  139. package/dist/server/host-policy.d.ts.map +1 -1
  140. package/dist/server/host-policy.js +5 -0
  141. package/dist/server/host-policy.js.map +1 -1
  142. package/dist/server/index.d.ts +1 -1
  143. package/dist/server/index.d.ts.map +1 -1
  144. package/dist/server/index.js.map +1 -1
  145. package/dist/server/procedure-server.d.ts +68 -7
  146. package/dist/server/procedure-server.d.ts.map +1 -1
  147. package/dist/server/procedure-server.js +124 -24
  148. package/dist/server/procedure-server.js.map +1 -1
  149. package/dist/server/request-auth.d.ts +51 -4
  150. package/dist/server/request-auth.d.ts.map +1 -1
  151. package/dist/server/request-auth.js +116 -5
  152. package/dist/server/request-auth.js.map +1 -1
  153. package/dist/server/types.d.ts +73 -5
  154. package/dist/server/types.d.ts.map +1 -1
  155. package/dist/server/types.js.map +1 -1
  156. package/package.json +7 -3
  157. package/src/adapters/http/client/transport.ts +11 -1
  158. package/src/adapters/http/client/types.ts +22 -0
  159. package/src/adapters/http/server/cors.ts +144 -0
  160. package/src/adapters/http/server/handler.ts +624 -0
  161. package/src/adapters/http/server/strategies.ts +16 -6
  162. package/src/adapters/http/server/transport.ts +55 -478
  163. package/src/adapters/http/server/types.ts +15 -0
  164. package/src/adapters/websocket/client/transport.ts +4 -12
  165. package/src/adapters/websocket/server/transport.ts +166 -16
  166. package/src/adapters/websocket/server/types.ts +11 -3
  167. package/src/adapters/websocket/server/upgrade-router.ts +77 -0
  168. package/src/adapters/websocket/server/upgrade.ts +88 -0
  169. package/src/adapters/websocket/shared/rpc.ts +9 -2
  170. package/src/browser.ts +85 -2
  171. package/src/client/client.ts +83 -1
  172. package/src/events/publisher.ts +75 -11
  173. package/src/events/rows.ts +48 -0
  174. package/src/index.ts +1 -1
  175. package/src/node.ts +21 -0
  176. package/src/procedures/capability.ts +21 -0
  177. package/src/procedures/collection/changes.ts +245 -0
  178. package/src/procedures/collection/index.ts +57 -1
  179. package/src/procedures/collection/partition.ts +123 -0
  180. package/src/procedures/collection/procedures.ts +206 -51
  181. package/src/procedures/collection/query.ts +1009 -0
  182. package/src/procedures/collection/schemas.ts +196 -7
  183. package/src/procedures/collection/storage.ts +78 -6
  184. package/src/procedures/elicit.ts +320 -0
  185. package/src/procedures/hints.ts +91 -0
  186. package/src/procedures/index.ts +83 -2
  187. package/src/procedures/invoke.ts +33 -2
  188. package/src/procedures/progress.ts +25 -0
  189. package/src/procedures/registry.ts +4 -0
  190. package/src/procedures/types.ts +65 -0
  191. package/src/procedures/untrusted.ts +50 -0
  192. package/src/server/browser-guard.ts +48 -6
  193. package/src/server/host-policy.ts +13 -1
  194. package/src/server/index.ts +3 -0
  195. package/src/server/procedure-server.ts +147 -26
  196. package/src/server/request-auth.ts +128 -6
  197. package/src/server/types.ts +82 -5
@@ -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,10 @@ 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";
62
+ import { partitionOfCall, storageOfCall, withPartition, type PartitionInput } from "./partition.js";
53
63
 
54
64
  // =============================================================================
55
65
  // Handler Factory
@@ -66,15 +76,91 @@ function getCollectionFromPath(path: string[]): string {
66
76
  return path[1]!;
67
77
  }
68
78
 
79
+ /** The options of a query from the input of `find`. */
80
+ function queryOptionsOf(input: FindInput, policy: StorageQueryPolicy): StorageQueryOptions {
81
+ // A find without a limit gets the maxLimit of the policy (roadmap CLI-14)
82
+ const limit = input.limit ?? (Number.isFinite(policy.maxLimit) ? policy.maxLimit : undefined);
83
+ return {
84
+ ...(input.sort !== undefined && { sort: input.sort }),
85
+ ...(input.skip !== undefined && { skip: input.skip }),
86
+ ...(limit !== undefined && { limit }),
87
+ };
88
+ }
89
+
90
+ /** The options of the change events of the collection procedures (roadmap CLI-15). */
91
+ export interface CollectionChangeEventsOptions {
92
+ /**
93
+ * The topic of the change events of a call, or undefined for no event. It gets the partition of
94
+ * the call (roadmap CLI-17).
95
+ * @default `collectionChangeTopic(name, partition)`: `collections.<name>`, or `collections.<name>.<partition>`
96
+ */
97
+ topic?: ((collection: string, ctx: ProcedureContext, partition: string | undefined) => string | undefined) | undefined;
98
+ /**
99
+ * True: the events also carry the values of the changed items. Each row `{ id, value }` passes
100
+ * the output filter of the host (`admitItem`) for each subscriber, and a refused row stays out of
101
+ * that event: its id goes to `withheld`. Without it, the events carry only the ids and the
102
+ * versions, and a subscriber reads the values with `changes`.
103
+ * @default false
104
+ */
105
+ values?: boolean | undefined;
106
+ }
107
+
108
+ /** The options of the collection procedures. */
109
+ export interface CollectionProceduresOptions {
110
+ /**
111
+ * The query policy of `find`: the operators, and the limits of a selector and of the result. A
112
+ * selector or a limit that the policy does not permit is a `VALIDATION_ERROR`. The policy also
113
+ * goes to `query()` of the storage. `$regex` is off unless the policy has it in `operators`.
114
+ * @default DEFAULT_QUERY_POLICY, with `maxLimit` DEFAULT_FIND_LIMIT (1000)
115
+ */
116
+ queryPolicy?: StorageQueryPolicy | undefined;
117
+ /**
118
+ * The change events of the writes (roadmap CLI-15). Each write publishes a
119
+ * `CollectionChangeEvent` on the bus of its context, while a subscriber listens to the topic.
120
+ * The event has the ids and the versions, and the values only with `values`. False: the
121
+ * procedures publish no event.
122
+ * @default true, with the topic `collections.<name>` and no values
123
+ */
124
+ changeEvents?: boolean | CollectionChangeEventsOptions | undefined;
125
+ }
126
+
127
+ /** The write options of an input with an origin. */
128
+ function writeOptionsOf(input: { origin?: string }): StorageWriteOptions | undefined {
129
+ return input.origin === undefined ? undefined : { origin: input.origin };
130
+ }
131
+
69
132
  /**
70
- * Get storage from context, throwing if repository is not available.
133
+ * The patches of a storage without `patch()` that run, for each id. A patch of an id waits for the
134
+ * patch before it, so two patches of one process keep each field. A write of another writer
135
+ * between the read and the write of a patch is lost: give the storage a `patch()` for one step.
71
136
  */
72
- function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureContext["repository"]>["getStorage"]> {
73
- if (!ctx.repository) {
74
- throw new Error("Repository not available in procedure context");
137
+ const PATCHES = new WeakMap<object, Map<string, Promise<unknown>>>();
138
+
139
+ /** This function patches an item of a storage without `patch()`: a read, a merge and a write. */
140
+ function patchInParts(storage: CollectionStorage<unknown>, id: string, fields: StoragePatch, options?: StorageWriteOptions): Promise<unknown> {
141
+ let running = PATCHES.get(storage);
142
+ if (!running) {
143
+ running = new Map();
144
+ PATCHES.set(storage, running);
75
145
  }
76
- const collection = getCollectionFromPath(ctx.path);
77
- return ctx.repository.getStorage<T>(collection);
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;
78
164
  }
79
165
 
80
166
  // =============================================================================
@@ -85,6 +171,7 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
85
171
  * Create collection procedures for a specific collection name.
86
172
  *
87
173
  * @param collectionName - Name of the collection
174
+ * @param options - The query policy of `find`
88
175
  * @returns Array of procedure definitions
89
176
  *
90
177
  * @example
@@ -93,24 +180,44 @@ function getStorage<T>(ctx: ProcedureContext): ReturnType<NonNullable<ProcedureC
93
180
  * registry.registerAll(userProcedures);
94
181
  * ```
95
182
  */
96
- export function createCollectionProcedures(collectionName: string): AnyProcedure[] {
183
+ export function createCollectionProcedures(collectionName: string, options: CollectionProceduresOptions = {}): AnyProcedure[] {
97
184
  const basePath = ["collections", collectionName];
185
+ // A find of a caller gets a limit: DEFAULT_FIND_LIMIT, unless the policy gives its own maxLimit
186
+ const queryPolicy: StorageQueryPolicy = { ...options.queryPolicy, maxLimit: options.queryPolicy?.maxLimit ?? DEFAULT_FIND_LIMIT };
98
187
  // Each collection is a resource of its own (CAPABILITIES-2026-10): a capability can permit one
99
188
  const resource = `collection:${collectionName}`;
189
+ // The topic of the change events of a call (roadmap CLI-15), with its partition (roadmap CLI-17)
190
+ const events = options.changeEvents ?? true;
191
+ const topicRule: ((collection: string, ctx: ProcedureContext, partition: string | undefined) => string | undefined) | undefined =
192
+ events === false ? undefined : (events !== true && events.topic) || ((name: string, _ctx: ProcedureContext, partition: string | undefined) => collectionChangeTopic(name, partition));
193
+ const topicOf = (ctx: ProcedureContext, partition: string | undefined): string | undefined => topicRule?.(collectionName, ctx, partition);
194
+ const withValues = events !== false && events !== true && events.values === true;
195
+ /** A write, and then its change event on the bus of the context. */
196
+ 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);
198
+ /**
199
+ * 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.
201
+ */
202
+ const storageOf = async (input: PartitionInput, ctx: ProcedureContext): Promise<{ storage: CollectionStorage<unknown>; partition: string | undefined }> => {
203
+ const collection = getCollectionFromPath(ctx.path);
204
+ const partition = await partitionOfCall(ctx, collection, input.partition);
205
+ return { storage: storageOfCall<unknown>(ctx, collection, partition), partition };
206
+ };
100
207
 
101
208
  return [
102
209
  // Get
103
210
  defineProcedure({
104
211
  path: [...basePath, "get"],
105
- input: getInputSchema,
212
+ input: withPartition(getInputSchema),
106
213
  output: getOutputSchema,
107
214
  metadata: {
108
215
  effects: { reads: [resource], writes: [] },
109
216
  description: `Get item from ${collectionName} by ID`,
110
217
  tags: ["collections", collectionName, "read"],
111
218
  },
112
- handler: async (input: GetInput, ctx : ProcedureContext) => {
113
- const storage = getStorage(ctx);
219
+ handler: async (input: GetInput & PartitionInput, ctx: ProcedureContext) => {
220
+ const { storage } = await storageOf(input, ctx);
114
221
  const value = await storage.get(input.id);
115
222
  if (input.found !== true) return value;
116
223
  // The output with `found` keeps a stored null apart from a missing id, in one call
@@ -122,47 +229,47 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
122
229
  // Set
123
230
  defineProcedure({
124
231
  path: [...basePath, "set"],
125
- input: setInputSchema,
232
+ input: withPartition(setInputSchema),
126
233
  output: setOutputSchema,
127
234
  metadata: {
128
235
  effects: { reads: [resource], writes: [resource], idempotent: true },
129
236
  description: `Set item in ${collectionName}`,
130
237
  tags: ["collections", collectionName, "write"],
131
238
  },
132
- handler: async (input: SetInput, ctx: ProcedureContext) => {
133
- const storage = getStorage(ctx);
134
- await storage.set(input.id, input.value);
239
+ 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, () => storage.set(input.id, input.value, writeOptionsOf(input)));
135
242
  },
136
243
  }),
137
244
 
138
245
  // Delete
139
246
  defineProcedure({
140
247
  path: [...basePath, "delete"],
141
- input: deleteInputSchema,
248
+ input: withPartition(deleteInputSchema),
142
249
  output: deleteOutputSchema,
143
250
  metadata: {
144
251
  effects: { reads: [resource], writes: [resource], idempotent: true },
145
252
  description: `Delete item from ${collectionName}`,
146
253
  tags: ["collections", collectionName, "write"],
147
254
  },
148
- handler: async (input: DeleteInput, ctx: ProcedureContext) => {
149
- const storage = getStorage(ctx);
150
- return storage.delete(input.id);
255
+ 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, () => storage.delete(input.id, writeOptionsOf(input)));
151
258
  },
152
259
  }),
153
260
 
154
261
  // Has
155
262
  defineProcedure({
156
263
  path: [...basePath, "has"],
157
- input: hasInputSchema,
264
+ input: withPartition(hasInputSchema),
158
265
  output: hasOutputSchema,
159
266
  metadata: {
160
267
  effects: { reads: [resource], writes: [] },
161
268
  description: `Check if item exists in ${collectionName}`,
162
269
  tags: ["collections", collectionName, "read"],
163
270
  },
164
- handler: async (input: HasInput, ctx: ProcedureContext) => {
165
- const storage = getStorage(ctx);
271
+ handler: async (input: HasInput & PartitionInput, ctx: ProcedureContext) => {
272
+ const { storage } = await storageOf(input, ctx);
166
273
  return storage.has(input.id);
167
274
  },
168
275
  }),
@@ -170,15 +277,15 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
170
277
  // GetAll
171
278
  defineProcedure({
172
279
  path: [...basePath, "getAll"],
173
- input: getAllInputSchema,
280
+ input: withPartition(getAllInputSchema),
174
281
  output: getAllOutputSchema,
175
282
  metadata: {
176
283
  effects: { reads: [resource], writes: [] },
177
284
  description: `Get all items from ${collectionName}`,
178
285
  tags: ["collections", collectionName, "read"],
179
286
  },
180
- handler: async (_input: GetAllInput, ctx: ProcedureContext) => {
181
- const storage = getStorage(ctx);
287
+ handler: async (input: GetAllInput & PartitionInput, ctx: ProcedureContext) => {
288
+ const { storage } = await storageOf(input, ctx);
182
289
  return storage.getAll();
183
290
  },
184
291
  }),
@@ -186,15 +293,15 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
186
293
  // Entries: each item with its id, for a sync (roadmap R2)
187
294
  defineProcedure({
188
295
  path: [...basePath, "entries"],
189
- input: entriesInputSchema,
296
+ input: withPartition(entriesInputSchema),
190
297
  output: entriesOutputSchema,
191
298
  metadata: {
192
299
  effects: { reads: [resource], writes: [] },
193
300
  description: `Get all items of ${collectionName} with their ids`,
194
301
  tags: ["collections", collectionName, "read"],
195
302
  },
196
- handler: async (_input: EntriesInput, ctx: ProcedureContext) => {
197
- const storage = getStorage(ctx);
303
+ handler: async (input: EntriesInput & PartitionInput, ctx: ProcedureContext) => {
304
+ const { storage } = await storageOf(input, ctx);
198
305
  if (!storage.getEntries) {
199
306
  throw new Error(`The storage of ${collectionName} cannot give the ids of its items`);
200
307
  }
@@ -205,15 +312,15 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
205
312
  // Ids: the ids of the items without the values, for a full sync
206
313
  defineProcedure({
207
314
  path: [...basePath, "ids"],
208
- input: idsInputSchema,
315
+ input: withPartition(idsInputSchema),
209
316
  output: idsOutputSchema,
210
317
  metadata: {
211
318
  effects: { reads: [resource], writes: [] },
212
319
  description: `Get the ids of the items of ${collectionName}, without the values`,
213
320
  tags: ["collections", collectionName, "read"],
214
321
  },
215
- handler: async (_input: IdsInput, ctx: ProcedureContext) => {
216
- const storage = getStorage(ctx);
322
+ handler: async (input: IdsInput & PartitionInput, ctx: ProcedureContext) => {
323
+ const { storage } = await storageOf(input, ctx);
217
324
  if (storage.getIds) return storage.getIds();
218
325
  // A storage without getIds() gives its entries: the values stay on the server
219
326
  if (storage.getEntries) return (await storage.getEntries()).map(([id]) => id);
@@ -224,17 +331,20 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
224
331
  // Changes: the items that changed after a version, for a delta sync (roadmap R28)
225
332
  defineProcedure({
226
333
  path: [...basePath, "changes"],
227
- input: changesInputSchema,
334
+ input: withPartition(changesInputSchema),
228
335
  output: changesOutputSchema,
229
336
  metadata: {
230
337
  effects: { reads: [resource], writes: [] },
231
338
  description: `Get the items of ${collectionName} that changed after a version`,
232
339
  tags: ["collections", collectionName, "read"],
233
340
  },
234
- handler: async (input: ChangesInput, ctx: ProcedureContext): Promise<ChangesOutput> => {
235
- const storage = getStorage(ctx);
341
+ handler: async (input: ChangesInput & PartitionInput, ctx: ProcedureContext): Promise<ChangesOutput> => {
342
+ const { storage, partition } = await storageOf(input, ctx);
236
343
  if (storage.getChangesSince) {
237
344
  const changes = await storage.getChangesSince(input.since);
345
+ // The chain of the change events starts at this version: the caller then applies the
346
+ // next event without a read (roadmap CLI-15)
347
+ if (topicRule) startChangeChain(ctx.bus, storage, topicOf(ctx, partition), changes.version);
238
348
  return {
239
349
  version: changes.version,
240
350
  full: changes.full,
@@ -252,18 +362,63 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
252
362
  },
253
363
  }),
254
364
 
365
+ // Find: the items that match a JSON selector, with an order, a skip and a limit (roadmap CLI-14)
366
+ defineProcedure({
367
+ path: [...basePath, "find"],
368
+ input: withPartition(findInputSchemaOf(queryPolicy)),
369
+ output: findOutputSchema,
370
+ metadata: {
371
+ effects: { reads: [resource], writes: [] },
372
+ description: `Find the items of ${collectionName} that match a selector, with their ids, an order, a skip and a limit`,
373
+ tags: ["collections", collectionName, "read", "query"],
374
+ },
375
+ handler: async (input: FindInput & PartitionInput, ctx: ProcedureContext) => {
376
+ const { storage } = await storageOf(input, ctx);
377
+ const selector = input.selector ?? {};
378
+ const query = queryOptionsOf(input, queryPolicy);
379
+ // The storage runs the selector itself when it can: a database does not read each item. It
380
+ // gets the policy of the host, so it permits what the procedure permits.
381
+ if (storage.query) return (await storage.query(selector, query, queryPolicy)).map(([id, value]) => ({ id, value }));
382
+ if (!storage.getEntries) {
383
+ throw new Error(`The storage of ${collectionName} cannot run a selector or give the ids of its items`);
384
+ }
385
+ return queryEntries(await storage.getEntries(), selector, query, queryPolicy).map(([id, value]) => ({ id, value }));
386
+ },
387
+ }),
388
+
389
+ // Patch: merge fields into one item, a JSON Merge Patch (roadmap CLI-14)
390
+ defineProcedure({
391
+ path: [...basePath, "patch"],
392
+ input: withPartition(patchInputSchema),
393
+ output: patchOutputSchema,
394
+ metadata: {
395
+ effects: { reads: [resource], writes: [resource], idempotent: true },
396
+ description: `Merge fields into one item of ${collectionName}: a JSON Merge Patch, or the operations $set and $unset`,
397
+ tags: ["collections", collectionName, "write"],
398
+ },
399
+ handler: async (input: PatchInput & PartitionInput, ctx: ProcedureContext) => {
400
+ const { storage, partition } = await storageOf(input, ctx);
401
+ const write = writeOptionsOf(input);
402
+ const value = await changing(ctx, partition, storage, [input.id], input.origin, () =>
403
+ storage.patch ? storage.patch(input.id, input.fields, write) : patchInParts(storage, input.id, input.fields, write),
404
+ );
405
+ const output: GetFoundOutput = value === undefined ? { found: false } : { found: true, value };
406
+ return output;
407
+ },
408
+ }),
409
+
255
410
  // Size
256
411
  defineProcedure({
257
412
  path: [...basePath, "size"],
258
- input: sizeInputSchema,
413
+ input: withPartition(sizeInputSchema),
259
414
  output: sizeOutputSchema,
260
415
  metadata: {
261
416
  effects: { reads: [resource], writes: [] },
262
417
  description: `Get number of items in ${collectionName}`,
263
418
  tags: ["collections", collectionName, "read"],
264
419
  },
265
- handler: async (_input: SizeInput, ctx: ProcedureContext) => {
266
- const storage = getStorage(ctx);
420
+ handler: async (input: SizeInput & PartitionInput, ctx: ProcedureContext) => {
421
+ const { storage } = await storageOf(input, ctx);
267
422
  return storage.size();
268
423
  },
269
424
  }),
@@ -271,31 +426,31 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
271
426
  // Clear
272
427
  defineProcedure({
273
428
  path: [...basePath, "clear"],
274
- input: clearInputSchema,
429
+ input: withPartition(clearInputSchema),
275
430
  output: clearOutputSchema,
276
431
  metadata: {
277
432
  effects: { reads: [resource], writes: [resource], idempotent: true },
278
433
  description: `Clear all items from ${collectionName}`,
279
434
  tags: ["collections", collectionName, "write"],
280
435
  },
281
- handler: async (_input: ClearInput, ctx: ProcedureContext) => {
282
- const storage = getStorage(ctx);
283
- await storage.clear();
436
+ handler: async (input: ClearInput & PartitionInput, ctx: ProcedureContext) => {
437
+ const { storage, partition } = await storageOf(input, ctx);
438
+ await changing(ctx, partition, storage, "*", input.origin, () => storage.clear(writeOptionsOf(input)));
284
439
  },
285
440
  }),
286
441
 
287
442
  // GetBatch
288
443
  defineProcedure({
289
444
  path: [...basePath, "getBatch"],
290
- input: getBatchInputSchema,
445
+ input: withPartition(getBatchInputSchema),
291
446
  output: getBatchOutputSchema,
292
447
  metadata: {
293
448
  effects: { reads: [resource], writes: [] },
294
449
  description: `Get multiple items from ${collectionName}`,
295
450
  tags: ["collections", collectionName, "read", "batch"],
296
451
  },
297
- handler: async (input: GetBatchInput, ctx: ProcedureContext) => {
298
- const storage = getStorage(ctx);
452
+ handler: async (input: GetBatchInput & PartitionInput, ctx: ProcedureContext) => {
453
+ const { storage } = await storageOf(input, ctx);
299
454
  const result = await storage.getBatch(input.ids);
300
455
  // Convert Map to plain object
301
456
  return Object.fromEntries(result);
@@ -305,36 +460,36 @@ export function createCollectionProcedures(collectionName: string): AnyProcedure
305
460
  // SetBatch
306
461
  defineProcedure({
307
462
  path: [...basePath, "setBatch"],
308
- input: setBatchInputSchema,
463
+ input: withPartition(setBatchInputSchema),
309
464
  output: setBatchOutputSchema,
310
465
  metadata: {
311
466
  effects: { reads: [resource], writes: [resource], idempotent: true },
312
467
  description: `Set multiple items in ${collectionName}`,
313
468
  tags: ["collections", collectionName, "write", "batch"],
314
469
  },
315
- handler: async (input: SetBatchInput, ctx: ProcedureContext) => {
316
- const storage = getStorage(ctx);
470
+ handler: async (input: SetBatchInput & PartitionInput, ctx: ProcedureContext) => {
471
+ const { storage, partition } = await storageOf(input, ctx);
317
472
  const items: Array<[string, unknown]> = input.items.map((item) => [
318
473
  item.id,
319
474
  item.value,
320
475
  ]);
321
- await storage.setBatch(items);
476
+ await changing(ctx, partition, storage, items.map(([id]) => id), input.origin, () => storage.setBatch(items, writeOptionsOf(input)));
322
477
  },
323
478
  }),
324
479
 
325
480
  // DeleteBatch
326
481
  defineProcedure({
327
482
  path: [...basePath, "deleteBatch"],
328
- input: deleteBatchInputSchema,
483
+ input: withPartition(deleteBatchInputSchema),
329
484
  output: deleteBatchOutputSchema,
330
485
  metadata: {
331
486
  effects: { reads: [resource], writes: [resource], idempotent: true },
332
487
  description: `Delete multiple items from ${collectionName}`,
333
488
  tags: ["collections", collectionName, "write", "batch"],
334
489
  },
335
- handler: async (input: DeleteBatchInput, ctx: ProcedureContext) => {
336
- const storage = getStorage(ctx);
337
- return storage.deleteBatch(input.ids);
490
+ 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, () => storage.deleteBatch(input.ids, writeOptionsOf(input)));
338
493
  },
339
494
  }),
340
495
  ];