@abloatai/ablo 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/CHANGELOG.md +46 -1
  2. package/README.md +33 -28
  3. package/dist/BaseSyncedStore.d.ts +83 -0
  4. package/dist/BaseSyncedStore.js +194 -2
  5. package/dist/Model.d.ts +42 -0
  6. package/dist/Model.js +103 -44
  7. package/dist/agent/session.js +3 -3
  8. package/dist/ai-sdk/coordination-context.js +4 -0
  9. package/dist/ai-sdk/index.d.ts +56 -47
  10. package/dist/ai-sdk/index.js +56 -47
  11. package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
  12. package/dist/ai-sdk/intent-broadcast.js +11 -4
  13. package/dist/ai-sdk/wrap.d.ts +14 -11
  14. package/dist/ai-sdk/wrap.js +11 -13
  15. package/dist/auth/credentialSource.d.ts +34 -0
  16. package/dist/auth/credentialSource.js +63 -0
  17. package/dist/auth/index.d.ts +2 -22
  18. package/dist/auth/index.js +4 -42
  19. package/dist/auth/schemas.d.ts +35 -0
  20. package/dist/auth/schemas.js +53 -0
  21. package/dist/client/Ablo.d.ts +160 -42
  22. package/dist/client/Ablo.js +145 -75
  23. package/dist/client/ApiClient.d.ts +20 -4
  24. package/dist/client/ApiClient.js +166 -28
  25. package/dist/client/auth.d.ts +14 -5
  26. package/dist/client/auth.js +60 -7
  27. package/dist/client/createInternalComponents.d.ts +2 -0
  28. package/dist/client/createInternalComponents.js +8 -1
  29. package/dist/client/createModelProxy.d.ts +130 -66
  30. package/dist/client/createModelProxy.js +152 -49
  31. package/dist/client/httpClient.d.ts +71 -0
  32. package/dist/client/httpClient.js +69 -0
  33. package/dist/client/identity.d.ts +2 -6
  34. package/dist/client/identity.js +49 -11
  35. package/dist/client/index.d.ts +1 -0
  36. package/dist/client/index.js +1 -0
  37. package/dist/client/registerDataSource.d.ts +3 -3
  38. package/dist/client/registerDataSource.js +11 -9
  39. package/dist/client/validateAbloOptions.js +1 -1
  40. package/dist/core/DatabaseManager.js +30 -2
  41. package/dist/core/openIDBWithTimeout.d.ts +36 -0
  42. package/dist/core/openIDBWithTimeout.js +88 -1
  43. package/dist/errorCodes.d.ts +70 -1
  44. package/dist/errorCodes.js +108 -9
  45. package/dist/errors.d.ts +2 -2
  46. package/dist/errors.js +72 -22
  47. package/dist/index.d.ts +17 -8
  48. package/dist/index.js +15 -6
  49. package/dist/keys/index.d.ts +16 -1
  50. package/dist/keys/index.js +26 -6
  51. package/dist/mutators/UndoManager.d.ts +158 -50
  52. package/dist/mutators/UndoManager.js +345 -22
  53. package/dist/mutators/inverseOp.d.ts +129 -0
  54. package/dist/mutators/inverseOp.js +74 -0
  55. package/dist/mutators/readerActions.d.ts +1 -1
  56. package/dist/mutators/undoApply.d.ts +42 -0
  57. package/dist/mutators/undoApply.js +143 -0
  58. package/dist/query/client.d.ts +10 -9
  59. package/dist/query/client.js +3 -6
  60. package/dist/react/AbloProvider.d.ts +23 -126
  61. package/dist/react/AbloProvider.js +62 -199
  62. package/dist/react/context.d.ts +31 -0
  63. package/dist/react/useAblo.d.ts +2 -2
  64. package/dist/react/useCurrentUserId.d.ts +1 -1
  65. package/dist/react/useCurrentUserId.js +1 -1
  66. package/dist/react/useMutators.js +19 -12
  67. package/dist/schema/ddl.d.ts +34 -3
  68. package/dist/schema/ddl.js +162 -4
  69. package/dist/schema/index.d.ts +5 -1
  70. package/dist/schema/index.js +13 -1
  71. package/dist/schema/model.d.ts +11 -0
  72. package/dist/schema/model.js +2 -0
  73. package/dist/schema/openapi.d.ts +28 -0
  74. package/dist/schema/openapi.js +118 -0
  75. package/dist/schema/plane.d.ts +23 -0
  76. package/dist/schema/plane.js +19 -0
  77. package/dist/schema/relation.d.ts +20 -0
  78. package/dist/schema/serialize.d.ts +4 -0
  79. package/dist/schema/serialize.js +4 -0
  80. package/dist/schema/sync-delta-row.d.ts +157 -0
  81. package/dist/schema/sync-delta-row.js +102 -0
  82. package/dist/schema/sync-delta-wire.d.ts +180 -0
  83. package/dist/schema/sync-delta-wire.js +102 -0
  84. package/dist/server/adapter.d.ts +156 -0
  85. package/dist/server/adapter.js +19 -0
  86. package/dist/server/commit.d.ts +82 -0
  87. package/dist/server/commit.js +1 -0
  88. package/dist/server/index.d.ts +14 -0
  89. package/dist/server/index.js +1 -0
  90. package/dist/server/next.d.ts +51 -0
  91. package/dist/server/next.js +47 -0
  92. package/dist/server/read-config.d.ts +60 -0
  93. package/dist/server/read-config.js +8 -0
  94. package/dist/server/storage-mode.d.ts +17 -0
  95. package/dist/server/storage-mode.js +12 -0
  96. package/dist/source/adapter.d.ts +65 -0
  97. package/dist/source/adapter.js +20 -0
  98. package/dist/source/adapters/drizzle.d.ts +43 -0
  99. package/dist/source/adapters/drizzle.js +185 -0
  100. package/dist/source/adapters/memory.d.ts +12 -0
  101. package/dist/source/adapters/memory.js +114 -0
  102. package/dist/source/adapters/prisma.d.ts +57 -0
  103. package/dist/source/adapters/prisma.js +176 -0
  104. package/dist/source/conformance.d.ts +32 -0
  105. package/dist/source/conformance.js +134 -0
  106. package/dist/source/contract.d.ts +144 -0
  107. package/dist/source/contract.js +99 -0
  108. package/dist/source/index.d.ts +62 -10
  109. package/dist/source/index.js +99 -0
  110. package/dist/source/migrations.d.ts +14 -0
  111. package/dist/source/migrations.js +39 -0
  112. package/dist/source/next.d.ts +33 -0
  113. package/dist/source/next.js +26 -0
  114. package/dist/sync/BootstrapHelper.d.ts +10 -0
  115. package/dist/sync/BootstrapHelper.js +10 -15
  116. package/dist/sync/ConnectionManager.d.ts +55 -1
  117. package/dist/sync/ConnectionManager.js +155 -16
  118. package/dist/sync/HydrationCoordinator.d.ts +93 -17
  119. package/dist/sync/HydrationCoordinator.js +238 -39
  120. package/dist/sync/NetworkProbe.d.ts +58 -24
  121. package/dist/sync/NetworkProbe.js +118 -42
  122. package/dist/sync/SyncWebSocket.d.ts +45 -70
  123. package/dist/sync/SyncWebSocket.js +70 -36
  124. package/dist/sync/createIntentStream.js +10 -1
  125. package/dist/types/streams.d.ts +9 -0
  126. package/dist/utils/mobx-setup.js +1 -0
  127. package/dist/webhooks/events.d.ts +38 -0
  128. package/dist/webhooks/events.js +40 -0
  129. package/dist/webhooks/index.d.ts +10 -0
  130. package/dist/webhooks/index.js +10 -0
  131. package/dist/wire/errorEnvelope.d.ts +34 -0
  132. package/dist/wire/errorEnvelope.js +86 -0
  133. package/dist/wire/frames.d.ts +119 -0
  134. package/dist/wire/frames.js +1 -0
  135. package/dist/wire/index.d.ts +24 -0
  136. package/dist/wire/index.js +21 -0
  137. package/dist/wire/listEnvelope.d.ts +45 -0
  138. package/dist/wire/listEnvelope.js +17 -0
  139. package/docs/api.md +47 -44
  140. package/docs/cli.md +44 -44
  141. package/docs/client-behavior.md +30 -30
  142. package/docs/coordination.md +33 -36
  143. package/docs/data-sources.md +35 -15
  144. package/docs/examples/agent-human.md +45 -43
  145. package/docs/examples/ai-sdk-tool.md +20 -16
  146. package/docs/examples/existing-python-backend.md +16 -12
  147. package/docs/examples/nextjs.md +14 -12
  148. package/docs/examples/scoped-agent.md +1 -1
  149. package/docs/examples/server-agent.md +24 -21
  150. package/docs/guarantees.md +15 -13
  151. package/docs/index.md +2 -2
  152. package/docs/integration-guide.md +30 -30
  153. package/docs/interaction-model.md +19 -23
  154. package/docs/mcp/claude-code.md +3 -3
  155. package/docs/mcp/cursor.md +1 -1
  156. package/docs/mcp/windsurf.md +2 -2
  157. package/docs/mcp.md +6 -6
  158. package/docs/quickstart.md +41 -31
  159. package/docs/react.md +13 -9
  160. package/docs/schema-contract.md +12 -10
  161. package/docs/the-loop.md +21 -0
  162. package/examples/data-source/README.md +4 -5
  163. package/examples/data-source/customer-server.ts +27 -25
  164. package/llms.txt +28 -5
  165. package/package.json +43 -3
@@ -0,0 +1,185 @@
1
+ /**
2
+ * Drizzle Data Source adapter. Same adapter interface + conformance as `prismaDataSource`,
3
+ * built against Drizzle's REAL API (read from drizzle-orm's own source/docs):
4
+ * - `db.transaction(async (tx) => …)` — interactive transaction (commit/rollback).
5
+ * - `db.execute(sql`…`)` — parametrized raw SQL; `sql.identifier()` safely quotes
6
+ * dynamic table/column names, `sql`${value}`` parametrizes values.
7
+ *
8
+ * SCHEMA-DRIVEN COLUMNS. Unlike Prisma — whose delegate applies the model's
9
+ * `@map` for free — this adapter writes raw SQL, so it would otherwise bypass any
10
+ * field→column translation. It therefore derives every table + column name from
11
+ * the SAME rule the provisioner uses (`generateProvisionPlan`):
12
+ * table = `model.tableName ?? key`
13
+ * column = `fieldMeta.column ?? camelToSnake(field)` (+ the model's tenancy column)
14
+ * so `ablo migrate` (which emits `operator_id`) and this adapter (which now writes
15
+ * `operator_id`) COMPOSE. Define the schema once, point Ablo at your Postgres —
16
+ * no hand-written parallel Drizzle table. The adapter is the translation boundary:
17
+ * its public surface (rows in/out, outbox `data`) is field-keyed (the SDK shape);
18
+ * the physical columns it reads/writes are snake_case.
19
+ *
20
+ * IMPORTANT GOTCHAS (from drizzle-orm docs):
21
+ * 1. Interactive `db.transaction` requires a driver that supports it. Neon's
22
+ * `neon-http` driver does NOT (single-shot only) — use `neon-serverless`
23
+ * (WebSocket) or `pg`. With neon-http the commit path throws at runtime.
24
+ * 2. `db.execute` result shape is driver-specific (postgres-js returns an
25
+ * array-like RowList; node-postgres returns `{ rows }`). `rowsOf()`
26
+ * normalizes both.
27
+ *
28
+ * We use `sql` + `db.execute` for ALL writes (not the fluent builder) so the
29
+ * adapter is one small, fully-typed unit with no per-driver builder generics.
30
+ */
31
+ import { sql } from 'drizzle-orm';
32
+ import { outboxEventSchema } from '../contract.js';
33
+ import { adapterTableMigrations } from '../migrations.js';
34
+ import { toSchemaJSON } from '../../schema/serialize.js';
35
+ import { camelToSnake, snakeToCamel } from '../../schema/ddl.js';
36
+ import { tenancyColumn } from '../../schema/tenancy.js';
37
+ function rowsOf(result) {
38
+ return Array.isArray(result) ? result : result.rows;
39
+ }
40
+ function rowId(op) {
41
+ const id = op.id ?? op.input?.id;
42
+ if (typeof id !== 'string' || id.length === 0) {
43
+ throw new Error(`operation on "${op.model}" requires an id`);
44
+ }
45
+ return id;
46
+ }
47
+ /** `col1, col2` as a safely-quoted identifier list. */
48
+ const identList = (cols) => sql.join(cols.map((c) => sql.identifier(c)), sql `, `);
49
+ function buildColumnMaps(schema) {
50
+ const json = toSchemaJSON(schema);
51
+ const out = new Map();
52
+ for (const [key, model] of Object.entries(json.models)) {
53
+ const fieldToColumn = new Map();
54
+ const columnToField = new Map();
55
+ const register = (field, column) => {
56
+ // The default rule already covers `camelToSnake(field)`; only record real
57
+ // divergences so the reverse map never shadows a clean round-trip.
58
+ if (column === camelToSnake(field))
59
+ return;
60
+ fieldToColumn.set(field, column);
61
+ columnToField.set(column, field);
62
+ };
63
+ for (const [field, meta] of Object.entries(model.fields)) {
64
+ if (meta.column)
65
+ register(field, meta.column);
66
+ }
67
+ const orgColumn = tenancyColumn(model.tenancy);
68
+ if (orgColumn)
69
+ register('organizationId', orgColumn);
70
+ out.set(key, { table: model.tableName ?? key, fieldToColumn, columnToField });
71
+ }
72
+ return out;
73
+ }
74
+ export function drizzleDataSource(db, schema) {
75
+ const maps = buildColumnMaps(schema);
76
+ const modelColumns = (model) => {
77
+ const mc = maps.get(model);
78
+ if (!mc)
79
+ throw new Error(`drizzleDataSource: no model "${model}" in schema`);
80
+ return mc;
81
+ };
82
+ const columnFor = (mc, field) => mc.fieldToColumn.get(field) ?? camelToSnake(field);
83
+ const fieldFor = (mc, column) => mc.columnToField.get(column) ?? snakeToCamel(column);
84
+ /** Field-keyed (SDK shape) → column-keyed (physical), for INSERT/UPDATE. */
85
+ const toColumns = (mc, row) => {
86
+ const out = {};
87
+ for (const k of Object.keys(row))
88
+ out[columnFor(mc, k)] = row[k];
89
+ return out;
90
+ };
91
+ /** Column-keyed (RETURNING * / SELECT *) → field-keyed (SDK shape), for reads + results. */
92
+ const toFields = (mc, row) => {
93
+ const out = {};
94
+ for (const k of Object.keys(row))
95
+ out[fieldFor(mc, k)] = row[k];
96
+ return out;
97
+ };
98
+ const applyOperation = async (tx, op) => {
99
+ const mc = modelColumns(op.model);
100
+ const table = sql.identifier(mc.table);
101
+ const id = rowId(op);
102
+ const input = op.input ?? {};
103
+ if (op.type === 'DELETE') {
104
+ const deleted = rowsOf(await tx.execute(sql `DELETE FROM ${table} WHERE id = ${id} RETURNING *`));
105
+ return deleted[0] ? toFields(mc, deleted[0]) : { id };
106
+ }
107
+ if (op.type === 'CREATE') {
108
+ const data = toColumns(mc, { id, ...input });
109
+ const cols = Object.keys(data);
110
+ const values = sql.join(cols.map((c) => sql `${data[c]}`), sql `, `);
111
+ const inserted = rowsOf(await tx.execute(sql `INSERT INTO ${table} (${identList(cols)}) VALUES (${values}) RETURNING *`));
112
+ return inserted[0] ? toFields(mc, inserted[0]) : { id, ...input };
113
+ }
114
+ // UPDATE / ARCHIVE / UNARCHIVE — a SET clause + the lifecycle field. The
115
+ // lifecycle field is `archivedAt` (camelCase) and goes through `toColumns`
116
+ // like any other, so it lands in `archived_at` — same column the provisioner
117
+ // emits and the Prisma adapter writes (no per-adapter casing divergence).
118
+ const patch = toColumns(mc, {
119
+ ...input,
120
+ ...(op.type === 'ARCHIVE' ? { archivedAt: new Date() } : {}),
121
+ ...(op.type === 'UNARCHIVE' ? { archivedAt: null } : {}),
122
+ });
123
+ const assignments = sql.join(Object.keys(patch).map((c) => sql `${sql.identifier(c)} = ${patch[c]}`), sql `, `);
124
+ const updated = rowsOf(await tx.execute(sql `UPDATE ${table} SET ${assignments} WHERE id = ${id} RETURNING *`));
125
+ return updated[0] ? toFields(mc, updated[0]) : { id, ...input };
126
+ };
127
+ return {
128
+ capabilities: { transactions: true, propose: false, schemaIntrospection: true },
129
+ migrations() {
130
+ return adapterTableMigrations();
131
+ },
132
+ async read(req) {
133
+ const mc = modelColumns(req.model);
134
+ const table = sql.identifier(mc.table);
135
+ if (req.kind === 'load') {
136
+ const rows = rowsOf(await db.execute(sql `SELECT * FROM ${table} WHERE id = ${req.id} LIMIT 1`));
137
+ return rows.map((r) => toFields(mc, r));
138
+ }
139
+ const limit = req.query?.limit ?? 1000;
140
+ const rows = rowsOf(await db.execute(sql `SELECT * FROM ${table} LIMIT ${limit}`));
141
+ return rows.map((r) => toFields(mc, r));
142
+ },
143
+ async commit(change) {
144
+ return db.transaction(async (tx) => {
145
+ const cached = rowsOf(await tx.execute(sql `SELECT response FROM ablo_idempotency WHERE client_tx_id = ${change.clientTxId} LIMIT 1`));
146
+ if (cached.length > 0)
147
+ return { rows: cached[0].response };
148
+ const rows = [];
149
+ for (const [index, op] of change.operations.entries()) {
150
+ const row = await applyOperation(tx, op);
151
+ rows.push(row);
152
+ const entityId = String(row.id ?? rowId(op));
153
+ await tx.execute(sql `
154
+ INSERT INTO ablo_outbox (id, model, entity_id, type, data, client_tx_id, occurred_at)
155
+ VALUES (
156
+ ${`${change.clientTxId}:${index}`}, ${op.model}, ${entityId}, ${op.type},
157
+ ${op.type === 'DELETE' ? null : JSON.stringify(row)}::jsonb, ${change.clientTxId}, ${Date.now()}
158
+ )`);
159
+ }
160
+ await tx.execute(sql `
161
+ INSERT INTO ablo_idempotency (client_tx_id, response)
162
+ VALUES (${change.clientTxId}, ${JSON.stringify(rows)}::jsonb)`);
163
+ return { rows };
164
+ });
165
+ },
166
+ async events(cursor, limit) {
167
+ const after = cursor ?? '0';
168
+ const rows = rowsOf(await db.execute(sql `
169
+ SELECT cursor, id, model, entity_id, type, data, organization_id, client_tx_id, occurred_at
170
+ FROM ablo_outbox WHERE cursor > ${after} ORDER BY cursor ASC LIMIT ${limit}`));
171
+ const events = rows.map((r) => outboxEventSchema.parse({
172
+ id: r.id,
173
+ model: r.model,
174
+ entityId: r.entity_id,
175
+ type: r.type,
176
+ data: r.data ?? null,
177
+ organizationId: r.organization_id ?? null,
178
+ clientTxId: r.client_tx_id ?? null,
179
+ occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
180
+ cursor: String(r.cursor),
181
+ }));
182
+ return { events, nextCursor: events.length > 0 ? events[events.length - 1].cursor : null };
183
+ },
184
+ };
185
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * In-memory reference Data Source adapter — the canonical correct implementation
3
+ * of the adapter interface. It is the test double for the bridge/handler AND the thing the
4
+ * conformance suite runs against to prove the suite itself is real (same role as
5
+ * the server's `memoryTenantDirectory`). A new ORM adapter is "done" when it
6
+ * passes the same suite this one passes.
7
+ *
8
+ * It models the real semantics minimally but faithfully: one canonical row store
9
+ * per model, an idempotency ledger keyed by `clientTxId`, and a monotonic outbox.
10
+ */
11
+ import type { DataSourceAdapter } from '../adapter.js';
12
+ export declare function memoryDataSource(): DataSourceAdapter;
@@ -0,0 +1,114 @@
1
+ /**
2
+ * In-memory reference Data Source adapter — the canonical correct implementation
3
+ * of the adapter interface. It is the test double for the bridge/handler AND the thing the
4
+ * conformance suite runs against to prove the suite itself is real (same role as
5
+ * the server's `memoryTenantDirectory`). A new ORM adapter is "done" when it
6
+ * passes the same suite this one passes.
7
+ *
8
+ * It models the real semantics minimally but faithfully: one canonical row store
9
+ * per model, an idempotency ledger keyed by `clientTxId`, and a monotonic outbox.
10
+ */
11
+ function rowId(op) {
12
+ const id = op.id ?? op.input?.id;
13
+ if (typeof id !== 'string' || id.length === 0) {
14
+ throw new Error(`operation on "${op.model}" requires an id`);
15
+ }
16
+ return id;
17
+ }
18
+ export function memoryDataSource() {
19
+ /** model → (id → row). */
20
+ const store = new Map();
21
+ /** clientTxId → the rows that commit returned (idempotency ledger). */
22
+ const idempotency = new Map();
23
+ /** Append-only outbox; `cursor` is the 1-based index as a string. */
24
+ const outbox = [];
25
+ const modelStore = (model) => {
26
+ let m = store.get(model);
27
+ if (!m) {
28
+ m = new Map();
29
+ store.set(model, m);
30
+ }
31
+ return m;
32
+ };
33
+ const applyOperation = (op) => {
34
+ const m = modelStore(op.model);
35
+ const id = rowId(op);
36
+ switch (op.type) {
37
+ case 'CREATE': {
38
+ const row = { id, ...(op.input ?? {}) };
39
+ m.set(id, row);
40
+ return row;
41
+ }
42
+ case 'UPDATE':
43
+ case 'ARCHIVE':
44
+ case 'UNARCHIVE': {
45
+ const prev = m.get(id) ?? { id };
46
+ const row = {
47
+ ...prev,
48
+ ...(op.input ?? {}),
49
+ ...(op.type === 'ARCHIVE' ? { archivedAt: Date.now() } : {}),
50
+ ...(op.type === 'UNARCHIVE' ? { archivedAt: null } : {}),
51
+ };
52
+ m.set(id, row);
53
+ return row;
54
+ }
55
+ case 'DELETE': {
56
+ const prev = m.get(id) ?? { id };
57
+ m.delete(id);
58
+ return prev;
59
+ }
60
+ }
61
+ };
62
+ return {
63
+ capabilities: { transactions: true, propose: false, schemaIntrospection: false },
64
+ migrations() {
65
+ // In-memory: no table-creation SQL. A real ORM adapter ships ablo_idempotency + ablo_outbox here.
66
+ return [];
67
+ },
68
+ async read(req) {
69
+ const m = store.get(req.model);
70
+ if (!m)
71
+ return [];
72
+ if (req.kind === 'load') {
73
+ const row = m.get(req.id);
74
+ return row ? [row] : [];
75
+ }
76
+ let rows = [...m.values()];
77
+ const limit = req.query?.limit;
78
+ if (typeof limit === 'number')
79
+ rows = rows.slice(0, limit);
80
+ return rows;
81
+ },
82
+ async commit(change) {
83
+ // Idempotency: a duplicate clientTxId returns the original rows, no re-apply.
84
+ const cached = idempotency.get(change.clientTxId);
85
+ if (cached)
86
+ return { rows: cached };
87
+ const rows = [];
88
+ for (const [index, op] of change.operations.entries()) {
89
+ const row = applyOperation(op);
90
+ rows.push(row);
91
+ // Transactional outbox: one event per op, monotonic cursor.
92
+ outbox.push({
93
+ id: `${change.clientTxId}:${index}`,
94
+ model: op.model,
95
+ entityId: String(row.id ?? rowId(op)),
96
+ type: op.type,
97
+ data: op.type === 'DELETE' ? null : row,
98
+ clientTxId: change.clientTxId,
99
+ cursor: String(outbox.length + 1),
100
+ });
101
+ }
102
+ idempotency.set(change.clientTxId, rows);
103
+ return { rows };
104
+ },
105
+ async events(cursor, limit) {
106
+ const after = cursor ? Number(cursor) : 0;
107
+ const page = outbox.filter((e) => Number(e.cursor) > after).slice(0, limit);
108
+ return {
109
+ events: page,
110
+ nextCursor: page.length > 0 ? page[page.length - 1].cursor : null,
111
+ };
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Prisma Data Source adapter. The first real ORM adapter (Auth.js pattern: one
3
+ * package per ORM, all behind the `DataSourceAdapter` interface, all proven by the
4
+ * same conformance suite the in-memory reference passes).
5
+ *
6
+ * It owns the transactional outbox + idempotency so the customer never writes
7
+ * them: `commit` runs the app-row mutations, the `ablo_outbox` append, and the
8
+ * `ablo_idempotency` record in ONE `prisma.$transaction`. `migrations()` ships
9
+ * the table-creation SQL for those two tables.
10
+ *
11
+ * No `@prisma/client` dependency: the client is accepted structurally
12
+ * (`PrismaLike`), so this compiles in the SDK package and is unit-testable with
13
+ * a fake, while a real `PrismaClient` satisfies it at the call site.
14
+ */
15
+ import type { DataSourceAdapter, Row } from '../adapter.js';
16
+ import type { SchemaRecord, Schema } from '../../schema/schema.js';
17
+ /** A Prisma model delegate (the subset we call). */
18
+ export interface PrismaDelegate {
19
+ findUnique(args: {
20
+ where: {
21
+ id: string;
22
+ };
23
+ }): Promise<Row | null>;
24
+ findMany(args: {
25
+ where?: Record<string, unknown>;
26
+ take?: number;
27
+ orderBy?: Record<string, 'asc' | 'desc'>;
28
+ }): Promise<Row[]>;
29
+ create(args: {
30
+ data: Row;
31
+ }): Promise<Row>;
32
+ update(args: {
33
+ where: {
34
+ id: string;
35
+ };
36
+ data: Row;
37
+ }): Promise<Row>;
38
+ delete(args: {
39
+ where: {
40
+ id: string;
41
+ };
42
+ }): Promise<Row>;
43
+ }
44
+ /** The raw-SQL surface used for the adapter-owned tables. */
45
+ export interface PrismaRaw {
46
+ $executeRawUnsafe(query: string, ...values: unknown[]): Promise<number>;
47
+ $queryRawUnsafe<T = unknown>(query: string, ...values: unknown[]): Promise<T>;
48
+ }
49
+ /** A Prisma client (or interactive-transaction client) — structural, no SDK dependency. */
50
+ export interface PrismaLike extends PrismaRaw {
51
+ $transaction<T>(fn: (tx: PrismaLike & PrismaRaw) => Promise<T>): Promise<T>;
52
+ }
53
+ export interface PrismaDataSourceOptions {
54
+ /** Map a schema model name → its Prisma delegate name. Default: lower-first-letter. */
55
+ readonly delegateName?: (model: string) => string;
56
+ }
57
+ export declare function prismaDataSource<S extends SchemaRecord>(prisma: PrismaLike, schema: Schema<S>, options?: PrismaDataSourceOptions): DataSourceAdapter;
@@ -0,0 +1,176 @@
1
+ /**
2
+ * Prisma Data Source adapter. The first real ORM adapter (Auth.js pattern: one
3
+ * package per ORM, all behind the `DataSourceAdapter` interface, all proven by the
4
+ * same conformance suite the in-memory reference passes).
5
+ *
6
+ * It owns the transactional outbox + idempotency so the customer never writes
7
+ * them: `commit` runs the app-row mutations, the `ablo_outbox` append, and the
8
+ * `ablo_idempotency` record in ONE `prisma.$transaction`. `migrations()` ships
9
+ * the table-creation SQL for those two tables.
10
+ *
11
+ * No `@prisma/client` dependency: the client is accepted structurally
12
+ * (`PrismaLike`), so this compiles in the SDK package and is unit-testable with
13
+ * a fake, while a real `PrismaClient` satisfies it at the call site.
14
+ */
15
+ import { outboxEventSchema } from '../contract.js';
16
+ import { adapterTableMigrations } from '../migrations.js';
17
+ const lowerFirst = (s) => (s ? s[0].toLowerCase() + s.slice(1) : s);
18
+ /**
19
+ * Resolve a model's Prisma delegate by name. This is the ONE irreducible cast in
20
+ * the adapter layer, and it's a genuine type-system limit, not laziness:
21
+ *
22
+ * - Inside `prisma.$transaction(tx => …)` the writes MUST go through the
23
+ * transactional client `tx`, and the model is only known as a runtime string.
24
+ * - Prisma's client (and transaction handle) is NOMINALLY keyed (`{ task: TaskDelegate; … }`), so a
25
+ * dynamic `tx[name]` is `unknown` to the compiler — there is no key to infer.
26
+ *
27
+ * Dynamic property access on a statically-keyed type cannot be typed without an
28
+ * assertion; this is the reflection boundary, validated at runtime (`findMany` is
29
+ * a function) right after. `ablo generate` removes even this by emitting a typed
30
+ * `model → delegate` map, at which point this helper is replaced by a lookup.
31
+ */
32
+ function delegateFor(client, name) {
33
+ const delegate = client[name];
34
+ if (!delegate || typeof delegate.findMany !== 'function') {
35
+ throw new Error(`prismaDataSource: no Prisma delegate "${name}" on the client`);
36
+ }
37
+ return delegate;
38
+ }
39
+ /** Translate a Source `where` tuple set into a Prisma `where` object. */
40
+ function toPrismaWhere(where) {
41
+ const out = {};
42
+ for (const clause of where ?? []) {
43
+ const [field] = clause;
44
+ if (clause.length === 2) {
45
+ out[field] = clause[1];
46
+ continue;
47
+ }
48
+ const [, op, value] = clause;
49
+ switch (op) {
50
+ case '=':
51
+ out[field] = value;
52
+ break;
53
+ case '!=':
54
+ out[field] = { not: value };
55
+ break;
56
+ case '<':
57
+ out[field] = { lt: value };
58
+ break;
59
+ case '<=':
60
+ out[field] = { lte: value };
61
+ break;
62
+ case '>':
63
+ out[field] = { gt: value };
64
+ break;
65
+ case '>=':
66
+ out[field] = { gte: value };
67
+ break;
68
+ case 'IN':
69
+ out[field] = { in: value };
70
+ break;
71
+ case 'NOT IN':
72
+ out[field] = { notIn: value };
73
+ break;
74
+ case 'LIKE':
75
+ case 'ILIKE':
76
+ out[field] = { contains: value, mode: op === 'ILIKE' ? 'insensitive' : 'default' };
77
+ break;
78
+ case 'NOT LIKE':
79
+ case 'NOT ILIKE':
80
+ out[field] = { not: { contains: value } };
81
+ break;
82
+ case 'IS':
83
+ case 'IS NOT':
84
+ out[field] = op === 'IS' ? value : { not: value };
85
+ break;
86
+ }
87
+ }
88
+ return out;
89
+ }
90
+ function findManyArgs(query) {
91
+ return {
92
+ where: toPrismaWhere(query?.where),
93
+ ...(typeof query?.limit === 'number' ? { take: query.limit } : {}),
94
+ ...(query?.orderBy ? { orderBy: { [query.orderBy]: query.order ?? 'asc' } } : {}),
95
+ };
96
+ }
97
+ function rowId(op) {
98
+ const id = op.id ?? op.input?.id;
99
+ if (typeof id !== 'string' || id.length === 0) {
100
+ throw new Error(`operation on "${op.model}" requires an id`);
101
+ }
102
+ return id;
103
+ }
104
+ export function prismaDataSource(prisma, schema, options = {}) {
105
+ const delegateName = options.delegateName ?? lowerFirst;
106
+ void schema; // reserved for codegen-typed reads / model validation
107
+ const applyOperation = async (tx, op) => {
108
+ const delegate = delegateFor(tx, delegateName(op.model));
109
+ const id = rowId(op);
110
+ switch (op.type) {
111
+ case 'CREATE':
112
+ return delegate.create({ data: { id, ...(op.input ?? {}) } });
113
+ case 'UPDATE':
114
+ return delegate.update({ where: { id }, data: { ...(op.input ?? {}) } });
115
+ case 'ARCHIVE':
116
+ return delegate.update({ where: { id }, data: { ...(op.input ?? {}), archivedAt: new Date() } });
117
+ case 'UNARCHIVE':
118
+ return delegate.update({ where: { id }, data: { ...(op.input ?? {}), archivedAt: null } });
119
+ case 'DELETE':
120
+ return delegate.delete({ where: { id } });
121
+ }
122
+ };
123
+ return {
124
+ capabilities: { transactions: true, propose: false, schemaIntrospection: true },
125
+ migrations() {
126
+ return adapterTableMigrations();
127
+ },
128
+ async read(req) {
129
+ const delegate = delegateFor(prisma, delegateName(req.model));
130
+ if (req.kind === 'load') {
131
+ const row = await delegate.findUnique({ where: { id: req.id } });
132
+ return row ? [row] : [];
133
+ }
134
+ return delegate.findMany(findManyArgs(req.query));
135
+ },
136
+ async commit(change) {
137
+ return prisma.$transaction(async (tx) => {
138
+ // Idempotency: a duplicate clientTxId returns the original rows, no re-apply.
139
+ const cached = await tx.$queryRawUnsafe(`SELECT response FROM ablo_idempotency WHERE client_tx_id = $1 LIMIT 1`, change.clientTxId);
140
+ if (cached.length > 0)
141
+ return { rows: cached[0].response };
142
+ const rows = [];
143
+ for (const [index, op] of change.operations.entries()) {
144
+ const row = await applyOperation(tx, op);
145
+ rows.push(row);
146
+ const entityId = String(row.id ?? rowId(op));
147
+ // Transactional outbox: one event per op, written in THIS transaction.
148
+ await tx.$executeRawUnsafe(`INSERT INTO ablo_outbox (id, model, entity_id, type, data, client_tx_id, occurred_at)
149
+ VALUES ($1, $2, $3, $4, $5::jsonb, $6, $7)`, `${change.clientTxId}:${index}`, op.model, entityId, op.type, JSON.stringify(op.type === 'DELETE' ? null : row), change.clientTxId, Date.now());
150
+ }
151
+ await tx.$executeRawUnsafe(`INSERT INTO ablo_idempotency (client_tx_id, response) VALUES ($1, $2::jsonb)`, change.clientTxId, JSON.stringify(rows));
152
+ return { rows };
153
+ });
154
+ },
155
+ async events(cursor, limit) {
156
+ const after = cursor ? cursor : '0';
157
+ const rows = await prisma.$queryRawUnsafe(`SELECT cursor, id, model, entity_id, type, data, organization_id, client_tx_id, occurred_at
158
+ FROM ablo_outbox WHERE cursor > $1 ORDER BY cursor ASC LIMIT $2`, after, limit);
159
+ const events = rows.map((r) => outboxEventSchema.parse({
160
+ id: r.id,
161
+ model: r.model,
162
+ entityId: r.entity_id,
163
+ type: r.type,
164
+ data: r.data ?? null,
165
+ organizationId: r.organization_id ?? null,
166
+ clientTxId: r.client_tx_id ?? null,
167
+ occurredAt: r.occurred_at != null ? Number(r.occurred_at) : null,
168
+ cursor: String(r.cursor),
169
+ }));
170
+ return {
171
+ events,
172
+ nextCursor: events.length > 0 ? events[events.length - 1].cursor : null,
173
+ };
174
+ },
175
+ };
176
+ }
@@ -0,0 +1,32 @@
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 adapter interface 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 type { DataSourceAdapter } from './adapter.js';
20
+ export type MakeAdapter = () => DataSourceAdapter | Promise<DataSourceAdapter>;
21
+ /** A single conformance check. `run` throws on failure. */
22
+ export interface ConformanceCheck {
23
+ readonly name: string;
24
+ run(): Promise<void>;
25
+ }
26
+ export declare function dataSourceConformanceChecks(make: MakeAdapter): ConformanceCheck[];
27
+ /**
28
+ * Register the conformance checks with a test runner's `it`/`test` function.
29
+ * `register(name, fn)` — pass vitest/jest `it` or `node:test` `test`.
30
+ */
31
+ export declare function runDataSourceTests(make: MakeAdapter, register: (name: string, fn: () => Promise<void>) => void): void;
32
+ export { memoryDataSource } from './adapters/memory.js';