@abloatai/ablo 0.23.0 → 0.25.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 +33 -0
- package/README.md +42 -33
- package/dist/ai-sdk/coordinated-tool.d.ts +101 -0
- package/dist/ai-sdk/coordinated-tool.js +129 -0
- package/dist/ai-sdk/index.d.ts +42 -0
- package/dist/ai-sdk/index.js +42 -0
- package/dist/cli.cjs +948 -529
- package/dist/client/Ablo.js +4 -0
- package/dist/interfaces/index.d.ts +8 -0
- package/dist/sync/BootstrapHelper.d.ts +16 -0
- package/dist/sync/BootstrapHelper.js +27 -0
- package/dist/sync/schemas.d.ts +1 -0
- package/dist/sync/schemas.js +3 -0
- package/docs/data-sources.md +12 -5
- package/llms.txt +4 -2
- package/package.json +1 -1
package/dist/client/Ablo.js
CHANGED
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { z } from 'zod';
|
|
22
22
|
import { baseFieldsSchema } from '../schema/schema.js';
|
|
23
|
+
import { schemaHash } from '../schema/serialize.js';
|
|
23
24
|
import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, AbloNotFoundError, translateHttpError, toAbloError, claimedError } from '../errors.js';
|
|
24
25
|
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
25
26
|
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
@@ -222,6 +223,9 @@ function deriveConfigFromSchema(schema) {
|
|
|
222
223
|
defaultNonCreatePriority: 50,
|
|
223
224
|
essentialFields: {},
|
|
224
225
|
classNameFallbackMap: {},
|
|
226
|
+
// Hash this client's schema once so bootstrap can detect drift against the
|
|
227
|
+
// server's active hash (same `schemaHash` the CLI push + server compute).
|
|
228
|
+
expectedSchemaHash: schemaHash(schema),
|
|
225
229
|
};
|
|
226
230
|
}
|
|
227
231
|
// ── Auto model registration from schema ───────────────────────────────────
|
|
@@ -380,6 +380,14 @@ export interface SyncEngineConfig {
|
|
|
380
380
|
* e.g., { TaskModel: 'Task', ProjectModel: 'Project' }
|
|
381
381
|
*/
|
|
382
382
|
classNameFallbackMap: Readonly<Record<string, string>>;
|
|
383
|
+
/**
|
|
384
|
+
* Content hash of the schema THIS client was built against (the same
|
|
385
|
+
* `schemaHash()` the CLI push + server compute). Used purely to detect
|
|
386
|
+
* schema drift: when the server reports a different active hash on bootstrap,
|
|
387
|
+
* the SDK warns the developer to run `ablo push` — otherwise drift only
|
|
388
|
+
* surfaces later as an opaque DB constraint error. Advisory, not enforced.
|
|
389
|
+
*/
|
|
390
|
+
expectedSchemaHash?: string;
|
|
383
391
|
}
|
|
384
392
|
/**
|
|
385
393
|
* Allows consumers to extend the WebSocket event map with
|
|
@@ -18,6 +18,12 @@ export interface BootstrapData {
|
|
|
18
18
|
/** Model types whose server-side query failed (timeout, RLS error, etc.) */
|
|
19
19
|
failedModels?: string[];
|
|
20
20
|
timestamp: number;
|
|
21
|
+
/**
|
|
22
|
+
* The server's ACTIVE schema content hash for this tenant (same `schemaHash`
|
|
23
|
+
* the CLI push computes). Present once the tenant has pushed a schema; the
|
|
24
|
+
* client compares it to its own `config.expectedSchemaHash` to warn on drift.
|
|
25
|
+
*/
|
|
26
|
+
schemaHash?: string;
|
|
21
27
|
}
|
|
22
28
|
export interface BootstrapFetchResult {
|
|
23
29
|
notModified: boolean;
|
|
@@ -72,7 +78,17 @@ import { type ValidatedServerDelta } from './schemas.js';
|
|
|
72
78
|
export declare class BootstrapHelper {
|
|
73
79
|
private options;
|
|
74
80
|
private abortController;
|
|
81
|
+
/** Warn about schema drift at most once per helper. */
|
|
82
|
+
private schemaDriftWarned;
|
|
75
83
|
get baseUrl(): string;
|
|
84
|
+
/**
|
|
85
|
+
* Advisory schema-drift check: compare the server's active schema hash (on the
|
|
86
|
+
* bootstrap response) against the hash this client was built with. A mismatch
|
|
87
|
+
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
88
|
+
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
89
|
+
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
90
|
+
*/
|
|
91
|
+
private warnOnSchemaDrift;
|
|
76
92
|
constructor(options: BootstrapOptions);
|
|
77
93
|
/**
|
|
78
94
|
* Update the offline-cache namespace once auth has resolved the server-side
|
|
@@ -10,9 +10,34 @@ import { parseBootstrapResponse } from './schemas.js';
|
|
|
10
10
|
export class BootstrapHelper {
|
|
11
11
|
options;
|
|
12
12
|
abortController = null;
|
|
13
|
+
/** Warn about schema drift at most once per helper. */
|
|
14
|
+
schemaDriftWarned = false;
|
|
13
15
|
get baseUrl() {
|
|
14
16
|
return this.options.baseUrl;
|
|
15
17
|
}
|
|
18
|
+
/**
|
|
19
|
+
* Advisory schema-drift check: compare the server's active schema hash (on the
|
|
20
|
+
* bootstrap response) against the hash this client was built with. A mismatch
|
|
21
|
+
* means the app's schema and the deployed schema have diverged — reads/writes
|
|
22
|
+
* relying on undeployed changes will later fail with an opaque DB constraint
|
|
23
|
+
* error. Warn once, actionably; never throws or blocks the bootstrap.
|
|
24
|
+
*/
|
|
25
|
+
warnOnSchemaDrift(serverHash) {
|
|
26
|
+
if (this.schemaDriftWarned || !serverHash)
|
|
27
|
+
return;
|
|
28
|
+
const clientHash = getContext().config.expectedSchemaHash;
|
|
29
|
+
if (!clientHash || clientHash === serverHash)
|
|
30
|
+
return;
|
|
31
|
+
this.schemaDriftWarned = true;
|
|
32
|
+
// Self-brand the message ("Ablo:") rather than rely on the default logger's
|
|
33
|
+
// `[Ablo]` namespace — consumers wiring their own logger (pino, etc.) lose
|
|
34
|
+
// that prefix, and a drift warning that reads like the app's own log is
|
|
35
|
+
// worse than none. The brand tells them at a glance who is talking.
|
|
36
|
+
getContext().logger.warn(`Ablo: Schema drift detected — this app was built against schema ${clientHash}, ` +
|
|
37
|
+
`but the deployed schema is ${serverHash}. Operations that depend on schema ` +
|
|
38
|
+
`changes not yet deployed will fail later with an opaque database error. Run ` +
|
|
39
|
+
`\`ablo push\` to deploy your schema (or update this app to match the deployed one).`, { clientSchemaHash: clientHash, serverSchemaHash: serverHash });
|
|
40
|
+
}
|
|
16
41
|
constructor(options) {
|
|
17
42
|
// Defaults are spread first; the explicit `baseUrl` then takes precedence
|
|
18
43
|
// and is computed from `options.baseUrl` (or the localhost fallback).
|
|
@@ -254,6 +279,7 @@ export class BootstrapHelper {
|
|
|
254
279
|
}
|
|
255
280
|
const rawJson = await res.json();
|
|
256
281
|
const data = parseBootstrapResponse(rawJson);
|
|
282
|
+
this.warnOnSchemaDrift(data.schemaHash);
|
|
257
283
|
// Persist payload for offline
|
|
258
284
|
try {
|
|
259
285
|
if (this.options.cacheScope) {
|
|
@@ -326,6 +352,7 @@ export class BootstrapHelper {
|
|
|
326
352
|
}
|
|
327
353
|
const rawJson = await response.json();
|
|
328
354
|
const data = parseBootstrapResponse(rawJson);
|
|
355
|
+
this.warnOnSchemaDrift(data.schemaHash);
|
|
329
356
|
// Save a copy for offline
|
|
330
357
|
try {
|
|
331
358
|
if (this.options.cacheScope) {
|
package/dist/sync/schemas.d.ts
CHANGED
|
@@ -70,6 +70,7 @@ export declare const BootstrapResponseSchema: z.ZodObject<{
|
|
|
70
70
|
deltaCount: z.ZodOptional<z.ZodNumber>;
|
|
71
71
|
failedModels: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
72
72
|
timestamp: z.ZodDefault<z.ZodNumber>;
|
|
73
|
+
schemaHash: z.ZodOptional<z.ZodString>;
|
|
73
74
|
}, z.core.$loose>;
|
|
74
75
|
export type ValidatedBootstrapResponse = z.infer<typeof BootstrapResponseSchema>;
|
|
75
76
|
/**
|
package/dist/sync/schemas.js
CHANGED
|
@@ -54,6 +54,9 @@ export const BootstrapResponseSchema = z
|
|
|
54
54
|
deltaCount: z.number().optional(),
|
|
55
55
|
failedModels: z.array(z.string()).optional(),
|
|
56
56
|
timestamp: z.number().default(() => Date.now()),
|
|
57
|
+
// Server's active schema hash (drift detection). Optional: absent from
|
|
58
|
+
// older servers / tenants that have never pushed a schema.
|
|
59
|
+
schemaHash: z.string().optional(),
|
|
57
60
|
})
|
|
58
61
|
.passthrough();
|
|
59
62
|
// ─── Parse Helpers ───────────────────────────────────────────────────────────
|
package/docs/data-sources.md
CHANGED
|
@@ -6,7 +6,8 @@ schema, and never migrates it**. Your application keeps writing to its own
|
|
|
6
6
|
Postgres through its own backend, exactly as it does today; Ablo only tails the
|
|
7
7
|
changes and fans the confirmed rows out to every connected human and agent. This
|
|
8
8
|
is the same model ElectricSQL, PowerSync, and Zero use — a publication plus a
|
|
9
|
-
replication slot
|
|
9
|
+
replication slot. Ablo consumes the logical-replication stream; your application
|
|
10
|
+
continues to own the write path.
|
|
10
11
|
|
|
11
12
|
> **Just trying Ablo?** You don't need a database at all to start. The hosted
|
|
12
13
|
> **sandbox** can host rows in Ablo's test plane — pass an `apiKey` only and omit
|
|
@@ -94,8 +95,10 @@ Re-run it until every item is green.
|
|
|
94
95
|
|
|
95
96
|
### 4. Point Ablo at the database with the replication role
|
|
96
97
|
|
|
97
|
-
Give Ablo the connection string for the **replication role** you created
|
|
98
|
-
|
|
98
|
+
Give Ablo the connection string for the **replication role** you created — a
|
|
99
|
+
`REPLICATION`-attributed role that streams the WAL and `SELECT`s, nothing more
|
|
100
|
+
(not a read-only account; see the privilege note below). The same value `--check`
|
|
101
|
+
validated:
|
|
99
102
|
|
|
100
103
|
```bash
|
|
101
104
|
# .env — server runtime only, never the browser
|
|
@@ -156,8 +159,12 @@ Operational reality you should know up front:
|
|
|
156
159
|
WAL accumulates and consumes disk. **Ablo monitors slot lag and WAL retention**
|
|
157
160
|
and surfaces it so you're never surprised by disk pressure; an abandoned slot is
|
|
158
161
|
dropped rather than left to grow unbounded.
|
|
159
|
-
- **The role
|
|
160
|
-
|
|
162
|
+
- **The role's privilege footprint is narrow and precise — not a "read-only"
|
|
163
|
+
account.** It carries the `REPLICATION` attribute, which lets it stream the WAL
|
|
164
|
+
and `SELECT`; it cannot `INSERT`/`UPDATE`/`DELETE`, run DDL, or own objects, and
|
|
165
|
+
the recipe never grants it more. (For a security review, state it that way — a
|
|
166
|
+
logical-replication role is a real privilege, just a tightly-scoped one — rather
|
|
167
|
+
than calling it "read-only", which a reviewer will correctly push back on.)
|
|
161
168
|
|
|
162
169
|
What Ablo explicitly does **not** do:
|
|
163
170
|
|
package/llms.txt
CHANGED
|
@@ -122,9 +122,11 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
|
|
|
122
122
|
|
|
123
123
|
## Storage Boundary
|
|
124
124
|
|
|
125
|
-
|
|
125
|
+
THE ONE PATH IS LOGICAL REPLICATION. In production every schema model is backed by YOUR OWN database, and Ablo connects to it by CONSUMING ITS WRITE-AHEAD LOG (logical replication) — the Electric/PowerSync/Zero model. Ablo tails the WAL and fans changes out; YOUR APPLICATION CONTINUES TO OWN THE WRITE PATH (your own backend, your existing API). Ablo never runs DDL on, writes to, owns, or migrates your database. Connect once with `npx ablo connect` (prints the setup SQL: `wal_level=logical`, a publication, a `REPLICATION` role) then `npx ablo connect --register`. Validate with `npx ablo connect --check`. The role's privilege footprint is narrow but real — a `REPLICATION`-attributed role that streams the WAL and `SELECT`s; do NOT describe it as "read-only" (a security reviewer will push back — it is a replication privilege, just tightly scoped).
|
|
126
126
|
|
|
127
|
-
|
|
127
|
+
FALLBACK (only when the database can't grant a `REPLICATION` role, e.g. a locked-down managed DB): a signed Data Source endpoint that hands Ablo an ORM `adapter` (Drizzle default; Prisma and Kysely supported) — it owns the transaction, exactly-once idempotency, and outbox in ONE pass; your credentials never leave your infrastructure.
|
|
128
|
+
|
|
129
|
+
DEPRECATED — being removed, do NOT recommend or scaffold: the `databaseUrl` constructor option (the dial-in, where Ablo held a read/write connection and committed directly to your DB). The end-state constructor is `Ablo({ schema, apiKey })`; the database is connected out of band via `ablo connect`, not through `databaseUrl`. (Note: `ablo init` still defaults to `--storage direct` scaffolding `databaseUrl` — that default is itself slated to lead with `ablo connect`; prefer logical replication regardless of what `init` scaffolds.)
|
|
128
130
|
|
|
129
131
|
GOTCHA the user WILL hit: `DATABASE_URL` must use a NON-superuser, NON-BYPASSRLS role (Ablo enforces row-level security; owner roles are rejected with `database_role_cannot_enforce_rls`). Neon's and Supabase's default dashboard connection strings use the database OWNER (e.g. `neondb_owner`) and are rejected. EASIEST: `npx ablo migrate` detects the unsafe role and creates the scoped one automatically from the user's machine (owner credential never reaches Ablo; new DATABASE_URL written to the env file). Manual alternative — create a scoped role first: `CREATE ROLE ablo_app LOGIN PASSWORD '...' NOSUPERUSER NOBYPASSRLS; GRANT CREATE, CONNECT ON DATABASE <db> TO ablo_app; GRANT CREATE, USAGE ON SCHEMA public TO ablo_app;` — then swap user/password into the same host/db string.
|
|
130
132
|
|