@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.
- package/CHANGELOG.md +46 -1
- package/README.md +33 -28
- package/dist/BaseSyncedStore.d.ts +83 -0
- package/dist/BaseSyncedStore.js +194 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- 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 +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- 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 +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- 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 +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +158 -50
- package/dist/mutators/UndoManager.js +345 -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 +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/context.d.ts +31 -0
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +34 -3
- package/dist/schema/ddl.js +162 -4
- package/dist/schema/index.d.ts +5 -1
- package/dist/schema/index.js +13 -1
- 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 +4 -0
- package/dist/schema/serialize.js +4 -0
- 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 +65 -0
- package/dist/source/adapter.js +20 -0
- package/dist/source/adapters/drizzle.d.ts +43 -0
- package/dist/source/adapters/drizzle.js +185 -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 +176 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +144 -0
- package/dist/source/contract.js +99 -0
- package/dist/source/index.d.ts +62 -10
- package/dist/source/index.js +99 -0
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -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 +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -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.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A Stripe-style webhook event delivered to the customer's endpoint. Verified
|
|
3
|
+
* (via the Standard Webhooks library) before the customer reads it.
|
|
4
|
+
*/
|
|
5
|
+
export interface AbloWebhookEvent {
|
|
6
|
+
/** Stable event id = `String(syncId)`. Dedupe by this (idempotency). */
|
|
7
|
+
readonly id: string;
|
|
8
|
+
/** `<model>.<verb>`, e.g. `"slide.updated"` — switch on this. */
|
|
9
|
+
readonly type: string;
|
|
10
|
+
/** Wire model name, e.g. `"Slide"`. */
|
|
11
|
+
readonly model: string;
|
|
12
|
+
/** The changed row's id. */
|
|
13
|
+
readonly objectId: string;
|
|
14
|
+
/** Monotonic transaction-log position. ORDER by this (and dedupe). */
|
|
15
|
+
readonly syncId: number;
|
|
16
|
+
/** The post-change row (the object), or `null` on a delete. Like Stripe's
|
|
17
|
+
* `event.data.object`. */
|
|
18
|
+
readonly data: Record<string, unknown> | null;
|
|
19
|
+
/** ISO timestamp the change was committed. */
|
|
20
|
+
readonly createdAt: string;
|
|
21
|
+
}
|
|
22
|
+
/** The minimal delta shape the mapping reads (a `ServerSyncDelta` satisfies it). */
|
|
23
|
+
export interface WebhookSourceDelta {
|
|
24
|
+
readonly id: number;
|
|
25
|
+
readonly actionType: string;
|
|
26
|
+
readonly modelName: string;
|
|
27
|
+
readonly modelId: string;
|
|
28
|
+
/** `jsonb` — parsed object, raw JSON string, or null. */
|
|
29
|
+
readonly data: Record<string, unknown> | string | null;
|
|
30
|
+
readonly createdAt: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Map a committed delta to a customer-facing webhook event. Returns `null` for
|
|
34
|
+
* internal sync deltas (permission/group changes) that aren't customer events —
|
|
35
|
+
* the caller skips those (no webhook emitted). Pure: the `syncId` and timestamp
|
|
36
|
+
* come from the delta, so the mapping is deterministic.
|
|
37
|
+
*/
|
|
38
|
+
export declare function deltaToWebhookEvent(delta: WebhookSourceDelta): AbloWebhookEvent | null;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The customer-facing verb per delta action. Only the CRUD-ish actions become
|
|
3
|
+
* webhook events; `C`overing / `G`roupAdded / `S`groupRemoved are internal sync
|
|
4
|
+
* mechanics (permission/visibility), NOT customer events → no webhook.
|
|
5
|
+
*/
|
|
6
|
+
const ACTION_VERB = {
|
|
7
|
+
I: 'created',
|
|
8
|
+
U: 'updated',
|
|
9
|
+
D: 'deleted',
|
|
10
|
+
A: 'archived',
|
|
11
|
+
V: 'unarchived',
|
|
12
|
+
};
|
|
13
|
+
function parseRow(data) {
|
|
14
|
+
if (data == null)
|
|
15
|
+
return null;
|
|
16
|
+
if (typeof data === 'string') {
|
|
17
|
+
return data === '' ? null : JSON.parse(data);
|
|
18
|
+
}
|
|
19
|
+
return data;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Map a committed delta to a customer-facing webhook event. Returns `null` for
|
|
23
|
+
* internal sync deltas (permission/group changes) that aren't customer events —
|
|
24
|
+
* the caller skips those (no webhook emitted). Pure: the `syncId` and timestamp
|
|
25
|
+
* come from the delta, so the mapping is deterministic.
|
|
26
|
+
*/
|
|
27
|
+
export function deltaToWebhookEvent(delta) {
|
|
28
|
+
const verb = ACTION_VERB[delta.actionType];
|
|
29
|
+
if (!verb)
|
|
30
|
+
return null; // C / G / S — internal sync mechanics, not a customer event
|
|
31
|
+
return {
|
|
32
|
+
id: String(delta.id),
|
|
33
|
+
type: `${delta.modelName.toLowerCase()}.${verb}`,
|
|
34
|
+
model: delta.modelName,
|
|
35
|
+
objectId: delta.modelId,
|
|
36
|
+
syncId: delta.id,
|
|
37
|
+
data: parseRow(delta.data),
|
|
38
|
+
createdAt: delta.createdAt,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/webhooks` — the webhook event catalog + delta mapping.
|
|
3
|
+
*
|
|
4
|
+
* Customers import {@link AbloWebhookEvent} to type their handler; the server
|
|
5
|
+
* uses {@link deltaToWebhookEvent} to turn transaction-log deltas into events
|
|
6
|
+
* for Svix to deliver. Signature verification is NOT here — the customer uses
|
|
7
|
+
* the open Standard Webhooks library (`svix` / `standardwebhooks`), so Ablo
|
|
8
|
+
* ships no crypto.
|
|
9
|
+
*/
|
|
10
|
+
export { deltaToWebhookEvent, type AbloWebhookEvent, type WebhookSourceDelta, } from './events.js';
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/webhooks` — the webhook event catalog + delta mapping.
|
|
3
|
+
*
|
|
4
|
+
* Customers import {@link AbloWebhookEvent} to type their handler; the server
|
|
5
|
+
* uses {@link deltaToWebhookEvent} to turn transaction-log deltas into events
|
|
6
|
+
* for Svix to deliver. Signature verification is NOT here — the customer uses
|
|
7
|
+
* the open Standard Webhooks library (`svix` / `standardwebhooks`), so Ablo
|
|
8
|
+
* ships no crypto.
|
|
9
|
+
*/
|
|
10
|
+
export { deltaToWebhookEvent, } from './events.js';
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The canonical wire envelope — Stripe's error-object shape. Every HTTP error
|
|
2
|
+
* response and every structured frame error carries this exact set of keys,
|
|
3
|
+
* regardless of which route or transport produced it. */
|
|
4
|
+
export interface ErrorEnvelope {
|
|
5
|
+
readonly type: string;
|
|
6
|
+
readonly code?: string;
|
|
7
|
+
readonly param?: string;
|
|
8
|
+
readonly message: string;
|
|
9
|
+
readonly doc_url?: string;
|
|
10
|
+
readonly request_id?: string;
|
|
11
|
+
}
|
|
12
|
+
/** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
|
|
13
|
+
* match status semantics (a validation error is a 400, a permission error a
|
|
14
|
+
* 403), so a throw site only picks the right class + code and the status
|
|
15
|
+
* follows — an explicit `httpStatus` is passed only when it diverges (e.g. a
|
|
16
|
+
* 404 on the base class, a 503 on AbloServerError). Mirrors the same table in
|
|
17
|
+
* apps/sync-server's self-contained `errors.ts`. */
|
|
18
|
+
export declare function statusForType(type: string): number;
|
|
19
|
+
/**
|
|
20
|
+
* Convert ANY thrown value into the canonical {@link ErrorEnvelope} plus an
|
|
21
|
+
* HTTP status. A typed {@link AbloError} is serialized via its own `toJSON`
|
|
22
|
+
* (so `code`/`param`/`doc_url`/structured `details` survive) and gets its
|
|
23
|
+
* status from an explicit `httpStatus` or, failing that, {@link statusForType}.
|
|
24
|
+
* Anything else degrades to a 500 `internal_error` envelope — never a bare
|
|
25
|
+
* framework "Internal Server Error" text body, and never a raw error string
|
|
26
|
+
* leaked onto the wire as an unregistered code.
|
|
27
|
+
*
|
|
28
|
+
* `requestId` is stamped into the body when the error didn't already carry one,
|
|
29
|
+
* so the response and the `x-request-id` header agree for support correlation.
|
|
30
|
+
*/
|
|
31
|
+
export declare function errorEnvelope(err: unknown, requestId?: string): {
|
|
32
|
+
body: ErrorEnvelope;
|
|
33
|
+
status: number;
|
|
34
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ERROR egress — turn ANY thrown value into Stripe's error-object envelope plus
|
|
3
|
+
* an HTTP status, so every error response across the Ablo surface carries the
|
|
4
|
+
* identical `{ type, code, param, message, doc_url, request_id }` shape
|
|
5
|
+
* regardless of which route or service produced it.
|
|
6
|
+
*
|
|
7
|
+
* This is the wire-PRODUCE counterpart to `errors.ts`'s wire-PARSE
|
|
8
|
+
* (`translateHttpError`/`errorFromWire`). It lives in `wire/` — not in the main
|
|
9
|
+
* SDK entry — so a server-side consumer (a Next.js route) can import it without
|
|
10
|
+
* dragging in the client runtime (mobx/react/IndexedDB).
|
|
11
|
+
*
|
|
12
|
+
* The classifier is the UNIVERSAL baseline: a typed {@link AbloError} passes
|
|
13
|
+
* through (subclass + code + httpStatus preserved), everything else degrades to
|
|
14
|
+
* a 500 `internal_error`. Service-specific normalization that needs a DB driver
|
|
15
|
+
* (apps/sync-server classifies raw Postgres SQLSTATE + MutatorError) is layered
|
|
16
|
+
* on top in that service — it is intentionally NOT pulled into the shared,
|
|
17
|
+
* dependency-free contract.
|
|
18
|
+
*/
|
|
19
|
+
import { AbloError, docUrlForCode } from '../errors.js';
|
|
20
|
+
import { errorCodeSpec } from '../errorCodes.js';
|
|
21
|
+
/** {@link AbloError} subclass → default HTTP status. The subclass is chosen to
|
|
22
|
+
* match status semantics (a validation error is a 400, a permission error a
|
|
23
|
+
* 403), so a throw site only picks the right class + code and the status
|
|
24
|
+
* follows — an explicit `httpStatus` is passed only when it diverges (e.g. a
|
|
25
|
+
* 404 on the base class, a 503 on AbloServerError). Mirrors the same table in
|
|
26
|
+
* apps/sync-server's self-contained `errors.ts`. */
|
|
27
|
+
export function statusForType(type) {
|
|
28
|
+
switch (type) {
|
|
29
|
+
case 'AbloAuthenticationError':
|
|
30
|
+
return 401;
|
|
31
|
+
case 'AbloPermissionError':
|
|
32
|
+
return 403;
|
|
33
|
+
case 'AbloValidationError':
|
|
34
|
+
return 400;
|
|
35
|
+
case 'AbloRateLimitError':
|
|
36
|
+
return 429;
|
|
37
|
+
case 'AbloIdempotencyError':
|
|
38
|
+
case 'AbloStaleContextError':
|
|
39
|
+
case 'AbloClaimedError':
|
|
40
|
+
return 409;
|
|
41
|
+
case 'AbloConnectionError':
|
|
42
|
+
return 503;
|
|
43
|
+
case 'AbloServerError':
|
|
44
|
+
return 500;
|
|
45
|
+
default:
|
|
46
|
+
return 500;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Convert ANY thrown value into the canonical {@link ErrorEnvelope} plus an
|
|
51
|
+
* HTTP status. A typed {@link AbloError} is serialized via its own `toJSON`
|
|
52
|
+
* (so `code`/`param`/`doc_url`/structured `details` survive) and gets its
|
|
53
|
+
* status from an explicit `httpStatus` or, failing that, {@link statusForType}.
|
|
54
|
+
* Anything else degrades to a 500 `internal_error` envelope — never a bare
|
|
55
|
+
* framework "Internal Server Error" text body, and never a raw error string
|
|
56
|
+
* leaked onto the wire as an unregistered code.
|
|
57
|
+
*
|
|
58
|
+
* `requestId` is stamped into the body when the error didn't already carry one,
|
|
59
|
+
* so the response and the `x-request-id` header agree for support correlation.
|
|
60
|
+
*/
|
|
61
|
+
export function errorEnvelope(err, requestId) {
|
|
62
|
+
if (err instanceof AbloError) {
|
|
63
|
+
// Status precedence: an explicit httpStatus wins; else the code's canonical
|
|
64
|
+
// status from the registry (so `new AbloError('…', { code: 'entity_not_found' })`
|
|
65
|
+
// is a 404 without the throw site repeating it); else the subclass default.
|
|
66
|
+
const status = err.httpStatus ??
|
|
67
|
+
(err.code ? errorCodeSpec(err.code)?.httpStatus : undefined) ??
|
|
68
|
+
statusForType(err.type);
|
|
69
|
+
const body = err.toJSON();
|
|
70
|
+
return {
|
|
71
|
+
body: requestId && body.request_id === undefined ? { ...body, request_id: requestId } : body,
|
|
72
|
+
status,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
76
|
+
return {
|
|
77
|
+
body: {
|
|
78
|
+
type: 'AbloServerError',
|
|
79
|
+
code: 'internal_error',
|
|
80
|
+
message,
|
|
81
|
+
doc_url: docUrlForCode('internal_error'),
|
|
82
|
+
...(requestId ? { request_id: requestId } : {}),
|
|
83
|
+
},
|
|
84
|
+
status: 500,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/wire` — canonical COMMIT-PATH frame contract.
|
|
3
|
+
*
|
|
4
|
+
* These are the WebSocket (and HTTP-fallback) message shapes for the
|
|
5
|
+
* write path: the client's `commit` / `mutation` frames and the server's
|
|
6
|
+
* `mutation_result` ack. They live here — not in the server app and not
|
|
7
|
+
* inlined in the SDK's `SyncWebSocket` — so the client, the server, and
|
|
8
|
+
* any future `@abloatai/ablo/server` host all import ONE definition
|
|
9
|
+
* and cannot drift.
|
|
10
|
+
*
|
|
11
|
+
* Scope note: the delta/sync frames (`sync_response`, `delta`) are NOT
|
|
12
|
+
* here yet — they reference `SyncDelta`, which currently has two
|
|
13
|
+
* definitions (server `db/deltas` vs package `core`) pending unification.
|
|
14
|
+
* They stay server-local until that lands. Everything in this file
|
|
15
|
+
* depends only on package-canonical types (`OnStaleMode`, `ErrorCode`,
|
|
16
|
+
* `RequiredCapability`), so it is safe to share today.
|
|
17
|
+
*
|
|
18
|
+
* Changing any shape here is a wire-contract change — it requires
|
|
19
|
+
* coordinated client + server updates.
|
|
20
|
+
*/
|
|
21
|
+
import type { OnStaleMode } from '../coordination/index.js';
|
|
22
|
+
import type { ErrorCode, RequiredCapability } from '../errors.js';
|
|
23
|
+
/**
|
|
24
|
+
* A single operation within a {@link CommitMessage} batch. The atomic unit
|
|
25
|
+
* the server's commit executor applies (and, once the mutator seam lands,
|
|
26
|
+
* the raw-op fallback path when no named mutator is registered).
|
|
27
|
+
*/
|
|
28
|
+
export interface CommitOperation {
|
|
29
|
+
type: 'CREATE' | 'UPDATE' | 'DELETE' | 'ARCHIVE' | 'UNARCHIVE';
|
|
30
|
+
model: string;
|
|
31
|
+
id?: string | null;
|
|
32
|
+
input?: Record<string, unknown> | null;
|
|
33
|
+
/**
|
|
34
|
+
* Per-op client transaction id. Stamped onto `sync_deltas.transaction_id`
|
|
35
|
+
* so the originating client can recognize the broadcast as an echo of its
|
|
36
|
+
* own optimistic mutation. Distinct from the batch-level `clientTxId`
|
|
37
|
+
* (which keys `mutation_log` for retry idempotency).
|
|
38
|
+
*/
|
|
39
|
+
transactionId?: string | null;
|
|
40
|
+
/**
|
|
41
|
+
* Watermark from `context.capture`. The server checks whether the target
|
|
42
|
+
* has received deltas since this id; if so the operation's `onStale` mode
|
|
43
|
+
* applies.
|
|
44
|
+
*/
|
|
45
|
+
readAt?: number | null;
|
|
46
|
+
/**
|
|
47
|
+
* Mode on stale detection. `'reject'` (default) throws
|
|
48
|
+
* AbloStaleContextError; `'force'` applies unconditionally. `'flag'` /
|
|
49
|
+
* `'merge'` are reserved, not yet implemented.
|
|
50
|
+
*/
|
|
51
|
+
onStale?: OnStaleMode | null;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Client → Server single named-mutation frame. The named-mutator write
|
|
55
|
+
* primitive (intent + args), as opposed to the raw-op {@link CommitMessage}
|
|
56
|
+
* batch. Server-side mutator dispatch resolves `mutatorName` against the
|
|
57
|
+
* host-provided registry.
|
|
58
|
+
*/
|
|
59
|
+
export interface MutationMessage {
|
|
60
|
+
type: 'mutation';
|
|
61
|
+
payload: {
|
|
62
|
+
mutatorName: string;
|
|
63
|
+
input: unknown;
|
|
64
|
+
clientTxId: string;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Client → Server "commit this batch of operations" frame. Formerly named
|
|
69
|
+
* `batch_ack` / `BatchAckMessage` — renamed pre-stable to the customer-facing
|
|
70
|
+
* verb (`commit`) consistently across the wire and the SDK method
|
|
71
|
+
* (`MutationExecutor.commit`).
|
|
72
|
+
*/
|
|
73
|
+
export interface CommitMessage {
|
|
74
|
+
type: 'commit';
|
|
75
|
+
payload: {
|
|
76
|
+
operations: CommitOperation[];
|
|
77
|
+
clientTxId: string;
|
|
78
|
+
/**
|
|
79
|
+
* Optional turn handle. When the SDK opens a turn via
|
|
80
|
+
* `SyncAgent.beginTurn(...)`, subsequent commits within the handle's
|
|
81
|
+
* scope auto-attach the `turnId` here. The Hub validates the turn
|
|
82
|
+
* belongs to the same agent and is open, then threads it onto every
|
|
83
|
+
* delta's `caused_by_task_id` column. Absent for human-direct commits
|
|
84
|
+
* and for SDKs that predate the turn protocol — those produce deltas
|
|
85
|
+
* with `caused_by_task_id = NULL`, which the audit pane treats as "no
|
|
86
|
+
* prompt-side context recorded."
|
|
87
|
+
*/
|
|
88
|
+
causedByTaskId?: string | null;
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Wire ack for a `commit` frame. Payload mirrors the canonical
|
|
93
|
+
* `CommitReceipt` shape so WebSocket, HTTP `/v1/commits`, and persisted
|
|
94
|
+
* `AgentJob.result.receipt` all carry identical fields.
|
|
95
|
+
*
|
|
96
|
+
* `object`, `status`, and `ops` are typed optional because pre-unification
|
|
97
|
+
* WS clients didn't ship them; servers always populate them on the way out.
|
|
98
|
+
* New clients can rely on them.
|
|
99
|
+
*/
|
|
100
|
+
export interface MutationResultMessage {
|
|
101
|
+
type: 'mutation_result';
|
|
102
|
+
payload: {
|
|
103
|
+
object?: 'commit_receipt';
|
|
104
|
+
clientTxId: string;
|
|
105
|
+
serverTxId: string;
|
|
106
|
+
success: boolean;
|
|
107
|
+
status?: 'confirmed' | 'rejected';
|
|
108
|
+
lastSyncId?: number;
|
|
109
|
+
ops?: number;
|
|
110
|
+
error?: {
|
|
111
|
+
code: ErrorCode;
|
|
112
|
+
message: string;
|
|
113
|
+
field?: string;
|
|
114
|
+
/** Structured rejection body (x402-style) emitted when the cap
|
|
115
|
+
* verifier denies the commit. */
|
|
116
|
+
requiredCapability?: RequiredCapability;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
|
|
3
|
+
* client-runtime (mobx / react / IndexedDB) dependency, so a server-side
|
|
4
|
+
* consumer — a Next.js route handler, an edge function — can import the
|
|
5
|
+
* envelope producers without pulling in the whole sync client.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, both Stripe-shaped and used across every Ablo surface:
|
|
8
|
+
* - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
|
|
9
|
+
* {@link statusForType} turn any thrown value into
|
|
10
|
+
* `{ type, code, param, message, doc_url, request_id }`.
|
|
11
|
+
* - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
|
|
12
|
+
* uniform `{ object: 'list', data, has_more, next_cursor }` collection.
|
|
13
|
+
*
|
|
14
|
+
* The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
|
|
15
|
+
* helpers are re-exported so a route can THROW the right typed error and
|
|
16
|
+
* SERIALIZE it through a single import.
|
|
17
|
+
*/
|
|
18
|
+
export { errorEnvelope, statusForType } from './errorEnvelope.js';
|
|
19
|
+
export type { ErrorEnvelope } from './errorEnvelope.js';
|
|
20
|
+
export { listEnvelope } from './listEnvelope.js';
|
|
21
|
+
export type { ListEnvelope } from './listEnvelope.js';
|
|
22
|
+
export type { CommitOperation, MutationMessage, CommitMessage, MutationResultMessage, } from './frames.js';
|
|
23
|
+
export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
|
|
24
|
+
export type { ErrorCode, WireErrorCode } from '../errors.js';
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@abloatai/ablo/wire` — the canonical HTTP/frame WIRE CONTRACT, with no
|
|
3
|
+
* client-runtime (mobx / react / IndexedDB) dependency, so a server-side
|
|
4
|
+
* consumer — a Next.js route handler, an edge function — can import the
|
|
5
|
+
* envelope producers without pulling in the whole sync client.
|
|
6
|
+
*
|
|
7
|
+
* Two halves, both Stripe-shaped and used across every Ablo surface:
|
|
8
|
+
* - ERROR egress — {@link errorEnvelope} / {@link ErrorEnvelope} /
|
|
9
|
+
* {@link statusForType} turn any thrown value into
|
|
10
|
+
* `{ type, code, param, message, doc_url, request_id }`.
|
|
11
|
+
* - LIST egress — {@link listEnvelope} / {@link ListEnvelope} stamp the
|
|
12
|
+
* uniform `{ object: 'list', data, has_more, next_cursor }` collection.
|
|
13
|
+
*
|
|
14
|
+
* The {@link AbloError} hierarchy + {@link docUrlForCode} + the wire-PARSE
|
|
15
|
+
* helpers are re-exported so a route can THROW the right typed error and
|
|
16
|
+
* SERIALIZE it through a single import.
|
|
17
|
+
*/
|
|
18
|
+
export { errorEnvelope, statusForType } from './errorEnvelope.js';
|
|
19
|
+
export { listEnvelope } from './listEnvelope.js';
|
|
20
|
+
// The error surface a wire consumer needs to throw, classify, and serialize.
|
|
21
|
+
export { AbloError, AbloAuthenticationError, AbloPermissionError, AbloValidationError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, SyncSessionError, docUrlForCode, translateHttpError, errorFromWire, toAbloError, ERROR_CONTRACT_VERSION, } from '../errors.js';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The canonical Ablo LIST envelope — the one shape every endpoint that returns
|
|
3
|
+
* a collection uses, so a consumer can detect + paginate any list uniformly
|
|
4
|
+
* instead of learning a per-endpoint payload key (`{ keys }`, `{ origins }`,
|
|
5
|
+
* `{ events }`, `{ buckets }`…).
|
|
6
|
+
*
|
|
7
|
+
* `{ object: 'list', data: [...], has_more, next_cursor }` is the shape the
|
|
8
|
+
* hosted `GET /v1/models/:model` endpoint already emits (apps/sync-server
|
|
9
|
+
* `routes/query.ts`) and that `@ablo/mcp` already consumes — promoted here so
|
|
10
|
+
* sync-web's dashboard lists, the SDK, and any future surface produce the
|
|
11
|
+
* identical envelope from one definition.
|
|
12
|
+
*
|
|
13
|
+
* The field NAMES are Stripe's (`object`/`has_more`/`next_cursor`), not
|
|
14
|
+
* PlanetScale's (`type`/`cursor_start`/`has_next`): the rest of the Ablo API is
|
|
15
|
+
* Stripe-modeled, so this keeps one vocabulary across the surface. The
|
|
16
|
+
* PlanetScale discipline we deliberately borrow is *"every list is the same
|
|
17
|
+
* envelope"* — not the concrete key names.
|
|
18
|
+
*/
|
|
19
|
+
export interface ListEnvelope<T> {
|
|
20
|
+
/** Discriminator — always `'list'`. Lets a generic client recognise a
|
|
21
|
+
* paginated collection without per-endpoint special-casing. */
|
|
22
|
+
readonly object: 'list';
|
|
23
|
+
/** The page of results. Always present (an empty array when there are none),
|
|
24
|
+
* never omitted, so `body.data` is a stable access path. */
|
|
25
|
+
readonly data: readonly T[];
|
|
26
|
+
/** Whether more results exist past this page. Drive "load more" off this,
|
|
27
|
+
* not off `data.length === limit` (ambiguous on an exact-multiple page). */
|
|
28
|
+
readonly has_more: boolean;
|
|
29
|
+
/** Opaque cursor to pass back as `?starting_after=` for the next page, or
|
|
30
|
+
* `null` when {@link has_more} is `false`. */
|
|
31
|
+
readonly next_cursor: string | null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
|
|
35
|
+
*
|
|
36
|
+
* Pagination stays the caller's responsibility (fetch `limit + 1`, decide
|
|
37
|
+
* `hasMore`, derive the cursor from the last row's order key) — this only
|
|
38
|
+
* applies the envelope so no endpoint hand-rolls the shape. The defaults model
|
|
39
|
+
* the common "small, unpaginated collection" case (`has_more: false`,
|
|
40
|
+
* `next_cursor: null`); a paginated endpoint passes both explicitly.
|
|
41
|
+
*/
|
|
42
|
+
export declare function listEnvelope<T>(data: readonly T[], opts?: {
|
|
43
|
+
hasMore?: boolean;
|
|
44
|
+
nextCursor?: string | null;
|
|
45
|
+
}): ListEnvelope<T>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stamp the uniform {@link ListEnvelope} onto an already-resolved page of rows.
|
|
3
|
+
*
|
|
4
|
+
* Pagination stays the caller's responsibility (fetch `limit + 1`, decide
|
|
5
|
+
* `hasMore`, derive the cursor from the last row's order key) — this only
|
|
6
|
+
* applies the envelope so no endpoint hand-rolls the shape. The defaults model
|
|
7
|
+
* the common "small, unpaginated collection" case (`has_more: false`,
|
|
8
|
+
* `next_cursor: null`); a paginated endpoint passes both explicitly.
|
|
9
|
+
*/
|
|
10
|
+
export function listEnvelope(data, opts = {}) {
|
|
11
|
+
return {
|
|
12
|
+
object: 'list',
|
|
13
|
+
data,
|
|
14
|
+
has_more: opts.hasMore ?? false,
|
|
15
|
+
next_cursor: opts.nextCursor ?? null,
|
|
16
|
+
};
|
|
17
|
+
}
|
package/docs/api.md
CHANGED
|
@@ -31,10 +31,10 @@ const schema = defineSchema({
|
|
|
31
31
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
32
32
|
|
|
33
33
|
await ablo.ready();
|
|
34
|
-
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
34
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
35
35
|
if (!report) throw new Error('Row not found');
|
|
36
36
|
|
|
37
|
-
await ablo.weatherReports.update('report_stockholm', { status: 'ready' },
|
|
37
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
|
|
38
38
|
```
|
|
39
39
|
|
|
40
40
|
For end-to-end app setup across React, existing backends, Data Source, and
|
|
@@ -44,29 +44,29 @@ agents, read the [Integration Guide](./integration-guide.md).
|
|
|
44
44
|
|
|
45
45
|
Each schema model becomes a typed model on the client:
|
|
46
46
|
|
|
47
|
-
- `ablo.weatherReports.retrieve(id)` reads one row asynchronously (server read).
|
|
47
|
+
- `ablo.weatherReports.retrieve({ id })` reads one row asynchronously (server read).
|
|
48
48
|
- `ablo.weatherReports.list({ where })` reads a collection asynchronously (server read).
|
|
49
49
|
- `ablo.weatherReports.get(id)` reads one row synchronously from the local graph.
|
|
50
|
-
- `ablo.weatherReports.create(data)` creates a row.
|
|
51
|
-
- `ablo.weatherReports.update(id, data, options
|
|
52
|
-
- `ablo.weatherReports.delete(id, options
|
|
50
|
+
- `ablo.weatherReports.create({ data })` creates a row.
|
|
51
|
+
- `ablo.weatherReports.update({ id, data, ...options })` updates a row.
|
|
52
|
+
- `ablo.weatherReports.delete({ id, ...options })` deletes a row.
|
|
53
53
|
|
|
54
54
|
`retrieve`/`list` and `get`/`getAll`/`getCount` are not aliases. Use
|
|
55
|
-
`retrieve(id)` or `list({ where })` when the row may not be local yet — they
|
|
55
|
+
`retrieve({ id })` or `list({ where })` when the row may not be local yet — they
|
|
56
56
|
hydrate pool → IndexedDB → network. Use `get(id)` / `getAll({ where })` /
|
|
57
57
|
`getCount({ where })` for a cheap synchronous snapshot of what is already in
|
|
58
58
|
the local graph.
|
|
59
59
|
|
|
60
60
|
| Method | Returns | Use when |
|
|
61
61
|
|---|---|---|
|
|
62
|
-
| `retrieve(id)` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
|
|
62
|
+
| `retrieve({ id })` | `Promise<T \| undefined>` | You need one row, hydrating from local store and server. |
|
|
63
63
|
| `list({ where })` | `Promise<T[]>` | You need to hydrate a collection from local store and server. |
|
|
64
64
|
| `get(id)` | `T \| undefined` | You want a synchronous snapshot of one local row. |
|
|
65
65
|
| `getAll(options?)` | `T[]` | You want a synchronous snapshot of a local collection. |
|
|
66
66
|
| `getCount(options?)` | `number` | You want a synchronous count of local rows. |
|
|
67
|
-
| `create(data, options
|
|
68
|
-
| `update(id, data, options
|
|
69
|
-
| `delete(id, options
|
|
67
|
+
| `create({ data, ...options })` | `Promise<T>` | You want to create through the schema model. |
|
|
68
|
+
| `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
|
|
69
|
+
| `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
|
|
70
70
|
|
|
71
71
|
`retrieve`, `list`, `create`, `update`, and `delete` are the main path — they go
|
|
72
72
|
through the server. `get` / `getAll` / `getCount` are **synchronous reads**
|
|
@@ -79,11 +79,13 @@ Use `snapshot` when a write should reject if the row changed mid-flight:
|
|
|
79
79
|
```ts
|
|
80
80
|
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
81
81
|
|
|
82
|
-
await ablo.weatherReports.update(
|
|
83
|
-
'report_stockholm',
|
|
84
|
-
{ status: 'ready' },
|
|
85
|
-
|
|
86
|
-
|
|
82
|
+
await ablo.weatherReports.update({
|
|
83
|
+
id: 'report_stockholm',
|
|
84
|
+
data: { status: 'ready' },
|
|
85
|
+
readAt: snap.stamp,
|
|
86
|
+
onStale: 'reject',
|
|
87
|
+
wait: 'confirmed',
|
|
88
|
+
});
|
|
87
89
|
```
|
|
88
90
|
|
|
89
91
|
Protected write options:
|
|
@@ -105,11 +107,11 @@ two writers serialize instead of clobbering. A claim is temporary: it expires
|
|
|
105
107
|
on its own if the holder stops, and is never saved as a row.
|
|
106
108
|
|
|
107
109
|
You coordinate a row with calls on its model, beside `create`/`update`/`retrieve`:
|
|
108
|
-
`ablo.<model>.claim(id
|
|
109
|
-
`ablo.<model>.claim.state(id)` reads who currently holds it (synchronous, never
|
|
110
|
-
blocks), and `ablo.<model>.claim.release(id)` releases it early. The full
|
|
111
|
-
coordination surface is `claim.state(id)` / `claim.queue(id)` /
|
|
112
|
-
`claim.release(id)` / `claim.reorder(id, order)` hanging off `claim`.
|
|
110
|
+
`ablo.<model>.claim({ id })` takes the claim and returns a handle,
|
|
111
|
+
`ablo.<model>.claim.state({ id })` reads who currently holds it (synchronous, never
|
|
112
|
+
blocks), and `ablo.<model>.claim.release({ id })` releases it early. The full
|
|
113
|
+
coordination surface is `claim.state({ id })` / `claim.queue({ id })` /
|
|
114
|
+
`claim.release({ id })` / `claim.reorder({ id, order })` hanging off `claim`.
|
|
113
115
|
|
|
114
116
|
### The Claim State Object
|
|
115
117
|
|
|
@@ -140,7 +142,7 @@ coordination surface is `claim.state(id)` / `claim.queue(id)` /
|
|
|
140
142
|
### Lifecycle
|
|
141
143
|
|
|
142
144
|
```
|
|
143
|
-
claim(id)
|
|
145
|
+
claim({ id }) update({ id }) lands
|
|
144
146
|
(free) ───────────▶ active ───────────────────────▶ committed
|
|
145
147
|
│
|
|
146
148
|
┌───────────┴───────────┐
|
|
@@ -149,16 +151,16 @@ coordination surface is `claim.state(id)` / `claim.queue(id)` /
|
|
|
149
151
|
(release w/o write) (TTL; holder died)
|
|
150
152
|
```
|
|
151
153
|
|
|
152
|
-
A target is free when `ablo.<model>.claim.state(id)` is `null`. Terminal
|
|
154
|
+
A target is free when `ablo.<model>.claim.state({ id })` is `null`. Terminal
|
|
153
155
|
states drop out of the live stream, so a present claim is either `active` (the
|
|
154
156
|
holder) or `queued` (waiting in the FIFO line behind the holder; see
|
|
155
|
-
`claim.queue(id)`).
|
|
157
|
+
`claim.queue({ id })`).
|
|
156
158
|
|
|
157
159
|
### Reading and claiming
|
|
158
160
|
|
|
159
|
-
`claim.state(id)` is the read side for observers: synchronous, never blocks, and
|
|
160
|
-
returns the live claim state object (or `null`). `claim(id
|
|
161
|
-
side: it takes the claim and returns
|
|
161
|
+
`claim.state({ id })` is the read side for observers: synchronous, never blocks, and
|
|
162
|
+
returns the live claim state object (or `null`). `claim({ id })` is the write
|
|
163
|
+
side: it takes the claim and returns a `ClaimHandle`. Claims don't lock — if someone else
|
|
162
164
|
already holds the row, `claim` waits for them to finish, re-reads the fresh row,
|
|
163
165
|
then hands it to you, so you always proceed from current state. Default reads
|
|
164
166
|
return the row even while someone is mid-edit; if a server read should not
|
|
@@ -166,31 +168,32 @@ return a row while it's claimed, pass `ifClaimed: 'wait'` to wait for the claim
|
|
|
166
168
|
to clear, or `ifClaimed: 'fail'` to error out instead.
|
|
167
169
|
|
|
168
170
|
```ts
|
|
169
|
-
const claim = ablo.weatherReports.claim.state('report_stockholm');
|
|
171
|
+
const claim = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
170
172
|
if (claim) {
|
|
171
173
|
claim.heldBy;
|
|
172
174
|
claim.action;
|
|
173
175
|
}
|
|
174
176
|
|
|
175
|
-
const
|
|
176
|
-
'report_stockholm',
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
);
|
|
177
|
+
const handle = await ablo.weatherReports.claim({
|
|
178
|
+
id: 'report_stockholm',
|
|
179
|
+
action: 'editing',
|
|
180
|
+
ttl: '2m',
|
|
181
|
+
});
|
|
182
|
+
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
|
|
183
|
+
await handle.release();
|
|
180
184
|
```
|
|
181
185
|
|
|
182
|
-
Writes go through the normal `ablo.<model>.update(id, data)`. While you hold
|
|
186
|
+
Writes go through the normal `ablo.<model>.update({ id, data })`. While you hold
|
|
183
187
|
a claim on `id`, that `update` rejects with `AbloStaleContextError` if the row
|
|
184
188
|
changed underneath you since you took the claim, so you re-read before retrying.
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
need to release early.
|
|
189
|
+
Call `handle.release()` (or `ablo.weatherReports.claim.release({ id })`) to release
|
|
190
|
+
the claim when your work is done.
|
|
188
191
|
|
|
189
192
|
## Agent
|
|
190
193
|
|
|
191
194
|
Most agents should import the same schema as the app and call
|
|
192
|
-
`ablo.<model>.list(...)`, `ablo.<model>.claim(
|
|
193
|
-
`ablo.<model>.update(
|
|
195
|
+
`ablo.<model>.list(...)`, `ablo.<model>.claim({ id })`, and
|
|
196
|
+
`ablo.<model>.update({ id, data })`.
|
|
194
197
|
|
|
195
198
|
## HTTP API
|
|
196
199
|
|
|
@@ -203,12 +206,12 @@ endpoint documents that model's real field contract instead of a generic blob.
|
|
|
203
206
|
|
|
204
207
|
| SDK call | HTTP |
|
|
205
208
|
|---|---|
|
|
206
|
-
| `ablo.<model>.create(data)` | `POST /v1/models/{model}` |
|
|
209
|
+
| `ablo.<model>.create({ data })` | `POST /v1/models/{model}` |
|
|
207
210
|
| `ablo.<model>.list({ where })` | `GET /v1/models/{model}` |
|
|
208
|
-
| `ablo.<model>.retrieve(id)` | `GET /v1/models/{model}/{id}` |
|
|
209
|
-
| `ablo.<model>.update(id, data)` | `PATCH /v1/models/{model}/{id}` |
|
|
210
|
-
| `ablo.<model>.delete(id)` | `DELETE /v1/models/{model}/{id}` |
|
|
211
|
-
| `ablo.<model>.claim(id)` | `POST /v1/models/{model}/{id}/claim` |
|
|
211
|
+
| `ablo.<model>.retrieve({ id })` | `GET /v1/models/{model}/{id}` |
|
|
212
|
+
| `ablo.<model>.update({ id, data })` | `PATCH /v1/models/{model}/{id}` |
|
|
213
|
+
| `ablo.<model>.delete({ id })` | `DELETE /v1/models/{model}/{id}` |
|
|
214
|
+
| `ablo.<model>.claim({ id })` | `POST /v1/models/{model}/{id}/claim` |
|
|
212
215
|
| (release a claim) | `DELETE /v1/models/{model}/{id}/claim` |
|
|
213
216
|
|
|
214
217
|
Auth is a bearer API key: `Authorization: Bearer sk_…`. Mutations take an
|