@abloatai/ablo 0.29.3 → 0.30.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 +31 -0
- package/README.md +3 -2
- package/dist/BaseSyncedStore.js +3 -1
- package/dist/Database.js +29 -2
- package/dist/SyncClient.d.ts +5 -3
- package/dist/SyncClient.js +8 -4
- package/dist/ai-sdk/coordinatedTool.d.ts +0 -1
- package/dist/ai-sdk/coordinatedTool.js +0 -1
- package/dist/ai-sdk/coordinationContext.js +1 -5
- package/dist/cli.cjs +377 -84
- package/dist/client/Ablo.d.ts +1 -1
- package/dist/client/Ablo.js +24 -36
- package/dist/client/auth.d.ts +8 -18
- package/dist/client/auth.js +16 -96
- package/dist/client/createModelProxy.d.ts +16 -11
- package/dist/client/createModelProxy.js +162 -20
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/httpTransport.js +263 -89
- package/dist/client/options.d.ts +0 -15
- package/dist/client/resourceTypes.d.ts +13 -26
- package/dist/client/schemaConfig.js +5 -0
- package/dist/client/writeOptionsSchema.d.ts +1 -0
- package/dist/client/writeOptionsSchema.js +11 -2
- package/dist/client/wsMutationExecutor.d.ts +3 -5
- package/dist/coordination/schema.d.ts +17 -13
- package/dist/coordination/schema.js +36 -10
- package/dist/errorCodes.d.ts +9 -4
- package/dist/errorCodes.js +10 -5
- package/dist/errors.js +4 -9
- package/dist/index.d.ts +4 -0
- package/dist/interfaces/index.d.ts +31 -19
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +4 -1
- package/dist/schema/schema.d.ts +13 -0
- package/dist/schema/select.js +7 -0
- package/dist/schema/syncDeltaRow.d.ts +31 -13
- package/dist/schema/syncDeltaRow.js +33 -15
- package/dist/schema/tenancy.d.ts +52 -16
- package/dist/schema/tenancy.js +86 -25
- package/dist/server/commit.d.ts +8 -24
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/readConfig.d.ts +10 -0
- package/dist/source/adapter.d.ts +26 -15
- package/dist/source/adapter.js +5 -3
- package/dist/source/adapters/drizzle.js +35 -9
- package/dist/source/adapters/kysely.d.ts +33 -61
- package/dist/source/adapters/kysely.js +170 -177
- package/dist/source/adapters/kyselyMutationCore.d.ts +76 -0
- package/dist/source/adapters/kyselyMutationCore.js +125 -0
- package/dist/source/adapters/memory.d.ts +1 -1
- package/dist/source/adapters/memory.js +24 -10
- package/dist/source/adapters/prisma.js +35 -8
- package/dist/source/conformance.d.ts +26 -28
- package/dist/source/conformance.js +142 -69
- package/dist/source/contract.d.ts +46 -4
- package/dist/source/contract.js +66 -5
- package/dist/source/factory.d.ts +3 -3
- package/dist/source/factory.js +24 -3
- package/dist/source/idempotency.d.ts +46 -0
- package/dist/source/idempotency.js +129 -0
- package/dist/source/index.d.ts +6 -4
- package/dist/source/index.js +5 -3
- package/dist/source/migrations.d.ts +9 -5
- package/dist/source/migrations.js +39 -6
- package/dist/source/types.d.ts +88 -14
- package/dist/source/types.js +16 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +0 -1
- package/dist/sync/BootstrapFetcher.js +7 -0
- package/dist/sync/SyncWebSocket.d.ts +5 -27
- package/dist/sync/awaitClaimGrant.d.ts +6 -0
- package/dist/sync/awaitClaimGrant.js +12 -2
- package/dist/sync/commitFrames.d.ts +2 -4
- package/dist/sync/commitFrames.js +1 -0
- package/dist/sync/createClaimStream.d.ts +1 -1
- package/dist/sync/createClaimStream.js +13 -15
- package/dist/sync/deltaPipeline.d.ts +1 -1
- package/dist/sync/deltaPipeline.js +1 -1
- package/dist/sync/participants.d.ts +2 -3
- package/dist/sync/participants.js +2 -2
- package/dist/sync/wsFrameHandlers.js +35 -15
- package/dist/testing/mocks/MockMutationExecutor.d.ts +21 -0
- package/dist/testing/mocks/MockMutationExecutor.js +53 -1
- package/dist/transactions/TransactionQueue.d.ts +61 -9
- package/dist/transactions/TransactionQueue.js +502 -114
- package/dist/transactions/commitEnvelope.d.ts +4 -0
- package/dist/transactions/commitEnvelope.js +25 -0
- package/dist/transactions/commitPayload.d.ts +23 -5
- package/dist/transactions/commitPayload.js +3 -0
- package/dist/transactions/deltaConfirmation.d.ts +2 -1
- package/dist/transactions/deltaConfirmation.js +22 -4
- package/dist/transactions/durableWriteStore.d.ts +8 -1
- package/dist/transactions/httpCommitEnvelope.d.ts +3 -1
- package/dist/transactions/httpCommitEnvelope.js +30 -0
- package/dist/transactions/replayValidation.d.ts +4 -0
- package/dist/transactions/replayValidation.js +6 -0
- package/dist/types/streams.d.ts +29 -17
- package/dist/wire/commit.d.ts +671 -0
- package/dist/wire/commit.js +300 -0
- package/dist/wire/delta.d.ts +4 -0
- package/dist/wire/delta.js +9 -0
- package/dist/wire/frames.d.ts +17 -45
- package/dist/wire/index.d.ts +2 -0
- package/dist/wire/index.js +4 -0
- package/docs/agent-messaging.md +4 -5
- package/docs/agents.md +1 -2
- package/docs/api-keys.md +1 -2
- package/docs/client-behavior.md +4 -6
- package/docs/coordination.md +64 -9
- package/docs/data-sources.md +4 -7
- package/docs/integration-guide.md +6 -8
- package/docs/migration.md +0 -3
- package/docs/quickstart.md +7 -8
- package/docs/schema-contract.md +7 -8
- package/package.json +4 -4
- package/dist/client/registerDataSource.d.ts +0 -19
- package/dist/client/registerDataSource.js +0 -66
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* session-error detection, online-status checks, and the transport that carries
|
|
7
7
|
* mutations to your backend. The SDK ships sensible no-op defaults where it can.
|
|
8
8
|
*/
|
|
9
|
-
import type {
|
|
9
|
+
import type { ReadDependency, ParticipantKind } from '../coordination/schema.js';
|
|
10
|
+
import type { CommitStatus, MutationCommitResultInput } from '../wire/commit.js';
|
|
10
11
|
export interface SyncLogger {
|
|
11
12
|
debug(message: string, ...args: unknown[]): void;
|
|
12
13
|
info(message: string, ...args: unknown[]): void;
|
|
@@ -177,22 +178,12 @@ export interface ModelDebugLoggerContract {
|
|
|
177
178
|
logCreation(modelName: string, data: unknown, constructor: unknown): void;
|
|
178
179
|
logObservableSetup(modelName: string, observableProps: string[], computedProps: string[]): void;
|
|
179
180
|
}
|
|
180
|
-
/**
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
* an `AbloStaleContextError`, the commit succeeds and reports the collision
|
|
187
|
-
* here so the caller can reconcile. See {@link StaleNotification}.
|
|
188
|
-
*/
|
|
189
|
-
notifications?: StaleNotification[];
|
|
190
|
-
/**
|
|
191
|
-
* Ids of update or delete targets that matched no rows. Present, and non-empty,
|
|
192
|
-
* only when a write missed the row it addressed.
|
|
193
|
-
*/
|
|
194
|
-
missingIds?: string[];
|
|
195
|
-
}
|
|
181
|
+
/**
|
|
182
|
+
* Result returned by an injected mutation executor. The input type is inferred
|
|
183
|
+
* from the runtime compatibility schema: legacy `{lastSyncId}` remains valid,
|
|
184
|
+
* while every explicit queued result requires a WAL correlation.
|
|
185
|
+
*/
|
|
186
|
+
export type CommitResult = MutationCommitResultInput;
|
|
196
187
|
/**
|
|
197
188
|
* Per-call options accepted by any mutation, passed as the last argument.
|
|
198
189
|
* Every field is optional; omitted fields fall back to sensible defaults.
|
|
@@ -208,9 +199,15 @@ export interface CommitResult {
|
|
|
208
199
|
export interface MutationOptions {
|
|
209
200
|
idempotencyKey?: string | null;
|
|
210
201
|
label?: string;
|
|
211
|
-
wait?:
|
|
202
|
+
wait?: CommitStatus;
|
|
212
203
|
readAt?: number | null;
|
|
213
204
|
onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
205
|
+
/**
|
|
206
|
+
* The fencing token (Option B) of the held claim this write belongs to. The
|
|
207
|
+
* server validates it against the entity's persisted high-water and rejects a
|
|
208
|
+
* stale token. Sourced from the claim handle, never set by hand.
|
|
209
|
+
*/
|
|
210
|
+
fenceToken?: number | null;
|
|
214
211
|
/** The id (or `{ id }`) of the claim this write belongs to. This is the
|
|
215
212
|
* low-level reference the commit carries so the write is attributed to a claim
|
|
216
213
|
* and can pass the holder's own lock. It is distinct from the `claim` handle on
|
|
@@ -243,7 +240,7 @@ export interface MutationOptions {
|
|
|
243
240
|
* `claim` are deliberately absent: both are resolved on the client before a write
|
|
244
241
|
* is staged, so neither reaches this layer.
|
|
245
242
|
*/
|
|
246
|
-
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'onStale' | 'idempotencyKey' | 'label'>;
|
|
243
|
+
export type WriteOptions = Pick<MutationOptions, 'readAt' | 'onStale' | 'idempotencyKey' | 'label' | 'fenceToken'>;
|
|
247
244
|
/** A single mutation within a batch. Its `options` travel with it so the server
|
|
248
245
|
* can cache and replay the operation for idempotent retries. */
|
|
249
246
|
export interface MutationOperation {
|
|
@@ -265,6 +262,11 @@ export interface MutationOperation {
|
|
|
265
262
|
transactionId?: string;
|
|
266
263
|
readAt?: number | null;
|
|
267
264
|
onStale?: 'reject' | 'overwrite' | 'notify' | null;
|
|
265
|
+
/**
|
|
266
|
+
* The fencing token (Option B) carried on the wire for this op — the held
|
|
267
|
+
* claim's token, validated against the entity's high-water at commit.
|
|
268
|
+
*/
|
|
269
|
+
fenceToken?: number | null;
|
|
268
270
|
/**
|
|
269
271
|
* Per-operation idempotency and audit metadata. `idempotencyKey` is also the
|
|
270
272
|
* cache key the server uses to de-duplicate retries; `label` is stored for
|
|
@@ -365,6 +367,16 @@ export interface SyncEngineConfig {
|
|
|
365
367
|
* enforced.
|
|
366
368
|
*/
|
|
367
369
|
expectedSchemaHash?: string;
|
|
370
|
+
/**
|
|
371
|
+
* Set only when the bound schema is a projection (`selectModels`/`omitModels`):
|
|
372
|
+
* the content hash of the full source schema the subset was cut from. A subset
|
|
373
|
+
* hashes differently from its full schema, so without this a projection-bound
|
|
374
|
+
* client always reports drift against a server running the full schema. The
|
|
375
|
+
* drift check treats the client as in-sync when the server's active hash
|
|
376
|
+
* matches either `expectedSchemaHash` or this source hash. Advisory, like the
|
|
377
|
+
* hash above.
|
|
378
|
+
*/
|
|
379
|
+
expectedSourceSchemaHash?: string;
|
|
368
380
|
}
|
|
369
381
|
/**
|
|
370
382
|
* Extends the WebSocket event map with your own collaboration events, such as
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from
|
|
|
25
25
|
export { relation, type RelationDef, type RelationType } from './relation.js';
|
|
26
26
|
export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type PolicyInput, } from './tenancy.js';
|
|
27
27
|
export { residencySchema, DEFAULT_RESIDENCY, type ModelResidency, planeSchema, DEFAULT_PLANE, type SchemaPlane, } from './residency.js';
|
|
28
|
-
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_RESIDENCY, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './syncDeltaRow.js';
|
|
28
|
+
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_DATA_CLASSIFICATION, DELTA_PHYSICAL_STORAGE, DELTA_RESIDENCY, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './syncDeltaRow.js';
|
|
29
29
|
export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, type SyncDeltaAction, type WireDeltaData, type ParticipantRef, type SyncDeltaWireCore, type ClientSyncDelta, type ServerSyncDelta, } from '../wire/delta.js';
|
|
30
30
|
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, type ConflictAxis, } from './model.js';
|
|
31
31
|
export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwrite, agentsReject, agentsNotify, systemOverwrite, systemReject, systemNotify, type ConflictRule, } from './coordination.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -38,7 +38,10 @@ planeSchema,
|
|
|
38
38
|
DEFAULT_PLANE, } from './residency.js';
|
|
39
39
|
// The stored `sync_deltas` row, described as Zod schemas grouped by subsystem and by
|
|
40
40
|
// which database the columns live in.
|
|
41
|
-
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema,
|
|
41
|
+
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_DATA_CLASSIFICATION, DELTA_PHYSICAL_STORAGE,
|
|
42
|
+
// Deliberate back-compat re-export; physical storage is the precise name.
|
|
43
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
44
|
+
DELTA_RESIDENCY, } from './syncDeltaRow.js';
|
|
42
45
|
// The wire delta contract — the server-to-client projection of the stored row.
|
|
43
46
|
// Both the client SDK and the server derive their `SyncDelta` type from these with
|
|
44
47
|
// `z.infer`, so the two ends cannot drift apart.
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -114,6 +114,19 @@ export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
|
114
114
|
* to derive a participant's allowed sync-group set.
|
|
115
115
|
*/
|
|
116
116
|
readonly identityRoles: readonly IdentityRole[];
|
|
117
|
+
/**
|
|
118
|
+
* Set only on a projection produced by `selectModels`/`omitModels`: the
|
|
119
|
+
* content hash of the FULL source schema the subset was cut from. A subset
|
|
120
|
+
* hashes differently from the full schema it belongs to, so a projection-bound
|
|
121
|
+
* client would otherwise always report drift against a server running the full
|
|
122
|
+
* schema. The drift check treats the client as in-sync when the server's active
|
|
123
|
+
* hash matches EITHER this client's own (subset) hash or this source hash — so
|
|
124
|
+
* a current projection stays quiet while a genuinely behind server still warns.
|
|
125
|
+
* Absent on a schema authored directly, where the client's own hash is the
|
|
126
|
+
* deployed hash and plain equality is correct. Excluded from `toSchemaJSON`, so
|
|
127
|
+
* stamping it never perturbs `schemaHash`.
|
|
128
|
+
*/
|
|
129
|
+
readonly sourceSchemaHash?: string;
|
|
117
130
|
}
|
|
118
131
|
/**
|
|
119
132
|
* Infer the full model type from a schema.
|
package/dist/schema/select.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
* a record's fan-out — the selected set must be closed under `parent` edges.
|
|
23
23
|
*/
|
|
24
24
|
import { AbloValidationError } from '../errors.js';
|
|
25
|
+
import { schemaHash } from './serialize.js';
|
|
25
26
|
export function selectModels(schema, keys) {
|
|
26
27
|
const keep = new Set(keys);
|
|
27
28
|
const models = {};
|
|
@@ -51,6 +52,12 @@ export function selectModels(schema, keys) {
|
|
|
51
52
|
models: models,
|
|
52
53
|
validators: validators,
|
|
53
54
|
identityRoles: schema.identityRoles,
|
|
55
|
+
// Record the full source's hash so the drift check can recognize this subset
|
|
56
|
+
// as current against a server running that full schema. Prefer the source's
|
|
57
|
+
// OWN `sourceSchemaHash` when it is itself a projection, so a subset-of-a-
|
|
58
|
+
// subset still points at the original full schema rather than an intermediate
|
|
59
|
+
// one. `schemaHash` ignores this field, so re-projecting stays deterministic.
|
|
60
|
+
sourceSchemaHash: schema.sourceSchemaHash ?? schemaHash(schema),
|
|
54
61
|
};
|
|
55
62
|
}
|
|
56
63
|
/**
|
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* serves:
|
|
5
5
|
*
|
|
6
6
|
* - {@link syncDeltaCoreSchema} — the sync-protocol slice: everything a client
|
|
7
|
-
* needs to reconstruct the change, plus the tenant key. This is
|
|
8
|
-
*
|
|
7
|
+
* needs to reconstruct the change, plus the tenant key. This is a portable
|
|
8
|
+
* payload shape; it is not a statement that the row is physically stored in
|
|
9
|
+
* the customer's database.
|
|
9
10
|
* - {@link deltaAttributionSchema} — who made the change, and on whose authority.
|
|
10
11
|
* - {@link deltaProvenanceSchema} — which AI task, if any, produced it.
|
|
11
12
|
*
|
|
12
13
|
* {@link syncDeltaRowSchema} composes all three into the full stored row.
|
|
13
|
-
* {@link
|
|
14
|
-
*
|
|
14
|
+
* {@link DELTA_DATA_CLASSIFICATION} states which slices contain customer data,
|
|
15
|
+
* while {@link DELTA_PHYSICAL_STORAGE} states where the runtime persists them.
|
|
16
|
+
* Keeping those axes separate prevents a portable schema slice from being cited
|
|
17
|
+
* incorrectly as a physical-residency guarantee.
|
|
15
18
|
*
|
|
16
19
|
* This is the stored shape. The delta broadcast to clients is a narrower projection
|
|
17
20
|
* of it — see {@link import('../wire/delta.js').syncDeltaWireCoreSchema} — and
|
|
@@ -29,10 +32,11 @@ export declare const backfillProvenanceSchema: z.ZodEnum<{
|
|
|
29
32
|
export type BackfillProvenance = z.infer<typeof backfillProvenanceSchema>;
|
|
30
33
|
/**
|
|
31
34
|
* Everything a client needs to materialize the change, plus the tenant key. This
|
|
32
|
-
* is the portable
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* is the portable shape an authoritative WAL change or endpoint event carries.
|
|
36
|
+
* The runtime persists the resulting full row in Ablo's tenant-scoped ordered
|
|
37
|
+
* sync log; it is not written atomically with a direct application-row mutation.
|
|
38
|
+
* `id`, `createdAt`, and `syncGroups` are assigned when the source change is
|
|
39
|
+
* appended, so they are optional at the ingestion boundary.
|
|
36
40
|
*/
|
|
37
41
|
export declare const syncDeltaCoreSchema: z.ZodObject<{
|
|
38
42
|
id: z.ZodOptional<z.ZodUnion<readonly [z.ZodBigInt, z.ZodNumber]>>;
|
|
@@ -45,6 +49,7 @@ export declare const syncDeltaCoreSchema: z.ZodObject<{
|
|
|
45
49
|
organizationId: z.ZodNullable<z.ZodString>;
|
|
46
50
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
47
51
|
transactionId: z.ZodNullable<z.ZodString>;
|
|
52
|
+
sourceChangeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
48
53
|
}, z.core.$strip>;
|
|
49
54
|
export type SyncDeltaCore = z.infer<typeof syncDeltaCoreSchema>;
|
|
50
55
|
export declare const deltaAttributionSchema: z.ZodObject<{
|
|
@@ -93,6 +98,7 @@ export declare const syncDeltaRowSchema: z.ZodObject<{
|
|
|
93
98
|
organizationId: z.ZodNullable<z.ZodString>;
|
|
94
99
|
createdAt: z.ZodOptional<z.ZodString>;
|
|
95
100
|
transactionId: z.ZodNullable<z.ZodString>;
|
|
101
|
+
sourceChangeId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
96
102
|
createdBy: z.ZodNullable<z.ZodString>;
|
|
97
103
|
actorId: z.ZodNullable<z.ZodString>;
|
|
98
104
|
actorKind: z.ZodNullable<z.ZodEnum<{
|
|
@@ -123,14 +129,26 @@ export declare const syncDeltaRowSchema: z.ZodObject<{
|
|
|
123
129
|
causedByTaskId: z.ZodNullable<z.ZodString>;
|
|
124
130
|
}, z.core.$strip>;
|
|
125
131
|
export type SyncDeltaRow = z.infer<typeof syncDeltaRowSchema>;
|
|
132
|
+
/** Which slices contain retained customer row data versus control metadata. */
|
|
133
|
+
export declare const DELTA_DATA_CLASSIFICATION: {
|
|
134
|
+
readonly core: "customer-data";
|
|
135
|
+
readonly attribution: "control-metadata";
|
|
136
|
+
readonly provenance: "control-metadata";
|
|
137
|
+
};
|
|
126
138
|
/**
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
139
|
+
* Where the current runtime physically persists each slice. All three live in
|
|
140
|
+
* Ablo's tenant-scoped `sync_deltas` log. `core` contains full post-change row
|
|
141
|
+
* payloads (and optional previous payloads), so `control` here must not be read
|
|
142
|
+
* as "metadata only."
|
|
131
143
|
*/
|
|
144
|
+
export declare const DELTA_PHYSICAL_STORAGE: {
|
|
145
|
+
readonly core: "control";
|
|
146
|
+
readonly attribution: "control";
|
|
147
|
+
readonly provenance: "control";
|
|
148
|
+
};
|
|
149
|
+
/** @deprecated Use `DELTA_PHYSICAL_STORAGE`; classification is a separate axis. */
|
|
132
150
|
export declare const DELTA_RESIDENCY: {
|
|
133
|
-
readonly core: "
|
|
151
|
+
readonly core: "control";
|
|
134
152
|
readonly attribution: "control";
|
|
135
153
|
readonly provenance: "control";
|
|
136
154
|
};
|
|
@@ -4,14 +4,17 @@
|
|
|
4
4
|
* serves:
|
|
5
5
|
*
|
|
6
6
|
* - {@link syncDeltaCoreSchema} — the sync-protocol slice: everything a client
|
|
7
|
-
* needs to reconstruct the change, plus the tenant key. This is
|
|
8
|
-
*
|
|
7
|
+
* needs to reconstruct the change, plus the tenant key. This is a portable
|
|
8
|
+
* payload shape; it is not a statement that the row is physically stored in
|
|
9
|
+
* the customer's database.
|
|
9
10
|
* - {@link deltaAttributionSchema} — who made the change, and on whose authority.
|
|
10
11
|
* - {@link deltaProvenanceSchema} — which AI task, if any, produced it.
|
|
11
12
|
*
|
|
12
13
|
* {@link syncDeltaRowSchema} composes all three into the full stored row.
|
|
13
|
-
* {@link
|
|
14
|
-
*
|
|
14
|
+
* {@link DELTA_DATA_CLASSIFICATION} states which slices contain customer data,
|
|
15
|
+
* while {@link DELTA_PHYSICAL_STORAGE} states where the runtime persists them.
|
|
16
|
+
* Keeping those axes separate prevents a portable schema slice from being cited
|
|
17
|
+
* incorrectly as a physical-residency guarantee.
|
|
15
18
|
*
|
|
16
19
|
* This is the stored shape. The delta broadcast to clients is a narrower projection
|
|
17
20
|
* of it — see {@link import('../wire/delta.js').syncDeltaWireCoreSchema} — and
|
|
@@ -31,10 +34,11 @@ const deltaDataSchema = z.record(z.string(), z.unknown()).nullable();
|
|
|
31
34
|
// ── Core — the sync-protocol slice ────────────────────────────────────────────
|
|
32
35
|
/**
|
|
33
36
|
* Everything a client needs to materialize the change, plus the tenant key. This
|
|
34
|
-
* is the portable
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
37
|
+
* is the portable shape an authoritative WAL change or endpoint event carries.
|
|
38
|
+
* The runtime persists the resulting full row in Ablo's tenant-scoped ordered
|
|
39
|
+
* sync log; it is not written atomically with a direct application-row mutation.
|
|
40
|
+
* `id`, `createdAt`, and `syncGroups` are assigned when the source change is
|
|
41
|
+
* appended, so they are optional at the ingestion boundary.
|
|
38
42
|
*/
|
|
39
43
|
export const syncDeltaCoreSchema = z.object({
|
|
40
44
|
/** Monotonically increasing sync id, assigned by the server when the delta is appended; absent on an outbox marker. */
|
|
@@ -52,6 +56,12 @@ export const syncDeltaCoreSchema = z.object({
|
|
|
52
56
|
/** ISO 8601 timestamp, assigned by the server when the delta is appended. */
|
|
53
57
|
createdAt: z.string().optional(),
|
|
54
58
|
transactionId: z.string().nullable(),
|
|
59
|
+
/**
|
|
60
|
+
* Stable identity of the authoritative WAL row or endpoint event. It is
|
|
61
|
+
* nullable for hosted/legacy rows and is never exposed as the optimistic
|
|
62
|
+
* client transaction id.
|
|
63
|
+
*/
|
|
64
|
+
sourceChangeId: z.string().min(1).nullable().optional(),
|
|
55
65
|
});
|
|
56
66
|
// ── Attribution — who made the change, and on whose authority ─────────────────
|
|
57
67
|
export const deltaAttributionSchema = z.object({
|
|
@@ -71,19 +81,27 @@ export const deltaProvenanceSchema = z.object({
|
|
|
71
81
|
/** Foreign key to the task record for the AI turn that produced this commit; null when no task applies. */
|
|
72
82
|
causedByTaskId: z.string().nullable(),
|
|
73
83
|
});
|
|
74
|
-
// ── Full stored row and
|
|
84
|
+
// ── Full stored row, classification, and physical storage ─────────────────────
|
|
75
85
|
/** The complete `sync_deltas` row: core, attribution, and provenance combined. */
|
|
76
86
|
export const syncDeltaRowSchema = syncDeltaCoreSchema
|
|
77
87
|
.extend(deltaAttributionSchema.shape)
|
|
78
88
|
.extend(deltaProvenanceSchema.shape);
|
|
89
|
+
/** Which slices contain retained customer row data versus control metadata. */
|
|
90
|
+
export const DELTA_DATA_CLASSIFICATION = {
|
|
91
|
+
core: 'customer-data',
|
|
92
|
+
attribution: 'control-metadata',
|
|
93
|
+
provenance: 'control-metadata',
|
|
94
|
+
};
|
|
79
95
|
/**
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
96
|
+
* Where the current runtime physically persists each slice. All three live in
|
|
97
|
+
* Ablo's tenant-scoped `sync_deltas` log. `core` contains full post-change row
|
|
98
|
+
* payloads (and optional previous payloads), so `control` here must not be read
|
|
99
|
+
* as "metadata only."
|
|
84
100
|
*/
|
|
85
|
-
export const
|
|
86
|
-
core: '
|
|
101
|
+
export const DELTA_PHYSICAL_STORAGE = {
|
|
102
|
+
core: 'control',
|
|
87
103
|
attribution: 'control',
|
|
88
104
|
provenance: 'control',
|
|
89
105
|
};
|
|
106
|
+
/** @deprecated Use `DELTA_PHYSICAL_STORAGE`; classification is a separate axis. */
|
|
107
|
+
export const DELTA_RESIDENCY = DELTA_PHYSICAL_STORAGE;
|
package/dist/schema/tenancy.d.ts
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* model is built, so the authoring vocabulary never reaches the wire or any
|
|
15
15
|
* consumer.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* The canonical form is an exhaustively switchable discriminated union, so the type
|
|
18
|
+
* system enforces the isolation boundary at every consumer. The authoring form is a
|
|
19
|
+
* flat object whose `by` names the choice and whose per-branch rules are checked by a
|
|
20
|
+
* refinement — the opt-out from tenant scoping (`{ by: 'none' }`) and the
|
|
21
|
+
* source-derived case (`{ by: 'source' }`) are explicit, named values rather than
|
|
22
|
+
* falsy flags, important because `none` makes an entire table readable across tenants.
|
|
20
23
|
*/
|
|
21
24
|
import { z } from 'zod';
|
|
22
25
|
/** The default physical tenancy column. This is the single canonical definition of that column name. */
|
|
@@ -49,36 +52,69 @@ export declare const tenancySchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
49
52
|
}, z.core.$strip>;
|
|
50
53
|
}, z.core.$strip>, z.ZodObject<{
|
|
51
54
|
kind: z.ZodLiteral<"none">;
|
|
55
|
+
}, z.core.$strip>, z.ZodObject<{
|
|
56
|
+
kind: z.ZodLiteral<"source">;
|
|
52
57
|
}, z.core.$strip>], "kind">;
|
|
53
58
|
export type Tenancy = z.infer<typeof tenancySchema>;
|
|
54
59
|
/**
|
|
55
60
|
* The authoring form of tenancy — what a schema author writes as the model's
|
|
56
61
|
* `policy` option. The name follows SQL row-level security, where a policy is the
|
|
57
|
-
* rule that decides which rows a tenant may read.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
62
|
+
* rule that decides which rows a tenant may read.
|
|
63
|
+
*
|
|
64
|
+
* The four choices `by` can take:
|
|
60
65
|
*
|
|
61
66
|
* - `{ by: 'column' }` — a row-local tenancy column (the default). `column`
|
|
62
67
|
* overrides the name (default {@link DEFAULT_ORG_COLUMN}).
|
|
63
68
|
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when
|
|
64
69
|
* this table has no tenancy column of its own. `parentKey` (default `'id'`) and
|
|
65
70
|
* `parentTenantColumn` (default {@link DEFAULT_ORG_COLUMN}) are optional overrides.
|
|
71
|
+
* - `{ by: 'source' }` — derive the organization from the connected source that
|
|
72
|
+
* registered the model, for a single-tenant connected database that carries no
|
|
73
|
+
* `organization_id` column. The registration is the tenant boundary.
|
|
66
74
|
* - `{ by: 'none' }` — genuinely global or reference data. This makes the whole
|
|
67
75
|
* table readable across tenants, so it is correct only for tables that have no tenant.
|
|
76
|
+
*
|
|
77
|
+
* The runtime schema is one flat object rather than a discriminated union: `by`
|
|
78
|
+
* selects the branch, and the {@link https://zod.dev | superRefine} pass enforces
|
|
79
|
+
* the per-branch field rules (`parent` requires `fk` and `parent`; the other three
|
|
80
|
+
* take no branch fields). The exported {@link PolicyInput} type is hand-written as a
|
|
81
|
+
* precise union so the compiler still requires the right fields per `by` — the flat
|
|
82
|
+
* schema validates at runtime, the union guides at author time, and the type guard
|
|
83
|
+
* below keeps the two from drifting.
|
|
68
84
|
*/
|
|
69
|
-
export declare const policyInputSchema: z.
|
|
70
|
-
by: z.
|
|
85
|
+
export declare const policyInputSchema: z.ZodObject<{
|
|
86
|
+
by: z.ZodEnum<{
|
|
87
|
+
none: "none";
|
|
88
|
+
source: "source";
|
|
89
|
+
column: "column";
|
|
90
|
+
parent: "parent";
|
|
91
|
+
}>;
|
|
71
92
|
column: z.ZodOptional<z.ZodString>;
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fk: z.ZodString;
|
|
75
|
-
parent: z.ZodString;
|
|
93
|
+
fk: z.ZodOptional<z.ZodString>;
|
|
94
|
+
parent: z.ZodOptional<z.ZodString>;
|
|
76
95
|
parentKey: z.ZodOptional<z.ZodString>;
|
|
77
96
|
parentTenantColumn: z.ZodOptional<z.ZodString>;
|
|
78
|
-
}, z.core.$strip
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
97
|
+
}, z.core.$strip>;
|
|
98
|
+
/**
|
|
99
|
+
* The precise authoring type. Hand-written rather than inferred from
|
|
100
|
+
* {@link policyInputSchema} so the compiler enforces the per-branch fields the flat
|
|
101
|
+
* runtime schema can only check at parse time — an author writing `{ by: 'parent' }`
|
|
102
|
+
* gets a type error for the missing `fk`/`parent`, not a runtime one.
|
|
103
|
+
*/
|
|
104
|
+
export type PolicyInput = {
|
|
105
|
+
by: 'column';
|
|
106
|
+
column?: string;
|
|
107
|
+
} | {
|
|
108
|
+
by: 'parent';
|
|
109
|
+
fk: string;
|
|
110
|
+
parent: string;
|
|
111
|
+
parentKey?: string;
|
|
112
|
+
parentTenantColumn?: string;
|
|
113
|
+
} | {
|
|
114
|
+
by: 'source';
|
|
115
|
+
} | {
|
|
116
|
+
by: 'none';
|
|
117
|
+
};
|
|
82
118
|
/**
|
|
83
119
|
* Normalizes the authoring {@link PolicyInput} into the canonical {@link Tenancy}.
|
|
84
120
|
* Called once, when the model is built, so that `ModelDef`, `ModelJSON`, and every
|
package/dist/schema/tenancy.js
CHANGED
|
@@ -14,9 +14,12 @@
|
|
|
14
14
|
* model is built, so the authoring vocabulary never reaches the wire or any
|
|
15
15
|
* consumer.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* The canonical form is an exhaustively switchable discriminated union, so the type
|
|
18
|
+
* system enforces the isolation boundary at every consumer. The authoring form is a
|
|
19
|
+
* flat object whose `by` names the choice and whose per-branch rules are checked by a
|
|
20
|
+
* refinement — the opt-out from tenant scoping (`{ by: 'none' }`) and the
|
|
21
|
+
* source-derived case (`{ by: 'source' }`) are explicit, named values rather than
|
|
22
|
+
* falsy flags, important because `none` makes an entire table readable across tenants.
|
|
20
23
|
*/
|
|
21
24
|
import { z } from 'zod';
|
|
22
25
|
/** The default physical tenancy column. This is the single canonical definition of that column name. */
|
|
@@ -46,41 +49,97 @@ export const tenancySchema = z.discriminatedUnion('kind', [
|
|
|
46
49
|
z.object({ kind: z.literal('parent'), via: scopedViaRefSchema }),
|
|
47
50
|
/** Not tenant-scoped (global / reference data). */
|
|
48
51
|
z.object({ kind: z.literal('none') }),
|
|
52
|
+
/**
|
|
53
|
+
* Scoped by the data source itself: the model carries no tenancy column, and the
|
|
54
|
+
* owning organization is derived from the connected source that registered it — the
|
|
55
|
+
* registration is the tenant boundary. This is the natural default for a
|
|
56
|
+
* single-tenant connected database (`ablo connect`), which holds one org's rows and
|
|
57
|
+
* so has no per-row `organization_id` to read. Carries no payload for the same
|
|
58
|
+
* reason `none` does: the binding lives in the source registration, not the model.
|
|
59
|
+
*/
|
|
60
|
+
z.object({ kind: z.literal('source') }),
|
|
49
61
|
]);
|
|
50
62
|
/**
|
|
51
63
|
* The authoring form of tenancy — what a schema author writes as the model's
|
|
52
64
|
* `policy` option. The name follows SQL row-level security, where a policy is the
|
|
53
|
-
* rule that decides which rows a tenant may read.
|
|
54
|
-
*
|
|
55
|
-
*
|
|
65
|
+
* rule that decides which rows a tenant may read.
|
|
66
|
+
*
|
|
67
|
+
* The four choices `by` can take:
|
|
56
68
|
*
|
|
57
69
|
* - `{ by: 'column' }` — a row-local tenancy column (the default). `column`
|
|
58
70
|
* overrides the name (default {@link DEFAULT_ORG_COLUMN}).
|
|
59
71
|
* - `{ by: 'parent', fk, parent }` — inherit tenancy through a foreign key when
|
|
60
72
|
* this table has no tenancy column of its own. `parentKey` (default `'id'`) and
|
|
61
73
|
* `parentTenantColumn` (default {@link DEFAULT_ORG_COLUMN}) are optional overrides.
|
|
74
|
+
* - `{ by: 'source' }` — derive the organization from the connected source that
|
|
75
|
+
* registered the model, for a single-tenant connected database that carries no
|
|
76
|
+
* `organization_id` column. The registration is the tenant boundary.
|
|
62
77
|
* - `{ by: 'none' }` — genuinely global or reference data. This makes the whole
|
|
63
78
|
* table readable across tenants, so it is correct only for tables that have no tenant.
|
|
79
|
+
*
|
|
80
|
+
* The runtime schema is one flat object rather than a discriminated union: `by`
|
|
81
|
+
* selects the branch, and the {@link https://zod.dev | superRefine} pass enforces
|
|
82
|
+
* the per-branch field rules (`parent` requires `fk` and `parent`; the other three
|
|
83
|
+
* take no branch fields). The exported {@link PolicyInput} type is hand-written as a
|
|
84
|
+
* precise union so the compiler still requires the right fields per `by` — the flat
|
|
85
|
+
* schema validates at runtime, the union guides at author time, and the type guard
|
|
86
|
+
* below keeps the two from drifting.
|
|
64
87
|
*/
|
|
65
|
-
export const policyInputSchema = z
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
z.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
88
|
+
export const policyInputSchema = z
|
|
89
|
+
.object({
|
|
90
|
+
by: z.enum(['column', 'parent', 'none', 'source']),
|
|
91
|
+
/** `by: 'column'` — override the physical tenancy column name. Default {@link DEFAULT_ORG_COLUMN}. */
|
|
92
|
+
column: z.string().min(1).optional(),
|
|
93
|
+
/** `by: 'parent'` — column on this table that points at the parent (for example, `'slideId'`). */
|
|
94
|
+
fk: z.string().min(1).optional(),
|
|
95
|
+
/** `by: 'parent'` — parent table name (e.g. `'slides'`). */
|
|
96
|
+
parent: z.string().min(1).optional(),
|
|
97
|
+
/** `by: 'parent'` — column on the parent that `fk` references. Default `'id'`. */
|
|
98
|
+
parentKey: z.string().min(1).optional(),
|
|
99
|
+
/** `by: 'parent'` — column on the parent holding the tenant id. Default {@link DEFAULT_ORG_COLUMN}. */
|
|
100
|
+
parentTenantColumn: z.string().min(1).optional(),
|
|
101
|
+
})
|
|
102
|
+
.superRefine((policy, ctx) => {
|
|
103
|
+
if (policy.by === 'parent') {
|
|
104
|
+
if (!policy.fk) {
|
|
105
|
+
ctx.addIssue({
|
|
106
|
+
code: z.ZodIssueCode.custom,
|
|
107
|
+
path: ['fk'],
|
|
108
|
+
message: "A `by: 'parent'` policy needs `fk` — the column on this table that points at the parent row whose tenancy it inherits.",
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (!policy.parent) {
|
|
112
|
+
ctx.addIssue({
|
|
113
|
+
code: z.ZodIssueCode.custom,
|
|
114
|
+
path: ['parent'],
|
|
115
|
+
message: "A `by: 'parent'` policy needs `parent` — the name of the parent table this table is scoped through.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
// A `column` policy may carry `column`; the parent fields belong only to `parent`.
|
|
120
|
+
// Flag a field that landed on the wrong branch so a typo doesn't silently no-op.
|
|
121
|
+
const parentOnly = ['fk', 'parent', 'parentKey', 'parentTenantColumn'];
|
|
122
|
+
if (policy.by !== 'parent') {
|
|
123
|
+
for (const field of parentOnly) {
|
|
124
|
+
if (policy[field] !== undefined) {
|
|
125
|
+
ctx.addIssue({
|
|
126
|
+
code: z.ZodIssueCode.custom,
|
|
127
|
+
path: [field],
|
|
128
|
+
message: `\`${field}\` only applies to a \`by: 'parent'\` policy; remove it or set \`by: 'parent'\`.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (policy.by !== 'column' && policy.column !== undefined) {
|
|
134
|
+
ctx.addIssue({
|
|
135
|
+
code: z.ZodIssueCode.custom,
|
|
136
|
+
path: ['column'],
|
|
137
|
+
message: "`column` only applies to a `by: 'column'` policy; remove it or set `by: 'column'`.",
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
const _policyInputAssignable = true;
|
|
142
|
+
void _policyInputAssignable;
|
|
84
143
|
/**
|
|
85
144
|
* Normalizes the authoring {@link PolicyInput} into the canonical {@link Tenancy}.
|
|
86
145
|
* Called once, when the model is built, so that `ModelDef`, `ModelJSON`, and every
|
|
@@ -103,6 +162,8 @@ export function resolvePolicy(input) {
|
|
|
103
162
|
parentOrgColumn: input.parentTenantColumn,
|
|
104
163
|
},
|
|
105
164
|
};
|
|
165
|
+
case 'source':
|
|
166
|
+
return { kind: 'source' };
|
|
106
167
|
case 'none':
|
|
107
168
|
return { kind: 'none' };
|
|
108
169
|
}
|
package/dist/server/commit.d.ts
CHANGED
|
@@ -15,8 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import type { ParticipantKind, ConfirmationState } from '../schema/syncDeltaRow.js';
|
|
17
17
|
import type { ParticipantRef } from '../wire/delta.js';
|
|
18
|
+
import type { CommitExecutionResultInput } from '../wire/commit.js';
|
|
18
19
|
import type { Environment } from '../environment.js';
|
|
19
|
-
import type {
|
|
20
|
+
import type { ReadDependency } from '../coordination/schema.js';
|
|
20
21
|
export interface CommitContext {
|
|
21
22
|
participantId: string;
|
|
22
23
|
/**
|
|
@@ -30,6 +31,8 @@ export interface CommitContext {
|
|
|
30
31
|
* targets the organization's default project.
|
|
31
32
|
*/
|
|
32
33
|
projectId?: string;
|
|
34
|
+
/** Exact sandbox plane derived from the authenticated key, when present. */
|
|
35
|
+
sandboxId?: string;
|
|
33
36
|
/** Optional external account scope forwarded to storage resolvers. */
|
|
34
37
|
accountScope?: string;
|
|
35
38
|
/**
|
|
@@ -96,27 +99,8 @@ export interface CommitContext {
|
|
|
96
99
|
reads?: ReadDependency[] | null;
|
|
97
100
|
}
|
|
98
101
|
/**
|
|
99
|
-
* The
|
|
100
|
-
*
|
|
101
|
-
*
|
|
102
|
-
* 0 when the batch produced no deltas (no operations, or all of them were no-ops).
|
|
102
|
+
* The server execution receipt persisted in `mutation_log`. Its runtime schema
|
|
103
|
+
* lives with the HTTP/WS settlement contract so queued correlation cannot drift
|
|
104
|
+
* between cache, transport, and client.
|
|
103
105
|
*/
|
|
104
|
-
export
|
|
105
|
-
lastSyncId: number;
|
|
106
|
-
firstSyncId: number;
|
|
107
|
-
/**
|
|
108
|
-
* Stale-context notifications for operations the committer guarded with
|
|
109
|
-
* `onStale: 'notify'`. Non-empty only when a guarded write collided with a
|
|
110
|
-
* concurrent change; the committer heals from these instead of receiving an
|
|
111
|
-
* `AbloStaleContextError`. See {@link StaleNotification}.
|
|
112
|
-
*/
|
|
113
|
-
notifications?: StaleNotification[];
|
|
114
|
-
/**
|
|
115
|
-
* Ids of update or delete targets that matched no rows — the row does not exist, or
|
|
116
|
-
* lies outside the caller's organization. Surfacing them here lets a client turn a
|
|
117
|
-
* silent no-op into an `AbloNotFoundError`. Non-empty only when at least one
|
|
118
|
-
* operation missed. The ids are globally unique, so a caller can match its own
|
|
119
|
-
* target id against this set without ambiguity.
|
|
120
|
-
*/
|
|
121
|
-
missingIds?: string[];
|
|
122
|
-
}
|
|
106
|
+
export type CommitResult = CommitExecutionResultInput;
|