@abloatai/transaction 0.51.0 → 0.53.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 (216) hide show
  1. package/dist/ablo.d.ts +1 -1
  2. package/dist/ablo.js +1 -1
  3. package/dist/ai-sdk/updateTool.d.ts +1 -1
  4. package/dist/ai-sdk/updateTool.js +1 -1
  5. package/dist/auth/apiKey.d.ts +0 -6
  6. package/dist/auth/apiKey.d.ts.map +1 -1
  7. package/dist/auth/apiKey.js +13 -60
  8. package/dist/auth/apiKey.js.map +1 -1
  9. package/dist/auth/baseUrl.d.ts +38 -0
  10. package/dist/auth/baseUrl.d.ts.map +1 -0
  11. package/dist/auth/baseUrl.js +126 -0
  12. package/dist/auth/baseUrl.js.map +1 -0
  13. package/dist/auth/capability.d.ts +2 -2
  14. package/dist/auth/capability.js +1 -1
  15. package/dist/auth/index.d.ts +15 -2
  16. package/dist/auth/index.d.ts.map +1 -1
  17. package/dist/auth/index.js +35 -21
  18. package/dist/auth/index.js.map +1 -1
  19. package/dist/auth/schemas.d.ts +24 -1
  20. package/dist/auth/schemas.d.ts.map +1 -1
  21. package/dist/auth/schemas.js +8 -2
  22. package/dist/auth/schemas.js.map +1 -1
  23. package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
  24. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
  25. package/dist/auth/sessionIssuanceProtocol.js +48 -0
  26. package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
  27. package/dist/auth/sessionMint.js +2 -2
  28. package/dist/branches.d.ts +34 -33
  29. package/dist/branches.d.ts.map +1 -1
  30. package/dist/branches.js +2 -4
  31. package/dist/branches.js.map +1 -1
  32. package/dist/coordination/schema.d.ts +3 -2
  33. package/dist/coordination/schema.d.ts.map +1 -1
  34. package/dist/coordination/schema.js +7 -2
  35. package/dist/coordination/schema.js.map +1 -1
  36. package/dist/errorCodes.d.ts +7 -3
  37. package/dist/errorCodes.d.ts.map +1 -1
  38. package/dist/errorCodes.js +8 -4
  39. package/dist/errorCodes.js.map +1 -1
  40. package/dist/errors.d.ts +1 -1
  41. package/dist/errors.js +1 -1
  42. package/dist/index.d.ts +2 -0
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js.map +1 -1
  45. package/dist/resources/httpResources.d.ts +35 -2
  46. package/dist/resources/httpResources.d.ts.map +1 -1
  47. package/dist/resources/httpResources.js +10 -1
  48. package/dist/resources/httpResources.js.map +1 -1
  49. package/dist/resources/modelOperations.d.ts +18 -6
  50. package/dist/resources/modelOperations.d.ts.map +1 -1
  51. package/dist/resources/modelOperations.js.map +1 -1
  52. package/dist/resources/where.d.ts +67 -2
  53. package/dist/resources/where.d.ts.map +1 -1
  54. package/dist/resources/where.js +73 -0
  55. package/dist/resources/where.js.map +1 -1
  56. package/dist/schema/ddl.d.ts.map +1 -1
  57. package/dist/schema/ddl.js +9 -12
  58. package/dist/schema/ddl.js.map +1 -1
  59. package/dist/schema/field.d.ts +3 -3
  60. package/dist/schema/field.js +2 -2
  61. package/dist/schema/fieldRef.d.ts +1 -1
  62. package/dist/schema/generate.d.ts +2 -3
  63. package/dist/schema/generate.d.ts.map +1 -1
  64. package/dist/schema/generate.js +2 -8
  65. package/dist/schema/generate.js.map +1 -1
  66. package/dist/schema/index.d.ts +2 -2
  67. package/dist/schema/index.js +3 -3
  68. package/dist/schema/model.d.ts +7 -7
  69. package/dist/schema/model.js +2 -2
  70. package/dist/schema/openapi.d.ts.map +1 -1
  71. package/dist/schema/openapi.js +63 -21
  72. package/dist/schema/openapi.js.map +1 -1
  73. package/dist/schema/queries.d.ts +1 -1
  74. package/dist/schema/queries.js +1 -1
  75. package/dist/schema/relation.d.ts +8 -8
  76. package/dist/schema/relation.js +5 -5
  77. package/dist/schema/schema.d.ts +24 -37
  78. package/dist/schema/schema.d.ts.map +1 -1
  79. package/dist/schema/schema.js +15 -28
  80. package/dist/schema/schema.js.map +1 -1
  81. package/dist/schema/sugar.d.ts +7 -7
  82. package/dist/schema/sugar.js +5 -5
  83. package/dist/server/adapter.d.ts +2 -0
  84. package/dist/server/adapter.d.ts.map +1 -1
  85. package/dist/server/commit.d.ts +5 -1
  86. package/dist/server/commit.d.ts.map +1 -1
  87. package/dist/server/index.d.ts +3 -2
  88. package/dist/server/index.d.ts.map +1 -1
  89. package/dist/server/index.js.map +1 -1
  90. package/dist/server/readConfig.d.ts +109 -57
  91. package/dist/server/readConfig.d.ts.map +1 -1
  92. package/dist/server/readConfig.js +19 -5
  93. package/dist/server/readConfig.js.map +1 -1
  94. package/dist/source/adapters/drizzle.d.ts.map +1 -1
  95. package/dist/source/adapters/drizzle.js +5 -0
  96. package/dist/source/adapters/drizzle.js.map +1 -1
  97. package/dist/source/adapters/kysely.d.ts.map +1 -1
  98. package/dist/source/adapters/kysely.js +33 -8
  99. package/dist/source/adapters/kysely.js.map +1 -1
  100. package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
  101. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  102. package/dist/source/adapters/kyselyMutationCore.js +58 -20
  103. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  104. package/dist/source/adapters/memory.d.ts.map +1 -1
  105. package/dist/source/adapters/memory.js +5 -0
  106. package/dist/source/adapters/memory.js.map +1 -1
  107. package/dist/source/adapters/prisma.d.ts.map +1 -1
  108. package/dist/source/adapters/prisma.js +6 -1
  109. package/dist/source/adapters/prisma.js.map +1 -1
  110. package/dist/source/conformance.js +21 -21
  111. package/dist/source/contract.d.ts +17 -0
  112. package/dist/source/contract.d.ts.map +1 -1
  113. package/dist/source/contract.js +31 -0
  114. package/dist/source/contract.js.map +1 -1
  115. package/dist/source/index.d.ts +1 -1
  116. package/dist/source/index.d.ts.map +1 -1
  117. package/dist/source/index.js +1 -1
  118. package/dist/source/index.js.map +1 -1
  119. package/dist/source/types.d.ts +1 -0
  120. package/dist/source/types.d.ts.map +1 -1
  121. package/dist/source/types.js.map +1 -1
  122. package/dist/transactionLayer.d.ts +3 -3
  123. package/dist/transactionLayer.js +3 -3
  124. package/dist/transport/httpClient.d.ts +13 -4
  125. package/dist/transport/httpClient.d.ts.map +1 -1
  126. package/dist/transport/httpClient.js +4 -2
  127. package/dist/transport/httpClient.js.map +1 -1
  128. package/dist/transport/httpTransport.d.ts +1 -0
  129. package/dist/transport/httpTransport.d.ts.map +1 -1
  130. package/dist/transport/httpTransport.js +79 -11
  131. package/dist/transport/httpTransport.js.map +1 -1
  132. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  133. package/dist/transport/wsFrameHandlers.js +3 -0
  134. package/dist/transport/wsFrameHandlers.js.map +1 -1
  135. package/dist/types/streams.d.ts +2 -2
  136. package/dist/wire/accountResponses.d.ts +18 -13
  137. package/dist/wire/accountResponses.d.ts.map +1 -1
  138. package/dist/wire/accountResponses.js +5 -11
  139. package/dist/wire/accountResponses.js.map +1 -1
  140. package/dist/wire/claims.d.ts +1 -0
  141. package/dist/wire/claims.d.ts.map +1 -1
  142. package/dist/wire/claims.js +7 -1
  143. package/dist/wire/claims.js.map +1 -1
  144. package/dist/wire/commit.d.ts +144 -0
  145. package/dist/wire/commit.d.ts.map +1 -1
  146. package/dist/wire/commit.js +22 -1
  147. package/dist/wire/commit.js.map +1 -1
  148. package/dist/wire/frames.d.ts +4 -0
  149. package/dist/wire/frames.d.ts.map +1 -1
  150. package/dist/wire/inboundFrames.d.ts +22 -0
  151. package/dist/wire/inboundFrames.d.ts.map +1 -1
  152. package/dist/wire/index.d.ts +3 -3
  153. package/dist/wire/index.d.ts.map +1 -1
  154. package/dist/wire/index.js +2 -2
  155. package/dist/wire/index.js.map +1 -1
  156. package/dist/wire/listEnvelope.d.ts +20 -0
  157. package/dist/wire/listEnvelope.d.ts.map +1 -1
  158. package/dist/wire/listEnvelope.js +20 -0
  159. package/dist/wire/listEnvelope.js.map +1 -1
  160. package/dist/wire/protocol.d.ts +1 -1
  161. package/dist/wire/protocol.js +1 -1
  162. package/package.json +1 -1
  163. package/src/ablo.ts +1 -1
  164. package/src/ai-sdk/updateTool.ts +1 -1
  165. package/src/auth/apiKey.ts +13 -61
  166. package/src/auth/baseUrl.ts +144 -0
  167. package/src/auth/capability.ts +3 -3
  168. package/src/auth/index.ts +62 -21
  169. package/src/auth/schemas.ts +16 -2
  170. package/src/auth/sessionIssuanceProtocol.ts +57 -0
  171. package/src/auth/sessionMint.ts +2 -2
  172. package/src/branches.ts +2 -4
  173. package/src/coordination/schema.ts +7 -2
  174. package/src/errorCodes.ts +29 -5
  175. package/src/errors.ts +1 -1
  176. package/src/index.ts +2 -0
  177. package/src/resources/httpResources.ts +45 -2
  178. package/src/resources/modelOperations.ts +18 -6
  179. package/src/resources/where.ts +70 -4
  180. package/src/schema/ddl.ts +9 -13
  181. package/src/schema/field.ts +3 -3
  182. package/src/schema/fieldRef.ts +1 -1
  183. package/src/schema/generate.ts +2 -8
  184. package/src/schema/index.ts +3 -3
  185. package/src/schema/model.ts +7 -7
  186. package/src/schema/openapi.ts +64 -21
  187. package/src/schema/queries.ts +1 -1
  188. package/src/schema/relation.ts +8 -8
  189. package/src/schema/residency.ts +0 -1
  190. package/src/schema/schema.ts +33 -58
  191. package/src/schema/sugar.ts +7 -7
  192. package/src/server/adapter.ts +2 -0
  193. package/src/server/commit.ts +5 -1
  194. package/src/server/index.ts +8 -2
  195. package/src/server/readConfig.ts +111 -57
  196. package/src/source/adapters/drizzle.ts +5 -0
  197. package/src/source/adapters/kysely.ts +48 -10
  198. package/src/source/adapters/kyselyMutationCore.ts +69 -22
  199. package/src/source/adapters/memory.ts +5 -0
  200. package/src/source/adapters/prisma.ts +6 -1
  201. package/src/source/conformance.ts +21 -21
  202. package/src/source/contract.ts +34 -0
  203. package/src/source/index.ts +2 -0
  204. package/src/source/types.ts +1 -0
  205. package/src/transactionLayer.ts +3 -3
  206. package/src/transport/httpClient.ts +23 -6
  207. package/src/transport/httpTransport.ts +88 -11
  208. package/src/transport/wsFrameHandlers.ts +3 -0
  209. package/src/types/global.ts +1 -1
  210. package/src/types/streams.ts +2 -2
  211. package/src/wire/accountResponses.ts +5 -11
  212. package/src/wire/claims.ts +7 -1
  213. package/src/wire/commit.ts +31 -1
  214. package/src/wire/index.ts +10 -1
  215. package/src/wire/listEnvelope.ts +23 -0
  216. package/src/wire/protocol.ts +1 -1
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Reusable Kysely row-mutation core.
3
3
  *
4
- * This is the single schema-field ↔ physical-column translation used by both
4
+ * This is the single schema-field ↔ database-column translation used by both
5
5
  * Kysely transports. It deliberately knows nothing about outboxes, logical
6
6
  * markers, or idempotency: wrappers compose those transaction policies around
7
7
  * the same `applyOperation` implementation, so direct and endpoint DML cannot
@@ -133,6 +133,11 @@ export function kyselyOperationRowId(operation: Operation): string {
133
133
  return id;
134
134
  }
135
135
 
136
+ function suppliedOperationRowId(operation: Operation): string | undefined {
137
+ const id = operation.id ?? operation.input?.id;
138
+ return typeof id === 'string' && id.length > 0 ? id : undefined;
139
+ }
140
+
136
141
  /** The transport-independent Kysely field/column mutation boundary. */
137
142
  export interface KyselyMutationCore {
138
143
  read(request: AdapterReadRequest): Promise<readonly Row[]>;
@@ -159,16 +164,23 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
159
164
  columns.fieldToColumn.get(field) ?? camelToSnake(field);
160
165
  const fieldFor = (columns: ModelColumns, column: string): string =>
161
166
  columns.columnToField.get(column) ?? snakeToCamel(column);
162
-
163
167
  const toColumns = (columns: ModelColumns, row: Row): Row => {
164
168
  const out: Row = {};
165
- for (const key of Object.keys(row)) out[columnFor(columns, key)] = row[key];
169
+ for (const key of Object.keys(row)) {
170
+ out[columnFor(columns, key)] = row[key];
171
+ }
166
172
  return out;
167
173
  };
168
174
 
169
175
  const toFields = (columns: ModelColumns, row: Row): Row => {
170
176
  const out: Row = {};
171
- for (const key of Object.keys(row)) out[fieldFor(columns, key)] = row[key];
177
+ for (const key of Object.keys(row)) {
178
+ const field = fieldFor(columns, key);
179
+ const value = row[key];
180
+ out[field] = field === 'id' && value !== null && value !== undefined
181
+ ? String(value)
182
+ : value;
183
+ }
172
184
  return out;
173
185
  };
174
186
 
@@ -179,7 +191,7 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
179
191
  const rows = await db
180
192
  .selectFrom(columns.table)
181
193
  .selectAll()
182
- .where('id', '=', request.id)
194
+ .where(columnFor(columns, 'id'), '=', request.id)
183
195
  .limit(1)
184
196
  .execute();
185
197
  return rows.map((row) => toFields(columns, row));
@@ -194,13 +206,53 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
194
206
 
195
207
  async applyOperation(transaction, operation): Promise<Row> {
196
208
  const columns = modelColumns(operation.model);
197
- const id = kyselyOperationRowId(operation);
198
209
  const input = operation.input ?? {};
210
+ if (operation.where && operation.type !== 'UPDATE') {
211
+ throw new AbloValidationError('where is supported only for UPDATE operations', {
212
+ code: 'commit_operation_invalid',
213
+ param: 'where',
214
+ });
215
+ }
216
+
217
+ if (operation.type === 'CREATE') {
218
+ const suppliedId = suppliedOperationRowId(operation);
219
+ const createInput = operation.id == null
220
+ ? Object.fromEntries(Object.entries(input).filter(([field]) => field !== 'id'))
221
+ : input;
222
+ const inserted = await transaction
223
+ .insertInto(columns.table)
224
+ .values(toColumns(columns, {
225
+ ...createInput,
226
+ ...(operation.id == null
227
+ ? {}
228
+ : suppliedId
229
+ ? { id: suppliedId }
230
+ : {}),
231
+ }))
232
+ .returningAll()
233
+ .execute();
234
+ if (!inserted[0]) {
235
+ throw new AbloValidationError(
236
+ `${operation.type} on "${operation.model}" returned no source row`,
237
+ { code: 'source_adapter_misconfigured' },
238
+ );
239
+ }
240
+ const row = toFields(columns, inserted[0]);
241
+ if (typeof row.id !== 'string' || row.id.length === 0) {
242
+ throw new AbloValidationError(
243
+ `${operation.type} on "${operation.model}" did not return a canonical id`,
244
+ { code: 'source_adapter_misconfigured' },
245
+ );
246
+ }
247
+ return row;
248
+ }
249
+
250
+ const id = kyselyOperationRowId(operation);
199
251
 
200
252
  if (operation.type === 'DELETE') {
201
253
  const deleted = await transaction
202
254
  .deleteFrom(columns.table)
203
- .where('id', '=', id)
255
+ .where(columnFor(columns, 'id'), '=', id)
204
256
  .returningAll()
205
257
  .execute();
206
258
  if (!deleted[0]) {
@@ -212,30 +264,25 @@ export function createKyselyMutationCore<S extends SchemaRecord>(
212
264
  return toFields(columns, deleted[0]);
213
265
  }
214
266
 
215
- if (operation.type === 'CREATE') {
216
- const inserted = await transaction
217
- .insertInto(columns.table)
218
- .values(toColumns(columns, { id, ...input }))
219
- .returningAll()
220
- .execute();
221
- return inserted[0] ? toFields(columns, inserted[0]) : { id, ...input };
222
- }
223
-
224
267
  const patch = toColumns(columns, {
225
268
  ...input,
226
269
  ...(operation.type === 'ARCHIVE' ? { archivedAt: new Date() } : {}),
227
270
  ...(operation.type === 'UNARCHIVE' ? { archivedAt: null } : {}),
228
271
  });
229
- const updated = await transaction
272
+ let update = transaction
230
273
  .updateTable(columns.table)
231
274
  .set(patch)
232
- .where('id', '=', id)
233
- .returningAll()
234
- .execute();
275
+ .where(columnFor(columns, 'id'), '=', id);
276
+ for (const [column, value] of Object.entries(toColumns(columns, operation.where ?? {}))) {
277
+ update = update.where(column, '=', value);
278
+ }
279
+ const updated = await update.returningAll().execute();
235
280
  if (!updated[0]) {
236
281
  throw new AbloValidationError(
237
- `${operation.type} on "${operation.model}/${id}" matched no source row`,
238
- { code: 'mutate_update_entity_not_found' },
282
+ operation.where
283
+ ? `The conditional ${operation.type} on "${operation.model}/${id}" did not match`
284
+ : `${operation.type} on "${operation.model}/${id}" matched no source row`,
285
+ { code: operation.where ? 'precondition_failed' : 'mutate_update_entity_not_found' },
239
286
  );
240
287
  }
241
288
  return toFields(columns, updated[0]);
@@ -54,6 +54,11 @@ export function memoryDataSource(): DataSourceAdapter {
54
54
  };
55
55
 
56
56
  const applyOperation = (op: Operation): Row => {
57
+ if (op.where) {
58
+ throw new AbloValidationError('The memory adapter does not support conditional operations', {
59
+ code: 'source_adapter_misconfigured',
60
+ });
61
+ }
57
62
  const m = modelStore(op.model);
58
63
  const id = rowId(op);
59
64
  switch (op.type) {
@@ -80,7 +80,7 @@ const lowerFirst = (s: string): string => (s ? s.charAt(0).toLowerCase() + s.sli
80
80
  * the adapter, and it reflects a real limit of the type system rather than a
81
81
  * shortcut. Writes inside `prisma.$transaction(tx => …)` must go through the
82
82
  * transactional client `tx`, and the model is known only as a runtime string.
83
- * Prisma keys its client by fixed property names (`{ task: TaskDelegate; … }`), so
83
+ * Prisma keys its client by fixed property names (`{ item: ItemDelegate; … }`), so
84
84
  * a dynamic `tx[name]` lookup is `unknown` to the compiler: there is no static key
85
85
  * to infer from a string. The cast is checked at runtime immediately afterward by
86
86
  * confirming that `findMany` is a function on the resolved delegate.
@@ -149,6 +149,11 @@ export function prismaDataSource<S extends SchemaRecord>(
149
149
  void schema; // held for typed reads and model validation
150
150
 
151
151
  const applyOperation = async (tx: PrismaRaw, op: Operation): Promise<Row> => {
152
+ if (op.where) {
153
+ throw new AbloValidationError('The Prisma adapter does not support conditional operations', {
154
+ code: 'source_adapter_misconfigured',
155
+ });
156
+ }
152
157
  const delegate = delegateFor(tx, delegateName(op.model));
153
158
  const id = rowId(op);
154
159
  switch (op.type) {
@@ -75,7 +75,7 @@ export function mutationConformanceChecks(
75
75
  const adapter = await make();
76
76
  const result = await adapter.commit(
77
77
  request('corr_create', [
78
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
78
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
79
79
  ]),
80
80
  );
81
81
  assert.equal(result.rows.length, 1, 'one row returned');
@@ -90,12 +90,12 @@ export function mutationConformanceChecks(
90
90
  run: async () => {
91
91
  const adapter = await make();
92
92
  await adapter.commit(request('corr_load', [
93
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
93
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
94
94
  ]));
95
- const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
95
+ const found = await adapter.read({ kind: 'load', model: 'item', id: 't1' });
96
96
  assert.equal(found.length, 1);
97
97
  assert.equal(found[0]?.title, 'A');
98
- const missing = await adapter.read({ kind: 'load', model: 'task', id: 'nope' });
98
+ const missing = await adapter.read({ kind: 'load', model: 'item', id: 'nope' });
99
99
  assert.equal(missing.length, 0, 'unknown id reads empty');
100
100
  },
101
101
  },
@@ -104,10 +104,10 @@ export function mutationConformanceChecks(
104
104
  run: async () => {
105
105
  const adapter = await make();
106
106
  await adapter.commit(request('corr_list', [
107
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
108
- { type: 'CREATE', model: 'task', id: 't2', input: { title: 'B' } },
107
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
108
+ { type: 'CREATE', model: 'item', id: 't2', input: { title: 'B' } },
109
109
  ]));
110
- const rows = await adapter.read({ kind: 'list', model: 'task' });
110
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
111
111
  assert.deepEqual(rows.map((row) => row.id).sort(), ['t1', 't2']);
112
112
  },
113
113
  },
@@ -116,12 +116,12 @@ export function mutationConformanceChecks(
116
116
  run: async () => {
117
117
  const adapter = await make();
118
118
  const commitRequest = request('corr_replay', [
119
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A', n: 1 } },
119
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A', n: 1 } },
120
120
  ]);
121
121
  const first = await adapter.commit(commitRequest);
122
122
  const second = await adapter.commit(commitRequest);
123
123
  assert.deepEqual(second.rows, first.rows, 'replay returns the original rows');
124
- const rows = await adapter.read({ kind: 'list', model: 'task' });
124
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
125
125
  assert.equal(rows.length, 1, 'no duplicate row');
126
126
  },
127
127
  },
@@ -131,13 +131,13 @@ export function mutationConformanceChecks(
131
131
  const adapter = await make();
132
132
  await adapter.commit(request(
133
133
  'corr_conflict',
134
- [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }],
134
+ [{ type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } }],
135
135
  'a'.repeat(64),
136
136
  ));
137
137
  await assert.rejects(
138
138
  adapter.commit(request(
139
139
  'corr_conflict',
140
- [{ type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } }],
140
+ [{ type: 'UPDATE', model: 'item', id: 't1', input: { title: 'B' } }],
141
141
  'b'.repeat(64),
142
142
  )),
143
143
  (error: unknown) =>
@@ -153,14 +153,14 @@ export function mutationConformanceChecks(
153
153
  run: async () => {
154
154
  const adapter = await make();
155
155
  const commitRequest = request('corr_concurrent', [
156
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
156
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
157
157
  ]);
158
158
  const [first, second] = await Promise.all([
159
159
  adapter.commit(commitRequest),
160
160
  adapter.commit(commitRequest),
161
161
  ]);
162
162
  assert.deepEqual(second.rows, first.rows);
163
- const rows = await adapter.read({ kind: 'list', model: 'task' });
163
+ const rows = await adapter.read({ kind: 'list', model: 'item' });
164
164
  assert.equal(rows.length, 1, 'concurrent retries produced one row');
165
165
  },
166
166
  },
@@ -169,12 +169,12 @@ export function mutationConformanceChecks(
169
169
  run: async () => {
170
170
  const adapter = await make();
171
171
  await adapter.commit(request('corr_create_2', [
172
- { type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
172
+ { type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
173
173
  ]));
174
174
  await adapter.commit(request('corr_update_2', [
175
- { type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } },
175
+ { type: 'UPDATE', model: 'item', id: 't1', input: { title: 'B' } },
176
176
  ]));
177
- const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
177
+ const found = await adapter.read({ kind: 'load', model: 'item', id: 't1' });
178
178
  assert.equal(found[0]?.title, 'B', 'update applied');
179
179
  },
180
180
  },
@@ -200,7 +200,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
200
200
  const request = change('corr_outbox_replay', [
201
201
  {
202
202
  type: 'CREATE',
203
- model: 'task',
203
+ model: 'item',
204
204
  id: 't1',
205
205
  input: { title: 'A' },
206
206
  transactionId: 'op_outbox_replay',
@@ -223,7 +223,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
223
223
  await adapter.commit(change('corr_event', [
224
224
  {
225
225
  type: 'CREATE',
226
- model: 'task',
226
+ model: 'item',
227
227
  id: 't1',
228
228
  input: { title: 'A' },
229
229
  transactionId: 'op_event',
@@ -232,7 +232,7 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
232
232
  const page = await adapter.events(null, 100);
233
233
  const event = page.events.find((candidate) => candidate.entityId === 't1');
234
234
  assert.ok(event, 'event for the committed row');
235
- assert.equal(event.model, 'task');
235
+ assert.equal(event.model, 'item');
236
236
  assert.equal(event.type, 'CREATE');
237
237
  assert.equal(event.correlationId, 'corr_event');
238
238
  assert.equal(event.transactionId, 'op_event');
@@ -244,10 +244,10 @@ export function endpointConformanceChecks(make: MakeAdapter): ConformanceCheck[]
244
244
  run: async () => {
245
245
  const adapter = await make();
246
246
  await adapter.commit(change('corr_page_a', [
247
- { type: 'CREATE', model: 'task', id: 't1', input: {} },
247
+ { type: 'CREATE', model: 'item', id: 't1', input: {} },
248
248
  ]));
249
249
  await adapter.commit(change('corr_page_b', [
250
- { type: 'CREATE', model: 'task', id: 't2', input: {} },
250
+ { type: 'CREATE', model: 'item', id: 't2', input: {} },
251
251
  ]));
252
252
 
253
253
  const first = await adapter.events(null, 1);
@@ -79,6 +79,40 @@ export const sourceCommitEchoOperationSchema = z.strictObject({
79
79
  transactionId: z.string().min(1).max(ABLO_SOURCE_CLIENT_TX_ID_MAX_LENGTH),
80
80
  });
81
81
 
82
+ /**
83
+ * Pre-write correlation evidence. A database-generated CREATE has no row id
84
+ * until its INSERT returns, so the adapter receives the same operation shape
85
+ * with that one value absent and finalizes it inside the writing transaction.
86
+ */
87
+ const sourceCommitEchoIntentOperationSchema = sourceCommitEchoOperationSchema.extend({
88
+ id: z.string().min(1).nullish(),
89
+ });
90
+
91
+ export const sourceCommitEchoIntentSchema = z
92
+ .strictObject({
93
+ version: z.literal(1),
94
+ correlationId: correlationIdSchema,
95
+ operations: z
96
+ .array(sourceCommitEchoIntentOperationSchema)
97
+ .min(1)
98
+ .max(ABLO_SOURCE_ECHO_MAX_OPERATIONS)
99
+ .readonly(),
100
+ })
101
+ .superRefine(({ operations }, ctx) => {
102
+ const seen = new Set<string>();
103
+ for (const [index, operation] of operations.entries()) {
104
+ if (seen.has(operation.transactionId)) {
105
+ ctx.addIssue({
106
+ code: 'custom',
107
+ path: ['operations', index, 'transactionId'],
108
+ message: 'WAL echo operation transactionIds must be unique',
109
+ });
110
+ }
111
+ seen.add(operation.transactionId);
112
+ }
113
+ });
114
+ export type SourceCommitEchoIntent = z.infer<typeof sourceCommitEchoIntentSchema>;
115
+
82
116
  export const sourceCommitEchoMarkerSchema = z
83
117
  .strictObject({
84
118
  version: z.literal(1),
@@ -129,6 +129,7 @@ export {
129
129
  sourceCommitEchoSchema,
130
130
  sourceCommitEchoOperationSchema,
131
131
  sourceCommitEchoMarkerSchema,
132
+ sourceCommitEchoIntentSchema,
132
133
  changeSetSchema,
133
134
  outboxEventSchema,
134
135
  eventsPageSchema,
@@ -141,6 +142,7 @@ export {
141
142
  type Migration,
142
143
  type AdapterCapabilities,
143
144
  type SourceCommitEchoMarkerWire,
145
+ type SourceCommitEchoIntent,
144
146
  } from './contract.js';
145
147
  export { prismaDataSource, type PrismaLike, type PrismaDataSourceOptions } from './adapters/prisma.js';
146
148
  export {
@@ -105,6 +105,7 @@ export interface SourceOperation {
105
105
  readonly model: string;
106
106
  readonly id?: string | null;
107
107
  readonly input?: Record<string, unknown> | null;
108
+ readonly where?: Record<string, unknown> | null;
108
109
  readonly transactionId?: string | null;
109
110
  readonly readAt?: number | null;
110
111
  readonly onStale?: OnStaleMode | null;
@@ -17,9 +17,9 @@
17
17
  * over a socket, and it joins the seam when its core carrier type exists —
18
18
  * see docs/plans/transaction-core-language.md, Tier 3.
19
19
  *
20
- * This is the contract, not a runtime: nothing in this package implements it
21
- * yet. The first explicit implementation (a headless client, a test harness)
22
- * gains a compile-time conformance pin against this interface.
20
+ * This is the contract, not a runtime. The headless HTTP client implements it
21
+ * as a proxy to the hosted authority; other compositions gain the same
22
+ * compile-time conformance pin against this interface.
23
23
  */
24
24
 
25
25
  import type { CommitMessage } from './wire/frames.js';
@@ -32,10 +32,12 @@ import type {
32
32
  HttpTransportModel,
33
33
  ModelReadOptions,
34
34
  ModelMutationOptions,
35
+ ModelList,
35
36
  CreateSessionParams,
36
37
  AbloSession,
37
38
  SessionResource,
38
39
  } from '../resources/httpResources.js';
40
+ import { modelList } from '../resources/httpResources.js';
39
41
  import type {
40
42
  ModelCreateParams,
41
43
  ModelDeleteParams,
@@ -128,7 +130,12 @@ export interface HttpModelClient<T, C = T> {
128
130
  * Reads the rows matching a filter. Same resolution as `get`, in bulk,
129
131
  * and deduplicated so concurrent identical calls share one request.
130
132
  */
131
- list(options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]>;
133
+ /**
134
+ * The rows are an array as before; `hasMore` and `nextCursor` on the result
135
+ * say whether the collection continues past this page. Pass `nextCursor`
136
+ * back as `cursor`, with the same `where` and `orderBy`, to walk it.
137
+ */
138
+ list(options?: ServerReadOptions<T>): Promise<ModelList<CapturedRow<T>>>;
132
139
  /**
133
140
  * Creates a row and returns it, including any framework-applied defaults.
134
141
  * Passing an id that already exists is idempotent: the existing row is
@@ -166,10 +173,10 @@ export interface HttpModelClient<T, C = T> {
166
173
  * duplicated.
167
174
  *
168
175
  * ```ts
169
- * await ablo.tasks.track({ id: 'task_42' });
176
+ * await ablo.items.track({ id: 'item_42' });
170
177
  * // …minutes of other work later, on the next write…
171
178
  * const res = await ablo.commits.create({ operations: [ … ] });
172
- * res.notifications; // populated if task_42 moved in the meantime
179
+ * res.notifications; // populated if item_42 moved in the meantime
173
180
  * ```
174
181
  *
175
182
  * The returned `notifications` are only the tracks that had ALREADY fired at
@@ -209,6 +216,10 @@ export type AbloHttpClient<S extends SchemaRecord> = {
209
216
  readonly logs: HttpLogsResource;
210
217
  /** Server-confirmed authority of the credential, populated by `ready()`. */
211
218
  readonly identity: EffectiveAuthority | null;
219
+ /**
220
+ * Waits for requests already using this client and commits already scheduled
221
+ * on its commit lane. Stop accepting application work before calling it.
222
+ */
212
223
  dispose(): Promise<void>;
213
224
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
214
225
  getAuthToken(): Promise<string | null>;
@@ -423,10 +434,16 @@ function createHttpModelClient<T, C = T>(
423
434
  return read.data as CapturedRow<T> | undefined;
424
435
  };
425
436
 
426
- const list = async (options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]> => {
437
+ const list = async (
438
+ options?: ServerReadOptions<T>,
439
+ ): Promise<ModelList<CapturedRow<T>>> => {
427
440
  const snapshot = await protocol.list(options);
441
+ const page = modelList<CapturedRow<T>>(
442
+ snapshot.data as readonly CapturedRow<T>[],
443
+ snapshot,
444
+ );
428
445
  const registry = readSetContext?.getStore();
429
- if (!registry) return [...snapshot.data] as CapturedRow<T>[];
446
+ if (!registry) return page;
430
447
  if (!snapshot.evidence) {
431
448
  throw new AbloConnectionError(
432
449
  `${modelName}.list did not return row evidence. Upgrade the Ablo server or use get({ id }).`,
@@ -447,7 +464,7 @@ function createHttpModelClient<T, C = T>(
447
464
  }
448
465
  capturePointRead(readSetContext, clientIdentity, modelName, id, row, stamp);
449
466
  }
450
- return [...snapshot.data] as CapturedRow<T>[];
467
+ return page;
451
468
  };
452
469
 
453
470
  // Claim acquisition performs its authoritative read only after the grant.