@abloatai/ablo 0.24.0 → 0.26.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/AGENTS.md +5 -3
- package/CHANGELOG.md +33 -0
- package/README.md +45 -36
- package/dist/BaseSyncedStore.d.ts +72 -195
- package/dist/BaseSyncedStore.js +240 -625
- package/dist/Database.d.ts +8 -9
- package/dist/Database.js +9 -14
- package/dist/LazyReferenceCollection.js +5 -0
- package/dist/Model.js +6 -8
- package/dist/ModelRegistry.d.ts +25 -6
- package/dist/ModelRegistry.js +12 -6
- package/dist/NetworkMonitor.js +1 -1
- package/dist/ObjectPool.js +9 -3
- package/dist/SyncClient.d.ts +15 -5
- package/dist/SyncClient.js +152 -66
- package/dist/SyncEngineContext.d.ts +1 -3
- package/dist/SyncEngineContext.js +1 -2
- package/dist/agent/Agent.d.ts +21 -8
- package/dist/agent/Agent.js +26 -13
- package/dist/ai-sdk/coordination-context.js +9 -3
- package/dist/ai-sdk/wrap.d.ts +1 -1
- package/dist/ai-sdk/wrap.js +1 -1
- package/dist/auth/credentialPolicy.d.ts +10 -2
- package/dist/auth/credentialSource.d.ts +1 -13
- package/dist/auth/credentialSource.js +6 -13
- package/dist/auth/index.js +3 -3
- package/dist/cli.cjs +583 -287
- package/dist/client/Ablo.d.ts +35 -764
- package/dist/client/Ablo.js +64 -688
- package/dist/client/ApiClient.d.ts +14 -1
- package/dist/client/ApiClient.js +63 -9
- package/dist/client/auth.d.ts +8 -17
- package/dist/client/auth.js +42 -6
- package/dist/client/consoleLogger.d.ts +36 -0
- package/dist/client/consoleLogger.js +45 -0
- package/dist/client/createInternalComponents.js +1 -1
- package/dist/client/createModelProxy.js +10 -6
- package/dist/client/credentialEndpoint.d.ts +63 -0
- package/dist/client/credentialEndpoint.js +87 -0
- package/dist/client/hostedEndpoints.d.ts +24 -0
- package/dist/client/hostedEndpoints.js +24 -0
- package/dist/client/httpClient.d.ts +1 -1
- package/dist/client/identity.js +2 -1
- package/dist/client/modelRegistration.d.ts +14 -0
- package/dist/client/modelRegistration.js +321 -0
- package/dist/client/options.d.ts +396 -0
- package/dist/client/options.js +10 -0
- package/dist/client/resourceTypes.d.ts +344 -0
- package/dist/client/resourceTypes.js +13 -0
- package/dist/client/schemaConfig.d.ts +56 -0
- package/dist/client/schemaConfig.js +188 -0
- package/dist/client/sessionMint.d.ts +1 -1
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/client/wsMutationExecutor.d.ts +33 -0
- package/dist/client/wsMutationExecutor.js +77 -0
- package/dist/context.js +0 -3
- package/dist/coordination/index.d.ts +8 -1
- package/dist/coordination/index.js +24 -1
- package/dist/coordination/trace.d.ts +0 -1
- package/dist/coordination/trace.js +0 -1
- package/dist/core/DatabaseManager.d.ts +0 -1
- package/dist/core/DatabaseManager.js +26 -24
- package/dist/core/QueryProcessor.js +7 -8
- package/dist/core/QueryView.d.ts +14 -2
- package/dist/core/QueryView.js +4 -5
- package/dist/core/StoreManager.d.ts +2 -2
- package/dist/core/StoreManager.js +5 -1
- package/dist/core/index.d.ts +1 -1
- package/dist/core/openIDBWithTimeout.js +15 -11
- package/dist/core/query-utils.js +1 -1
- package/dist/core/storeContract.d.ts +143 -0
- package/dist/core/storeContract.js +16 -0
- package/dist/errorCodes.d.ts +11 -2
- package/dist/errorCodes.js +155 -137
- package/dist/errors.d.ts +18 -7
- package/dist/errors.js +14 -1
- package/dist/index.js +5 -0
- package/dist/interfaces/index.d.ts +6 -20
- package/dist/keys/index.js +18 -9
- package/dist/mutators/UndoManager.js +6 -1
- package/dist/mutators/defineMutators.d.ts +1 -3
- package/dist/policy/types.d.ts +1 -1
- package/dist/query/client.d.ts +11 -0
- package/dist/query/client.js +81 -52
- package/dist/react/AbloProvider.d.ts +6 -10
- package/dist/react/AbloProvider.js +22 -15
- package/dist/react/context.d.ts +2 -118
- package/dist/react/useAblo.js +1 -1
- package/dist/react/useErrorListener.js +1 -1
- package/dist/react/useMutationFailureListener.js +1 -1
- package/dist/react/useMutators.js +7 -1
- package/dist/react/useReactive.js +0 -1
- package/dist/react/useUndoScope.js +2 -2
- package/dist/schema/ddlLock.d.ts +39 -0
- package/dist/schema/ddlLock.js +51 -0
- package/dist/schema/field.js +1 -1
- package/dist/schema/index.d.ts +4 -3
- package/dist/schema/index.js +15 -5
- package/dist/schema/model.d.ts +3 -3
- package/dist/schema/model.js +2 -2
- package/dist/schema/openapi.js +5 -1
- package/dist/schema/queries.d.ts +1 -1
- package/dist/schema/queries.js +1 -0
- package/dist/schema/residency.d.ts +35 -0
- package/dist/schema/residency.js +27 -0
- package/dist/schema/roles.d.ts +25 -0
- package/dist/schema/roles.js +37 -0
- package/dist/schema/schema.d.ts +3 -3
- package/dist/schema/schema.js +11 -7
- package/dist/schema/serialize.d.ts +8 -8
- package/dist/schema/sync-delta-row.d.ts +2 -2
- package/dist/schema/sync-delta-row.js +2 -2
- package/dist/server/storage-mode.d.ts +8 -1
- package/dist/server/storage-mode.js +14 -4
- package/dist/source/adapter.d.ts +1 -1
- package/dist/source/adapters/drizzle.js +4 -3
- package/dist/source/adapters/kysely.js +4 -3
- package/dist/source/adapters/memory.js +1 -1
- package/dist/source/adapters/prisma.js +5 -4
- package/dist/source/conformance.js +10 -8
- package/dist/source/connector.js +4 -2
- package/dist/source/factory.d.ts +106 -0
- package/dist/source/factory.js +273 -0
- package/dist/source/index.d.ts +11 -461
- package/dist/source/index.js +14 -416
- package/dist/source/next.d.ts +1 -1
- package/dist/source/next.js +1 -1
- package/dist/source/pushQueue.d.ts +1 -1
- package/dist/source/pushQueue.js +6 -3
- package/dist/source/signing.d.ts +59 -0
- package/dist/source/signing.js +142 -0
- package/dist/source/types.d.ts +334 -0
- package/dist/source/types.js +44 -0
- package/dist/stores/ObjectStore.d.ts +1 -1
- package/dist/stores/ObjectStore.js +23 -23
- package/dist/stores/SyncActionStore.d.ts +1 -1
- package/dist/stores/SyncActionStore.js +70 -35
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/AreaOfInterestManager.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.js +1 -1
- package/dist/sync/BootstrapHelper.d.ts +1 -3
- package/dist/sync/BootstrapHelper.js +29 -8
- package/dist/sync/ConnectionManager.d.ts +8 -0
- package/dist/sync/ConnectionManager.js +19 -4
- package/dist/sync/HydrationCoordinator.d.ts +12 -0
- package/dist/sync/HydrationCoordinator.js +25 -5
- package/dist/sync/NetworkProbe.d.ts +3 -2
- package/dist/sync/NetworkProbe.js +7 -6
- package/dist/sync/SyncWebSocket.d.ts +71 -120
- package/dist/sync/SyncWebSocket.js +324 -630
- package/dist/sync/awaitClaimGrant.js +27 -19
- package/dist/sync/bootstrapApply.d.ts +60 -0
- package/dist/sync/bootstrapApply.js +65 -0
- package/dist/sync/commitFrames.d.ts +43 -0
- package/dist/sync/commitFrames.js +94 -0
- package/dist/sync/createClaimStream.js +3 -2
- package/dist/sync/createSnapshot.d.ts +1 -3
- package/dist/sync/credentialLifecycle.d.ts +175 -0
- package/dist/sync/credentialLifecycle.js +329 -0
- package/dist/sync/deltaPipeline.d.ts +111 -0
- package/dist/sync/deltaPipeline.js +262 -0
- package/dist/sync/groupChange.d.ts +111 -0
- package/dist/sync/groupChange.js +246 -0
- package/dist/sync/heartbeat.d.ts +62 -0
- package/dist/sync/heartbeat.js +91 -0
- package/dist/sync/participants.d.ts +8 -8
- package/dist/sync/syncCursor.d.ts +44 -0
- package/dist/sync/syncCursor.js +59 -0
- package/dist/sync/syncPlan.d.ts +62 -0
- package/dist/sync/syncPlan.js +51 -0
- package/dist/sync/wsFrameHandlers.d.ts +117 -0
- package/dist/sync/wsFrameHandlers.js +378 -0
- package/dist/testing/fixtures/bootstrap.d.ts +9 -11
- package/dist/testing/fixtures/deltas.d.ts +1 -1
- package/dist/testing/fixtures/models.js +1 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +0 -3
- package/dist/testing/helpers/sync-engine-harness.js +2 -1
- package/dist/testing/index.d.ts +1 -1
- package/dist/testing/index.js +1 -1
- package/dist/testing/mocks/MockSyncContext.d.ts +1 -17
- package/dist/testing/mocks/MockSyncContext.js +1 -32
- package/dist/testing/mocks/MockSyncStore.d.ts +4 -4
- package/dist/testing/mocks/MockWebSocket.d.ts +2 -2
- package/dist/transactions/TransactionQueue.d.ts +20 -56
- package/dist/transactions/TransactionQueue.js +101 -487
- package/dist/transactions/TransactionStore.d.ts +18 -0
- package/dist/transactions/TransactionStore.js +51 -0
- package/dist/transactions/coalesceRules.d.ts +34 -0
- package/dist/transactions/coalesceRules.js +117 -0
- package/dist/transactions/commitPayload.d.ts +134 -0
- package/dist/transactions/commitPayload.js +152 -0
- package/dist/transactions/deltaConfirmation.d.ts +60 -0
- package/dist/transactions/deltaConfirmation.js +223 -0
- package/dist/transactions/optimistic.d.ts +24 -0
- package/dist/transactions/optimistic.js +45 -0
- package/dist/transactions/persistedReplay.d.ts +93 -0
- package/dist/transactions/persistedReplay.js +105 -0
- package/dist/types/index.d.ts +3 -3
- package/dist/types/index.js +0 -5
- package/dist/types/modelData.d.ts +12 -0
- package/dist/types/modelData.js +11 -0
- package/dist/types/participant.d.ts +21 -0
- package/dist/types/participant.js +12 -0
- package/dist/types/streams.d.ts +8 -14
- package/dist/utils/mobx-setup.d.ts +1 -1
- package/dist/utils/mobx-setup.js +2 -2
- package/dist/wire/errorEnvelope.d.ts +11 -2
- package/dist/wire/errorEnvelope.js +14 -2
- package/dist/wire/frames.d.ts +90 -0
- package/dist/wire/frames.js +55 -1
- package/dist/wire/index.d.ts +4 -1
- package/dist/wire/index.js +20 -1
- package/dist/wire/protocol.d.ts +45 -0
- package/dist/wire/protocol.js +45 -0
- package/dist/wire/protocolVersion.d.ts +56 -0
- package/dist/wire/protocolVersion.js +63 -0
- package/docs/api-keys.md +4 -3
- package/docs/data-sources.md +12 -5
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/identity.md +4 -4
- package/docs/integration-guide.md +1 -1
- package/docs/react.md +1 -1
- package/docs/sessions.md +5 -7
- package/llms.txt +4 -2
- package/package.json +15 -13
- package/dist/client/index.d.ts +0 -36
- package/dist/client/index.js +0 -33
- package/dist/config/index.d.ts +0 -10
- package/dist/config/index.js +0 -12
- package/dist/interfaces/headless.d.ts +0 -95
- package/dist/interfaces/headless.js +0 -41
- package/dist/query/index.d.ts +0 -6
- package/dist/query/index.js +0 -5
- package/dist/realtime/index.d.ts +0 -10
- package/dist/realtime/index.js +0 -9
- package/dist/schema/plane.d.ts +0 -23
- package/dist/schema/plane.js +0 -19
- package/dist/server/next.d.ts +0 -51
- package/dist/server/next.js +0 -47
- package/dist/sync/OfflineFlush.d.ts +0 -9
- package/dist/sync/OfflineFlush.js +0 -22
- package/dist/sync/OfflineTransactionStore.d.ts +0 -37
- package/dist/sync/OfflineTransactionStore.js +0 -263
- package/dist/transactions/index.d.ts +0 -16
- package/dist/transactions/index.js +0 -7
package/dist/react/context.d.ts
CHANGED
|
@@ -1,123 +1,7 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
-
import type { Model } from '../Model.js';
|
|
3
|
-
import type { ModelScope } from '../types/index.js';
|
|
4
|
-
import type { QueryView, QueryViewOptions } from '../core/QueryView.js';
|
|
5
|
-
import type { ViewRegistry } from '../core/ViewRegistry.js';
|
|
6
2
|
import type { Schema } from '../schema/schema.js';
|
|
7
|
-
import type {
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A single LOCAL mutation as observed off the commit stream — the substrate
|
|
11
|
-
* the undo system records from. One is emitted per local create/update/
|
|
12
|
-
* delete/archive (remote/collaborator deltas never appear here: they apply
|
|
13
|
-
* through a separate pool path that doesn't queue mutations). `previousData`
|
|
14
|
-
* holds the pre-edit field values (captured from the model's
|
|
15
|
-
* `modifiedProperties` first-old-wins baseline), so an inverse op is fully
|
|
16
|
-
* derivable from the event alone — no separate snapshot pass.
|
|
17
|
-
*
|
|
18
|
-
* This mirrors how Yjs's `UndoManager` derives reverse-ops by observing the
|
|
19
|
-
* doc and Liveblocks' `room.history` records room ops: undo listens to the
|
|
20
|
-
* one place all local writes converge, rather than wrapping the write call.
|
|
21
|
-
*/
|
|
22
|
-
export interface LocalMutation {
|
|
23
|
-
type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
24
|
-
/** Registered model name (e.g. `'SlideLayer'`); resolved to a schema key by the recorder. */
|
|
25
|
-
modelName: string;
|
|
26
|
-
modelId: string;
|
|
27
|
-
/** New field values (create/update). */
|
|
28
|
-
data?: Record<string, unknown> | null;
|
|
29
|
-
/** Pre-edit field values (update → inverse patch; delete → full re-create row). */
|
|
30
|
-
previousData?: Record<string, unknown> | null;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Minimal store interface that the SDK hooks need.
|
|
34
|
-
* Consumers provide their concrete store (e.g., SyncedStore) that implements this.
|
|
35
|
-
*/
|
|
36
|
-
export interface SyncStoreContract {
|
|
37
|
-
/**
|
|
38
|
-
* Subscribe to the LOCAL mutation stream (optimistic, pre-ack) for undo
|
|
39
|
-
* recording. Optional so minimal test doubles can omit it — when absent,
|
|
40
|
-
* undo scopes simply record nothing. The concrete store
|
|
41
|
-
* (`BaseSyncedStore`) wires this to the TransactionQueue's
|
|
42
|
-
* `transaction:created` event. Returns an unsubscribe function.
|
|
43
|
-
*/
|
|
44
|
-
subscribeLocalMutations?(handler: (mutation: LocalMutation) => void): () => void;
|
|
45
|
-
retrieve(modelClass: abstract new (...args: never[]) => Model, id: string): Model | undefined;
|
|
46
|
-
queryByClass(modelClass: abstract new (...args: never[]) => Model, options?: {
|
|
47
|
-
predicate?: (model: Model) => boolean;
|
|
48
|
-
scope?: ModelScope;
|
|
49
|
-
orderBy?: keyof Model;
|
|
50
|
-
order?: 'asc' | 'desc';
|
|
51
|
-
limit?: number;
|
|
52
|
-
offset?: number;
|
|
53
|
-
}): {
|
|
54
|
-
data: Model[];
|
|
55
|
-
};
|
|
56
|
-
/**
|
|
57
|
-
* Save (create or update) one entity. Calling `save` in a tight loop
|
|
58
|
-
* produces a single wire commit with one `batchIndex`: the SyncClient
|
|
59
|
-
* debounces IDB persistence and the server push to one microtask, and
|
|
60
|
-
* TransactionQueue coalesces every transaction staged in the tick into
|
|
61
|
-
* one batch. There is intentionally no `saveMany` — Zero, Replicache,
|
|
62
|
-
* and the rest of the local-first lineage all expose one-row writes
|
|
63
|
-
* and rely on the implicit tick boundary.
|
|
64
|
-
*
|
|
65
|
-
* `skipValidation` exists for trusted bulk paths (AI sandbox layer
|
|
66
|
-
* generation, PPTX import, hydration) where the producer has already
|
|
67
|
-
* type-checked and per-row Zod is a measurable cost.
|
|
68
|
-
*/
|
|
69
|
-
save(model: Model, options?: {
|
|
70
|
-
skipValidation?: boolean;
|
|
71
|
-
}): Promise<void>;
|
|
72
|
-
delete(model: Model): Promise<void>;
|
|
73
|
-
archive(model: Model): Promise<void>;
|
|
74
|
-
unarchive(model: Model): Promise<void>;
|
|
75
|
-
/** The ObjectPool — for entity/collection lookups by ID or typename. */
|
|
76
|
-
pool: {
|
|
77
|
-
get(id: string): Model | undefined;
|
|
78
|
-
getByTypeName(typename: string, scope?: ModelScope): Model[];
|
|
79
|
-
getByForeignKey(modelName: string, fieldName: string, fieldValue: string): Model[];
|
|
80
|
-
createFromData(data: Record<string, unknown>): Model | null;
|
|
81
|
-
hasForeignKeyIndex(typename: string, fieldName: string): boolean;
|
|
82
|
-
createView<T extends Record<string, unknown>>(typename: string, options?: QueryViewOptions<T>): QueryView<T>;
|
|
83
|
-
viewRegistry: ViewRegistry;
|
|
84
|
-
};
|
|
85
|
-
/**
|
|
86
|
-
* Reactive sync-status getters. Powered by MobX `computed` inside
|
|
87
|
-
* `BaseSyncedStore`, so they're safe to read in `observer` components
|
|
88
|
-
* and inside `reaction(() => store.isReady, ...)`. Consumers that
|
|
89
|
-
* don't want to touch MobX should prefer the `useSyncStatus()` hook.
|
|
90
|
-
*/
|
|
91
|
-
readonly isReady: boolean;
|
|
92
|
-
readonly isSyncing: boolean;
|
|
93
|
-
readonly isOffline: boolean;
|
|
94
|
-
readonly isReconnecting: boolean;
|
|
95
|
-
readonly isError: boolean;
|
|
96
|
-
readonly hasUnsyncedChanges: boolean;
|
|
97
|
-
/**
|
|
98
|
-
* Area-of-interest (dynamic read subscription). `enterScope`/`leaveScope`
|
|
99
|
-
* move the connection's read interest as the user navigates (open/close a
|
|
100
|
-
* deck, sheet, doc); `pinScope`/`unpinScope` express prominence (an active
|
|
101
|
-
* claim keeps a group subscribed). Each resolves the scope through the same
|
|
102
|
-
* resolver the claim path uses, so read interest and write claims agree on
|
|
103
|
-
* the sync-group string. Optional so minimal test doubles can omit them;
|
|
104
|
-
* no-ops before the socket exists. The concrete store (`BaseSyncedStore`)
|
|
105
|
-
* forwards to its `AreaOfInterestManager`.
|
|
106
|
-
*/
|
|
107
|
-
enterScope?(scope: ParticipantScope, opts?: {
|
|
108
|
-
hydrate?: boolean;
|
|
109
|
-
}): Promise<void>;
|
|
110
|
-
leaveScope?(scope: ParticipantScope): Promise<void>;
|
|
111
|
-
pinScope?(scope: ParticipantScope): Promise<void>;
|
|
112
|
-
unpinScope?(scope: ParticipantScope): Promise<void>;
|
|
113
|
-
/**
|
|
114
|
-
* Raw MobX-observable `SyncStatus` record. `useSyncStatus()` reads
|
|
115
|
-
* `state`, `progress`, `pendingChanges`, `isSessionError`, `error`
|
|
116
|
-
* from this to build its tagged union. Exposed on the contract so
|
|
117
|
-
* consumer-facing hooks and test doubles can manipulate it directly.
|
|
118
|
-
*/
|
|
119
|
-
readonly syncStatus: SyncStatus;
|
|
120
|
-
}
|
|
3
|
+
import type { SyncStoreContract } from '../core/storeContract.js';
|
|
4
|
+
export type { SyncStoreContract, LocalMutation } from '../core/storeContract.js';
|
|
121
5
|
export interface SyncReactContext {
|
|
122
6
|
store: SyncStoreContract;
|
|
123
7
|
/** Current organization ID for default entity context */
|
package/dist/react/useAblo.js
CHANGED
|
@@ -58,7 +58,7 @@ export function useAblo(modelOrSelect, id, options) {
|
|
|
58
58
|
useEffect(() => {
|
|
59
59
|
if (!engine || id === undefined)
|
|
60
60
|
return;
|
|
61
|
-
return engine.claims.onChange(() => setClaimVersion((version) => version + 1));
|
|
61
|
+
return engine.claims.onChange(() => { setClaimVersion((version) => version + 1); });
|
|
62
62
|
}, [engine, id]);
|
|
63
63
|
const selected = useReactive(() => {
|
|
64
64
|
if (!engine || !isSelectorOnly || typeof modelOrSelect !== 'function') {
|
|
@@ -34,6 +34,6 @@ export function useErrorListener(listener) {
|
|
|
34
34
|
const ref = useRef(listener);
|
|
35
35
|
ref.current = listener;
|
|
36
36
|
useEffect(() => {
|
|
37
|
-
return ctx.subscribeError((err) => ref.current(err));
|
|
37
|
+
return ctx.subscribeError((err) => { ref.current(err); });
|
|
38
38
|
}, [ctx]);
|
|
39
39
|
}
|
|
@@ -33,6 +33,6 @@ export function useMutationFailureListener(listener) {
|
|
|
33
33
|
const engine = ctx.engine;
|
|
34
34
|
if (!engine)
|
|
35
35
|
return;
|
|
36
|
-
return engine.onMutationFailure((payload) => ref.current(payload));
|
|
36
|
+
return engine.onMutationFailure((payload) => { ref.current(payload); });
|
|
37
37
|
}, [ctx, ctx.engine]);
|
|
38
38
|
}
|
|
@@ -30,7 +30,13 @@ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
|
|
|
30
30
|
continue;
|
|
31
31
|
const invokers = {};
|
|
32
32
|
for (const mutatorName of Object.keys(group)) {
|
|
33
|
-
const
|
|
33
|
+
const maybeFn = group[mutatorName];
|
|
34
|
+
if (!maybeFn)
|
|
35
|
+
continue;
|
|
36
|
+
// Bind the narrowed value: `noUncheckedIndexedAccess` types the indexed
|
|
37
|
+
// read as `Fn | undefined`, and that narrowing doesn't survive into the
|
|
38
|
+
// deferred invoker closures below — a non-optional local does.
|
|
39
|
+
const fn = maybeFn;
|
|
34
40
|
const label = `${String(modelKey)}.${mutatorName}`;
|
|
35
41
|
invokers[mutatorName] = async (args) => {
|
|
36
42
|
// Recording path: wrap the transaction so each write snapshots its
|
|
@@ -98,7 +98,6 @@ export function useReactive(compute, equals = defaultEquals) {
|
|
|
98
98
|
onChange();
|
|
99
99
|
}
|
|
100
100
|
});
|
|
101
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
102
101
|
}, [subscribeVersion]);
|
|
103
102
|
const getSnapshot = useCallback(() => snapshotRef.current.value, []);
|
|
104
103
|
return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
|
|
@@ -30,7 +30,7 @@ function getManager(key, factory) {
|
|
|
30
30
|
export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
|
|
31
31
|
const { store, organizationId, schema: ctxSchema } = useSyncContext();
|
|
32
32
|
const isExplicit = typeof schemaOrName !== 'string';
|
|
33
|
-
const schema = isExplicit ? schemaOrName : ctxSchema;
|
|
33
|
+
const schema = isExplicit ? (schemaOrName) : ctxSchema;
|
|
34
34
|
const name = isExplicit ? nameOrOptions : schemaOrName;
|
|
35
35
|
const options = (isExplicit ? maybeOptions : nameOrOptions);
|
|
36
36
|
if (!schema) {
|
|
@@ -58,7 +58,7 @@ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
|
|
|
58
58
|
// `canUndo`/`canRedo` go stale in every consumer that didn't itself call
|
|
59
59
|
// undo/redo (e.g. a keyboard handler whose Cmd+Z gate then never fires).
|
|
60
60
|
useEffect(() => {
|
|
61
|
-
return scope.onChange(() => setTick((t) => t + 1));
|
|
61
|
+
return scope.onChange(() => { setTick((t) => t + 1); });
|
|
62
62
|
}, [scope]);
|
|
63
63
|
const size = scope.size();
|
|
64
64
|
return {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe-DDL LOCKING knobs — the ONE reader for how a schema change acquires
|
|
3
|
+
* (and retries for) table locks, shared by BOTH executors of the provision
|
|
4
|
+
* plan:
|
|
5
|
+
*
|
|
6
|
+
* - `ablo migrate` (cli/migrate.ts) — the customer runs the DDL themselves
|
|
7
|
+
* - the hosted executor (`apps/sync-server/src/schema/ddlExec.ts`)
|
|
8
|
+
*
|
|
9
|
+
* The two used to carry copy-pasted constants and had already drifted: the
|
|
10
|
+
* server honored `ABLO_SCHEMA_LOCK_ATTEMPTS` while the CLI hardcoded 5, so an
|
|
11
|
+
* operator tuning the knob got it applied by hosted push but silently ignored
|
|
12
|
+
* by `ablo migrate`. Both now resolve through here.
|
|
13
|
+
*
|
|
14
|
+
* The settings themselves are the battle-tested ones every mature migration
|
|
15
|
+
* tool uses (GitLab `with_lock_retries`, Strong Migrations, Doctolib
|
|
16
|
+
* `safe-pg-migrations`): a LOW `lock_timeout` so a blocked ALTER aborts fast
|
|
17
|
+
* instead of parking an ACCESS EXCLUSIVE request at the head of the lock
|
|
18
|
+
* queue (which would freeze every query on that table behind it), plus a
|
|
19
|
+
* bounded retry-with-backoff on the `55P03` abort.
|
|
20
|
+
*
|
|
21
|
+
* Env knobs (read at CALL time, not import time, so tests and long-lived
|
|
22
|
+
* processes see updates): `ABLO_SCHEMA_LOCK_TIMEOUT` / `ABLO_SCHEMA_LOCK_ATTEMPTS`
|
|
23
|
+
* — the older `ABLO_DDL_*` names are still honored so existing setups don't
|
|
24
|
+
* break.
|
|
25
|
+
*/
|
|
26
|
+
/** Postgres SQLSTATE `lock_not_available` — a `lock_timeout` abort. The ONE
|
|
27
|
+
* retryable DDL failure; everything else is a real error. */
|
|
28
|
+
export declare const PG_LOCK_NOT_AVAILABLE = "55P03";
|
|
29
|
+
/** The env subset the resolvers read — injectable for tests. */
|
|
30
|
+
export type DdlLockEnv = Readonly<Record<string, string | undefined>>;
|
|
31
|
+
/** `lock_timeout` for the DDL transaction (a Postgres duration string). */
|
|
32
|
+
export declare function resolveDdlLockTimeout(env?: DdlLockEnv): string;
|
|
33
|
+
/** How many times the whole DDL transaction is attempted on `55P03` lock
|
|
34
|
+
* contention. Always ≥ 1; a malformed value falls back to the default
|
|
35
|
+
* instead of silently disabling retries. */
|
|
36
|
+
export declare function resolveDdlMaxLockAttempts(env?: DdlLockEnv): number;
|
|
37
|
+
/** Backoff before retry N (1-based): capped exponential + a little jitter so
|
|
38
|
+
* two contending migrators don't re-collide in lockstep. */
|
|
39
|
+
export declare function ddlLockRetryBackoffMs(attempt: number): number;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe-DDL LOCKING knobs — the ONE reader for how a schema change acquires
|
|
3
|
+
* (and retries for) table locks, shared by BOTH executors of the provision
|
|
4
|
+
* plan:
|
|
5
|
+
*
|
|
6
|
+
* - `ablo migrate` (cli/migrate.ts) — the customer runs the DDL themselves
|
|
7
|
+
* - the hosted executor (`apps/sync-server/src/schema/ddlExec.ts`)
|
|
8
|
+
*
|
|
9
|
+
* The two used to carry copy-pasted constants and had already drifted: the
|
|
10
|
+
* server honored `ABLO_SCHEMA_LOCK_ATTEMPTS` while the CLI hardcoded 5, so an
|
|
11
|
+
* operator tuning the knob got it applied by hosted push but silently ignored
|
|
12
|
+
* by `ablo migrate`. Both now resolve through here.
|
|
13
|
+
*
|
|
14
|
+
* The settings themselves are the battle-tested ones every mature migration
|
|
15
|
+
* tool uses (GitLab `with_lock_retries`, Strong Migrations, Doctolib
|
|
16
|
+
* `safe-pg-migrations`): a LOW `lock_timeout` so a blocked ALTER aborts fast
|
|
17
|
+
* instead of parking an ACCESS EXCLUSIVE request at the head of the lock
|
|
18
|
+
* queue (which would freeze every query on that table behind it), plus a
|
|
19
|
+
* bounded retry-with-backoff on the `55P03` abort.
|
|
20
|
+
*
|
|
21
|
+
* Env knobs (read at CALL time, not import time, so tests and long-lived
|
|
22
|
+
* processes see updates): `ABLO_SCHEMA_LOCK_TIMEOUT` / `ABLO_SCHEMA_LOCK_ATTEMPTS`
|
|
23
|
+
* — the older `ABLO_DDL_*` names are still honored so existing setups don't
|
|
24
|
+
* break.
|
|
25
|
+
*/
|
|
26
|
+
/** Postgres SQLSTATE `lock_not_available` — a `lock_timeout` abort. The ONE
|
|
27
|
+
* retryable DDL failure; everything else is a real error. */
|
|
28
|
+
export const PG_LOCK_NOT_AVAILABLE = '55P03';
|
|
29
|
+
const DEFAULT_LOCK_TIMEOUT = '5s';
|
|
30
|
+
const DEFAULT_MAX_LOCK_ATTEMPTS = 5;
|
|
31
|
+
/** `lock_timeout` for the DDL transaction (a Postgres duration string). */
|
|
32
|
+
export function resolveDdlLockTimeout(env = process.env) {
|
|
33
|
+
return env.ABLO_SCHEMA_LOCK_TIMEOUT ?? env.ABLO_DDL_LOCK_TIMEOUT ?? DEFAULT_LOCK_TIMEOUT;
|
|
34
|
+
}
|
|
35
|
+
/** How many times the whole DDL transaction is attempted on `55P03` lock
|
|
36
|
+
* contention. Always ≥ 1; a malformed value falls back to the default
|
|
37
|
+
* instead of silently disabling retries. */
|
|
38
|
+
export function resolveDdlMaxLockAttempts(env = process.env) {
|
|
39
|
+
const raw = env.ABLO_SCHEMA_LOCK_ATTEMPTS ?? env.ABLO_DDL_LOCK_ATTEMPTS;
|
|
40
|
+
if (raw === undefined)
|
|
41
|
+
return DEFAULT_MAX_LOCK_ATTEMPTS;
|
|
42
|
+
const parsed = Number(raw);
|
|
43
|
+
if (!Number.isFinite(parsed))
|
|
44
|
+
return DEFAULT_MAX_LOCK_ATTEMPTS;
|
|
45
|
+
return Math.max(1, Math.floor(parsed));
|
|
46
|
+
}
|
|
47
|
+
/** Backoff before retry N (1-based): capped exponential + a little jitter so
|
|
48
|
+
* two contending migrators don't re-collide in lockstep. */
|
|
49
|
+
export function ddlLockRetryBackoffMs(attempt) {
|
|
50
|
+
return Math.min(60_000, 10 * 2 ** attempt) + Math.floor(Math.random() * 50);
|
|
51
|
+
}
|
package/dist/schema/field.js
CHANGED
|
@@ -42,7 +42,7 @@ function encodeMeta(meta) {
|
|
|
42
42
|
return META_PREFIX + JSON.stringify(meta);
|
|
43
43
|
}
|
|
44
44
|
function decodeMeta(description) {
|
|
45
|
-
if (!description
|
|
45
|
+
if (!description?.startsWith(META_PREFIX))
|
|
46
46
|
return null;
|
|
47
47
|
try {
|
|
48
48
|
return JSON.parse(description.slice(META_PREFIX.length));
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -24,16 +24,17 @@ export { z } from 'zod';
|
|
|
24
24
|
export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
|
|
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
|
-
export { planeSchema, DEFAULT_PLANE, type SchemaPlane } from './
|
|
28
|
-
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema,
|
|
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 './sync-delta-row.js';
|
|
29
29
|
export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, type SyncDeltaAction, type WireDeltaData, type ParticipantRef, type SyncDeltaWireCore, type ClientSyncDelta, type ServerSyncDelta, } from './sync-delta-wire.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';
|
|
32
32
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
33
|
-
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
|
|
33
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, intersectRequestedWithAllowed, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
|
|
34
34
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
35
35
|
export { selectModels } from './select.js';
|
|
36
36
|
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
|
37
|
+
export { PG_LOCK_NOT_AVAILABLE, resolveDdlLockTimeout, resolveDdlMaxLockAttempts, ddlLockRetryBackoffMs, type DdlLockEnv, } from './ddlLock.js';
|
|
37
38
|
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, type BackfillValue, type MigrationStep, type FieldChanges, type FieldColumnChange, type FieldTypeChange, type NullabilityChange, type EnumValuesChange, type IndexChange, type CastSafety, type FieldType, type RenameHints, type MigrationSignal, type MigrationClassification, type WarningCode, type BlockerCode, } from './diff.js';
|
|
38
39
|
export { generateTypes } from './generate.js';
|
|
39
40
|
export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -28,13 +28,19 @@ export { field, indexed, getFieldMeta } from './field.js';
|
|
|
28
28
|
export { relation } from './relation.js';
|
|
29
29
|
// Tenancy — the single source of truth for how a model's rows are tenant-scoped.
|
|
30
30
|
export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
31
|
-
//
|
|
32
|
-
// customer DB, `control` = Ablo's own). Sibling axis to `tenancy`.
|
|
33
|
-
|
|
31
|
+
// Model residency — which DB a model's rows live in (`tenant` portable to a
|
|
32
|
+
// customer's own DB, `control` = Ablo's own). Sibling axis to `tenancy`.
|
|
33
|
+
// (Renamed from "plane" — that word is the server's tenancy scope.)
|
|
34
|
+
export { residencySchema, DEFAULT_RESIDENCY,
|
|
35
|
+
// Deliberate back-compat re-export of the deprecated aliases:
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
37
|
+
planeSchema,
|
|
38
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
39
|
+
DEFAULT_PLANE, } from './residency.js';
|
|
34
40
|
// Decomposed sync-delta storage row (P0 of the control/tenant plane split —
|
|
35
41
|
// see docs/plans/sync-delta-zod-decomposition.md). Describes the existing
|
|
36
42
|
// `sync_deltas` columns as Zod schemas grouped by subsystem + database plane.
|
|
37
|
-
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema,
|
|
43
|
+
export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_RESIDENCY, } from './sync-delta-row.js';
|
|
38
44
|
// Canonical WIRE delta contract — the broadcast (server→client) projection of
|
|
39
45
|
// the stored row. The SDK client and the sync-server both derive their
|
|
40
46
|
// `SyncDelta` type from these via `z.infer` so the contract cannot drift.
|
|
@@ -49,13 +55,17 @@ export { coordination, humansOverwrite, humansReject, humansNotify, agentsOverwr
|
|
|
49
55
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
50
56
|
export { mutable, readOnly } from './sugar.js';
|
|
51
57
|
// Schema definition + type inference
|
|
52
|
-
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
|
|
58
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
|
|
53
59
|
// Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
|
|
54
60
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
55
61
|
// Schema projection — derive an app's subset from one canonical schema.
|
|
56
62
|
export { selectModels } from './select.js';
|
|
57
63
|
// Schema → Postgres DDL (pure; shared by the hosted server and the CLI)
|
|
58
64
|
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, } from './ddl.js';
|
|
65
|
+
// Safe-DDL locking knobs (lock_timeout + bounded 55P03 retry) — the ONE
|
|
66
|
+
// reader shared by `ablo migrate` and the hosted executor (ddlExec.ts), so an
|
|
67
|
+
// operator tuning ABLO_SCHEMA_LOCK_* gets the same behavior on both paths.
|
|
68
|
+
export { PG_LOCK_NOT_AVAILABLE, resolveDdlLockTimeout, resolveDdlMaxLockAttempts, ddlLockRetryBackoffMs, } from './ddlLock.js';
|
|
59
69
|
// Schema diff + migration planning (pure; SQL emission lowered by ddl.ts)
|
|
60
70
|
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, } from './diff.js';
|
|
61
71
|
// Schema → TypeScript type emission (the `generate` half; pure)
|
package/dist/schema/model.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ import type { EntityRole, GroupsInput } from './roles.js';
|
|
|
22
22
|
import { type FieldMeta } from './field.js';
|
|
23
23
|
import { type Tenancy, type PolicyInput } from './tenancy.js';
|
|
24
24
|
export type { ScopedViaRef, Tenancy, PolicyInput } from './tenancy.js';
|
|
25
|
-
import { type
|
|
25
|
+
import { type ModelResidency } from './residency.js';
|
|
26
26
|
import type { ConflictAxis } from '../policy/types.js';
|
|
27
27
|
export type { ConflictAxis } from '../policy/types.js';
|
|
28
28
|
/**
|
|
@@ -124,7 +124,7 @@ export interface ModelOptions {
|
|
|
124
124
|
* provisioning. `control` = Ablo's control plane (sync log, attribution,
|
|
125
125
|
* audit) — never emitted into a customer DB. See `./plane.ts`.
|
|
126
126
|
*/
|
|
127
|
-
plane?:
|
|
127
|
+
plane?: ModelResidency;
|
|
128
128
|
/**
|
|
129
129
|
* **Axis 2 — sync-group routing.** Decides which delta *channels* a row fans
|
|
130
130
|
* into. Orthogonal to {@link policy} (read access). One namespaced object
|
|
@@ -318,7 +318,7 @@ export interface ModelDef<Shape extends z.ZodRawShape = z.ZodRawShape, R extends
|
|
|
318
318
|
readonly tenancy: Tenancy;
|
|
319
319
|
/** Database plane — `tenant` (default) is portable to a customer DB; `control`
|
|
320
320
|
* is Ablo-only. See {@link ModelOptions.plane} and `./plane.ts`. */
|
|
321
|
-
readonly plane?:
|
|
321
|
+
readonly plane?: ModelResidency;
|
|
322
322
|
/** Scope-root marker. See {@link ModelOptions.scope}. */
|
|
323
323
|
readonly scope?: boolean | string;
|
|
324
324
|
/** Membership edge granting identity → scope-root access. See {@link ModelOptions.grants}. */
|
package/dist/schema/model.js
CHANGED
|
@@ -24,7 +24,7 @@ import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
|
|
|
24
24
|
// Postgres/Supabase RLS), normalized to the canonical `Tenancy` by
|
|
25
25
|
// `resolvePolicy` at build time.
|
|
26
26
|
import { resolvePolicy } from './tenancy.js';
|
|
27
|
-
import {
|
|
27
|
+
import { DEFAULT_RESIDENCY } from './residency.js';
|
|
28
28
|
/** Normalize the `entityRoles` option (single | array | undefined) to an array. */
|
|
29
29
|
function normalizeEntityRoles(input) {
|
|
30
30
|
if (!input)
|
|
@@ -93,7 +93,7 @@ export function model(shape, relations, options) {
|
|
|
93
93
|
// Axis 1 — normalize the `policy` authoring option into the one canonical
|
|
94
94
|
// tenancy descriptor (defaults to a row-local org column).
|
|
95
95
|
tenancy: resolvePolicy(options?.policy),
|
|
96
|
-
plane: options?.plane ??
|
|
96
|
+
plane: options?.plane ?? DEFAULT_RESIDENCY,
|
|
97
97
|
// Axis 2 — unpack the `groups` routing namespace into the wire fields the
|
|
98
98
|
// server reads (`scope`/`grants`/`entityRoles` on ModelDef/ModelJSON).
|
|
99
99
|
scope: options?.groups?.root,
|
package/dist/schema/openapi.js
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
// Dependency-free leaf (NOT client/auth.ts — that would pull the error
|
|
2
|
+
// registry + credential policy into the schema subpath and close a
|
|
3
|
+
// schema → client → errors → coordination → schema cycle).
|
|
4
|
+
import { ABLO_HOSTED_HTTP_BASE_URL } from '../client/hostedEndpoints.js';
|
|
1
5
|
function fieldSchema(f) {
|
|
2
6
|
switch (f.type) {
|
|
3
7
|
case 'number':
|
|
@@ -105,7 +109,7 @@ export function schemaToOpenApi(schema, options = {}) {
|
|
|
105
109
|
description: 'Generated from your pushed Ablo schema — these routes are your models. ' +
|
|
106
110
|
'Authenticate every request with your API key as a Bearer token.',
|
|
107
111
|
},
|
|
108
|
-
servers: [{ url: options.serverUrl ??
|
|
112
|
+
servers: [{ url: options.serverUrl ?? `${ABLO_HOSTED_HTTP_BASE_URL}/api` }],
|
|
109
113
|
security: [{ bearerAuth: [] }],
|
|
110
114
|
components: {
|
|
111
115
|
securitySchemes: {
|
package/dist/schema/queries.d.ts
CHANGED
|
@@ -158,7 +158,7 @@ export type InferQueryInput<Q extends QueryDef> = z.infer<Q['input']>;
|
|
|
158
158
|
* // Array<SlideLayer>
|
|
159
159
|
* ```
|
|
160
160
|
*/
|
|
161
|
-
export type InferQueryResult<S extends Schema, Q extends QueryDef> = Q extends QueryDef<z.ZodType, infer R> ? R extends InferModelNames<S> ?
|
|
161
|
+
export type InferQueryResult<S extends Schema, Q extends QueryDef> = Q extends QueryDef<z.ZodType, infer R> ? R extends InferModelNames<S> ? InferModel<S, R>[] : never : never;
|
|
162
162
|
/**
|
|
163
163
|
* Define a typed query set against a schema.
|
|
164
164
|
*
|
package/dist/schema/queries.js
CHANGED
|
@@ -56,6 +56,7 @@
|
|
|
56
56
|
* and lets the generic loader hydrate via a single
|
|
57
57
|
* `schema.models[queryDef.returns]` lookup.
|
|
58
58
|
*/
|
|
59
|
+
import { z } from 'zod';
|
|
59
60
|
import { AbloValidationError } from '../errors.js';
|
|
60
61
|
// ── query() factory ───────────────────────────────────────────────────────
|
|
61
62
|
/**
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model RESIDENCY — which database a model's rows live in. A sibling axis to
|
|
3
|
+
* `tenancy` (which says how rows are isolated *within* a database):
|
|
4
|
+
*
|
|
5
|
+
* - `tenant` — the tenant data plane. For a customer-connected database
|
|
6
|
+
* this is THEIR database; provisioning emits these tables there.
|
|
7
|
+
* - `control` — Ablo's control plane (the sync log, attribution, audit, …).
|
|
8
|
+
* Never emitted into a customer DB; lives only in Ablo's own DB.
|
|
9
|
+
*
|
|
10
|
+
* Named `residency` (not `plane`) because "plane" is the server's tenancy
|
|
11
|
+
* scope — (org, environment[, project, sandbox]) — the dominant meaning in the
|
|
12
|
+
* engine. The serialized model option is still the `plane` KEY (wire format,
|
|
13
|
+
* frozen until the next wire-version bump); only the TS vocabulary moved.
|
|
14
|
+
*
|
|
15
|
+
* P1 of the sync-delta decomposition (`docs/plans/sync-delta-zod-decomposition.md`):
|
|
16
|
+
* declaring the boundary lets provisioning *derive* "what a customer DB gets"
|
|
17
|
+
* (`residency === 'tenant'`) instead of hand-coding it. Defaults to `tenant`.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export declare const residencySchema: z.ZodEnum<{
|
|
21
|
+
tenant: "tenant";
|
|
22
|
+
control: "control";
|
|
23
|
+
}>;
|
|
24
|
+
export type ModelResidency = z.infer<typeof residencySchema>;
|
|
25
|
+
/** Default residency for a model that doesn't declare one — the tenant data plane. */
|
|
26
|
+
export declare const DEFAULT_RESIDENCY: ModelResidency;
|
|
27
|
+
/** @deprecated Use `residencySchema`. */
|
|
28
|
+
export declare const planeSchema: z.ZodEnum<{
|
|
29
|
+
tenant: "tenant";
|
|
30
|
+
control: "control";
|
|
31
|
+
}>;
|
|
32
|
+
/** @deprecated Use `ModelResidency`. */
|
|
33
|
+
export type SchemaPlane = ModelResidency;
|
|
34
|
+
/** @deprecated Use `DEFAULT_RESIDENCY`. */
|
|
35
|
+
export declare const DEFAULT_PLANE: "tenant";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model RESIDENCY — which database a model's rows live in. A sibling axis to
|
|
3
|
+
* `tenancy` (which says how rows are isolated *within* a database):
|
|
4
|
+
*
|
|
5
|
+
* - `tenant` — the tenant data plane. For a customer-connected database
|
|
6
|
+
* this is THEIR database; provisioning emits these tables there.
|
|
7
|
+
* - `control` — Ablo's control plane (the sync log, attribution, audit, …).
|
|
8
|
+
* Never emitted into a customer DB; lives only in Ablo's own DB.
|
|
9
|
+
*
|
|
10
|
+
* Named `residency` (not `plane`) because "plane" is the server's tenancy
|
|
11
|
+
* scope — (org, environment[, project, sandbox]) — the dominant meaning in the
|
|
12
|
+
* engine. The serialized model option is still the `plane` KEY (wire format,
|
|
13
|
+
* frozen until the next wire-version bump); only the TS vocabulary moved.
|
|
14
|
+
*
|
|
15
|
+
* P1 of the sync-delta decomposition (`docs/plans/sync-delta-zod-decomposition.md`):
|
|
16
|
+
* declaring the boundary lets provisioning *derive* "what a customer DB gets"
|
|
17
|
+
* (`residency === 'tenant'`) instead of hand-coding it. Defaults to `tenant`.
|
|
18
|
+
*/
|
|
19
|
+
import { z } from 'zod';
|
|
20
|
+
export const residencySchema = z.enum(['tenant', 'control']);
|
|
21
|
+
/** Default residency for a model that doesn't declare one — the tenant data plane. */
|
|
22
|
+
export const DEFAULT_RESIDENCY = 'tenant';
|
|
23
|
+
// ── Deprecated aliases (published schema subpath) ───────────────────────────
|
|
24
|
+
/** @deprecated Use `residencySchema`. */
|
|
25
|
+
export const planeSchema = residencySchema;
|
|
26
|
+
/** @deprecated Use `DEFAULT_RESIDENCY`. */
|
|
27
|
+
export const DEFAULT_PLANE = DEFAULT_RESIDENCY;
|
package/dist/schema/roles.d.ts
CHANGED
|
@@ -229,3 +229,28 @@ export declare function composeIdentitySyncGroups(identity: IdentityContext, sch
|
|
|
229
229
|
export declare function composeEntitySyncGroups(record: EntityContext, def: {
|
|
230
230
|
readonly entityRoles?: readonly EntityRole[];
|
|
231
231
|
}): readonly SyncGroup[];
|
|
232
|
+
/**
|
|
233
|
+
* Apply capability-style intersection to a client-requested sync-group
|
|
234
|
+
* set. Mirrors the cap-bearer path where Biscuit caveats narrow the
|
|
235
|
+
* client's requested set to what's actually authorized. Fully generic —
|
|
236
|
+
* it never inspects the group strings — so it lives here beside the
|
|
237
|
+
* composition helpers rather than in any product schema package
|
|
238
|
+
* (`@ablo/schema` re-exports it for compat).
|
|
239
|
+
*
|
|
240
|
+
* Behaviour:
|
|
241
|
+
* - If the client requested no groups, return the full identity-
|
|
242
|
+
* derived set (the default subscription scope for this participant).
|
|
243
|
+
* - If the client requested a non-empty subset, intersect against the
|
|
244
|
+
* identity-derived allowed set — drop any group the participant is
|
|
245
|
+
* not authorized to subscribe to. Logs the dropped groups via the
|
|
246
|
+
* optional `logDropped` callback for observability.
|
|
247
|
+
* - If the intersection is empty after filtering, fall back to the
|
|
248
|
+
* full allowed set rather than emit `[]` (which would degenerate to
|
|
249
|
+
* the server-side `['default']` fallback and produce silent zero-
|
|
250
|
+
* delta delivery).
|
|
251
|
+
*/
|
|
252
|
+
export declare function intersectRequestedWithAllowed(args: {
|
|
253
|
+
readonly requested: readonly string[];
|
|
254
|
+
readonly allowed: readonly string[];
|
|
255
|
+
readonly logDropped?: (dropped: readonly string[]) => void;
|
|
256
|
+
}): readonly string[];
|
package/dist/schema/roles.js
CHANGED
|
@@ -197,3 +197,40 @@ export function composeEntitySyncGroups(record, def) {
|
|
|
197
197
|
}
|
|
198
198
|
return Array.from(out);
|
|
199
199
|
}
|
|
200
|
+
/**
|
|
201
|
+
* Apply capability-style intersection to a client-requested sync-group
|
|
202
|
+
* set. Mirrors the cap-bearer path where Biscuit caveats narrow the
|
|
203
|
+
* client's requested set to what's actually authorized. Fully generic —
|
|
204
|
+
* it never inspects the group strings — so it lives here beside the
|
|
205
|
+
* composition helpers rather than in any product schema package
|
|
206
|
+
* (`@ablo/schema` re-exports it for compat).
|
|
207
|
+
*
|
|
208
|
+
* Behaviour:
|
|
209
|
+
* - If the client requested no groups, return the full identity-
|
|
210
|
+
* derived set (the default subscription scope for this participant).
|
|
211
|
+
* - If the client requested a non-empty subset, intersect against the
|
|
212
|
+
* identity-derived allowed set — drop any group the participant is
|
|
213
|
+
* not authorized to subscribe to. Logs the dropped groups via the
|
|
214
|
+
* optional `logDropped` callback for observability.
|
|
215
|
+
* - If the intersection is empty after filtering, fall back to the
|
|
216
|
+
* full allowed set rather than emit `[]` (which would degenerate to
|
|
217
|
+
* the server-side `['default']` fallback and produce silent zero-
|
|
218
|
+
* delta delivery).
|
|
219
|
+
*/
|
|
220
|
+
export function intersectRequestedWithAllowed(args) {
|
|
221
|
+
const { requested, allowed, logDropped } = args;
|
|
222
|
+
if (requested.length === 0)
|
|
223
|
+
return allowed;
|
|
224
|
+
const allowedSet = new Set(allowed);
|
|
225
|
+
const accepted = [];
|
|
226
|
+
const dropped = [];
|
|
227
|
+
for (const g of requested) {
|
|
228
|
+
if (allowedSet.has(g))
|
|
229
|
+
accepted.push(g);
|
|
230
|
+
else
|
|
231
|
+
dropped.push(g);
|
|
232
|
+
}
|
|
233
|
+
if (dropped.length > 0 && logDropped)
|
|
234
|
+
logDropped(dropped);
|
|
235
|
+
return accepted.length > 0 ? accepted : allowed;
|
|
236
|
+
}
|
package/dist/schema/schema.d.ts
CHANGED
|
@@ -23,7 +23,7 @@ import { z } from 'zod';
|
|
|
23
23
|
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
24
|
import type { RelationDef } from './relation.js';
|
|
25
25
|
import type { IdentityRole } from './roles.js';
|
|
26
|
-
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
|
|
26
|
+
export { type IdentityRole, type IdentityRoleSource, type IdentityContext, type EntityRole, type EntityRoleSource, type EntityContext, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, composeIdentitySyncGroups, composeEntitySyncGroups, intersectRequestedWithAllowed, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './roles.js';
|
|
27
27
|
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
28
28
|
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
29
29
|
/** Plug point for custom conventions (e.g. mixed legacy databases). */
|
|
@@ -228,7 +228,7 @@ export type UpdateValue<S extends Schema, ModelName extends keyof S['models']> =
|
|
|
228
228
|
*
|
|
229
229
|
* Matches Zero's `DeleteID<TableSchema>` from `zql/src/mutate/crud.ts`.
|
|
230
230
|
*/
|
|
231
|
-
export
|
|
231
|
+
export interface DeleteId<S extends Schema, ModelName extends keyof S['models']> {
|
|
232
232
|
id: string;
|
|
233
|
-
}
|
|
233
|
+
}
|
|
234
234
|
export declare function defineSchema<const S extends SchemaRecord>(models: S, options?: DefineSchemaOptions): Schema<S>;
|