@abloatai/ablo 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +208 -0
- package/LICENSE +201 -0
- package/NOTICE +12 -0
- package/README.md +230 -0
- package/dist/BaseSyncedStore.d.ts +709 -0
- package/dist/BaseSyncedStore.js +1843 -0
- package/dist/Database.d.ts +344 -0
- package/dist/Database.js +1259 -0
- package/dist/LazyReferenceCollection.d.ts +181 -0
- package/dist/LazyReferenceCollection.js +460 -0
- package/dist/Model.d.ts +339 -0
- package/dist/Model.js +715 -0
- package/dist/ModelRegistry.d.ts +200 -0
- package/dist/ModelRegistry.js +535 -0
- package/dist/NetworkMonitor.d.ts +27 -0
- package/dist/NetworkMonitor.js +73 -0
- package/dist/ObjectPool.d.ts +202 -0
- package/dist/ObjectPool.js +1106 -0
- package/dist/SyncClient.d.ts +489 -0
- package/dist/SyncClient.js +1555 -0
- package/dist/SyncEngineContext.d.ts +46 -0
- package/dist/SyncEngineContext.js +74 -0
- package/dist/adapters/alwaysOnline.d.ts +16 -0
- package/dist/adapters/alwaysOnline.js +19 -0
- package/dist/adapters/inMemoryStorage.d.ts +30 -0
- package/dist/adapters/inMemoryStorage.js +94 -0
- package/dist/agent/Agent.d.ts +358 -0
- package/dist/agent/Agent.js +500 -0
- package/dist/agent/index.d.ts +115 -0
- package/dist/agent/index.js +128 -0
- package/dist/agent/session.d.ts +90 -0
- package/dist/agent/session.js +156 -0
- package/dist/agent/types.d.ts +73 -0
- package/dist/agent/types.js +10 -0
- package/dist/ai-sdk/coordination-context.d.ts +51 -0
- package/dist/ai-sdk/coordination-context.js +107 -0
- package/dist/ai-sdk/index.d.ts +68 -0
- package/dist/ai-sdk/index.js +68 -0
- package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
- package/dist/ai-sdk/intent-broadcast.js +72 -0
- package/dist/ai-sdk/wrap.d.ts +67 -0
- package/dist/ai-sdk/wrap.js +45 -0
- package/dist/api/index.d.ts +10 -0
- package/dist/api/index.js +9 -0
- package/dist/auth/index.d.ts +137 -0
- package/dist/auth/index.js +246 -0
- package/dist/client/Ablo.d.ts +835 -0
- package/dist/client/Ablo.js +1440 -0
- package/dist/client/ApiClient.d.ts +200 -0
- package/dist/client/ApiClient.js +659 -0
- package/dist/client/auth.d.ts +79 -0
- package/dist/client/auth.js +81 -0
- package/dist/client/createInternalComponents.d.ts +44 -0
- package/dist/client/createInternalComponents.js +88 -0
- package/dist/client/createModelProxy.d.ts +152 -0
- package/dist/client/createModelProxy.js +199 -0
- package/dist/client/identity.d.ts +63 -0
- package/dist/client/identity.js +156 -0
- package/dist/client/index.d.ts +36 -0
- package/dist/client/index.js +33 -0
- package/dist/client/persistence.d.ts +7 -0
- package/dist/client/persistence.js +11 -0
- package/dist/client/validateAbloOptions.d.ts +42 -0
- package/dist/client/validateAbloOptions.js +43 -0
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +12 -0
- package/dist/context.d.ts +27 -0
- package/dist/context.js +58 -0
- package/dist/core/DatabaseManager.d.ts +108 -0
- package/dist/core/DatabaseManager.js +361 -0
- package/dist/core/QueryProcessor.d.ts +77 -0
- package/dist/core/QueryProcessor.js +262 -0
- package/dist/core/QueryView.d.ts +64 -0
- package/dist/core/QueryView.js +219 -0
- package/dist/core/StoreManager.d.ts +131 -0
- package/dist/core/StoreManager.js +334 -0
- package/dist/core/ViewRegistry.d.ts +20 -0
- package/dist/core/ViewRegistry.js +55 -0
- package/dist/core/index.d.ts +34 -0
- package/dist/core/index.js +59 -0
- package/dist/core/openIDBWithTimeout.d.ts +27 -0
- package/dist/core/openIDBWithTimeout.js +63 -0
- package/dist/core/query-utils.d.ts +37 -0
- package/dist/core/query-utils.js +60 -0
- package/dist/errors.d.ts +235 -0
- package/dist/errors.js +243 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +82 -0
- package/dist/interfaces/headless.d.ts +95 -0
- package/dist/interfaces/headless.js +41 -0
- package/dist/interfaces/index.d.ts +321 -0
- package/dist/interfaces/index.js +8 -0
- package/dist/mutators/RecordingTransaction.d.ts +36 -0
- package/dist/mutators/RecordingTransaction.js +216 -0
- package/dist/mutators/Transaction.d.ts +48 -0
- package/dist/mutators/Transaction.js +64 -0
- package/dist/mutators/UndoManager.d.ts +114 -0
- package/dist/mutators/UndoManager.js +143 -0
- package/dist/mutators/defineMutators.d.ts +55 -0
- package/dist/mutators/defineMutators.js +28 -0
- package/dist/policy/index.d.ts +19 -0
- package/dist/policy/index.js +18 -0
- package/dist/policy/types.d.ts +74 -0
- package/dist/policy/types.js +17 -0
- package/dist/principal.d.ts +44 -0
- package/dist/principal.js +49 -0
- package/dist/query/client.d.ts +43 -0
- package/dist/query/client.js +84 -0
- package/dist/query/index.d.ts +6 -0
- package/dist/query/index.js +5 -0
- package/dist/query/types.d.ts +143 -0
- package/dist/query/types.js +36 -0
- package/dist/react/AbloProvider.d.ts +205 -0
- package/dist/react/AbloProvider.js +398 -0
- package/dist/react/ClientSideSuspense.d.ts +36 -0
- package/dist/react/ClientSideSuspense.js +17 -0
- package/dist/react/DefaultFallback.d.ts +24 -0
- package/dist/react/DefaultFallback.js +43 -0
- package/dist/react/SyncGroupProvider.d.ts +19 -0
- package/dist/react/SyncGroupProvider.js +44 -0
- package/dist/react/context.d.ts +161 -0
- package/dist/react/context.js +35 -0
- package/dist/react/index.d.ts +64 -0
- package/dist/react/index.js +73 -0
- package/dist/react/internalContext.d.ts +35 -0
- package/dist/react/internalContext.js +3 -0
- package/dist/react/useAblo.d.ts +72 -0
- package/dist/react/useAblo.js +63 -0
- package/dist/react/useCurrentUserId.d.ts +21 -0
- package/dist/react/useCurrentUserId.js +33 -0
- package/dist/react/useErrorListener.d.ts +20 -0
- package/dist/react/useErrorListener.js +39 -0
- package/dist/react/useIntent.d.ts +29 -0
- package/dist/react/useIntent.js +42 -0
- package/dist/react/useMutate.d.ts +83 -0
- package/dist/react/useMutate.js +122 -0
- package/dist/react/useMutationFailureListener.d.ts +26 -0
- package/dist/react/useMutationFailureListener.js +38 -0
- package/dist/react/useMutators.d.ts +56 -0
- package/dist/react/useMutators.js +66 -0
- package/dist/react/usePresence.d.ts +32 -0
- package/dist/react/usePresence.js +41 -0
- package/dist/react/useQuery.d.ts +123 -0
- package/dist/react/useQuery.js +145 -0
- package/dist/react/useReactive.d.ts +35 -0
- package/dist/react/useReactive.js +111 -0
- package/dist/react/useReader.d.ts +69 -0
- package/dist/react/useReader.js +73 -0
- package/dist/react/useSyncStatus.d.ts +61 -0
- package/dist/react/useSyncStatus.js +76 -0
- package/dist/react/useUndoScope.d.ts +36 -0
- package/dist/react/useUndoScope.js +73 -0
- package/dist/realtime/index.d.ts +10 -0
- package/dist/realtime/index.js +9 -0
- package/dist/schema/field.d.ts +134 -0
- package/dist/schema/field.js +264 -0
- package/dist/schema/index.d.ts +29 -0
- package/dist/schema/index.js +38 -0
- package/dist/schema/model.d.ts +326 -0
- package/dist/schema/model.js +89 -0
- package/dist/schema/queries.d.ts +203 -0
- package/dist/schema/queries.js +145 -0
- package/dist/schema/relation.d.ts +172 -0
- package/dist/schema/relation.js +104 -0
- package/dist/schema/schema.d.ts +259 -0
- package/dist/schema/schema.js +188 -0
- package/dist/schema/sugar.d.ts +129 -0
- package/dist/schema/sugar.js +94 -0
- package/dist/source/index.d.ts +423 -0
- package/dist/source/index.js +320 -0
- package/dist/source/pushQueue.d.ts +112 -0
- package/dist/source/pushQueue.js +249 -0
- package/dist/stores/ObjectStore.d.ts +103 -0
- package/dist/stores/ObjectStore.js +371 -0
- package/dist/stores/ObjectStoreContract.d.ts +39 -0
- package/dist/stores/ObjectStoreContract.js +1 -0
- package/dist/stores/SyncActionStore.d.ts +101 -0
- package/dist/stores/SyncActionStore.js +481 -0
- package/dist/sync/BootstrapHelper.d.ts +127 -0
- package/dist/sync/BootstrapHelper.js +434 -0
- package/dist/sync/ConnectionManager.d.ts +136 -0
- package/dist/sync/ConnectionManager.js +465 -0
- package/dist/sync/HydrationCoordinator.d.ts +137 -0
- package/dist/sync/HydrationCoordinator.js +468 -0
- package/dist/sync/NetworkProbe.d.ts +43 -0
- package/dist/sync/NetworkProbe.js +113 -0
- package/dist/sync/OfflineFlush.d.ts +9 -0
- package/dist/sync/OfflineFlush.js +22 -0
- package/dist/sync/OfflineTransactionStore.d.ts +37 -0
- package/dist/sync/OfflineTransactionStore.js +263 -0
- package/dist/sync/SyncWebSocket.d.ts +663 -0
- package/dist/sync/SyncWebSocket.js +1336 -0
- package/dist/sync/createIntentStream.d.ts +33 -0
- package/dist/sync/createIntentStream.js +243 -0
- package/dist/sync/createPresenceStream.d.ts +46 -0
- package/dist/sync/createPresenceStream.js +192 -0
- package/dist/sync/createSnapshot.d.ts +33 -0
- package/dist/sync/createSnapshot.js +124 -0
- package/dist/sync/participants.d.ts +114 -0
- package/dist/sync/participants.js +336 -0
- package/dist/sync/schemas.d.ts +79 -0
- package/dist/sync/schemas.js +78 -0
- package/dist/testing/fixtures/bootstrap.d.ts +45 -0
- package/dist/testing/fixtures/bootstrap.js +53 -0
- package/dist/testing/fixtures/deltas.d.ts +86 -0
- package/dist/testing/fixtures/deltas.js +139 -0
- package/dist/testing/fixtures/models.d.ts +82 -0
- package/dist/testing/fixtures/models.js +270 -0
- package/dist/testing/helpers/react-wrapper.d.ts +66 -0
- package/dist/testing/helpers/react-wrapper.js +64 -0
- package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
- package/dist/testing/helpers/sync-engine-harness.js +70 -0
- package/dist/testing/helpers/wait.d.ts +25 -0
- package/dist/testing/helpers/wait.js +44 -0
- package/dist/testing/index.d.ts +21 -0
- package/dist/testing/index.js +32 -0
- package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
- package/dist/testing/mocks/MockMutationExecutor.js +139 -0
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
- package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
- package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
- package/dist/testing/mocks/MockSyncContext.js +100 -0
- package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
- package/dist/testing/mocks/MockSyncStore.js +171 -0
- package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
- package/dist/testing/mocks/MockWebSocket.js +117 -0
- package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
- package/dist/transactions/OptimisticEchoTracker.js +104 -0
- package/dist/transactions/TransactionQueue.d.ts +499 -0
- package/dist/transactions/TransactionQueue.js +1895 -0
- package/dist/transactions/index.d.ts +16 -0
- package/dist/transactions/index.js +7 -0
- package/dist/transactions/mutation-error-handler.d.ts +5 -0
- package/dist/transactions/mutation-error-handler.js +39 -0
- package/dist/types/global.d.ts +107 -0
- package/dist/types/global.js +38 -0
- package/dist/types/index.d.ts +241 -0
- package/dist/types/index.js +70 -0
- package/dist/types/streams.d.ts +495 -0
- package/dist/types/streams.js +11 -0
- package/dist/utils/asyncIterator.d.ts +41 -0
- package/dist/utils/asyncIterator.js +142 -0
- package/dist/utils/duration.d.ts +28 -0
- package/dist/utils/duration.js +47 -0
- package/dist/utils/mobx-setup.d.ts +42 -0
- package/dist/utils/mobx-setup.js +381 -0
- package/docs/api-keys.md +24 -0
- package/docs/api.md +230 -0
- package/docs/audit.md +81 -0
- package/docs/capabilities.md +163 -0
- package/docs/client-behavior.md +202 -0
- package/docs/data-sources.md +214 -0
- package/docs/examples/agent-human.md +84 -0
- package/docs/examples/ai-sdk-tool.md +92 -0
- package/docs/examples/existing-python-backend.md +249 -0
- package/docs/examples/nextjs.md +88 -0
- package/docs/examples/server-agent.md +86 -0
- package/docs/guarantees.md +148 -0
- package/docs/index.md +97 -0
- package/docs/integration-guide.md +493 -0
- package/docs/interaction-model.md +140 -0
- package/docs/mcp/claude-code.md +43 -0
- package/docs/mcp/cursor.md +53 -0
- package/docs/mcp/windsurf.md +46 -0
- package/docs/mcp.md +59 -0
- package/docs/quickstart.md +152 -0
- package/docs/react.md +115 -0
- package/docs/roadmap.md +45 -0
- package/examples/README.md +54 -0
- package/examples/data-source/README.md +102 -0
- package/examples/data-source/ablo-driver.ts +89 -0
- package/examples/data-source/customer-server.ts +208 -0
- package/examples/data-source/run.ts +101 -0
- package/examples/data-source/schema.ts +25 -0
- package/examples/quickstart.ts +54 -0
- package/examples/tsconfig.json +16 -0
- package/llms.txt +143 -0
- package/package.json +147 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useCallback } from 'react';
|
|
3
|
+
import { useSyncContext } from './context.js';
|
|
4
|
+
import { useReactive } from './useReactive.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reactive sync-status hook. Bridges MobX `store.syncStatus` +
|
|
7
|
+
* `store.isReady` into React via `useReactive` — concurrent-render
|
|
8
|
+
* safe and immune to the React #185 "getSnapshot should be cached"
|
|
9
|
+
* infinite-loop class of bugs.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* function StatusPill() {
|
|
13
|
+
* const status = useSyncStatus();
|
|
14
|
+
* switch (status.name) {
|
|
15
|
+
* case 'initial':
|
|
16
|
+
* case 'connecting': return <Pill progress={status.name === 'connecting' ? status.progress : 0}>Loading…</Pill>;
|
|
17
|
+
* case 'connected': return status.hasUnsyncedChanges ? <Pill>Saving…</Pill> : null;
|
|
18
|
+
* case 'reconnecting': return <Pill title={status.reason}>Reconnecting…</Pill>;
|
|
19
|
+
* case 'disconnected': return <Pill title={status.reason}>Offline</Pill>;
|
|
20
|
+
* case 'needs-auth': return null;
|
|
21
|
+
* }
|
|
22
|
+
* }
|
|
23
|
+
*/
|
|
24
|
+
export function useSyncStatus() {
|
|
25
|
+
const { store } = useSyncContext();
|
|
26
|
+
// `useReactive` tracks the MobX observables read inside deriveStatus
|
|
27
|
+
// (syncStatus.state/progress/pendingChanges/isSessionError/error +
|
|
28
|
+
// the computed isReady), caches the result by shape, and only
|
|
29
|
+
// notifies React when a variant transition actually occurs.
|
|
30
|
+
//
|
|
31
|
+
// Stabilize the closure on `store` identity so useReactive's
|
|
32
|
+
// swap-detection doesn't see a "swap" every render and unnecessarily
|
|
33
|
+
// re-subscribe its MobX reaction.
|
|
34
|
+
const compute = useCallback(() => deriveStatus(store), [store]);
|
|
35
|
+
return useReactive(compute, sameSnapshot);
|
|
36
|
+
}
|
|
37
|
+
/** Map the current store state into the discriminated union. */
|
|
38
|
+
function deriveStatus(store) {
|
|
39
|
+
const { state, progress, pendingChanges, isSessionError, error } = store.syncStatus;
|
|
40
|
+
if (isSessionError) {
|
|
41
|
+
return { name: 'needs-auth' };
|
|
42
|
+
}
|
|
43
|
+
if (state === 'reconnecting') {
|
|
44
|
+
return { name: 'reconnecting', reason: error?.message };
|
|
45
|
+
}
|
|
46
|
+
if (state === 'offline') {
|
|
47
|
+
return { name: 'disconnected', reason: 'offline' };
|
|
48
|
+
}
|
|
49
|
+
if (state === 'error') {
|
|
50
|
+
return { name: 'disconnected', reason: error?.message };
|
|
51
|
+
}
|
|
52
|
+
if (store.isReady) {
|
|
53
|
+
return { name: 'connected', hasUnsyncedChanges: pendingChanges > 0 };
|
|
54
|
+
}
|
|
55
|
+
// state is 'idle' or 'syncing' and not yet ready — bootstrap underway.
|
|
56
|
+
if (state === 'idle' || state === 'syncing') {
|
|
57
|
+
return { name: 'connecting', progress };
|
|
58
|
+
}
|
|
59
|
+
return { name: 'initial' };
|
|
60
|
+
}
|
|
61
|
+
function sameSnapshot(a, b) {
|
|
62
|
+
if (a.name !== b.name)
|
|
63
|
+
return false;
|
|
64
|
+
switch (a.name) {
|
|
65
|
+
case 'initial':
|
|
66
|
+
case 'needs-auth':
|
|
67
|
+
return true;
|
|
68
|
+
case 'connecting':
|
|
69
|
+
return a.progress === b.progress;
|
|
70
|
+
case 'connected':
|
|
71
|
+
return a.hasUnsyncedChanges === b.hasUnsyncedChanges;
|
|
72
|
+
case 'reconnecting':
|
|
73
|
+
case 'disconnected':
|
|
74
|
+
return a.reason === b.reason;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Schema } from '../schema/schema.js';
|
|
2
|
+
import type { UndoScope, UndoScopeOptions } from '../mutators/UndoManager.js';
|
|
3
|
+
import type { ResolveSchema } from '../types/global.js';
|
|
4
|
+
/**
|
|
5
|
+
* useUndoScope — per-surface undo/redo for mutator invocations.
|
|
6
|
+
*
|
|
7
|
+
* Zero deliberately does NOT ship a built-in undo API; consumers build one
|
|
8
|
+
* on top of mutation tracking. This is ours.
|
|
9
|
+
*
|
|
10
|
+
* Each named scope owns an independent undo/redo stack. Wire the returned
|
|
11
|
+
* `scope` into `useMutators(schema, mutators, { undoScope: scope })` and the
|
|
12
|
+
* invocations become recorded. `undo()` / `redo()` replay the inverses /
|
|
13
|
+
* forwards as new transactions that do NOT re-record (the manager pushes
|
|
14
|
+
* them between the two stacks explicitly).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* const { undo, redo, canUndo, canRedo, scope } = useUndoScope('deck-editor');
|
|
18
|
+
* const mutate = useMutators(schema, deckMutators, { undoScope: scope });
|
|
19
|
+
*
|
|
20
|
+
* // Cmd+Z handler
|
|
21
|
+
* useHotkey('mod+z', () => { if (canUndo) void undo(); });
|
|
22
|
+
*/
|
|
23
|
+
export interface UseUndoScopeResult<S extends Schema> {
|
|
24
|
+
/** Pass to `useMutators(..., { undoScope })` to enable recording. */
|
|
25
|
+
scope: UndoScope<S>;
|
|
26
|
+
undo: () => Promise<void>;
|
|
27
|
+
redo: () => Promise<void>;
|
|
28
|
+
canUndo: boolean;
|
|
29
|
+
canRedo: boolean;
|
|
30
|
+
/** Drop history. Use after sync errors / auth context changes. */
|
|
31
|
+
clear: () => void;
|
|
32
|
+
}
|
|
33
|
+
/** Per-surface undo/redo (explicit schema arg). */
|
|
34
|
+
export declare function useUndoScope<S extends Schema>(schema: S, name: string, options?: UndoScopeOptions): UseUndoScopeResult<S>;
|
|
35
|
+
/** Per-surface undo/redo via the `AbloSync` global augmentation. */
|
|
36
|
+
export declare function useUndoScope(name: string, options?: UndoScopeOptions): UseUndoScopeResult<ResolveSchema extends Schema ? ResolveSchema : Schema>;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useEffect, useMemo, useState } from 'react';
|
|
3
|
+
import { UndoManager } from '../mutators/UndoManager.js';
|
|
4
|
+
import { useSyncContext } from './context.js';
|
|
5
|
+
import { AbloValidationError } from '../errors.js';
|
|
6
|
+
// Module-level weak registry: `SyncStoreContract` → `UndoManager`.
|
|
7
|
+
// A single app wiring through one SyncProvider shares one manager across
|
|
8
|
+
// every useUndoScope call, so scopes with the same name are identity-equal.
|
|
9
|
+
// Storage erases the generic — `UndoManager<S>` is invariant in S, so
|
|
10
|
+
// the WeakMap can't hold the precise type alongside the typed factory
|
|
11
|
+
// signature. We re-assert at retrieval; the contract is "scope keys
|
|
12
|
+
// are unique per-app and the schema is the same across every call
|
|
13
|
+
// for that key."
|
|
14
|
+
const managers = new WeakMap();
|
|
15
|
+
function getManager(key, factory) {
|
|
16
|
+
let m = managers.get(key);
|
|
17
|
+
if (!m) {
|
|
18
|
+
// Generic-erasure boundary at storage. Concentrating the cast
|
|
19
|
+
// here so the retrieval path doesn't have to repeat it.
|
|
20
|
+
const created = factory();
|
|
21
|
+
managers.set(key, created);
|
|
22
|
+
m = created;
|
|
23
|
+
}
|
|
24
|
+
// Single typed cast at retrieval — `as UndoManager<S>` would be
|
|
25
|
+
// rejected (TS sees `UndoManager<Schema>` and `UndoManager<S>` as
|
|
26
|
+
// unrelated), but we're at the runtime/static schema-identity
|
|
27
|
+
// boundary the contract above pins.
|
|
28
|
+
return m;
|
|
29
|
+
}
|
|
30
|
+
export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
|
|
31
|
+
const { store, organizationId, schema: ctxSchema } = useSyncContext();
|
|
32
|
+
const isExplicit = typeof schemaOrName !== 'string';
|
|
33
|
+
const schema = isExplicit ? schemaOrName : ctxSchema;
|
|
34
|
+
const name = isExplicit ? nameOrOptions : schemaOrName;
|
|
35
|
+
const options = (isExplicit ? maybeOptions : nameOrOptions);
|
|
36
|
+
if (!schema) {
|
|
37
|
+
throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg ' +
|
|
38
|
+
'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'undo_scope_schema_missing' });
|
|
39
|
+
}
|
|
40
|
+
const scope = useMemo(() => {
|
|
41
|
+
// Store is the identity for the manager — one per SyncProvider.
|
|
42
|
+
const manager = getManager(store, () => new UndoManager(schema, store, organizationId));
|
|
43
|
+
return manager.getScope(name, options);
|
|
44
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
45
|
+
}, [store, organizationId, name]);
|
|
46
|
+
// Local tick forces re-render after undo/redo/clear so canUndo/canRedo
|
|
47
|
+
// reflect the new stack sizes. The scope itself doesn't emit React-
|
|
48
|
+
// friendly notifications; callers that want cross-component reactivity
|
|
49
|
+
// can wire a mobx observable or custom event bus on top.
|
|
50
|
+
const [, setTick] = useState(0);
|
|
51
|
+
// Reset tick when scope identity changes (new store / new orgId).
|
|
52
|
+
useEffect(() => {
|
|
53
|
+
setTick(0);
|
|
54
|
+
}, [scope]);
|
|
55
|
+
const size = scope.size();
|
|
56
|
+
return {
|
|
57
|
+
scope,
|
|
58
|
+
undo: async () => {
|
|
59
|
+
await scope.undo();
|
|
60
|
+
setTick((t) => t + 1);
|
|
61
|
+
},
|
|
62
|
+
redo: async () => {
|
|
63
|
+
await scope.redo();
|
|
64
|
+
setTick((t) => t + 1);
|
|
65
|
+
},
|
|
66
|
+
canUndo: size.undo > 0,
|
|
67
|
+
canRedo: size.redo > 0,
|
|
68
|
+
clear: () => {
|
|
69
|
+
scope.clear();
|
|
70
|
+
setTick((t) => t + 1);
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal compatibility entrypoint for the schema-powered realtime client.
|
|
3
|
+
*
|
|
4
|
+
* Use this build for applications that need typed model proxies,
|
|
5
|
+
* subscriptions, presence, offline queueing, and a long-lived WebSocket.
|
|
6
|
+
*/
|
|
7
|
+
export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
|
|
8
|
+
export type { AbloOptions, InternalAbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelOperations, } from '../client/Ablo.js';
|
|
9
|
+
import { Ablo } from '../client/Ablo.js';
|
|
10
|
+
export default Ablo;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal compatibility entrypoint for the schema-powered realtime client.
|
|
3
|
+
*
|
|
4
|
+
* Use this build for applications that need typed model proxies,
|
|
5
|
+
* subscriptions, presence, offline queueing, and a long-lived WebSocket.
|
|
6
|
+
*/
|
|
7
|
+
export { Ablo, computeFKDepthPriority } from '../client/Ablo.js';
|
|
8
|
+
import { Ablo } from '../client/Ablo.js';
|
|
9
|
+
export default Ablo;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Field Helpers
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around Zod that add sync-engine metadata (type tag, indexed).
|
|
5
|
+
* Metadata is stored in `z.describe()` as a JSON-encoded string so it
|
|
6
|
+
* survives `.optional()`, `.nullable()`, and `.default()` chain calls.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { field } from '@ablo/sync-engine/schema';
|
|
10
|
+
*
|
|
11
|
+
* const tasks = model({
|
|
12
|
+
* title: field.string(),
|
|
13
|
+
* projectId: field.string().indexed(), // fluent chain
|
|
14
|
+
* priority: field.number().optional(),
|
|
15
|
+
* status: field.enum(['todo', 'doing', 'done']),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* Or use Zod directly (no metadata, but still works):
|
|
19
|
+
* import { z } from 'zod';
|
|
20
|
+
*
|
|
21
|
+
* const tasks = model({
|
|
22
|
+
* title: z.string(),
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
/** Runtime metadata for a schema field, readable via `ModelDef.fields`. */
|
|
27
|
+
export interface FieldMeta {
|
|
28
|
+
/** Sync-engine type tag (maps to storage/serialization hints). */
|
|
29
|
+
type: 'string' | 'number' | 'boolean' | 'date' | 'enum' | 'json';
|
|
30
|
+
/** Whether the field was marked optional via `.optional()` or `.nullable()`. */
|
|
31
|
+
isOptional: boolean;
|
|
32
|
+
/** Whether the field was marked indexed via `.indexed()`. */
|
|
33
|
+
isIndexed: boolean;
|
|
34
|
+
/** For enums: the allowed values. */
|
|
35
|
+
enumValues?: readonly string[];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Extract FieldMeta from a Zod schema. Returns null if no sync-engine
|
|
39
|
+
* metadata is attached (e.g., raw `z.string()` usage).
|
|
40
|
+
*
|
|
41
|
+
* Walks through `.optional()` and `.nullable()` wrappers to find the
|
|
42
|
+
* underlying description.
|
|
43
|
+
*/
|
|
44
|
+
export declare function getFieldMeta(schema: z.ZodType): FieldMeta | null;
|
|
45
|
+
/**
|
|
46
|
+
* Fallback: infer FieldMeta directly from a raw Zod schema when no
|
|
47
|
+
* `field.*()` metadata was attached.
|
|
48
|
+
*
|
|
49
|
+
* Walks through `.optional()` / `.nullable()` / `.default()` wrappers
|
|
50
|
+
* to find the inner primitive, then maps Zod's `_def.typeName` to
|
|
51
|
+
* the sync-engine type tag. Used by `resolveFieldMeta` and by
|
|
52
|
+
* `model()` / `query()` at definition time.
|
|
53
|
+
*
|
|
54
|
+
* Kept as an internal helper rather than exported directly — the
|
|
55
|
+
* public API is `resolveFieldMeta`, which combines this fallback
|
|
56
|
+
* with the `getFieldMeta` fast path.
|
|
57
|
+
*/
|
|
58
|
+
export declare function inferFieldMetaFromZod(schema: z.ZodType): FieldMeta;
|
|
59
|
+
/**
|
|
60
|
+
* Resolve FieldMeta for any Zod schema — whether it was built with
|
|
61
|
+
* `field.*()` (which attaches sync-engine metadata) or with raw Zod
|
|
62
|
+
* (which requires fallback inference from `_def.typeName`).
|
|
63
|
+
*
|
|
64
|
+
* This is the single public entry point for "given a Zod field, tell
|
|
65
|
+
* me its sync-engine type tag and optionality." Both `model()` and
|
|
66
|
+
* `query()` use it to populate their `fields` / `inputFields` maps at
|
|
67
|
+
* definition time, and the schema serializer reads those maps at
|
|
68
|
+
* serialization time.
|
|
69
|
+
*
|
|
70
|
+
* Contract: always returns a value. Never returns null. Unknown Zod
|
|
71
|
+
* types fall through to `'string'` — this is intentional and matches
|
|
72
|
+
* the existing behavior that was previously duplicated in
|
|
73
|
+
* `model.ts:inferMetaFromZod`.
|
|
74
|
+
*/
|
|
75
|
+
export declare function resolveFieldMeta(schema: z.ZodType): FieldMeta;
|
|
76
|
+
export declare const field: {
|
|
77
|
+
/** String field */
|
|
78
|
+
readonly string: () => z.ZodString & {
|
|
79
|
+
indexed(): z.ZodString;
|
|
80
|
+
};
|
|
81
|
+
/** Number field */
|
|
82
|
+
readonly number: () => z.ZodNumber & {
|
|
83
|
+
indexed(): z.ZodNumber;
|
|
84
|
+
};
|
|
85
|
+
/** Boolean field */
|
|
86
|
+
readonly boolean: () => z.ZodBoolean & {
|
|
87
|
+
indexed(): z.ZodBoolean;
|
|
88
|
+
};
|
|
89
|
+
/** Date field */
|
|
90
|
+
readonly date: () => z.ZodDate & {
|
|
91
|
+
indexed(): z.ZodDate;
|
|
92
|
+
};
|
|
93
|
+
/** Enum field with constrained string values */
|
|
94
|
+
readonly enum: <const T extends readonly [string, ...string[]]>(values: T) => z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never> & {
|
|
95
|
+
indexed(): z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never>;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* JSON field. Three call shapes:
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* field.json() // unknown JSON blob
|
|
102
|
+
* field.json(z.array(z.string())) // typed JSON with Zod schema
|
|
103
|
+
* field.json({ icon: z.string().default('default') }) // typed sub-properties with defaults
|
|
104
|
+
* ```
|
|
105
|
+
*
|
|
106
|
+
* The third form is the key DX feature for metadata fields. It wraps the
|
|
107
|
+
* plain object in `z.object()` automatically, and the model runtime generates
|
|
108
|
+
* a `${field}Json` getter that parses the JSON string on read, applies Zod
|
|
109
|
+
* defaults, and caches the result.
|
|
110
|
+
*
|
|
111
|
+
* Example:
|
|
112
|
+
* ```ts
|
|
113
|
+
* const slideDecks = model({
|
|
114
|
+
* metadata: field.json({
|
|
115
|
+
* icon: z.string().default('presentation'),
|
|
116
|
+
* color: z.string().default('#F59E0B'),
|
|
117
|
+
* summary: z.string().optional(),
|
|
118
|
+
* }),
|
|
119
|
+
* });
|
|
120
|
+
*
|
|
121
|
+
* // At runtime:
|
|
122
|
+
* deck.metadata // raw JSON string (unchanged)
|
|
123
|
+
* deck.metadataJson // { icon: 'presentation', color: '#F59E0B', summary: undefined }
|
|
124
|
+
* deck.metadataJson.icon // 'presentation' (typed, with default)
|
|
125
|
+
* ```
|
|
126
|
+
*/
|
|
127
|
+
readonly json: <T extends z.ZodType = z.ZodUnknown>(schemaOrShape?: T | z.ZodRawShape) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>> & {
|
|
128
|
+
indexed(): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
129
|
+
};
|
|
130
|
+
/** Indexed string field (shorthand for `field.string().indexed()`). */
|
|
131
|
+
readonly id: () => z.ZodString;
|
|
132
|
+
};
|
|
133
|
+
/** Mark a Zod schema as indexed for fast lookups (function form). */
|
|
134
|
+
export declare function indexed<T extends z.ZodType>(schema: T): T;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Field Helpers
|
|
3
|
+
*
|
|
4
|
+
* Thin wrappers around Zod that add sync-engine metadata (type tag, indexed).
|
|
5
|
+
* Metadata is stored in `z.describe()` as a JSON-encoded string so it
|
|
6
|
+
* survives `.optional()`, `.nullable()`, and `.default()` chain calls.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* import { field } from '@ablo/sync-engine/schema';
|
|
10
|
+
*
|
|
11
|
+
* const tasks = model({
|
|
12
|
+
* title: field.string(),
|
|
13
|
+
* projectId: field.string().indexed(), // fluent chain
|
|
14
|
+
* priority: field.number().optional(),
|
|
15
|
+
* status: field.enum(['todo', 'doing', 'done']),
|
|
16
|
+
* });
|
|
17
|
+
*
|
|
18
|
+
* Or use Zod directly (no metadata, but still works):
|
|
19
|
+
* import { z } from 'zod';
|
|
20
|
+
*
|
|
21
|
+
* const tasks = model({
|
|
22
|
+
* title: z.string(),
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
// ── Helpers ───────────────────────────────────────────────────────────────
|
|
27
|
+
/** Distinguish a Zod schema from a plain object shape (ZodRawShape). */
|
|
28
|
+
function isZodSchema(value) {
|
|
29
|
+
return (typeof value === 'object' &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
'_def' in value &&
|
|
32
|
+
typeof value._def === 'object');
|
|
33
|
+
}
|
|
34
|
+
// ── Metadata encoding ─────────────────────────────────────────────────────
|
|
35
|
+
//
|
|
36
|
+
// We stash metadata in `.describe('__sync:{json}')` so it rides along with
|
|
37
|
+
// the Zod schema through `.optional()`, `.nullable()`, etc. At schema-build
|
|
38
|
+
// time we parse it back out into structured FieldMeta.
|
|
39
|
+
const META_PREFIX = '__sync:';
|
|
40
|
+
function encodeMeta(meta) {
|
|
41
|
+
return META_PREFIX + JSON.stringify(meta);
|
|
42
|
+
}
|
|
43
|
+
function decodeMeta(description) {
|
|
44
|
+
if (!description || !description.startsWith(META_PREFIX))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(description.slice(META_PREFIX.length));
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Extract FieldMeta from a Zod schema. Returns null if no sync-engine
|
|
55
|
+
* metadata is attached (e.g., raw `z.string()` usage).
|
|
56
|
+
*
|
|
57
|
+
* Walks through `.optional()` and `.nullable()` wrappers to find the
|
|
58
|
+
* underlying description.
|
|
59
|
+
*/
|
|
60
|
+
export function getFieldMeta(schema) {
|
|
61
|
+
let current = schema;
|
|
62
|
+
let isOptional = false;
|
|
63
|
+
// Unwrap optional / nullable / default to reach the inner type
|
|
64
|
+
// (these are the wrappers that preserve .describe() but may hide it).
|
|
65
|
+
// `instanceof` keeps the narrowing typed; no `_def` digging.
|
|
66
|
+
const MAX_UNWRAP = 5;
|
|
67
|
+
for (let i = 0; i < MAX_UNWRAP; i++) {
|
|
68
|
+
if (current instanceof z.ZodOptional) {
|
|
69
|
+
isOptional = true;
|
|
70
|
+
current = current.unwrap();
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (current instanceof z.ZodNullable) {
|
|
74
|
+
isOptional = true;
|
|
75
|
+
current = current.unwrap();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (current instanceof z.ZodDefault) {
|
|
79
|
+
// .removeDefault() — v4 deprecates in favor of .unwrap() but
|
|
80
|
+
// the installed @types only expose removeDefault on ZodDefault.
|
|
81
|
+
current = current.unwrap();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
break;
|
|
85
|
+
}
|
|
86
|
+
// The description lives on the innermost schema we reached.
|
|
87
|
+
const description = current.description ?? schema.description;
|
|
88
|
+
const base = decodeMeta(description);
|
|
89
|
+
if (!base)
|
|
90
|
+
return null;
|
|
91
|
+
return { ...base, isOptional };
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Fallback: infer FieldMeta directly from a raw Zod schema when no
|
|
95
|
+
* `field.*()` metadata was attached.
|
|
96
|
+
*
|
|
97
|
+
* Walks through `.optional()` / `.nullable()` / `.default()` wrappers
|
|
98
|
+
* to find the inner primitive, then maps Zod's `_def.typeName` to
|
|
99
|
+
* the sync-engine type tag. Used by `resolveFieldMeta` and by
|
|
100
|
+
* `model()` / `query()` at definition time.
|
|
101
|
+
*
|
|
102
|
+
* Kept as an internal helper rather than exported directly — the
|
|
103
|
+
* public API is `resolveFieldMeta`, which combines this fallback
|
|
104
|
+
* with the `getFieldMeta` fast path.
|
|
105
|
+
*/
|
|
106
|
+
export function inferFieldMetaFromZod(schema) {
|
|
107
|
+
let current = schema;
|
|
108
|
+
let isOptional = false;
|
|
109
|
+
const MAX_UNWRAP = 5;
|
|
110
|
+
for (let i = 0; i < MAX_UNWRAP; i++) {
|
|
111
|
+
if (current instanceof z.ZodOptional || current instanceof z.ZodNullable) {
|
|
112
|
+
isOptional = true;
|
|
113
|
+
current = current.unwrap();
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (current instanceof z.ZodDefault) {
|
|
117
|
+
current = current.unwrap();
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
let type = 'string';
|
|
123
|
+
let enumValues;
|
|
124
|
+
if (current instanceof z.ZodString) {
|
|
125
|
+
type = 'string';
|
|
126
|
+
}
|
|
127
|
+
else if (current instanceof z.ZodNumber) {
|
|
128
|
+
type = 'number';
|
|
129
|
+
}
|
|
130
|
+
else if (current instanceof z.ZodBoolean) {
|
|
131
|
+
type = 'boolean';
|
|
132
|
+
}
|
|
133
|
+
else if (current instanceof z.ZodDate) {
|
|
134
|
+
type = 'date';
|
|
135
|
+
}
|
|
136
|
+
else if (current instanceof z.ZodEnum) {
|
|
137
|
+
type = 'enum';
|
|
138
|
+
// ZodEnum.options is the public v4 accessor for enum values.
|
|
139
|
+
enumValues = current.options;
|
|
140
|
+
}
|
|
141
|
+
else if (current instanceof z.ZodObject ||
|
|
142
|
+
current instanceof z.ZodArray ||
|
|
143
|
+
current instanceof z.ZodRecord ||
|
|
144
|
+
current instanceof z.ZodUnion ||
|
|
145
|
+
current instanceof z.ZodUnknown) {
|
|
146
|
+
type = 'json';
|
|
147
|
+
}
|
|
148
|
+
return { type, isOptional, isIndexed: false, enumValues };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Resolve FieldMeta for any Zod schema — whether it was built with
|
|
152
|
+
* `field.*()` (which attaches sync-engine metadata) or with raw Zod
|
|
153
|
+
* (which requires fallback inference from `_def.typeName`).
|
|
154
|
+
*
|
|
155
|
+
* This is the single public entry point for "given a Zod field, tell
|
|
156
|
+
* me its sync-engine type tag and optionality." Both `model()` and
|
|
157
|
+
* `query()` use it to populate their `fields` / `inputFields` maps at
|
|
158
|
+
* definition time, and the schema serializer reads those maps at
|
|
159
|
+
* serialization time.
|
|
160
|
+
*
|
|
161
|
+
* Contract: always returns a value. Never returns null. Unknown Zod
|
|
162
|
+
* types fall through to `'string'` — this is intentional and matches
|
|
163
|
+
* the existing behavior that was previously duplicated in
|
|
164
|
+
* `model.ts:inferMetaFromZod`.
|
|
165
|
+
*/
|
|
166
|
+
export function resolveFieldMeta(schema) {
|
|
167
|
+
const attached = getFieldMeta(schema);
|
|
168
|
+
if (attached)
|
|
169
|
+
return attached;
|
|
170
|
+
return inferFieldMetaFromZod(schema);
|
|
171
|
+
}
|
|
172
|
+
// ── Chainable field builders ──────────────────────────────────────────────
|
|
173
|
+
//
|
|
174
|
+
// Each builder returns the underlying Zod schema (so `z.object(shape)` still
|
|
175
|
+
// works) with `.indexed()` added as a chainable method. `.optional()` and
|
|
176
|
+
// `.nullable()` still come from Zod itself and preserve the description.
|
|
177
|
+
/** Add `.indexed()` to a Zod schema without disturbing its type. */
|
|
178
|
+
function withIndexed(schema, baseMeta) {
|
|
179
|
+
const described = schema.describe(encodeMeta({ ...baseMeta, isIndexed: false }));
|
|
180
|
+
described.indexed = () => {
|
|
181
|
+
return schema.describe(encodeMeta({ ...baseMeta, isIndexed: true }));
|
|
182
|
+
};
|
|
183
|
+
return described;
|
|
184
|
+
}
|
|
185
|
+
export const field = {
|
|
186
|
+
/** String field */
|
|
187
|
+
string() {
|
|
188
|
+
return withIndexed(z.string(), { type: 'string' });
|
|
189
|
+
},
|
|
190
|
+
/** Number field */
|
|
191
|
+
number() {
|
|
192
|
+
return withIndexed(z.number(), { type: 'number' });
|
|
193
|
+
},
|
|
194
|
+
/** Boolean field */
|
|
195
|
+
boolean() {
|
|
196
|
+
return withIndexed(z.boolean(), { type: 'boolean' });
|
|
197
|
+
},
|
|
198
|
+
/** Date field */
|
|
199
|
+
date() {
|
|
200
|
+
return withIndexed(z.date(), { type: 'date' });
|
|
201
|
+
},
|
|
202
|
+
/** Enum field with constrained string values */
|
|
203
|
+
enum(values) {
|
|
204
|
+
return withIndexed(z.enum(values), { type: 'enum', enumValues: values });
|
|
205
|
+
},
|
|
206
|
+
/**
|
|
207
|
+
* JSON field. Three call shapes:
|
|
208
|
+
*
|
|
209
|
+
* ```ts
|
|
210
|
+
* field.json() // unknown JSON blob
|
|
211
|
+
* field.json(z.array(z.string())) // typed JSON with Zod schema
|
|
212
|
+
* field.json({ icon: z.string().default('default') }) // typed sub-properties with defaults
|
|
213
|
+
* ```
|
|
214
|
+
*
|
|
215
|
+
* The third form is the key DX feature for metadata fields. It wraps the
|
|
216
|
+
* plain object in `z.object()` automatically, and the model runtime generates
|
|
217
|
+
* a `${field}Json` getter that parses the JSON string on read, applies Zod
|
|
218
|
+
* defaults, and caches the result.
|
|
219
|
+
*
|
|
220
|
+
* Example:
|
|
221
|
+
* ```ts
|
|
222
|
+
* const slideDecks = model({
|
|
223
|
+
* metadata: field.json({
|
|
224
|
+
* icon: z.string().default('presentation'),
|
|
225
|
+
* color: z.string().default('#F59E0B'),
|
|
226
|
+
* summary: z.string().optional(),
|
|
227
|
+
* }),
|
|
228
|
+
* });
|
|
229
|
+
*
|
|
230
|
+
* // At runtime:
|
|
231
|
+
* deck.metadata // raw JSON string (unchanged)
|
|
232
|
+
* deck.metadataJson // { icon: 'presentation', color: '#F59E0B', summary: undefined }
|
|
233
|
+
* deck.metadataJson.icon // 'presentation' (typed, with default)
|
|
234
|
+
* ```
|
|
235
|
+
*/
|
|
236
|
+
json(schemaOrShape) {
|
|
237
|
+
let inner;
|
|
238
|
+
if (!schemaOrShape) {
|
|
239
|
+
inner = z.unknown();
|
|
240
|
+
}
|
|
241
|
+
else if (isZodSchema(schemaOrShape)) {
|
|
242
|
+
inner = schemaOrShape;
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
// Plain object shape → wrap in z.object() for the sub-property pattern
|
|
246
|
+
inner = z.object(schemaOrShape);
|
|
247
|
+
}
|
|
248
|
+
return withIndexed(inner, { type: 'json' });
|
|
249
|
+
},
|
|
250
|
+
/** Indexed string field (shorthand for `field.string().indexed()`). */
|
|
251
|
+
id() {
|
|
252
|
+
return field.string().indexed();
|
|
253
|
+
},
|
|
254
|
+
};
|
|
255
|
+
// ── Legacy function form (kept for backward compat) ──────────────────────
|
|
256
|
+
/** Mark a Zod schema as indexed for fast lookups (function form). */
|
|
257
|
+
export function indexed(schema) {
|
|
258
|
+
// Try to preserve existing metadata type tag if present.
|
|
259
|
+
const meta = decodeMeta(schema.description);
|
|
260
|
+
const newMeta = meta
|
|
261
|
+
? { ...meta, isIndexed: true }
|
|
262
|
+
: { type: 'string', isIndexed: true };
|
|
263
|
+
return schema.describe(encodeMeta(newMeta));
|
|
264
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ablo/sync-engine/schema — Schema Definition DSL
|
|
3
|
+
*
|
|
4
|
+
* Define your data models with Zod. Types are inferred automatically.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { z } from 'zod';
|
|
8
|
+
* import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
|
|
9
|
+
*
|
|
10
|
+
* export const schema = defineSchema({
|
|
11
|
+
* tasks: model({
|
|
12
|
+
* title: z.string(),
|
|
13
|
+
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
14
|
+
* projectId: z.string().optional(),
|
|
15
|
+
* }, {
|
|
16
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
17
|
+
* }),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* type Task = InferModel<typeof schema, 'tasks'>;
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export { z } from 'zod';
|
|
24
|
+
export { field, indexed, getFieldMeta, type FieldMeta } from './field.js';
|
|
25
|
+
export { relation, type RelationDef, type RelationType } from './relation.js';
|
|
26
|
+
export { model, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, } from './model.js';
|
|
27
|
+
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
28
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, type IdentityRole, type IdentityContext, } from './schema.js';
|
|
29
|
+
export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ablo/sync-engine/schema — Schema Definition DSL
|
|
3
|
+
*
|
|
4
|
+
* Define your data models with Zod. Types are inferred automatically.
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* import { z } from 'zod';
|
|
8
|
+
* import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
|
|
9
|
+
*
|
|
10
|
+
* export const schema = defineSchema({
|
|
11
|
+
* tasks: model({
|
|
12
|
+
* title: z.string(),
|
|
13
|
+
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
14
|
+
* projectId: z.string().optional(),
|
|
15
|
+
* }, {
|
|
16
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
17
|
+
* }),
|
|
18
|
+
* });
|
|
19
|
+
*
|
|
20
|
+
* type Task = InferModel<typeof schema, 'tasks'>;
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
// Re-export Zod for convenience (consumers can also import directly)
|
|
24
|
+
export { z } from 'zod';
|
|
25
|
+
// Field helpers (optional convenience wrappers around Zod)
|
|
26
|
+
export { field, indexed, getFieldMeta } from './field.js';
|
|
27
|
+
// Relation builders
|
|
28
|
+
export { relation } from './relation.js';
|
|
29
|
+
// Model builder
|
|
30
|
+
export { model, } from './model.js';
|
|
31
|
+
// Intent-first shorthand: `mutable.lazy({...})` and friends. Read the
|
|
32
|
+
// safety posture and load shape off the verb tokens; everything else
|
|
33
|
+
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
34
|
+
export { mutable, readOnly } from './sugar.js';
|
|
35
|
+
// Schema definition + type inference
|
|
36
|
+
export { defineSchema, composeIdentitySyncGroups, } from './schema.js';
|
|
37
|
+
// Query definition DSL + type inference
|
|
38
|
+
export { query, defineQueries, } from './queries.js';
|