@abloatai/ablo 0.5.1 → 0.7.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 +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
|
@@ -52,18 +52,17 @@ that same model path.
|
|
|
52
52
|
schema -> ablo.<model>.load(...) -> ablo.<model>.update(...)
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
|
|
55
|
+
Commits and receipts exist under the hood. Most apps do not create protocol
|
|
56
|
+
objects by hand.
|
|
57
57
|
|
|
58
58
|
## Pick The Backing Mode
|
|
59
59
|
|
|
60
60
|
Every schema model has a backing store. The SDK call shape stays the same.
|
|
61
61
|
|
|
62
|
-
| Mode
|
|
63
|
-
|
|
64
|
-
| Ablo-managed | Ablo
|
|
65
|
-
| Data Source
|
|
66
|
-
| Schema-less resource API | Custom runtime | A server worker, MCP route, or migration script intentionally cannot import the app schema. |
|
|
62
|
+
| Mode | Rows live in | Use when |
|
|
63
|
+
| ------------ | ----------------- | -------------------------------------------------------------------------------- |
|
|
64
|
+
| Ablo-managed | Ablo | New collaborative or agent-written state can live in Ablo. |
|
|
65
|
+
| Data Source | Your app database | You already have tables, service logic, and API endpoints that remain canonical. |
|
|
67
66
|
|
|
68
67
|
Do not pass a database URL to `Ablo(...)`. Application and agent code use
|
|
69
68
|
`ABLO_API_KEY`. If your database stays canonical, expose a signed Data Source
|
|
@@ -74,7 +73,7 @@ endpoint from your app and keep the database credentials inside your app.
|
|
|
74
73
|
Use the public `/sandbox` page to understand the state flow. It is a visual,
|
|
75
74
|
deterministic demo; it does not call your API key or mutate hosted Ablo data.
|
|
76
75
|
It is also built for coding agents: copy the sandbox prompt into Claude Code or
|
|
77
|
-
Codex and ask it to wire one real
|
|
76
|
+
Codex and ask it to wire one real model through the schema model API.
|
|
78
77
|
|
|
79
78
|
Use the authenticated org dashboard sandbox for real integration work. The
|
|
80
79
|
default sandbox is the equivalent of Stripe test mode:
|
|
@@ -92,7 +91,7 @@ and write path are ready for production.
|
|
|
92
91
|
When handing this to a coding agent, give it a concrete target:
|
|
93
92
|
|
|
94
93
|
```txt
|
|
95
|
-
Add Ablo
|
|
94
|
+
Add Ablo to this app for one model that humans and agents both edit.
|
|
96
95
|
Use the org sandbox sk_test_* key. Declare schema, add the Ablo client, replace
|
|
97
96
|
one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
|
|
98
97
|
wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
@@ -102,67 +101,65 @@ wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
|
102
101
|
|
|
103
102
|
Start with fields and relations. Keep load strategies, indexing hints, and
|
|
104
103
|
read-only/mutable shortcuts out of the first version unless you already need
|
|
105
|
-
|
|
104
|
+
them.
|
|
106
105
|
|
|
107
106
|
```ts
|
|
108
|
-
// src/ablo
|
|
107
|
+
// src/ablo/schema.ts
|
|
109
108
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
110
109
|
|
|
111
110
|
export const schema = defineSchema(
|
|
112
111
|
{
|
|
113
|
-
|
|
112
|
+
weatherReports: model({
|
|
114
113
|
id: z.string(),
|
|
115
114
|
projectId: z.string(),
|
|
116
|
-
|
|
117
|
-
status: z.enum(['
|
|
115
|
+
location: z.string(),
|
|
116
|
+
status: z.enum(['pending', 'ready']),
|
|
118
117
|
assigneeId: z.string().nullable(),
|
|
119
118
|
updatedAt: z.string(),
|
|
120
119
|
}),
|
|
121
120
|
},
|
|
122
121
|
{
|
|
123
|
-
// Identity-anchored sync-group roles. The server walks these to
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
// consumer-controlled
|
|
127
|
-
//
|
|
128
|
-
//
|
|
122
|
+
// Identity-anchored sync-group roles. The server walks these to build each
|
|
123
|
+
// participant's allowed subscription set from the resolved identity context.
|
|
124
|
+
// `kind` is the group prefix; `source` is the identity field to read — both
|
|
125
|
+
// consumer-controlled, no hardcoded `org:` / `user:` convention anywhere in
|
|
126
|
+
// the engine. Pure data (no closures), so the schema stays JSON-serializable.
|
|
127
|
+
// Omit `identityRoles` entirely if you don't need identity-derived scoping.
|
|
129
128
|
identityRoles: [
|
|
130
|
-
{
|
|
131
|
-
|
|
132
|
-
template: 'org:{id}',
|
|
133
|
-
extract: (i) => (i.organizationId ? [String(i.organizationId)] : []),
|
|
134
|
-
},
|
|
135
|
-
{
|
|
136
|
-
kind: 'participant',
|
|
137
|
-
template: 'user:{id}',
|
|
138
|
-
extract: (i) => (i.userId ? [String(i.userId)] : []),
|
|
139
|
-
},
|
|
129
|
+
identityRole({ kind: 'org', source: 'organizationId' }),
|
|
130
|
+
identityRole({ kind: 'user', source: 'userId' }),
|
|
140
131
|
],
|
|
141
|
-
}
|
|
132
|
+
}
|
|
142
133
|
);
|
|
143
134
|
```
|
|
144
135
|
|
|
145
136
|
### Declaring scope on a model
|
|
146
137
|
|
|
147
|
-
|
|
148
|
-
`
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
138
|
+
> **Canonical reference: [Identity & Sync Groups](./identity.md).** This is the
|
|
139
|
+
> short version — `scope` (root), `parent` (containment), `grants` (membership),
|
|
140
|
+
> and the model-form `scope` prop are all covered in depth there. Read it once;
|
|
141
|
+
> this guide only shows the minimal shape inline.
|
|
142
|
+
|
|
143
|
+
Per-row tenancy and per-entity sync-group anchors live on the `model(...)`
|
|
144
|
+
options. The two halves compose: the identity roles above produce a
|
|
145
|
+
participant's _allowed_ set; the per-model options below define how rows are
|
|
146
|
+
filtered server-side and which sync-group each row fans out on.
|
|
152
147
|
|
|
153
148
|
```ts
|
|
154
149
|
model(
|
|
155
|
-
{
|
|
150
|
+
{
|
|
151
|
+
/* fields */
|
|
152
|
+
},
|
|
156
153
|
/* relations */ {},
|
|
157
154
|
{
|
|
158
155
|
// Rows carry organization_id and bootstrap filters on it.
|
|
159
156
|
orgScoped: true,
|
|
160
157
|
|
|
161
|
-
//
|
|
162
|
-
//
|
|
163
|
-
|
|
164
|
-
}
|
|
165
|
-
)
|
|
158
|
+
// Scope root: rows form the group `matter:<id>`. Children point at it with
|
|
159
|
+
// `relation.belongsTo('matters', 'matterId', { parent: true })` to inherit.
|
|
160
|
+
scope: 'matter',
|
|
161
|
+
}
|
|
162
|
+
);
|
|
166
163
|
```
|
|
167
164
|
|
|
168
165
|
For rows that don't carry `organization_id` themselves but inherit
|
|
@@ -177,7 +174,7 @@ Trusted runtimes can use `ABLO_API_KEY`.
|
|
|
177
174
|
```ts
|
|
178
175
|
// src/ablo.ts
|
|
179
176
|
import Ablo from '@abloatai/ablo';
|
|
180
|
-
import { schema } from './ablo
|
|
177
|
+
import { schema } from './ablo/schema';
|
|
181
178
|
|
|
182
179
|
export const ablo = Ablo({
|
|
183
180
|
schema,
|
|
@@ -185,7 +182,7 @@ export const ablo = Ablo({
|
|
|
185
182
|
});
|
|
186
183
|
```
|
|
187
184
|
|
|
188
|
-
Browser apps should use the React provider or a scoped session
|
|
185
|
+
Browser apps should use the React provider or a scoped session token, not a
|
|
189
186
|
server API key in the bundle.
|
|
190
187
|
|
|
191
188
|
```tsx
|
|
@@ -193,7 +190,7 @@ server API key in the bundle.
|
|
|
193
190
|
'use client';
|
|
194
191
|
|
|
195
192
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
196
|
-
import { schema } from '@/ablo
|
|
193
|
+
import { schema } from '@/ablo/schema';
|
|
197
194
|
|
|
198
195
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
199
196
|
return <AbloProvider schema={schema}>{children}</AbloProvider>;
|
|
@@ -208,15 +205,14 @@ bundle, and signs server-to-server requests. It is the right credential
|
|
|
208
205
|
for trusted runtimes (Next.js server actions, background workers,
|
|
209
206
|
migration scripts) where the code reading it is yours.
|
|
210
207
|
|
|
211
|
-
A browser
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
exchange is the bridge between two credential shapes:
|
|
208
|
+
A browser is not that environment. The React provider exchanges your API key for
|
|
209
|
+
a short-lived, narrowly scoped bearer token. The browser holds that scoped token;
|
|
210
|
+
the API key never leaves the server. The exchange is the bridge between two
|
|
211
|
+
credential shapes:
|
|
216
212
|
|
|
217
213
|
```
|
|
218
214
|
trusted runtime browser / agent
|
|
219
|
-
ABLO_API_KEY ─exchange─►
|
|
215
|
+
ABLO_API_KEY ─exchange─► scoped token ────────► narrow scope, leased
|
|
220
216
|
(long-lived, (short-lived,
|
|
221
217
|
broad scope, per-actor scope,
|
|
222
218
|
server only) revocable)
|
|
@@ -225,10 +221,8 @@ ABLO_API_KEY ─exchange─► Capability token ────► narrow scope, l
|
|
|
225
221
|
This is the same shape as Stripe's
|
|
226
222
|
ephemeral keys (Issuing Elements expires in 15 minutes) and AWS STS
|
|
227
223
|
AssumeRole (returns time-bounded creds with the minimal needed scope).
|
|
228
|
-
You never
|
|
229
|
-
|
|
230
|
-
[Capabilities](./capabilities.md) for the design rationale and the
|
|
231
|
-
manual create/revoke surface that custom runtimes use.
|
|
224
|
+
You never type that token into your app; the SDK mints one when it needs one and
|
|
225
|
+
refreshes before expiry.
|
|
232
226
|
|
|
233
227
|
## 3. Read State
|
|
234
228
|
|
|
@@ -237,18 +231,18 @@ Use `load` when the row may not already be local.
|
|
|
237
231
|
```ts
|
|
238
232
|
await ablo.ready();
|
|
239
233
|
|
|
240
|
-
const [
|
|
241
|
-
if (!
|
|
234
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
235
|
+
if (!report) throw new Error('report not found');
|
|
242
236
|
```
|
|
243
237
|
|
|
244
|
-
Use `retrieve`, `list`, and `count` for synchronous
|
|
238
|
+
Use `retrieve`, `list`, and `count` for synchronous reads after data has
|
|
245
239
|
loaded.
|
|
246
240
|
|
|
247
241
|
```ts
|
|
248
|
-
const
|
|
249
|
-
const
|
|
242
|
+
const report = ablo.weatherReports.retrieve('report_stockholm');
|
|
243
|
+
const activeReports = ablo.weatherReports.list({
|
|
250
244
|
where: { projectId: 'proj_123' },
|
|
251
|
-
filter: (
|
|
245
|
+
filter: (report) => report.status !== 'ready',
|
|
252
246
|
orderBy: { updatedAt: 'desc' },
|
|
253
247
|
limit: 50,
|
|
254
248
|
});
|
|
@@ -261,17 +255,15 @@ In React, selector `useAblo` is the public read API:
|
|
|
261
255
|
|
|
262
256
|
import { useAblo } from '@abloatai/ablo/react';
|
|
263
257
|
|
|
264
|
-
export function
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
</button>
|
|
274
|
-
);
|
|
258
|
+
export function ReportRow({
|
|
259
|
+
report: serverReport,
|
|
260
|
+
}: {
|
|
261
|
+
report: { id: string; location: string; status: string };
|
|
262
|
+
}) {
|
|
263
|
+
const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
|
|
264
|
+
const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
|
|
265
|
+
|
|
266
|
+
return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
|
|
275
267
|
}
|
|
276
268
|
```
|
|
277
269
|
|
|
@@ -286,27 +278,23 @@ const ablo = useAblo();
|
|
|
286
278
|
For simple writes:
|
|
287
279
|
|
|
288
280
|
```ts
|
|
289
|
-
await ablo.
|
|
290
|
-
'task_123',
|
|
291
|
-
{ status: 'done' },
|
|
292
|
-
{ wait: 'confirmed' },
|
|
293
|
-
);
|
|
281
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
294
282
|
```
|
|
295
283
|
|
|
296
284
|
For writes based on state the user or agent already read, snapshot first and
|
|
297
285
|
reject stale updates:
|
|
298
286
|
|
|
299
287
|
```ts
|
|
300
|
-
const snap = ablo.snapshot({
|
|
288
|
+
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
301
289
|
|
|
302
|
-
await ablo.
|
|
303
|
-
'
|
|
304
|
-
{ status: '
|
|
290
|
+
await ablo.weatherReports.update(
|
|
291
|
+
'report_stockholm',
|
|
292
|
+
{ status: 'ready' },
|
|
305
293
|
{
|
|
306
294
|
readAt: snap.stamp,
|
|
307
295
|
onStale: 'reject',
|
|
308
296
|
wait: 'confirmed',
|
|
309
|
-
}
|
|
297
|
+
}
|
|
310
298
|
);
|
|
311
299
|
```
|
|
312
300
|
|
|
@@ -317,16 +305,16 @@ back optimistic local state and throw a typed `AbloError`.
|
|
|
317
305
|
|
|
318
306
|
There is no separate multiplayer setup.
|
|
319
307
|
|
|
320
|
-
If humans, server actions, and agents use the same schema
|
|
308
|
+
If humans, server actions, and agents use the same schema client, they
|
|
321
309
|
share the same stream:
|
|
322
310
|
|
|
323
311
|
```txt
|
|
324
|
-
human UI -> ablo.
|
|
325
|
-
agent -> ablo.
|
|
326
|
-
server -> ablo.
|
|
312
|
+
human UI -> ablo.weatherReports.update(...)
|
|
313
|
+
agent -> ablo.weatherReports.update(...)
|
|
314
|
+
server -> ablo.weatherReports.update(...)
|
|
327
315
|
```
|
|
328
316
|
|
|
329
|
-
Ablo coordinates those writes, fans out confirmed deltas, exposes active
|
|
317
|
+
Ablo coordinates those writes, fans out confirmed deltas, exposes active claims,
|
|
330
318
|
and lets callers reject stale writes with `readAt`.
|
|
331
319
|
|
|
332
320
|
Direct writes to your own database bypass that stream until your app reports the
|
|
@@ -342,7 +330,7 @@ the records that need multiplayer now and agent-safe writes later.
|
|
|
342
330
|
|
|
343
331
|
```txt
|
|
344
332
|
Button
|
|
345
|
-
-> ablo.
|
|
333
|
+
-> ablo.weatherReports.update(...)
|
|
346
334
|
-> Ablo
|
|
347
335
|
-> signed Data Source request
|
|
348
336
|
-> existing backend service
|
|
@@ -352,13 +340,13 @@ Button
|
|
|
352
340
|
|
|
353
341
|
The migration can be gradual:
|
|
354
342
|
|
|
355
|
-
1. Declare schema for one
|
|
343
|
+
1. Declare schema for one model, such as `reports`.
|
|
356
344
|
2. Keep existing server loads for first paint.
|
|
357
|
-
3. Add `useAblo((ablo) => ablo.
|
|
345
|
+
3. Add `useAblo((ablo) => ablo.weatherReports.retrieve(id)) ?? serverReport` for live rows.
|
|
358
346
|
4. Add one Data Source endpoint that calls the existing service layer.
|
|
359
|
-
5. Move one mutation button from `fetch('/api/
|
|
347
|
+
5. Move one mutation button from `fetch('/api/reports/...')` to `ablo.weatherReports.update(...)`.
|
|
360
348
|
6. Add an outbox/events path for writes that still happen outside Ablo.
|
|
361
|
-
7. Let agents use the same `ablo.
|
|
349
|
+
7. Let agents use the same `ablo.weatherReports.load(...)` and `ablo.weatherReports.update(...)`.
|
|
362
350
|
|
|
363
351
|
For the full Python shape, see
|
|
364
352
|
[Existing Python Backend](./examples/existing-python-backend.md).
|
|
@@ -370,7 +358,7 @@ Use a Data Source when your app database remains the source of truth.
|
|
|
370
358
|
```ts
|
|
371
359
|
// app/api/ablo/source/route.ts
|
|
372
360
|
import { dataSource } from '@abloatai/ablo';
|
|
373
|
-
import { schema } from '@/ablo
|
|
361
|
+
import { schema } from '@/ablo/schema';
|
|
374
362
|
import { db } from '@/db';
|
|
375
363
|
|
|
376
364
|
export const POST = dataSource({
|
|
@@ -390,13 +378,13 @@ export const POST = dataSource({
|
|
|
390
378
|
return { rows };
|
|
391
379
|
},
|
|
392
380
|
|
|
393
|
-
|
|
381
|
+
reports: {
|
|
394
382
|
async load({ id, context }) {
|
|
395
|
-
return context.auth.db.
|
|
383
|
+
return context.auth.db.report.findUnique({ where: { id } });
|
|
396
384
|
},
|
|
397
385
|
|
|
398
386
|
async list({ query, context }) {
|
|
399
|
-
return context.auth.db.
|
|
387
|
+
return context.auth.db.report.findMany({
|
|
400
388
|
take: query.limit ?? 100,
|
|
401
389
|
});
|
|
402
390
|
},
|
|
@@ -420,18 +408,19 @@ Agents should use the same model methods as the app when they can import the
|
|
|
420
408
|
schema.
|
|
421
409
|
|
|
422
410
|
```ts
|
|
423
|
-
const [
|
|
424
|
-
if (!
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
await
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
411
|
+
const [report] = await ablo.weatherReports.load({ where: { id: reportId } });
|
|
412
|
+
if (!report) return;
|
|
413
|
+
|
|
414
|
+
await ablo.weatherReports.claim(
|
|
415
|
+
reportId,
|
|
416
|
+
async (claimed) => {
|
|
417
|
+
await ablo.weatherReports.update(
|
|
418
|
+
claimed.id,
|
|
419
|
+
{ status: 'ready', forecast: await getForecast(claimed) },
|
|
420
|
+
{ wait: 'confirmed' }
|
|
421
|
+
);
|
|
422
|
+
},
|
|
423
|
+
{ wait: false, action: 'forecasting' }
|
|
435
424
|
);
|
|
436
425
|
```
|
|
437
426
|
|
|
@@ -439,38 +428,36 @@ Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
|
|
|
439
428
|
change.
|
|
440
429
|
|
|
441
430
|
```ts
|
|
442
|
-
const
|
|
443
|
-
description: 'Mark a
|
|
431
|
+
const completeReport = tool({
|
|
432
|
+
description: 'Mark a weather report ready with a forecast',
|
|
444
433
|
inputSchema: z.object({
|
|
445
|
-
|
|
446
|
-
|
|
434
|
+
reportId: z.string(),
|
|
435
|
+
forecast: z.string(),
|
|
447
436
|
}),
|
|
448
|
-
execute: async ({
|
|
449
|
-
const snap = ablo.snapshot({
|
|
450
|
-
return ablo.
|
|
451
|
-
|
|
452
|
-
{ status: '
|
|
453
|
-
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' }
|
|
437
|
+
execute: async ({ reportId, forecast }) => {
|
|
438
|
+
const snap = ablo.snapshot({ weatherReports: reportId });
|
|
439
|
+
return ablo.weatherReports.update(
|
|
440
|
+
reportId,
|
|
441
|
+
{ status: 'ready', forecast },
|
|
442
|
+
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' }
|
|
454
443
|
);
|
|
455
444
|
},
|
|
456
445
|
});
|
|
457
446
|
```
|
|
458
447
|
|
|
459
|
-
|
|
460
|
-
for custom server runtimes that intentionally cannot import the schema.
|
|
448
|
+
Keep agent writes on the same schema client surface as the app.
|
|
461
449
|
|
|
462
450
|
## Optional Surface
|
|
463
451
|
|
|
464
|
-
| Optional piece
|
|
465
|
-
|
|
466
|
-
| `/react`
|
|
467
|
-
| `/testing`
|
|
468
|
-
| `Data Source`
|
|
469
|
-
| `persistence: 'indexeddb'`
|
|
470
|
-
| `
|
|
471
|
-
| `snapshot` + `readAt`
|
|
472
|
-
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and
|
|
473
|
-
| `resource(...)` and `commits.create(...)` | Low-level protocol access for custom runtimes. |
|
|
452
|
+
| Optional piece | Why it exists |
|
|
453
|
+
| ----------------------------------------- | ----------------------------------------------------------------- |
|
|
454
|
+
| `/react` | Live React selectors, provider lifecycle, presence, sync status. |
|
|
455
|
+
| `/testing` | Test harnesses and deterministic mocks. |
|
|
456
|
+
| `Data Source` | Keep your app database canonical. |
|
|
457
|
+
| `persistence: 'indexeddb'` | Durable browser cache that survives reloads, for apps that need it. |
|
|
458
|
+
| `claim` / `claimState` / `queue` | Show active work and coordinate before a write. |
|
|
459
|
+
| `snapshot` + `readAt` | Reject writes based on stale state. |
|
|
460
|
+
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and read tuning. |
|
|
474
461
|
|
|
475
462
|
The first integration should not need most of these. Start with schema and
|
|
476
463
|
model methods, then add the optional pieces where the product actually needs
|
|
@@ -478,17 +465,16 @@ them.
|
|
|
478
465
|
|
|
479
466
|
## Method Cheatsheet
|
|
480
467
|
|
|
481
|
-
| Method
|
|
482
|
-
|
|
483
|
-
| `load({ where })`
|
|
484
|
-
| `retrieve(id)`
|
|
485
|
-
| `list(options?)`
|
|
486
|
-
| `count(options?)`
|
|
487
|
-
| `create(data, options?)`
|
|
488
|
-
| `update(id, data, options?)` | Update through the model
|
|
489
|
-
| `delete(id, options?)`
|
|
490
|
-
| `
|
|
491
|
-
| `
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
the low-level commit plane directly.
|
|
468
|
+
| Method | Use it for |
|
|
469
|
+
| ---------------------------- | ---------------------------------------------------------------- |
|
|
470
|
+
| `load({ where })` | Async hydration from backing store/server. |
|
|
471
|
+
| `retrieve(id)` | Synchronous read of one already-loaded row. |
|
|
472
|
+
| `list(options?)` | Synchronous collection read of loaded rows. |
|
|
473
|
+
| `count(options?)` | Synchronous count of loaded 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
|
+
| `claimState(id)` | See active work on a model row. |
|
|
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.
|