@abloatai/ablo 0.7.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.
- package/CHANGELOG.md +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +7 -3
- package/dist/schema/serialize.js +6 -2
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Source adapter conformance suite — the shared "is this adapter correct?"
|
|
3
|
+
* test set, in the Auth.js `@auth/adapter-test` mould. Every ORM adapter
|
|
4
|
+
* (Prisma/Drizzle/Kysely) and any hand-written handler runs THIS to prove,
|
|
5
|
+
* before production, the guarantees the spine promises. A new adapter is "done"
|
|
6
|
+
* when it passes — not when it compiles.
|
|
7
|
+
*
|
|
8
|
+
* Runner-agnostic: checks are plain async functions that throw (node:assert) on
|
|
9
|
+
* failure. `runDataSourceTests` registers them with whatever `it`/`test` you
|
|
10
|
+
* pass, so it works under vitest, jest, or node:test:
|
|
11
|
+
*
|
|
12
|
+
* import { it } from 'vitest';
|
|
13
|
+
* runDataSourceTests(memoryDataSource, it);
|
|
14
|
+
*
|
|
15
|
+
* Scope: this covers the ADAPTER contract (commit idempotency, read-after-write,
|
|
16
|
+
* the transactional outbox + cursor). Signature/scope rejection is a HANDLER
|
|
17
|
+
* concern (the adapter never sees a signature) and is tested separately.
|
|
18
|
+
*/
|
|
19
|
+
import assert from 'node:assert/strict';
|
|
20
|
+
const change = (clientTxId, ops) => ({
|
|
21
|
+
clientTxId,
|
|
22
|
+
operations: ops,
|
|
23
|
+
});
|
|
24
|
+
export function dataSourceConformanceChecks(make) {
|
|
25
|
+
return [
|
|
26
|
+
{
|
|
27
|
+
name: 'commit applies a CREATE and returns the canonical row',
|
|
28
|
+
run: async () => {
|
|
29
|
+
const adapter = await make();
|
|
30
|
+
const result = await adapter.commit(change('tx_create', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
31
|
+
assert.equal(result.rows.length, 1, 'one row returned');
|
|
32
|
+
assert.equal(result.rows[0].id, 't1');
|
|
33
|
+
assert.equal(result.rows[0].title, 'A');
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'read load returns a committed row, and null-equivalent for an unknown id',
|
|
38
|
+
run: async () => {
|
|
39
|
+
const adapter = await make();
|
|
40
|
+
await adapter.commit(change('tx1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
41
|
+
const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
|
|
42
|
+
assert.equal(found.length, 1);
|
|
43
|
+
assert.equal(found[0].title, 'A');
|
|
44
|
+
const missing = await adapter.read({ kind: 'load', model: 'task', id: 'nope' });
|
|
45
|
+
assert.equal(missing.length, 0, 'unknown id reads empty');
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'read list returns committed rows',
|
|
50
|
+
run: async () => {
|
|
51
|
+
const adapter = await make();
|
|
52
|
+
await adapter.commit(change('tx_list', [
|
|
53
|
+
{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } },
|
|
54
|
+
{ type: 'CREATE', model: 'task', id: 't2', input: { title: 'B' } },
|
|
55
|
+
]));
|
|
56
|
+
const rows = await adapter.read({ kind: 'list', model: 'task' });
|
|
57
|
+
const ids = rows.map((r) => r.id).sort();
|
|
58
|
+
assert.deepEqual(ids, ['t1', 't2']);
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
name: 'duplicate clientTxId is idempotent — same rows, applied once',
|
|
63
|
+
run: async () => {
|
|
64
|
+
const adapter = await make();
|
|
65
|
+
const cs = change('tx_dup', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A', n: 1 } }]);
|
|
66
|
+
const first = await adapter.commit(cs);
|
|
67
|
+
const second = await adapter.commit(cs);
|
|
68
|
+
assert.deepEqual(second.rows, first.rows, 'replay returns the original rows');
|
|
69
|
+
// Applied once: still exactly one row, and the outbox did not double up.
|
|
70
|
+
const rows = await adapter.read({ kind: 'list', model: 'task' });
|
|
71
|
+
assert.equal(rows.length, 1, 'no duplicate row');
|
|
72
|
+
const page = await adapter.events(null, 100);
|
|
73
|
+
const forTx = page.events.filter((e) => e.clientTxId === 'tx_dup');
|
|
74
|
+
assert.equal(forTx.length, 1, 'outbox not double-appended on replay');
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
name: 'commit appends outbox events with the originating clientTxId',
|
|
79
|
+
run: async () => {
|
|
80
|
+
const adapter = await make();
|
|
81
|
+
await adapter.commit(change('tx_evt', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
82
|
+
const page = await adapter.events(null, 100);
|
|
83
|
+
assert.ok(page.events.length >= 1, 'at least one event');
|
|
84
|
+
const evt = page.events.find((e) => e.entityId === 't1');
|
|
85
|
+
assert.ok(evt, 'event for the committed row');
|
|
86
|
+
assert.equal(evt?.model, 'task');
|
|
87
|
+
assert.equal(evt?.type, 'CREATE');
|
|
88
|
+
assert.equal(evt?.clientTxId, 'tx_evt');
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: 'events cursor advances and never re-delivers a page',
|
|
93
|
+
run: async () => {
|
|
94
|
+
const adapter = await make();
|
|
95
|
+
await adapter.commit(change('tx_a', [{ type: 'CREATE', model: 'task', id: 't1', input: {} }]));
|
|
96
|
+
await adapter.commit(change('tx_b', [{ type: 'CREATE', model: 'task', id: 't2', input: {} }]));
|
|
97
|
+
const first = await adapter.events(null, 1);
|
|
98
|
+
assert.equal(first.events.length, 1, 'respects limit');
|
|
99
|
+
assert.ok(first.nextCursor, 'returns a cursor');
|
|
100
|
+
const second = await adapter.events(first.nextCursor, 100);
|
|
101
|
+
// No overlap: the second page starts strictly after the first cursor.
|
|
102
|
+
const firstIds = new Set(first.events.map((e) => e.id));
|
|
103
|
+
for (const e of second.events) {
|
|
104
|
+
assert.ok(!firstIds.has(e.id), `event ${e.id} re-delivered across cursor`);
|
|
105
|
+
}
|
|
106
|
+
// Draining to the end yields a stable terminal cursor.
|
|
107
|
+
const drained = await adapter.events(second.nextCursor ?? first.nextCursor, 100);
|
|
108
|
+
assert.equal(drained.events.length, 0, 'fully drained');
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: 'a later UPDATE under a new clientTxId is applied (idempotency is per-tx)',
|
|
113
|
+
run: async () => {
|
|
114
|
+
const adapter = await make();
|
|
115
|
+
await adapter.commit(change('tx_c1', [{ type: 'CREATE', model: 'task', id: 't1', input: { title: 'A' } }]));
|
|
116
|
+
await adapter.commit(change('tx_u1', [{ type: 'UPDATE', model: 'task', id: 't1', input: { title: 'B' } }]));
|
|
117
|
+
const found = await adapter.read({ kind: 'load', model: 'task', id: 't1' });
|
|
118
|
+
assert.equal(found[0].title, 'B', 'update applied');
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
];
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Register the conformance checks with a test runner's `it`/`test` function.
|
|
125
|
+
* `register(name, fn)` — pass vitest/jest `it` or `node:test` `test`.
|
|
126
|
+
*/
|
|
127
|
+
export function runDataSourceTests(make, register) {
|
|
128
|
+
for (const check of dataSourceConformanceChecks(make)) {
|
|
129
|
+
register(check.name, check.run);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// Re-export the reference adapter so `@abloatai/ablo/source/conformance`
|
|
133
|
+
// exposes both the suite and the in-memory double in one import.
|
|
134
|
+
export { memoryDataSource } from './adapters/memory.js';
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Source adapter contract — Zod-first.
|
|
3
|
+
*
|
|
4
|
+
* The wire shapes an ORM adapter (Prisma/Drizzle/Kysely) consumes and produces
|
|
5
|
+
* are defined here as Zod schemas, not hand-typed interfaces, so they are
|
|
6
|
+
* VALIDATED at the boundary (a malformed op / outbox row is rejected at the
|
|
7
|
+
* edge, not deep inside a transaction) and every inferred type flows from one
|
|
8
|
+
* source. This mirrors the server's `tenant-connection.schema.ts` convention:
|
|
9
|
+
* schema-validate what crosses a trust boundary, infer the TS types from it.
|
|
10
|
+
*
|
|
11
|
+
* Scope note: the existing `SourceOperation` / `SourceEvent` interfaces in
|
|
12
|
+
* `index.ts` are the established cross-package wire types — this module does NOT
|
|
13
|
+
* redefine them. It owns the ADAPTER-level contract (the change envelope the
|
|
14
|
+
* adapter commits, the outbox row it persists, the migration it ships) and keeps
|
|
15
|
+
* `operationSchema` structurally compatible with `SourceOperation` (asserted at
|
|
16
|
+
* the bottom of the file) so the two never drift.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
/** Mirrors `SourceOperation['type']`. */
|
|
20
|
+
export declare const operationTypeSchema: z.ZodEnum<{
|
|
21
|
+
CREATE: "CREATE";
|
|
22
|
+
UPDATE: "UPDATE";
|
|
23
|
+
DELETE: "DELETE";
|
|
24
|
+
ARCHIVE: "ARCHIVE";
|
|
25
|
+
UNARCHIVE: "UNARCHIVE";
|
|
26
|
+
}>;
|
|
27
|
+
export type OperationType = z.infer<typeof operationTypeSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* One operation in a change set. Structurally compatible with `SourceOperation`
|
|
30
|
+
* (see the assertion below) — this is its runtime validator.
|
|
31
|
+
*/
|
|
32
|
+
export declare const operationSchema: z.ZodObject<{
|
|
33
|
+
type: z.ZodEnum<{
|
|
34
|
+
CREATE: "CREATE";
|
|
35
|
+
UPDATE: "UPDATE";
|
|
36
|
+
DELETE: "DELETE";
|
|
37
|
+
ARCHIVE: "ARCHIVE";
|
|
38
|
+
UNARCHIVE: "UNARCHIVE";
|
|
39
|
+
}>;
|
|
40
|
+
model: z.ZodString;
|
|
41
|
+
id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
42
|
+
input: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
43
|
+
transactionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
44
|
+
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
45
|
+
onStale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
46
|
+
reject: "reject";
|
|
47
|
+
force: "force";
|
|
48
|
+
flag: "flag";
|
|
49
|
+
merge: "merge";
|
|
50
|
+
}>>>;
|
|
51
|
+
}, z.core.$strip>;
|
|
52
|
+
export type Operation = z.infer<typeof operationSchema>;
|
|
53
|
+
/**
|
|
54
|
+
* The atomic unit an adapter commits: one or more operations under a single
|
|
55
|
+
* `clientTxId`. The `clientTxId` is the idempotency key — committing the same
|
|
56
|
+
* change set twice must produce the same result exactly once.
|
|
57
|
+
*/
|
|
58
|
+
export declare const changeSetSchema: z.ZodObject<{
|
|
59
|
+
operations: z.ZodArray<z.ZodObject<{
|
|
60
|
+
type: z.ZodEnum<{
|
|
61
|
+
CREATE: "CREATE";
|
|
62
|
+
UPDATE: "UPDATE";
|
|
63
|
+
DELETE: "DELETE";
|
|
64
|
+
ARCHIVE: "ARCHIVE";
|
|
65
|
+
UNARCHIVE: "UNARCHIVE";
|
|
66
|
+
}>;
|
|
67
|
+
model: z.ZodString;
|
|
68
|
+
id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
69
|
+
input: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
70
|
+
transactionId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
71
|
+
readAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
72
|
+
onStale: z.ZodOptional<z.ZodNullable<z.ZodEnum<{
|
|
73
|
+
reject: "reject";
|
|
74
|
+
force: "force";
|
|
75
|
+
flag: "flag";
|
|
76
|
+
merge: "merge";
|
|
77
|
+
}>>>;
|
|
78
|
+
}, z.core.$strip>>;
|
|
79
|
+
clientTxId: z.ZodString;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
export type ChangeSet = z.infer<typeof changeSetSchema>;
|
|
82
|
+
/**
|
|
83
|
+
* A row in the adapter-owned `ablo_outbox` table. Written in the SAME
|
|
84
|
+
* transaction as the app-row mutation (transactional outbox), then read back by
|
|
85
|
+
* `events()` and handed to Ablo. `cursor` is the monotonic ordering key Ablo
|
|
86
|
+
* round-trips to resume.
|
|
87
|
+
*/
|
|
88
|
+
export declare const outboxEventSchema: z.ZodObject<{
|
|
89
|
+
id: z.ZodString;
|
|
90
|
+
model: z.ZodString;
|
|
91
|
+
entityId: z.ZodString;
|
|
92
|
+
type: z.ZodEnum<{
|
|
93
|
+
CREATE: "CREATE";
|
|
94
|
+
UPDATE: "UPDATE";
|
|
95
|
+
DELETE: "DELETE";
|
|
96
|
+
ARCHIVE: "ARCHIVE";
|
|
97
|
+
UNARCHIVE: "UNARCHIVE";
|
|
98
|
+
}>;
|
|
99
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
100
|
+
organizationId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
101
|
+
clientTxId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
102
|
+
occurredAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
103
|
+
cursor: z.ZodString;
|
|
104
|
+
}, z.core.$strip>;
|
|
105
|
+
export type OutboxEvent = z.infer<typeof outboxEventSchema>;
|
|
106
|
+
/** A page of outbox events returned by `events()`. */
|
|
107
|
+
export declare const eventsPageSchema: z.ZodObject<{
|
|
108
|
+
events: z.ZodArray<z.ZodObject<{
|
|
109
|
+
id: z.ZodString;
|
|
110
|
+
model: z.ZodString;
|
|
111
|
+
entityId: z.ZodString;
|
|
112
|
+
type: z.ZodEnum<{
|
|
113
|
+
CREATE: "CREATE";
|
|
114
|
+
UPDATE: "UPDATE";
|
|
115
|
+
DELETE: "DELETE";
|
|
116
|
+
ARCHIVE: "ARCHIVE";
|
|
117
|
+
UNARCHIVE: "UNARCHIVE";
|
|
118
|
+
}>;
|
|
119
|
+
data: z.ZodOptional<z.ZodNullable<z.ZodRecord<z.ZodString, z.ZodUnknown>>>;
|
|
120
|
+
organizationId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
121
|
+
clientTxId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
122
|
+
occurredAt: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
123
|
+
cursor: z.ZodString;
|
|
124
|
+
}, z.core.$strip>>;
|
|
125
|
+
nextCursor: z.ZodNullable<z.ZodString>;
|
|
126
|
+
}, z.core.$strip>;
|
|
127
|
+
export type EventsPage = z.infer<typeof eventsPageSchema>;
|
|
128
|
+
/**
|
|
129
|
+
* A DDL migration an adapter ships so a customer never hand-writes the
|
|
130
|
+
* `ablo_idempotency` / `ablo_outbox` tables — `ablo init` emits these.
|
|
131
|
+
*/
|
|
132
|
+
export declare const migrationSchema: z.ZodObject<{
|
|
133
|
+
name: z.ZodString;
|
|
134
|
+
up: z.ZodString;
|
|
135
|
+
}, z.core.$strip>;
|
|
136
|
+
export type Migration = z.infer<typeof migrationSchema>;
|
|
137
|
+
/** What an adapter's backend can do — a capability profile (no behavior inference). */
|
|
138
|
+
export declare const adapterCapabilitiesSchema: z.ZodObject<{
|
|
139
|
+
transactions: z.ZodBoolean;
|
|
140
|
+
propose: z.ZodBoolean;
|
|
141
|
+
schemaIntrospection: z.ZodBoolean;
|
|
142
|
+
}, z.core.$strip>;
|
|
143
|
+
export type AdapterCapabilities = z.infer<typeof adapterCapabilitiesSchema>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data Source adapter contract — Zod-first.
|
|
3
|
+
*
|
|
4
|
+
* The wire shapes an ORM adapter (Prisma/Drizzle/Kysely) consumes and produces
|
|
5
|
+
* are defined here as Zod schemas, not hand-typed interfaces, so they are
|
|
6
|
+
* VALIDATED at the boundary (a malformed op / outbox row is rejected at the
|
|
7
|
+
* edge, not deep inside a transaction) and every inferred type flows from one
|
|
8
|
+
* source. This mirrors the server's `tenant-connection.schema.ts` convention:
|
|
9
|
+
* schema-validate what crosses a trust boundary, infer the TS types from it.
|
|
10
|
+
*
|
|
11
|
+
* Scope note: the existing `SourceOperation` / `SourceEvent` interfaces in
|
|
12
|
+
* `index.ts` are the established cross-package wire types — this module does NOT
|
|
13
|
+
* redefine them. It owns the ADAPTER-level contract (the change envelope the
|
|
14
|
+
* adapter commits, the outbox row it persists, the migration it ships) and keeps
|
|
15
|
+
* `operationSchema` structurally compatible with `SourceOperation` (asserted at
|
|
16
|
+
* the bottom of the file) so the two never drift.
|
|
17
|
+
*/
|
|
18
|
+
import { z } from 'zod';
|
|
19
|
+
const jsonObject = z.record(z.string(), z.unknown());
|
|
20
|
+
/** Mirrors `SourceOperation['type']`. */
|
|
21
|
+
export const operationTypeSchema = z.enum([
|
|
22
|
+
'CREATE',
|
|
23
|
+
'UPDATE',
|
|
24
|
+
'DELETE',
|
|
25
|
+
'ARCHIVE',
|
|
26
|
+
'UNARCHIVE',
|
|
27
|
+
]);
|
|
28
|
+
/**
|
|
29
|
+
* One operation in a change set. Structurally compatible with `SourceOperation`
|
|
30
|
+
* (see the assertion below) — this is its runtime validator.
|
|
31
|
+
*/
|
|
32
|
+
export const operationSchema = z.object({
|
|
33
|
+
type: operationTypeSchema,
|
|
34
|
+
model: z.string().min(1),
|
|
35
|
+
id: z.string().min(1).nullish(),
|
|
36
|
+
input: jsonObject.nullish(),
|
|
37
|
+
transactionId: z.string().nullish(),
|
|
38
|
+
readAt: z.number().nullish(),
|
|
39
|
+
onStale: z.enum(['reject', 'force', 'flag', 'merge']).nullish(),
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* The atomic unit an adapter commits: one or more operations under a single
|
|
43
|
+
* `clientTxId`. The `clientTxId` is the idempotency key — committing the same
|
|
44
|
+
* change set twice must produce the same result exactly once.
|
|
45
|
+
*/
|
|
46
|
+
export const changeSetSchema = z.object({
|
|
47
|
+
operations: z.array(operationSchema).min(1),
|
|
48
|
+
clientTxId: z.string().min(1),
|
|
49
|
+
});
|
|
50
|
+
/**
|
|
51
|
+
* A row in the adapter-owned `ablo_outbox` table. Written in the SAME
|
|
52
|
+
* transaction as the app-row mutation (transactional outbox), then read back by
|
|
53
|
+
* `events()` and handed to Ablo. `cursor` is the monotonic ordering key Ablo
|
|
54
|
+
* round-trips to resume.
|
|
55
|
+
*/
|
|
56
|
+
export const outboxEventSchema = z.object({
|
|
57
|
+
/** Stable, globally-unique id — Ablo's replay-protection key. */
|
|
58
|
+
id: z.string().min(1),
|
|
59
|
+
model: z.string().min(1),
|
|
60
|
+
entityId: z.string().min(1),
|
|
61
|
+
type: operationTypeSchema,
|
|
62
|
+
data: jsonObject.nullish(),
|
|
63
|
+
organizationId: z.string().nullish(),
|
|
64
|
+
/** Round-tripped so Ablo can filter SDK-origin echoes after a direct append. */
|
|
65
|
+
clientTxId: z.string().nullish(),
|
|
66
|
+
occurredAt: z.number().nullish(),
|
|
67
|
+
/** Monotonic ordering key (bigint as string). `events()` pages by `cursor > ?`. */
|
|
68
|
+
cursor: z.string().min(1),
|
|
69
|
+
});
|
|
70
|
+
/** A page of outbox events returned by `events()`. */
|
|
71
|
+
export const eventsPageSchema = z.object({
|
|
72
|
+
events: z.array(outboxEventSchema),
|
|
73
|
+
nextCursor: z.string().nullable(),
|
|
74
|
+
});
|
|
75
|
+
/**
|
|
76
|
+
* A DDL migration an adapter ships so a customer never hand-writes the
|
|
77
|
+
* `ablo_idempotency` / `ablo_outbox` tables — `ablo init` emits these.
|
|
78
|
+
*/
|
|
79
|
+
export const migrationSchema = z.object({
|
|
80
|
+
/** Stable name, used as the migration filename + applied-ledger key. */
|
|
81
|
+
name: z.string().min(1),
|
|
82
|
+
/** The forward SQL. */
|
|
83
|
+
up: z.string().min(1),
|
|
84
|
+
});
|
|
85
|
+
/** What an adapter's backend can do — a capability profile (no behavior inference). */
|
|
86
|
+
export const adapterCapabilitiesSchema = z.object({
|
|
87
|
+
/** `commit` is atomic across all operations in the change set. */
|
|
88
|
+
transactions: z.boolean(),
|
|
89
|
+
/** A dry-run `propose` is supported (else proposal lives above the adapter). */
|
|
90
|
+
propose: z.boolean(),
|
|
91
|
+
/** The backend can be introspected for its schema. */
|
|
92
|
+
schemaIntrospection: z.boolean(),
|
|
93
|
+
});
|
|
94
|
+
const _operationContractInSync = [
|
|
95
|
+
true,
|
|
96
|
+
true,
|
|
97
|
+
];
|
|
98
|
+
void _operationContractInSync;
|
package/dist/source/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Schema, SchemaRecord, InferCreate } from '../schema/schema.js';
|
|
2
|
+
import type { DataSourceAdapter } from './adapter.js';
|
|
2
3
|
export type SourcePrimitive = string | number | boolean | null;
|
|
3
4
|
export type SourceWhere = readonly [field: string, value: SourcePrimitive] | readonly [
|
|
4
5
|
field: string,
|
|
@@ -84,10 +85,11 @@ export interface SourceDelta {
|
|
|
84
85
|
* `sync_deltas` and fan them out to connected clients exactly like
|
|
85
86
|
* SDK-originated commits.
|
|
86
87
|
*
|
|
87
|
-
* The events handler can return everything from the outbox unfiltered
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
88
|
+
* The events handler can return everything from the outbox unfiltered. Ablo
|
|
89
|
+
* dedupes stable `event.id` values and uses `clientTxId` to filter SDK-origin
|
|
90
|
+
* echoes after the direct append has already succeeded. If the direct append
|
|
91
|
+
* failed, the same outbox event repairs it on poll/push because no matching
|
|
92
|
+
* `mutation_log` row exists yet.
|
|
91
93
|
*/
|
|
92
94
|
export interface SourceEvent {
|
|
93
95
|
/**
|
|
@@ -122,6 +124,42 @@ export interface SourceEvent {
|
|
|
122
124
|
*/
|
|
123
125
|
readonly occurredAt?: number;
|
|
124
126
|
}
|
|
127
|
+
export interface SourceEventForOperationOptions {
|
|
128
|
+
/**
|
|
129
|
+
* Stable id from the customer's outbox table. This is Ablo's replay-
|
|
130
|
+
* protection key; retries must return the same id.
|
|
131
|
+
*/
|
|
132
|
+
readonly eventId: string;
|
|
133
|
+
readonly operation: SourceOperation;
|
|
134
|
+
/**
|
|
135
|
+
* Committed row id. Defaults to `operation.id`; pass this for generated-id
|
|
136
|
+
* CREATEs where the database assigns the id inside the transaction.
|
|
137
|
+
*/
|
|
138
|
+
readonly entityId?: string;
|
|
139
|
+
/**
|
|
140
|
+
* Canonical row payload after the write. Pass `null` for DELETE. When omitted
|
|
141
|
+
* the event carries no row payload, which is valid but less useful for
|
|
142
|
+
* realtime hydration.
|
|
143
|
+
*/
|
|
144
|
+
readonly data?: Record<string, unknown> | null;
|
|
145
|
+
/**
|
|
146
|
+
* Batch idempotency key from the Data Source commit request. Round-tripping it
|
|
147
|
+
* lets Ablo filter SDK-origin echoes after the direct append succeeds, while
|
|
148
|
+
* still using the outbox event to repair a failed direct append.
|
|
149
|
+
*/
|
|
150
|
+
readonly clientTxId?: string;
|
|
151
|
+
readonly organizationId?: string;
|
|
152
|
+
readonly occurredAt?: number | Date;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Build the source-event marker customers should write to their outbox table in
|
|
156
|
+
* the SAME transaction as their app-row mutation.
|
|
157
|
+
*
|
|
158
|
+
* This helper does not persist anything. It only standardizes the marker shape
|
|
159
|
+
* so Prisma/Drizzle/Kysely/raw-SQL adapters all emit the fields Ablo's
|
|
160
|
+
* reconciler expects.
|
|
161
|
+
*/
|
|
162
|
+
export declare function sourceEventForOperation(options: SourceEventForOperationOptions): SourceEvent;
|
|
125
163
|
export interface SourceCommitResult<Row = Record<string, unknown>> {
|
|
126
164
|
/**
|
|
127
165
|
* Canonical rows after the write. Ablo uses these to update hosted
|
|
@@ -181,7 +219,8 @@ export interface SourceHandlerContext<TAuth = unknown> {
|
|
|
181
219
|
* `webhook-id` from the signed request — globally unique per the
|
|
182
220
|
* Standard Webhooks spec. Customers should dedupe by this id to
|
|
183
221
|
* defend against replay (Ablo doesn't dedupe at the source-handler
|
|
184
|
-
* boundary;
|
|
222
|
+
* boundary; commit idempotency is `clientTxId`, and event replay
|
|
223
|
+
* protection is the outbox event `id`).
|
|
185
224
|
*/
|
|
186
225
|
readonly messageId?: string;
|
|
187
226
|
readonly signedAt?: number;
|
|
@@ -317,11 +356,10 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
|
317
356
|
* imports). Each returned event becomes a delta and fans out to
|
|
318
357
|
* connected clients.
|
|
319
358
|
*
|
|
320
|
-
*
|
|
321
|
-
*
|
|
322
|
-
*
|
|
323
|
-
*
|
|
324
|
-
* `clientTxId` and skip rows whose tag is non-null.
|
|
359
|
+
* Handlers may return the raw outbox feed. Ablo dedupes stable
|
|
360
|
+
* `event.id` values and filters SDK-origin echoes when rows carry
|
|
361
|
+
* the originating `clientTxId`; customers should persist both fields
|
|
362
|
+
* in their outbox table.
|
|
325
363
|
*/
|
|
326
364
|
readonly events?: SourceEventsHandler<TAuth>;
|
|
327
365
|
/**
|
|
@@ -329,6 +367,15 @@ export type AbloSourceOptions<S extends SchemaRecord, TAuth = unknown> = {
|
|
|
329
367
|
* `abloSource({ schema, files: { load, list, commit } })`.
|
|
330
368
|
*/
|
|
331
369
|
readonly models?: SourceModels<S, TAuth>;
|
|
370
|
+
/**
|
|
371
|
+
* An ORM adapter (`prismaDataSource(prisma, schema)`, …). When set, it serves
|
|
372
|
+
* ALL four operations — read (load/list), commit (idempotent + outbox), and
|
|
373
|
+
* events — so no hand-written `commit`/`events`/model handlers are needed. The
|
|
374
|
+
* adapter is consumed at the generic dispatch layer (rows are JSON on the wire),
|
|
375
|
+
* which is why it carries no per-model types and needs no cast at the call site.
|
|
376
|
+
* Mutually exclusive with hand-written handlers.
|
|
377
|
+
*/
|
|
378
|
+
readonly adapter?: DataSourceAdapter;
|
|
332
379
|
} & SourceModels<S, TAuth>;
|
|
333
380
|
export type SourceLoadRequest = {
|
|
334
381
|
readonly type: 'load';
|
|
@@ -392,6 +439,7 @@ export type DataSourceRequestContext = SourceRequestContext;
|
|
|
392
439
|
export type DataSourceOperation = SourceOperation;
|
|
393
440
|
export type DataSourceDelta = SourceDelta;
|
|
394
441
|
export type DataSourceEvent = SourceEvent;
|
|
442
|
+
export type DataSourceEventForOperationOptions = SourceEventForOperationOptions;
|
|
395
443
|
export type DataSourceCommitResult<Row = Record<string, unknown>> = SourceCommitResult<Row>;
|
|
396
444
|
export type DataSourceCommitParams<TAuth = unknown> = SourceCommitParams<TAuth>;
|
|
397
445
|
export type DataSourceScope = SourceScope;
|
|
@@ -415,3 +463,6 @@ export type DataSourceResponse<Row = Record<string, unknown>> = SourceResponse<R
|
|
|
415
463
|
export declare function abloSource<const S extends SchemaRecord, TAuth = unknown>(options: AbloSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
416
464
|
export declare function dataSource<const S extends SchemaRecord, TAuth = unknown>(options: DataSourceOptions<S, TAuth>): (request: Request) => Promise<Response>;
|
|
417
465
|
export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, type PushQueue, type PushQueueItem, type PushQueueOptions, type PushQueueStorage, } from './pushQueue.js';
|
|
466
|
+
export { type DataSourceAdapter, type AdapterReadRequest, type AdapterCommitResult, type Row as AdapterRow, } from './adapter.js';
|
|
467
|
+
export { operationSchema, operationTypeSchema, changeSetSchema, outboxEventSchema, eventsPageSchema, migrationSchema, adapterCapabilitiesSchema, type Operation, type ChangeSet, type OutboxEvent, type EventsPage, type Migration, type AdapterCapabilities, } from './contract.js';
|
|
468
|
+
export { prismaDataSource, type PrismaLike, type PrismaDataSourceOptions } from './adapters/prisma.js';
|
package/dist/source/index.js
CHANGED
|
@@ -1,3 +1,35 @@
|
|
|
1
|
+
import { changeSetSchema } from './contract.js';
|
|
2
|
+
/**
|
|
3
|
+
* Build the source-event marker customers should write to their outbox table in
|
|
4
|
+
* the SAME transaction as their app-row mutation.
|
|
5
|
+
*
|
|
6
|
+
* This helper does not persist anything. It only standardizes the marker shape
|
|
7
|
+
* so Prisma/Drizzle/Kysely/raw-SQL adapters all emit the fields Ablo's
|
|
8
|
+
* reconciler expects.
|
|
9
|
+
*/
|
|
10
|
+
export function sourceEventForOperation(options) {
|
|
11
|
+
const entityId = options.entityId ?? options.operation.id;
|
|
12
|
+
if (typeof entityId !== 'string' || entityId.length === 0) {
|
|
13
|
+
throw new Error('sourceEventForOperation requires operation.id or an explicit entityId');
|
|
14
|
+
}
|
|
15
|
+
const occurredAt = normalizeEventOccurredAt(options.occurredAt);
|
|
16
|
+
return {
|
|
17
|
+
id: options.eventId,
|
|
18
|
+
model: options.operation.model,
|
|
19
|
+
entityId,
|
|
20
|
+
type: options.operation.type,
|
|
21
|
+
...(options.data !== undefined ? { data: options.data } : {}),
|
|
22
|
+
...(options.organizationId ? { organizationId: options.organizationId } : {}),
|
|
23
|
+
...(options.clientTxId ? { clientTxId: options.clientTxId } : {}),
|
|
24
|
+
...(occurredAt !== undefined ? { occurredAt } : {}),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function normalizeEventOccurredAt(value) {
|
|
28
|
+
if (value === undefined)
|
|
29
|
+
return undefined;
|
|
30
|
+
const timestamp = value instanceof Date ? value.getTime() : value;
|
|
31
|
+
return Number.isFinite(timestamp) ? timestamp : undefined;
|
|
32
|
+
}
|
|
1
33
|
/**
|
|
2
34
|
* HTTP headers used on signed source requests. Conforms to the
|
|
3
35
|
* Standard Webhooks specification (https://www.standardwebhooks.com/)
|
|
@@ -26,6 +58,64 @@ function json(data, status = 200) {
|
|
|
26
58
|
headers: { 'Content-Type': 'application/json' },
|
|
27
59
|
});
|
|
28
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Serve a request from an ORM `adapter`. Routes the four operations to the spine
|
|
63
|
+
* (`read`/`commit`/`events`) and shapes the wire response. The adapter is the
|
|
64
|
+
* single point of dispatch — no per-model branching here.
|
|
65
|
+
*/
|
|
66
|
+
async function handleViaAdapter(adapter, body, scope) {
|
|
67
|
+
if (body.type === 'load') {
|
|
68
|
+
const rows = await adapter.read({
|
|
69
|
+
kind: 'load',
|
|
70
|
+
model: body.model,
|
|
71
|
+
id: body.id,
|
|
72
|
+
...(scope ? { scope } : {}),
|
|
73
|
+
});
|
|
74
|
+
return json({ row: rows[0] ?? null });
|
|
75
|
+
}
|
|
76
|
+
if (body.type === 'list') {
|
|
77
|
+
const rows = await adapter.read({
|
|
78
|
+
kind: 'list',
|
|
79
|
+
model: body.model,
|
|
80
|
+
...(body.query ? { query: body.query } : {}),
|
|
81
|
+
...(scope ? { scope } : {}),
|
|
82
|
+
});
|
|
83
|
+
return json({ rows });
|
|
84
|
+
}
|
|
85
|
+
if (body.type === 'commit') {
|
|
86
|
+
if (!body.clientTxId) {
|
|
87
|
+
return json({ error: 'source_commit_requires_client_tx_id', message: 'commit requires a clientTxId for idempotency' }, 400);
|
|
88
|
+
}
|
|
89
|
+
const parsed = changeSetSchema.safeParse({
|
|
90
|
+
operations: body.operations,
|
|
91
|
+
clientTxId: body.clientTxId,
|
|
92
|
+
});
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
return json({ error: 'source_commit_invalid', message: parsed.error.message }, 400);
|
|
95
|
+
}
|
|
96
|
+
const result = await adapter.commit(parsed.data);
|
|
97
|
+
return json({ rows: result.rows });
|
|
98
|
+
}
|
|
99
|
+
if (body.type === 'events') {
|
|
100
|
+
const page = await adapter.events(body.cursor ?? null, body.limit ?? 100);
|
|
101
|
+
return json({
|
|
102
|
+
events: page.events.map((event) => ({
|
|
103
|
+
id: event.id,
|
|
104
|
+
model: event.model,
|
|
105
|
+
entityId: event.entityId,
|
|
106
|
+
type: event.type,
|
|
107
|
+
...(event.data !== undefined && event.data !== null ? { data: event.data } : {}),
|
|
108
|
+
...(event.organizationId ? { organizationId: event.organizationId } : {}),
|
|
109
|
+
...(event.clientTxId ? { clientTxId: event.clientTxId } : {}),
|
|
110
|
+
...(event.occurredAt !== undefined && event.occurredAt !== null
|
|
111
|
+
? { occurredAt: event.occurredAt }
|
|
112
|
+
: {}),
|
|
113
|
+
})),
|
|
114
|
+
...(page.nextCursor !== null ? { nextCursor: page.nextCursor } : {}),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
return json({ error: 'unknown_source_request' }, 400);
|
|
118
|
+
}
|
|
29
119
|
async function readBody(request) {
|
|
30
120
|
if (typeof request.text === 'function') {
|
|
31
121
|
const rawBody = await request.text();
|
|
@@ -257,6 +347,12 @@ export function abloSource(options) {
|
|
|
257
347
|
signedAt: signature?.signedAt,
|
|
258
348
|
...(body.scope ? { scope: body.scope } : {}),
|
|
259
349
|
};
|
|
350
|
+
// Adapter path: when an ORM adapter is configured it serves every operation,
|
|
351
|
+
// consumed at this generic layer (rows are JSON on the wire), so no per-model
|
|
352
|
+
// handler lookup and no typed↔generic boundary.
|
|
353
|
+
if (options.adapter) {
|
|
354
|
+
return handleViaAdapter(options.adapter, body, context.scope);
|
|
355
|
+
}
|
|
260
356
|
if (body.type === 'load') {
|
|
261
357
|
const handlers = getModelHandlers(options, body.model);
|
|
262
358
|
if (!handlers?.load) {
|
|
@@ -321,3 +417,5 @@ export function dataSource(options) {
|
|
|
321
417
|
return abloSource(options);
|
|
322
418
|
}
|
|
323
419
|
export { createPushQueue, InMemoryPushQueueStorage, STANDARD_WEBHOOKS_RETRY_SCHEDULE, } from './pushQueue.js';
|
|
420
|
+
export { operationSchema, operationTypeSchema, changeSetSchema, outboxEventSchema, eventsPageSchema, migrationSchema, adapterCapabilitiesSchema, } from './contract.js';
|
|
421
|
+
export { prismaDataSource } from './adapters/prisma.js';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Next.js App Router adapter for Data Source. The core `dataSource()` already
|
|
3
|
+
* returns a Web-standard `(Request) => Promise<Response>`, which Next App Router
|
|
4
|
+
* accepts directly — so this is pure ergonomics: wire an ORM `adapter` in via the
|
|
5
|
+
* bridge and hand back a named `POST` so the customer's route file is the minimum:
|
|
6
|
+
*
|
|
7
|
+
* // app/api/ablo/source/route.ts
|
|
8
|
+
* import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
9
|
+
* import { prismaDataSource } from '@abloatai/ablo/source';
|
|
10
|
+
* import { schema } from '@/ablo/schema';
|
|
11
|
+
* import { prisma } from '@/lib/prisma';
|
|
12
|
+
*
|
|
13
|
+
* export const { POST } = dataSourceNext({
|
|
14
|
+
* schema,
|
|
15
|
+
* apiKey: process.env.ABLO_API_KEY!,
|
|
16
|
+
* adapter: prismaDataSource(prisma, schema),
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* Day-one scope: Next + the adapter form only. Hand-written handlers use the core
|
|
20
|
+
* `dataSource()` directly; Hono/Express are the same one-liner and land on demand
|
|
21
|
+
* — not pre-built.
|
|
22
|
+
*/
|
|
23
|
+
import type { SchemaRecord } from '../schema/schema.js';
|
|
24
|
+
import { type DataSourceOptions } from './index.js';
|
|
25
|
+
/**
|
|
26
|
+
* Next options ARE the core options — the `adapter` field lives on the core
|
|
27
|
+
* handler now, so there is no bridging, no cast, and no per-model-typed boundary
|
|
28
|
+
* at the call site. Pass `{ schema, apiKey, adapter }`.
|
|
29
|
+
*/
|
|
30
|
+
export type DataSourceNextOptions<S extends SchemaRecord, TAuth = unknown> = DataSourceOptions<S, TAuth>;
|
|
31
|
+
export declare function dataSourceNext<const S extends SchemaRecord, TAuth = unknown>(options: DataSourceNextOptions<S, TAuth>): {
|
|
32
|
+
readonly POST: (request: Request) => Promise<Response>;
|
|
33
|
+
};
|