@abloatai/ablo 0.23.0 → 0.24.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.
@@ -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) {
@@ -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
  /**
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.23.0",
3
+ "version": "0.24.0",
4
4
  "description": "The Collaboration Layer For AI Agents",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",