@abloatai/ablo 0.9.1 → 0.9.3

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 (79) hide show
  1. package/AGENTS.md +84 -0
  2. package/CHANGELOG.md +40 -0
  3. package/README.md +53 -27
  4. package/dist/BaseSyncedStore.d.ts +2 -36
  5. package/dist/BaseSyncedStore.js +11 -55
  6. package/dist/NetworkMonitor.js +4 -1
  7. package/dist/SyncClient.d.ts +22 -5
  8. package/dist/SyncClient.js +77 -0
  9. package/dist/SyncEngineContext.js +5 -1
  10. package/dist/agent/index.js +1 -1
  11. package/dist/api/index.d.ts +1 -1
  12. package/dist/auth/index.js +3 -1
  13. package/dist/cli.cjs +302645 -0
  14. package/dist/client/Ablo.d.ts +19 -52
  15. package/dist/client/Ablo.js +30 -106
  16. package/dist/client/ApiClient.d.ts +1 -113
  17. package/dist/client/ApiClient.js +39 -238
  18. package/dist/client/auth.js +32 -2
  19. package/dist/client/createInternalComponents.js +1 -1
  20. package/dist/client/createModelProxy.d.ts +9 -0
  21. package/dist/client/createModelProxy.js +34 -10
  22. package/dist/client/httpClient.d.ts +5 -6
  23. package/dist/client/httpClient.js +2 -3
  24. package/dist/client/index.d.ts +1 -1
  25. package/dist/client/persistence.d.ts +6 -1
  26. package/dist/client/persistence.js +1 -1
  27. package/dist/client/registerDataSource.d.ts +4 -4
  28. package/dist/client/registerDataSource.js +39 -31
  29. package/dist/client/writeOptionsSchema.d.ts +50 -0
  30. package/dist/client/writeOptionsSchema.js +57 -0
  31. package/dist/core/index.d.ts +18 -26
  32. package/dist/core/index.js +22 -46
  33. package/dist/errorCodes.d.ts +13 -0
  34. package/dist/errorCodes.js +19 -4
  35. package/dist/index.d.ts +3 -0
  36. package/dist/index.js +8 -1
  37. package/dist/interfaces/index.d.ts +14 -4
  38. package/dist/mutators/UndoManager.d.ts +48 -5
  39. package/dist/mutators/UndoManager.js +166 -1
  40. package/dist/react/AbloProvider.d.ts +18 -8
  41. package/dist/react/index.d.ts +1 -1
  42. package/dist/react/index.js +1 -1
  43. package/dist/react/useUndoScope.js +7 -0
  44. package/dist/schema/ddl.js +2 -1
  45. package/dist/schema/field.js +2 -1
  46. package/dist/schema/serialize.js +2 -1
  47. package/dist/server/commit.d.ts +4 -5
  48. package/dist/server/storage-mode.d.ts +7 -0
  49. package/dist/server/storage-mode.js +6 -0
  50. package/dist/source/adapters/drizzle.js +3 -2
  51. package/dist/source/adapters/kysely.d.ts +68 -0
  52. package/dist/source/adapters/kysely.js +210 -0
  53. package/dist/source/adapters/memory.js +2 -1
  54. package/dist/source/adapters/prisma.js +3 -2
  55. package/dist/source/index.js +2 -1
  56. package/dist/transactions/TransactionQueue.d.ts +6 -7
  57. package/dist/transactions/TransactionQueue.js +33 -9
  58. package/dist/types/streams.d.ts +2 -1
  59. package/dist/utils/duration.js +3 -2
  60. package/dist/wire/frames.d.ts +6 -8
  61. package/docs/api.md +1 -1
  62. package/docs/cli.md +17 -4
  63. package/docs/client-behavior.md +1 -1
  64. package/docs/data-sources.md +129 -125
  65. package/docs/examples/ai-sdk-tool.md +11 -5
  66. package/docs/examples/existing-python-backend.md +26 -4
  67. package/docs/examples/nextjs.md +3 -2
  68. package/docs/examples/scoped-agent.md +38 -11
  69. package/docs/guarantees.md +2 -2
  70. package/docs/identity.md +86 -59
  71. package/docs/index.md +2 -2
  72. package/docs/integration-guide.md +89 -61
  73. package/docs/mcp.md +1 -1
  74. package/docs/quickstart.md +84 -37
  75. package/docs/react.md +39 -28
  76. package/docs/schema-contract.md +2 -4
  77. package/llms-full.txt +360 -0
  78. package/llms.txt +30 -18
  79. package/package.json +23 -3
package/docs/identity.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Identity & Sync Groups
2
2
 
3
3
  This is the doc the Quickstart skips: **who is connecting, and which slice
4
- of shared state do they get?** If you've wired `<AbloProvider schema={schema}>`
4
+ of shared state do they get?** If you've wired `<AbloProvider client={ablo}>`
5
5
  and wondered where org / team / user actually come from — start here.
6
6
 
7
7
  ## Ablo does not do auth
@@ -37,9 +37,9 @@ runnable place, so the concepts below have code to attach to.
37
37
 
38
38
  The entire declaration surface is: `identityRoles` (who may see what), and on
39
39
  each model `scope` / `parent` / `grants` (which group a row fans out on), plus an
40
- optional `syncGroups` prop (narrowing). Read the three blocks first — a human
41
- gets their `org` / `team` scope, an agent gets one `deck` — then the sections
42
- after explain each.
40
+ optional `scope` setting on the client (narrowing). Read the three blocks first —
41
+ a human gets their `org` / `team` scope, an agent gets one `deck` — then the
42
+ sections after explain each.
43
43
 
44
44
  ```ts
45
45
  // 1. src/ablo/schema.ts — map identity → groups, and anchor each model to a group
@@ -74,23 +74,28 @@ export const schema = defineSchema(
74
74
  ```
75
75
 
76
76
  ```tsx
77
- // 2. app/providers.tsx — a HUMAN gets their full org / team scope
78
- <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
77
+ // 2. app/providers.tsx — a HUMAN gets their full org / team scope.
78
+ // teamIds is set on the client you build (Ablo({ schema, teamIds: user.teamIds })),
79
+ // not passed to the provider; the provider just takes that client.
80
+ <AbloProvider client={ablo} userId={user.id}>
79
81
  {children}
80
82
  </AbloProvider>
81
83
  ```
82
84
 
83
- ```tsx
85
+ ```ts
84
86
  // 3. an AGENT run inherits its user, narrowed to the entities in play.
85
87
  // Pass the MODEL form — { decks: id } — not a hand-built `deck:<id>` string;
86
- // the engine derives the group from each model's `scope`.
87
- <AbloProvider
88
- schema={schema}
89
- userId={user.id} // ceiling: the triggering user
90
- scope={{ decks: deckId }} // floor: just the deck it's working on
91
- >
88
+ // the engine derives the group from each model's `scope`. The narrowing lives
89
+ // on the client you build, then the provider mounts it.
90
+ const ablo = Ablo({
91
+ schema,
92
+ authEndpoint: '/api/ablo-session',
93
+ scope: { decks: deckId }, // floor: just the deck it's working on
94
+ });
95
+
96
+ <AbloProvider client={ablo} userId={user.id /* ceiling: the triggering user */}>
92
97
  {children}
93
- </AbloProvider>
98
+ </AbloProvider>;
94
99
  ```
95
100
 
96
101
  That's the whole surface. The rest of this doc is the *why* behind each line.
@@ -126,8 +131,8 @@ That's why you never write per-user scope code, but you always pass an agent's
126
131
  scope at the call site. A user's org/team/user don't change per request, so
127
132
  their scope is a **rule the schema derives automatically**. An agent's reach
128
133
  depends on *what it's working on*, which is only knowable at dispatch — so you
129
- pass its `syncGroups` **at the call site, in code**. The schema's only job for
130
- entities is to declare *that* a model is
134
+ set its `scope` on the client **at the dispatch site, in code**. The schema's
135
+ only job for entities is to declare *that* a model is
131
136
  entity-scopable and *what its group is named* (`scope: 'deck'` → `deck:{id}`);
132
137
  it never declares *which* entities a given agent gets. (A human can opt into the
133
138
  same runtime narrowing — a page scoped to one deck — but by default a human's
@@ -298,45 +303,62 @@ server, never by the browser.**
298
303
 
299
304
  ## Wiring the provider
300
305
 
301
- The provider props carry the identity your server resolved. In a Next.js app,
302
- resolve the user in a Server Component and pass it down:
306
+ The identity your server resolved is carried by the client you build and the
307
+ `userId` prop. In a Next.js app, resolve the user in a Server Component and pass
308
+ it down. Build the client once (the schema, `teamIds`, and any `scope` narrowing
309
+ live here), then hand it to the provider:
310
+
311
+ ```ts
312
+ // lib/ablo.ts
313
+ import Ablo from '@abloatai/ablo';
314
+ import { schema } from '@/ablo/schema';
315
+
316
+ // Build the client from the identity your server already resolved.
317
+ // teamIds → team sync groups via identityRoles.
318
+ export function makeAblo(user: { teamIds: string[] }) {
319
+ return Ablo({
320
+ schema,
321
+ authEndpoint: '/api/ablo-session',
322
+ teamIds: user.teamIds,
323
+ });
324
+ }
325
+ ```
303
326
 
304
327
  ```tsx
305
- // app/providers.tsx — 'use client'
328
+ // app/providers.tsx
329
+ 'use client';
330
+
331
+ import { useMemo } from 'react';
306
332
  import { AbloProvider } from '@abloatai/ablo/react';
307
- import { schema } from '@/ablo/schema';
333
+ import { makeAblo } from '@/lib/ablo';
308
334
 
309
335
  export function Providers({
310
336
  children,
311
- user, // { id, teamIds } — resolved server-side from YOUR auth
337
+ user, // { id, teamIds } — resolved server-side from YOUR auth
312
338
  }: {
313
339
  children: React.ReactNode;
314
340
  user: { id: string; teamIds: string[] };
315
341
  }) {
342
+ const ablo = useMemo(() => makeAblo(user), [user.id]);
316
343
  return (
317
- <AbloProvider
318
- schema={schema}
319
- userId={user.id}
320
- teamIds={user.teamIds}
321
- fallback={<AppSkeleton />}
322
- >
344
+ <AbloProvider client={ablo} userId={user.id} fallback={<AppSkeleton />}>
323
345
  {children}
324
346
  </AbloProvider>
325
347
  );
326
348
  }
327
349
  ```
328
350
 
329
- What each identity-related prop does — and just as importantly, does *not* do:
351
+ What carries identity — and just as importantly, what does *not* set the boundary:
330
352
 
331
- | Prop | Purpose |
353
+ | Where | Purpose |
332
354
  | ------------ | ------------------------------------------------------------------------------------------------ |
333
- | `userId` | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary — the server enforces scope from the authenticated request. |
334
- | `teamIds` | Team ids expanded into team sync groups via your `identityRoles`. |
335
- | `syncGroups` | Optional. **Narrows** the subscription to a subset of what auth already allows — it can never widen it. Use it to scope a page to one entity (e.g. `['deck:abc123']`). |
355
+ | `userId` prop | App-level participant id, used for app-owned fields and read by your `identityRole` `source`. **Not** the security boundary — the server enforces scope from the authenticated request. |
356
+ | `teamIds` (on the client) | Team ids expanded into team sync groups via your `identityRoles`. |
357
+ | `scope` (on the client) | Optional. **Narrows** the subscription to a subset of what auth already allows — it can never widen it. Use it to scope a page to one entity (e.g. `{ decks: 'abc123' }`). |
336
358
 
337
359
  Because the server is the boundary, a client that changes `userId` to another
338
360
  user's id does not gain their data — the server resolves and enforces the real
339
- identity on the connection. The props are how your app *tells* Ablo who it
361
+ identity on the connection. These are how your app *tells* Ablo who it
340
362
  already authenticated, not how it *proves* it.
341
363
 
342
364
  ## Agents are participants too
@@ -374,16 +396,19 @@ decks: model({ /* … */ }, {}, { orgScoped: true, scope: 'deck' }),
374
396
  Then a run subscribes only to the entity groups for the rows it works on — a
375
397
  subset of what its user could see:
376
398
 
377
- ```tsx
378
- // agent run triggered by `user`, working on one document + one deck
379
- <AbloProvider
380
- schema={schema}
381
- // identity inherited from the triggering user (the ceiling)
382
- userId={user.id}
399
+ ```ts
400
+ // agent run triggered by `user`, working on one document + one deck.
401
+ // The narrowing lives on the client; the provider just mounts it.
402
+ const ablo = Ablo({
403
+ schema,
404
+ authEndpoint: '/api/ablo-session',
383
405
  // authority narrowed to just the entities in play (the floor).
384
406
  // Model form — keyed by model, resolved to groups via each model's `scope`.
385
- scope={{ documents: documentId, decks: deckId }}
386
- >
407
+ scope: { documents: documentId, decks: deckId },
408
+ });
409
+
410
+ // identity inherited from the triggering user (the ceiling)
411
+ <AbloProvider client={ablo} userId={user.id}>
387
412
  ```
388
413
 
389
414
  As the run touches more entities its set **accretes** to cover them; it never
@@ -419,13 +444,13 @@ runs the [Coordinating long agent work](../README.md#coordinating-long-agent-wor
419
444
  `claim` loop is, to the scoping layer, that same participant — scoped to the row
420
445
  it claimed.
421
446
 
422
- ## Narrowing to specific entities — the `scope` prop
447
+ ## Narrowing to specific entities — the `scope` setting
423
448
 
424
449
  A human gets their full membership automatically (`identityRoles`). To narrow a
425
450
  session — a page on one deck, or an agent pointed at the entities it's working
426
- on — pass `scope`. You give it the **model and id(s)**; the engine builds the
427
- group string from the model's `scope` (Half 2), so you never hand-write
428
- `deck:<id>`.
451
+ on — set `scope` on the client you build (`Ablo({ schema, scope })`). You give it
452
+ the **model and id(s)**; the engine builds the group string from the model's
453
+ `scope` (Half 2), so you never hand-write `deck:<id>`.
429
454
 
430
455
  `scope` accepts four shapes, all resolved through the schema:
431
456
 
@@ -438,14 +463,16 @@ group string from the model's `scope` (Half 2), so you never hand-write
438
463
 
439
464
  ```tsx
440
465
  // a page on one deck
441
- <AbloProvider schema={schema} userId={user.id} scope={{ decks: deckId }} />
466
+ const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session', scope: { decks: deckId } });
467
+ <AbloProvider client={ablo} userId={user.id} />;
442
468
 
443
469
  // an agent working across two decks and a document
444
- <AbloProvider
445
- schema={schema}
446
- userId={user.id}
447
- scope={{ decks: [deckA, deckB], documents: docId }}
448
- />
470
+ const ablo = Ablo({
471
+ schema,
472
+ authEndpoint: '/api/ablo-session',
473
+ scope: { decks: [deckA, deckB], documents: docId },
474
+ });
475
+ <AbloProvider client={ablo} userId={user.id} />;
449
476
  ```
450
477
 
451
478
  The key is the **model** (`decks`), the value is **which id(s)** — the `deck:`
@@ -453,8 +480,8 @@ prefix comes from that model's `scope: 'deck'`, never from a string you compose.
453
480
 
454
481
  > **`scope` means one thing: sync-group scope.** It appears in two places that
455
482
  > are the same concept — the model option `scope: 'deck'` (declares a scope root,
456
- > [Half 2](#half-2--per-model-scope-row--group)) and this `scope` prop (subscribe
457
- > to it). The lifecycle filter on [`list()`](./api.md#model-methods) is a separate
483
+ > [Half 2](#half-2--per-model-scope-row--group)) and this `scope` client setting
484
+ > (subscribe to it). The lifecycle filter on [`list()`](./api.md#model-methods) is a separate
458
485
  > axis and is named **`state`** (`'live' | 'archived' | 'all'`, GitHub's
459
486
  > open/closed/all), precisely so it doesn't share the word.
460
487
 
@@ -485,9 +512,9 @@ how to reason about it.
485
512
  the room/shape and the server signs off, as in
486
513
  [Pusher's channel authorization endpoint](https://pusher.com/docs/channels/server_api/authorizing-users/),
487
514
  [ElectricSQL **gatekeeper auth**](https://github.com/electric-sql/electric/blob/main/examples/gatekeeper-auth/README.md),
488
- and Liveblocks **access tokens**. Ablo's `syncGroups` prop is the *narrowing*
489
- half of this — but it can only ever shrink the server-derived set, never grow
490
- it.
515
+ and Liveblocks **access tokens**. Ablo's client `scope` setting is the
516
+ *narrowing* half of this — but it can only ever shrink the server-derived set,
517
+ never grow it.
491
518
 
492
519
  The best practices Ablo inherits from that lineage:
493
520
 
@@ -503,9 +530,9 @@ The best practices Ablo inherits from that lineage:
503
530
  the line precisely: [token parameters are trusted and usable for access
504
531
  control; client parameters are not](https://docs.powersync.com/usage/sync-rules/advanced-topics/client-parameters).
505
532
  In Ablo terms, the identity your server vouches for is the *trusted* claim that
506
- sets scope; the provider's `userId` / `scope` props are *untrusted client
507
- input* — convenient for app-owned fields and narrowing, but never the boundary.
508
- This is why changing `userId` in the browser grants nothing.
533
+ sets scope; the `userId` prop and the client's `scope` setting are *untrusted
534
+ client input* — convenient for app-owned fields and narrowing, but never the
535
+ boundary. This is why changing `userId` in the browser grants nothing.
509
536
 
510
537
  3. **Scope by a hierarchical naming convention, declared once.** Ablo's `kind:id`
511
538
  group naming (`org:…` / `team:…` from `identityRoles`, `deck:…` from a model's
package/docs/index.md CHANGED
@@ -43,7 +43,7 @@ Three things stay true no matter how you use Ablo:
43
43
  - [Schema Contract](./schema-contract.md) — One schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
44
44
  - [CLI & Migrations](./cli.md) — `init` / `migrate` / `push` / `generate`, the shared Zod→Postgres type map, and structured migration errors.
45
45
  - [Identity & Sync Groups](./identity.md) — Use your own authentication; tell Ablo who's connecting and how org / team / user map to sync-group scope.
46
- - [Integration Guide](./integration-guide.md) — Choose Ablo-managed state, Data Source, React, multiplayer, and agent patterns.
46
+ - [Integration Guide](./integration-guide.md) — Connect your database via Data Source, plus React, multiplayer, and agent patterns.
47
47
  - [Guarantees](./guarantees.md) — What confirmed writes, stale checks, and claims guarantee.
48
48
  - [Interaction Model](./interaction-model.md) — The schema, claim, update, confirmation loop.
49
49
  - [API Reference](./api.md) — Model-by-model method shape.
@@ -57,7 +57,7 @@ Three things stay true no matter how you use Ablo:
57
57
  | Plane | Primitives | Purpose |
58
58
  |---|---|---|
59
59
  | State | `Schema`, `Model`, `Claim`, `Receipt` | The product path. Load, coordinate, write, confirm. |
60
- | Storage | `Managed State`, `Data Source` | Ablo stores declared models by default; existing app tables use a signed Data Source. |
60
+ | Storage | `Data Source` | Your rows live in your own database behind a signed Data Source endpoint. |
61
61
 
62
62
  ## Use cases
63
63
 
@@ -42,14 +42,11 @@ schema -> ablo.<model>.list(...) -> ablo.<model>.update(...)
42
42
  Commits and receipts exist under the hood. Most apps do not create protocol
43
43
  objects by hand.
44
44
 
45
- ## Pick The Backing Mode
45
+ ## Your Database
46
46
 
47
- Every schema model has a backing store. The SDK call shape stays the same.
48
-
49
- | Mode | Rows live in | Use when |
50
- | ------------ | ----------------- | -------------------------------------------------------------------------------- |
51
- | Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
52
- | Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
47
+ Every schema model is backed by **your own database**. You expose a signed Data
48
+ Source endpoint; Ablo coordinates each write and your app commits it to your
49
+ Postgres. The SDK call shape is the same everywhere.
53
50
 
54
51
  Do not pass a database URL to `Ablo(...)`. Application and agent code use
55
52
  `ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
@@ -170,17 +167,49 @@ export const ablo = Ablo({
170
167
  ```
171
168
 
172
169
  Browser apps should use the React provider or a scoped session token, not a
173
- server API key in the bundle.
170
+ server API key in the bundle. Build the client first, then hand it to the
171
+ provider — `AbloProvider` takes `{ client, userId?, onError?, fallback? }`, and
172
+ nothing else (`schema`, `teamIds`, `scope`, and `authEndpoint` all live on the
173
+ client now).
174
+
175
+ ```tsx
176
+ // src/ablo-client.ts
177
+ import Ablo from '@abloatai/ablo';
178
+ import { schema } from '@/ablo/schema';
179
+
180
+ // The browser never holds the API key. The client mints a short-lived token
181
+ // from your session route (see below) and refreshes it before expiry.
182
+ export const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
183
+ ```
174
184
 
175
185
  ```tsx
176
186
  // app/providers.tsx
177
187
  'use client';
178
188
 
179
189
  import { AbloProvider } from '@abloatai/ablo/react';
180
- import { schema } from '@/ablo/schema';
190
+ import { ablo } from '@/ablo-client';
181
191
 
182
192
  export function Providers({ children }: { children: React.ReactNode }) {
183
- return <AbloProvider schema={schema}>{children}</AbloProvider>;
193
+ return <AbloProvider client={ablo}>{children}</AbloProvider>;
194
+ }
195
+ ```
196
+
197
+ The session route mints the scoped token server-side, where the API key lives:
198
+
199
+ ```ts
200
+ // app/api/ablo-session/route.ts
201
+ import Ablo from '@abloatai/ablo';
202
+ import { schema } from '@/ablo/schema';
203
+ import { auth } from '@/auth';
204
+
205
+ export const runtime = 'nodejs';
206
+
207
+ const sync = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
208
+
209
+ export async function POST() {
210
+ const session = await auth(); // your own auth — returns the signed-in user
211
+ const { token } = await sync.sessions.create({ user: { id: session.userId } });
212
+ return Response.json({ token });
184
213
  }
185
214
  ```
186
215
 
@@ -214,10 +243,11 @@ refreshes before expiry.
214
243
  ## 3. Read State
215
244
 
216
245
  Reads come in two flavors, and you pick based on whether you can wait.
217
- `retrieve(id)` and `list({ where })` hit the server (and hydrate the local
218
- store) — they're async, so you `await` them. `get(id)`, `getAll({ where })`,
219
- and `getCount({ where })` read the already-synced local graph synchronously, so
220
- they're the ones you call in render.
246
+ `retrieve({ id })` and `list({ where })` hit the server (and hydrate the local
247
+ store) — they're async, so you `await` them. `get(id)` (positional),
248
+ `getAll({ where })`, and `getCount({ where })` read the already-synced local
249
+ graph synchronously, so they're the ones you call in render — and the ones you
250
+ use inside a `useAblo` selector, never the async `retrieve`/`list`.
221
251
 
222
252
  Use `retrieve` when the row may not be local yet — it fetches from the server
223
253
  and waits.
@@ -345,47 +375,41 @@ For the full Python shape, see
345
375
 
346
376
  ## 7. Data Source Endpoint
347
377
 
348
- Use a Data Source when your app database remains the source of truth.
378
+ Use a Data Source when your app database remains the source of truth. Wire the
379
+ route with `dataSourceNext` and an adapter — `prismaDataSource(prisma, schema)`
380
+ or `drizzleDataSource(db, schema)`. You don't hand-write `commit`; the adapter
381
+ owns transactional commit, idempotency, and reads.
349
382
 
350
383
  ```ts
351
384
  // app/api/ablo/source/route.ts
352
- import { dataSource } from '@abloatai/ablo';
385
+ import { dataSourceNext } from '@abloatai/ablo/source/next';
386
+ import { prismaDataSource } from '@abloatai/ablo/source';
353
387
  import { schema } from '@/ablo/schema';
354
- import { db } from '@/db';
355
-
356
- export const POST = dataSource({
357
- schema,
358
- apiKey: process.env.ABLO_API_KEY,
388
+ import { prisma } from '@/db';
359
389
 
360
- authorize() {
361
- return { db };
362
- },
390
+ export const runtime = 'nodejs';
363
391
 
364
- async commit({ operations, clientTxId, context }) {
365
- const rows = await context.auth.db.transaction(async (tx) => {
366
- await tx.idempotency.upsert({ key: clientTxId });
367
- return applyOperations(tx, operations);
368
- });
392
+ export const { POST } = dataSourceNext({
393
+ schema,
394
+ apiKey: process.env.ABLO_API_KEY!,
395
+ adapter: prismaDataSource(prisma, schema),
396
+ });
397
+ ```
369
398
 
370
- return { rows };
371
- },
399
+ With Drizzle, pass `drizzleDataSource(db, schema)` instead — the adapter takes
400
+ your Drizzle `db` and the Ablo `schema` (not your table objects):
372
401
 
373
- reports: {
374
- async load({ id, context }) {
375
- return context.auth.db.report.findUnique({ where: { id } });
376
- },
402
+ ```ts
403
+ import { drizzleDataSource } from '@abloatai/ablo/source/drizzle';
377
404
 
378
- async list({ query, context }) {
379
- return context.auth.db.report.findMany({
380
- take: query.limit ?? 100,
381
- });
382
- },
383
- },
405
+ export const { POST } = dataSourceNext({
406
+ schema,
407
+ apiKey: process.env.ABLO_API_KEY!,
408
+ adapter: drizzleDataSource(db, schema),
384
409
  });
385
410
  ```
386
411
 
387
- Ablo needs your Data Source endpoint and API key. External writes can be
388
- reported through an optional `events` handler on the same route. Your app
412
+ Ablo needs your Data Source endpoint and API key. Your app
389
413
  stores one Ablo credential:
390
414
 
391
415
  ```bash
@@ -404,16 +428,18 @@ which a human might touch the same row. Wrap that work in a claim. Claims don't
404
428
  lock. If another writer holds the row, `claim` waits for them, re-reads the
405
429
  fresh row, then hands it to you — so two writers serialize instead of clobbering.
406
430
 
407
- ```ts
408
- const report = await ablo.weatherReports.retrieve({ id: reportId });
409
- if (!report) return;
431
+ A claim is a disposable handle (`await using`), not a callback: read the fresh
432
+ row off `claim.data`, do your work, and the handle auto-releases when it leaves
433
+ scope.
410
434
 
435
+ ```ts
411
436
  await using claim = await ablo.weatherReports.claim({
412
437
  id: reportId,
413
- wait: false,
414
438
  action: 'forecasting',
415
439
  });
416
440
  const claimed = claim.data;
441
+ if (!claimed) return;
442
+
417
443
  await ablo.weatherReports.update({
418
444
  id: claimed.id,
419
445
  data: { status: 'ready', forecast: await getForecast(claimed) },
@@ -464,17 +490,19 @@ them.
464
490
 
465
491
  ## Method Cheatsheet
466
492
 
467
- | Method | Use it for |
468
- | ---------------------------- | --------------------------------------------------------------------------- |
469
- | `retrieve(id)` | Async read of one row from the server (await it). |
470
- | `list({ where })` | Async read of many rows from the server (await it). |
471
- | `get(id)` | Synchronous local read of one synced row (use in render). |
472
- | `getAll({ where })` | Synchronous local read of many synced rows. |
473
- | `getCount({ where })` | Synchronous local count of synced rows. |
474
- | `create(data, options?)` | Create through the model client. |
475
- | `update(id, data, options?)` | Update through the model client. |
476
- | `delete(id, options?)` | Delete through the model client. |
477
- | `claim.state({ id })` | See who is currently working on a row (synchronous). |
478
- | `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
479
-
480
- Keep first integrations on the model methods above.
493
+ | Method | Use it for |
494
+ | -------------------------------------- | -------------------------------------------------------------------------------- |
495
+ | `retrieve({ id })` | Async read of one row from the server (await it). |
496
+ | `list({ where })` | Async read of many rows from the server (await it). |
497
+ | `get(id)` | Synchronous local read of one synced row (positional id; use in render). |
498
+ | `getAll({ where })` | Synchronous local read of many synced rows. |
499
+ | `getCount({ where })` | Synchronous local count of synced rows. |
500
+ | `create({ data, id? })` | Create through the model client. |
501
+ | `update({ id, data, ...opts })` | Update through the model client. |
502
+ | `delete({ id, ...opts })` | Delete through the model client. |
503
+ | `claim.state({ id })` | See who is currently working on a row (synchronous). |
504
+ | `claim({ id, action?, ttl? })` | Acquire a disposable handle: wait for your turn, re-read, and hold the row. |
505
+
506
+ Keep first integrations on the model methods above. Every mutation and
507
+ server-read verb takes one options object; only the synchronous `get(id)` stays
508
+ positional.
package/docs/mcp.md CHANGED
@@ -83,7 +83,7 @@ Per-client walkthroughs:
83
83
  | `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
84
84
  | `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
85
85
  | `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
86
- | `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with `managed` or `data-source` storage. |
86
+ | `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with a `data-source` (your own database) endpoint. |
87
87
 
88
88
  #### Resources
89
89