@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.
- package/dist/ablo.d.ts +1 -1
- package/dist/ablo.js +1 -1
- package/dist/ai-sdk/updateTool.d.ts +1 -1
- package/dist/ai-sdk/updateTool.js +1 -1
- package/dist/auth/apiKey.d.ts +0 -6
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +13 -60
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/baseUrl.d.ts +38 -0
- package/dist/auth/baseUrl.d.ts.map +1 -0
- package/dist/auth/baseUrl.js +126 -0
- package/dist/auth/baseUrl.js.map +1 -0
- package/dist/auth/capability.d.ts +2 -2
- package/dist/auth/capability.js +1 -1
- package/dist/auth/index.d.ts +15 -2
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +35 -21
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/schemas.d.ts +24 -1
- package/dist/auth/schemas.d.ts.map +1 -1
- package/dist/auth/schemas.js +8 -2
- package/dist/auth/schemas.js.map +1 -1
- package/dist/auth/sessionIssuanceProtocol.d.ts +41 -0
- package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -0
- package/dist/auth/sessionIssuanceProtocol.js +48 -0
- package/dist/auth/sessionIssuanceProtocol.js.map +1 -0
- package/dist/auth/sessionMint.js +2 -2
- package/dist/branches.d.ts +34 -33
- package/dist/branches.d.ts.map +1 -1
- package/dist/branches.js +2 -4
- package/dist/branches.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -2
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -2
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +7 -3
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +8 -4
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.js +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/resources/httpResources.d.ts +35 -2
- package/dist/resources/httpResources.d.ts.map +1 -1
- package/dist/resources/httpResources.js +10 -1
- package/dist/resources/httpResources.js.map +1 -1
- package/dist/resources/modelOperations.d.ts +18 -6
- package/dist/resources/modelOperations.d.ts.map +1 -1
- package/dist/resources/modelOperations.js.map +1 -1
- package/dist/resources/where.d.ts +67 -2
- package/dist/resources/where.d.ts.map +1 -1
- package/dist/resources/where.js +73 -0
- package/dist/resources/where.js.map +1 -1
- package/dist/schema/ddl.d.ts.map +1 -1
- package/dist/schema/ddl.js +9 -12
- package/dist/schema/ddl.js.map +1 -1
- package/dist/schema/field.d.ts +3 -3
- package/dist/schema/field.js +2 -2
- package/dist/schema/fieldRef.d.ts +1 -1
- package/dist/schema/generate.d.ts +2 -3
- package/dist/schema/generate.d.ts.map +1 -1
- package/dist/schema/generate.js +2 -8
- package/dist/schema/generate.js.map +1 -1
- package/dist/schema/index.d.ts +2 -2
- package/dist/schema/index.js +3 -3
- package/dist/schema/model.d.ts +7 -7
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.d.ts.map +1 -1
- package/dist/schema/openapi.js +63 -21
- package/dist/schema/openapi.js.map +1 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -1
- package/dist/schema/relation.d.ts +8 -8
- package/dist/schema/relation.js +5 -5
- package/dist/schema/schema.d.ts +24 -37
- package/dist/schema/schema.d.ts.map +1 -1
- package/dist/schema/schema.js +15 -28
- package/dist/schema/schema.js.map +1 -1
- package/dist/schema/sugar.d.ts +7 -7
- package/dist/schema/sugar.js +5 -5
- package/dist/server/adapter.d.ts +2 -0
- package/dist/server/adapter.d.ts.map +1 -1
- package/dist/server/commit.d.ts +5 -1
- package/dist/server/commit.d.ts.map +1 -1
- package/dist/server/index.d.ts +3 -2
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/readConfig.d.ts +109 -57
- package/dist/server/readConfig.d.ts.map +1 -1
- package/dist/server/readConfig.js +19 -5
- package/dist/server/readConfig.js.map +1 -1
- package/dist/source/adapters/drizzle.d.ts.map +1 -1
- package/dist/source/adapters/drizzle.js +5 -0
- package/dist/source/adapters/drizzle.js.map +1 -1
- package/dist/source/adapters/kysely.d.ts.map +1 -1
- package/dist/source/adapters/kysely.js +33 -8
- package/dist/source/adapters/kysely.js.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts +1 -1
- package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
- package/dist/source/adapters/kyselyMutationCore.js +58 -20
- package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
- package/dist/source/adapters/memory.d.ts.map +1 -1
- package/dist/source/adapters/memory.js +5 -0
- package/dist/source/adapters/memory.js.map +1 -1
- package/dist/source/adapters/prisma.d.ts.map +1 -1
- package/dist/source/adapters/prisma.js +6 -1
- package/dist/source/adapters/prisma.js.map +1 -1
- package/dist/source/conformance.js +21 -21
- package/dist/source/contract.d.ts +17 -0
- package/dist/source/contract.d.ts.map +1 -1
- package/dist/source/contract.js +31 -0
- package/dist/source/contract.js.map +1 -1
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.d.ts.map +1 -1
- package/dist/source/index.js +1 -1
- package/dist/source/index.js.map +1 -1
- package/dist/source/types.d.ts +1 -0
- package/dist/source/types.d.ts.map +1 -1
- package/dist/source/types.js.map +1 -1
- package/dist/transactionLayer.d.ts +3 -3
- package/dist/transactionLayer.js +3 -3
- package/dist/transport/httpClient.d.ts +13 -4
- package/dist/transport/httpClient.d.ts.map +1 -1
- package/dist/transport/httpClient.js +4 -2
- package/dist/transport/httpClient.js.map +1 -1
- package/dist/transport/httpTransport.d.ts +1 -0
- package/dist/transport/httpTransport.d.ts.map +1 -1
- package/dist/transport/httpTransport.js +79 -11
- package/dist/transport/httpTransport.js.map +1 -1
- package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
- package/dist/transport/wsFrameHandlers.js +3 -0
- package/dist/transport/wsFrameHandlers.js.map +1 -1
- package/dist/types/streams.d.ts +2 -2
- package/dist/wire/accountResponses.d.ts +18 -13
- package/dist/wire/accountResponses.d.ts.map +1 -1
- package/dist/wire/accountResponses.js +5 -11
- package/dist/wire/accountResponses.js.map +1 -1
- package/dist/wire/claims.d.ts +1 -0
- package/dist/wire/claims.d.ts.map +1 -1
- package/dist/wire/claims.js +7 -1
- package/dist/wire/claims.js.map +1 -1
- package/dist/wire/commit.d.ts +144 -0
- package/dist/wire/commit.d.ts.map +1 -1
- package/dist/wire/commit.js +22 -1
- package/dist/wire/commit.js.map +1 -1
- package/dist/wire/frames.d.ts +4 -0
- package/dist/wire/frames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.d.ts +22 -0
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/index.d.ts +3 -3
- package/dist/wire/index.d.ts.map +1 -1
- package/dist/wire/index.js +2 -2
- package/dist/wire/index.js.map +1 -1
- package/dist/wire/listEnvelope.d.ts +20 -0
- package/dist/wire/listEnvelope.d.ts.map +1 -1
- package/dist/wire/listEnvelope.js +20 -0
- package/dist/wire/listEnvelope.js.map +1 -1
- package/dist/wire/protocol.d.ts +1 -1
- package/dist/wire/protocol.js +1 -1
- package/package.json +1 -1
- package/src/ablo.ts +1 -1
- package/src/ai-sdk/updateTool.ts +1 -1
- package/src/auth/apiKey.ts +13 -61
- package/src/auth/baseUrl.ts +144 -0
- package/src/auth/capability.ts +3 -3
- package/src/auth/index.ts +62 -21
- package/src/auth/schemas.ts +16 -2
- package/src/auth/sessionIssuanceProtocol.ts +57 -0
- package/src/auth/sessionMint.ts +2 -2
- package/src/branches.ts +2 -4
- package/src/coordination/schema.ts +7 -2
- package/src/errorCodes.ts +29 -5
- package/src/errors.ts +1 -1
- package/src/index.ts +2 -0
- package/src/resources/httpResources.ts +45 -2
- package/src/resources/modelOperations.ts +18 -6
- package/src/resources/where.ts +70 -4
- package/src/schema/ddl.ts +9 -13
- package/src/schema/field.ts +3 -3
- package/src/schema/fieldRef.ts +1 -1
- package/src/schema/generate.ts +2 -8
- package/src/schema/index.ts +3 -3
- package/src/schema/model.ts +7 -7
- package/src/schema/openapi.ts +64 -21
- package/src/schema/queries.ts +1 -1
- package/src/schema/relation.ts +8 -8
- package/src/schema/residency.ts +0 -1
- package/src/schema/schema.ts +33 -58
- package/src/schema/sugar.ts +7 -7
- package/src/server/adapter.ts +2 -0
- package/src/server/commit.ts +5 -1
- package/src/server/index.ts +8 -2
- package/src/server/readConfig.ts +111 -57
- package/src/source/adapters/drizzle.ts +5 -0
- package/src/source/adapters/kysely.ts +48 -10
- package/src/source/adapters/kyselyMutationCore.ts +69 -22
- package/src/source/adapters/memory.ts +5 -0
- package/src/source/adapters/prisma.ts +6 -1
- package/src/source/conformance.ts +21 -21
- package/src/source/contract.ts +34 -0
- package/src/source/index.ts +2 -0
- package/src/source/types.ts +1 -0
- package/src/transactionLayer.ts +3 -3
- package/src/transport/httpClient.ts +23 -6
- package/src/transport/httpTransport.ts +88 -11
- package/src/transport/wsFrameHandlers.ts +3 -0
- package/src/types/global.ts +1 -1
- package/src/types/streams.ts +2 -2
- package/src/wire/accountResponses.ts +5 -11
- package/src/wire/claims.ts +7 -1
- package/src/wire/commit.ts +31 -1
- package/src/wire/index.ts +10 -1
- package/src/wire/listEnvelope.ts +23 -0
- 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 ↔
|
|
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))
|
|
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))
|
|
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
|
-
|
|
272
|
+
let update = transaction
|
|
230
273
|
.updateTable(columns.table)
|
|
231
274
|
.set(patch)
|
|
232
|
-
.where('id', '=', id)
|
|
233
|
-
|
|
234
|
-
.
|
|
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
|
-
|
|
238
|
-
|
|
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 (`{
|
|
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: '
|
|
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: '
|
|
93
|
+
{ type: 'CREATE', model: 'item', id: 't1', input: { title: 'A' } },
|
|
94
94
|
]));
|
|
95
|
-
const found = await adapter.read({ kind: 'load', model: '
|
|
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: '
|
|
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: '
|
|
108
|
-
{ type: 'CREATE', model: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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: '
|
|
175
|
+
{ type: 'UPDATE', model: 'item', id: 't1', input: { title: 'B' } },
|
|
176
176
|
]));
|
|
177
|
-
const found = await adapter.read({ kind: 'load', model: '
|
|
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: '
|
|
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: '
|
|
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, '
|
|
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: '
|
|
247
|
+
{ type: 'CREATE', model: 'item', id: 't1', input: {} },
|
|
248
248
|
]));
|
|
249
249
|
await adapter.commit(change('corr_page_b', [
|
|
250
|
-
{ type: 'CREATE', model: '
|
|
250
|
+
{ type: 'CREATE', model: 'item', id: 't2', input: {} },
|
|
251
251
|
]));
|
|
252
252
|
|
|
253
253
|
const first = await adapter.events(null, 1);
|
package/src/source/contract.ts
CHANGED
|
@@ -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),
|
package/src/source/index.ts
CHANGED
|
@@ -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 {
|
package/src/source/types.ts
CHANGED
|
@@ -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;
|
package/src/transactionLayer.ts
CHANGED
|
@@ -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
|
|
21
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 (
|
|
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
|
|
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
|
|
467
|
+
return page;
|
|
451
468
|
};
|
|
452
469
|
|
|
453
470
|
// Claim acquisition performs its authoritative read only after the grant.
|