@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,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Query Definitions
|
|
3
|
+
*
|
|
4
|
+
* A query is a zod input schema + a string reference to a schema model
|
|
5
|
+
* that the query returns. Types flow via `z.infer` for inputs and
|
|
6
|
+
* `InferModel` for results — the same inference path `model` and
|
|
7
|
+
* `relation` already use. No second type system.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { z } from 'zod';
|
|
11
|
+
* import {
|
|
12
|
+
* defineSchema, defineQueries, model, query, relation,
|
|
13
|
+
* } from '@ablo/sync-engine/schema';
|
|
14
|
+
*
|
|
15
|
+
* const schema = defineSchema({
|
|
16
|
+
* slideLayer: model(
|
|
17
|
+
* { slideId: z.string(), type: z.string() },
|
|
18
|
+
* { slide: relation.belongsTo('slides', 'slideId') },
|
|
19
|
+
* { load: 'lazy', typename: 'SlideLayer', persist: {} },
|
|
20
|
+
* ),
|
|
21
|
+
* });
|
|
22
|
+
*
|
|
23
|
+
* const queries = defineQueries(schema, {
|
|
24
|
+
* slideLayersByDeck: query({
|
|
25
|
+
* input: z.object({ deckId: z.string() }),
|
|
26
|
+
* returns: 'slideLayer', // ← type-checked against schema.models
|
|
27
|
+
* }),
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* type Input = InferQueryInput<typeof queries.queries.slideLayersByDeck>;
|
|
31
|
+
* // ^? { deckId: string }
|
|
32
|
+
* type Result = InferQueryResult<
|
|
33
|
+
* typeof schema,
|
|
34
|
+
* typeof queries.queries.slideLayersByDeck
|
|
35
|
+
* >;
|
|
36
|
+
* // ^? Array<SlideLayer>
|
|
37
|
+
*
|
|
38
|
+
* Design notes:
|
|
39
|
+
*
|
|
40
|
+
* - `query()` accepts any string for `returns`. The constraint that it
|
|
41
|
+
* must reference a real schema model is applied when the query is
|
|
42
|
+
* passed to `defineQueries(schema, ...)`. This mirrors how
|
|
43
|
+
* `relation.belongsTo('projects', 'projectId')` accepts a plain
|
|
44
|
+
* string at the factory and defers the cross-reference check to
|
|
45
|
+
* schema assembly time.
|
|
46
|
+
*
|
|
47
|
+
* - Queries do NOT carry a `name` until they pass through
|
|
48
|
+
* `defineQueries()`. The name is assigned from the record key —
|
|
49
|
+
* same pattern as `defineSchema({ tasks: model(...) })` where the
|
|
50
|
+
* model name is the record key, not a field on the model factory.
|
|
51
|
+
*
|
|
52
|
+
* - All queries return an array of a single model type. Multi-model
|
|
53
|
+
* fetches (e.g., files + folders) are expressed as multiple queries
|
|
54
|
+
* in a single batch at dispatch time, not as "bundle" shapes in the
|
|
55
|
+
* schema. This keeps each `QueryDef` pointed at exactly one model
|
|
56
|
+
* and lets the generic loader hydrate via a single
|
|
57
|
+
* `schema.models[queryDef.returns]` lookup.
|
|
58
|
+
*/
|
|
59
|
+
import { AbloValidationError } from '../errors.js';
|
|
60
|
+
// ── query() factory ───────────────────────────────────────────────────────
|
|
61
|
+
/**
|
|
62
|
+
* Define a query.
|
|
63
|
+
*
|
|
64
|
+
* `TReturns` is a `const` generic so TypeScript preserves the literal
|
|
65
|
+
* type of the `returns` value at call time (e.g., `'slideLayer'`
|
|
66
|
+
* instead of widening to `string`). This is what lets `defineQueries`
|
|
67
|
+
* type-check `returns` against the schema's model keys without
|
|
68
|
+
* requiring the consumer to write `as const` manually.
|
|
69
|
+
*
|
|
70
|
+
* ```ts
|
|
71
|
+
* const slideLayersByDeck = query({
|
|
72
|
+
* input: z.object({ deckId: z.string() }),
|
|
73
|
+
* returns: 'slideLayer',
|
|
74
|
+
* });
|
|
75
|
+
* ```
|
|
76
|
+
*/
|
|
77
|
+
export function query(spec) {
|
|
78
|
+
return {
|
|
79
|
+
input: spec.input,
|
|
80
|
+
returns: spec.returns,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// ── defineQueries() factory ───────────────────────────────────────────────
|
|
84
|
+
/**
|
|
85
|
+
* Define a typed query set against a schema.
|
|
86
|
+
*
|
|
87
|
+
* Each entry's `returns` field is constrained to the schema's model
|
|
88
|
+
* names at compile time via the `Q` generic bound to `QueryRecord<S>`.
|
|
89
|
+
* A query whose `returns` does not match a known model will fail at
|
|
90
|
+
* the `defineQueries` call site with a TypeScript error, not at
|
|
91
|
+
* runtime.
|
|
92
|
+
*
|
|
93
|
+
* The factory also performs a runtime validation of the same
|
|
94
|
+
* invariant. This catches the edge case where the schema and the
|
|
95
|
+
* queries live in separate modules that drift out of sync — for
|
|
96
|
+
* example, when a developer removes a model from the schema without
|
|
97
|
+
* updating the queries that referenced it. The runtime error points
|
|
98
|
+
* at the specific offending query and lists the available models,
|
|
99
|
+
* which is a nicer failure mode than a generic "property does not
|
|
100
|
+
* exist" error deep inside the loader.
|
|
101
|
+
*
|
|
102
|
+
* Each resolved query gets its `name` populated from the record key:
|
|
103
|
+
* `queries.slideLayersByDeck.name === 'slideLayersByDeck'`. Wire
|
|
104
|
+
* dispatch, the Go registry, and the loader orchestrator all read
|
|
105
|
+
* `queryDef.name` directly rather than re-deriving it.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* const schema = defineSchema({
|
|
109
|
+
* slideLayer: model(
|
|
110
|
+
* { slideId: z.string() },
|
|
111
|
+
* {},
|
|
112
|
+
* { load: 'lazy', typename: 'SlideLayer', persist: {} },
|
|
113
|
+
* ),
|
|
114
|
+
* });
|
|
115
|
+
*
|
|
116
|
+
* const queries = defineQueries(schema, {
|
|
117
|
+
* slideLayersByDeck: query({
|
|
118
|
+
* input: z.object({ deckId: z.string() }),
|
|
119
|
+
* returns: 'slideLayer', // ← type-checked
|
|
120
|
+
* }),
|
|
121
|
+
* });
|
|
122
|
+
* ```
|
|
123
|
+
*/
|
|
124
|
+
export function defineQueries(schema, queries) {
|
|
125
|
+
// Rebuild the queries record with `name` populated per entry. Same
|
|
126
|
+
// shallow-spread strategy `defineSchema` uses for `typename` / `persist`
|
|
127
|
+
// defaults — preserves input references, avoids mutating `readonly`
|
|
128
|
+
// fields in place, and keeps each resolved `QueryDef` immutable from
|
|
129
|
+
// construction onward.
|
|
130
|
+
const resolvedQueries = {};
|
|
131
|
+
for (const [name, def] of Object.entries(queries)) {
|
|
132
|
+
if (!(def.returns in schema.models)) {
|
|
133
|
+
throw new AbloValidationError(`defineQueries: query "${name}" declares returns: "${def.returns}", ` +
|
|
134
|
+
`which is not a model in the schema. ` +
|
|
135
|
+
`Available models: ${Object.keys(schema.models).join(', ') || '(none)'}`, { code: 'query_returns_unknown_model' });
|
|
136
|
+
}
|
|
137
|
+
resolvedQueries[name] = { ...def, name };
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
schema,
|
|
141
|
+
// Cast back to Q: the rebuild only added `name` (already optional on
|
|
142
|
+
// QueryDef) to each entry, so the shape is structurally unchanged.
|
|
143
|
+
queries: resolvedQueries,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Relation Definitions
|
|
3
|
+
*
|
|
4
|
+
* Declarative relations between models. Used for:
|
|
5
|
+
* - FK index registration (ObjectPool.registerForeignKey)
|
|
6
|
+
* - Model create priority derivation (parents before children)
|
|
7
|
+
* - Query include/join support
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { relation } from '@ablo/sync-engine/schema';
|
|
11
|
+
*
|
|
12
|
+
* const taskRelations = {
|
|
13
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
14
|
+
* assignee: relation.belongsTo('users', 'assigneeId'),
|
|
15
|
+
* comments: relation.hasMany('comments', 'taskId'),
|
|
16
|
+
* };
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Options for `relation.belongsTo(...)`. All default to `false` — behavior
|
|
20
|
+
* is opt-in per relation.
|
|
21
|
+
*
|
|
22
|
+
* When `index: true`, the sync engine registers an O(1) foreign-key index
|
|
23
|
+
* on the child model's ObjectPool entry at construction time, so that
|
|
24
|
+
* `pool.getByForeignKey(childType, foreignKey, parentId)` becomes constant-
|
|
25
|
+
* time instead of a full scan. Use on hot paths like `SlideLayer.slideId`
|
|
26
|
+
* where you frequently want "all layers for this slide."
|
|
27
|
+
*
|
|
28
|
+
* When `enrich: true`, incoming deltas for the child model have their
|
|
29
|
+
* parent reference auto-populated from the ObjectPool before the model
|
|
30
|
+
* data lands. I.e., a delta for `Task { teamId: 't1' }` picks up the
|
|
31
|
+
* `teams:t1` entity from the pool and attaches it as `data.team`, so
|
|
32
|
+
* consumers can read `task.team` directly without a second lookup.
|
|
33
|
+
* If the parent isn't in the pool yet, enrichment silently no-ops
|
|
34
|
+
* (the child data is still applied) — enrichment is best-effort.
|
|
35
|
+
*
|
|
36
|
+
* When `defer: true`, the FK-create-priority computer (Tarjan SCC in
|
|
37
|
+
* `client/createSyncEngine.ts`) ignores this edge when building the
|
|
38
|
+
* dependency graph. Use on the *soft* side of a real cycle to break it
|
|
39
|
+
* deterministically — i.e. the side where you're willing to insert the
|
|
40
|
+
* child first with the FK left null and patch it in a follow-up
|
|
41
|
+
* UPDATE. The other side of the cycle then becomes a strict topological
|
|
42
|
+
* predecessor, so the child gets a higher priority than the parent
|
|
43
|
+
* instead of being tied with it.
|
|
44
|
+
*
|
|
45
|
+
* `defer` only affects priority computation, not what the engine sends
|
|
46
|
+
* on the wire — it does NOT auto-rewrite an INSERT to (insert-null +
|
|
47
|
+
* update-later). Pair with a Postgres `DEFERRABLE INITIALLY DEFERRED`
|
|
48
|
+
* constraint when you actually want the FK check to be relaxed at the
|
|
49
|
+
* database level. Example use case:
|
|
50
|
+
*
|
|
51
|
+
* ```ts
|
|
52
|
+
* layouts: model({ deckId: z.string().nullish() }, {
|
|
53
|
+
* // The deck-owns-layout link is nullable AND the consumer always
|
|
54
|
+
* // creates the layout first; mark it `defer` so SlideDeck can
|
|
55
|
+
* // commit ahead of Layout instead of being trapped in the same
|
|
56
|
+
* // SCC priority bucket.
|
|
57
|
+
* deck: relation.belongsTo('slideDecks', 'deckId', { defer: true }),
|
|
58
|
+
* }),
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export interface BelongsToOptions {
|
|
62
|
+
readonly index?: boolean;
|
|
63
|
+
readonly enrich?: boolean;
|
|
64
|
+
readonly defer?: boolean;
|
|
65
|
+
}
|
|
66
|
+
declare const __relationType: unique symbol;
|
|
67
|
+
declare const __relationTarget: unique symbol;
|
|
68
|
+
declare const __relationField: unique symbol;
|
|
69
|
+
export type RelationType = 'belongsTo' | 'hasMany' | 'hasOne';
|
|
70
|
+
/**
|
|
71
|
+
* A relation definition with embedded type information.
|
|
72
|
+
*
|
|
73
|
+
* The 4th generic `Options` captures per-relation options at the type
|
|
74
|
+
* level (currently only `belongsTo` uses this — `hasMany`/`hasOne`
|
|
75
|
+
* default to empty). The `const Opts` modifier on the `belongsTo`
|
|
76
|
+
* factory preserves literal inference: `{ enrich: true }` narrows to
|
|
77
|
+
* `true`, not `boolean`, so future type-level features (like
|
|
78
|
+
* `InferModel` auto-adding enriched-parent properties) can read the
|
|
79
|
+
* literal value off the relation def at compile time.
|
|
80
|
+
*
|
|
81
|
+
* `options` is always present at runtime — the factory assigns an
|
|
82
|
+
* empty object when the caller omits it, which keeps
|
|
83
|
+
* `relation.options.index` / `relation.options.enrich` safe to read
|
|
84
|
+
* without a null guard downstream.
|
|
85
|
+
*/
|
|
86
|
+
export interface RelationDef<Type extends RelationType = RelationType, Target extends string = string, Field extends string = string, Options extends BelongsToOptions = BelongsToOptions> {
|
|
87
|
+
readonly [__relationType]: Type;
|
|
88
|
+
readonly [__relationTarget]: Target;
|
|
89
|
+
readonly [__relationField]: Field;
|
|
90
|
+
/** Runtime metadata */
|
|
91
|
+
readonly type: Type;
|
|
92
|
+
readonly target: Target;
|
|
93
|
+
/**
|
|
94
|
+
* The child model's JS field that holds the parent's id. Always the
|
|
95
|
+
* camelCase schema field name — used by the client ObjectPool to read
|
|
96
|
+
* `model[foreignKey]`, by `LazyReferenceCollection` for IndexedDB
|
|
97
|
+
* index keys, and by `ModelRegistry` for cascade wiring. Never used
|
|
98
|
+
* verbatim in raw SQL.
|
|
99
|
+
*/
|
|
100
|
+
readonly foreignKey: Field;
|
|
101
|
+
/**
|
|
102
|
+
* The same foreign key expressed as a database column identifier. Set
|
|
103
|
+
* by `defineSchema` when a `casing` option is configured (e.g.
|
|
104
|
+
* `'snake_case'` produces `message_id` from `messageId`). Used by
|
|
105
|
+
* server-side SQL compilers to interpolate the real column name into
|
|
106
|
+
* queries — `postgres.camel`-style data-layer transforms do NOT rewrite
|
|
107
|
+
* identifiers embedded in raw SQL, so the translation has to happen
|
|
108
|
+
* somewhere, and schema-build time is the one-place-once answer.
|
|
109
|
+
*
|
|
110
|
+
* Defaults to {@link foreignKey} when `casing` is unset (identity) —
|
|
111
|
+
* the SDK stays backward-compatible for consumers whose DB columns
|
|
112
|
+
* already match their JS field names.
|
|
113
|
+
*/
|
|
114
|
+
readonly foreignKeyColumn: string;
|
|
115
|
+
readonly options: Options;
|
|
116
|
+
/**
|
|
117
|
+
* Optional sort field for `hasMany` relations. When set, the
|
|
118
|
+
* generated relation getter sorts results by this field. Populated
|
|
119
|
+
* by `relation.hasMany(target, fk, { orderBy: 'fieldName' })`.
|
|
120
|
+
*/
|
|
121
|
+
readonly _orderBy?: string;
|
|
122
|
+
}
|
|
123
|
+
export declare const relation: {
|
|
124
|
+
/**
|
|
125
|
+
* This model belongs to another model via a foreign key.
|
|
126
|
+
* e.g., Task belongs to Project via projectId
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* // Simple reference (no options)
|
|
130
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
131
|
+
*
|
|
132
|
+
* // Register an FK index for O(1) child lookups
|
|
133
|
+
* slide: relation.belongsTo('slides', 'slideId', { index: true }),
|
|
134
|
+
*
|
|
135
|
+
* // Auto-populate the parent on delta arrival
|
|
136
|
+
* team: relation.belongsTo('teams', 'teamId', { enrich: true }),
|
|
137
|
+
*
|
|
138
|
+
* // Both
|
|
139
|
+
* parent: relation.belongsTo('threads', 'parentId', { index: true, enrich: true }),
|
|
140
|
+
*
|
|
141
|
+
* // Mark the soft side of a cycle so the priority computer breaks
|
|
142
|
+
* // the cycle deterministically instead of tying the two models.
|
|
143
|
+
* deck: relation.belongsTo('slideDecks', 'deckId', { defer: true }),
|
|
144
|
+
* ```
|
|
145
|
+
*/
|
|
146
|
+
readonly belongsTo: <Target extends string, Field extends string, const Opts extends BelongsToOptions = Record<string, never>>(target: Target, foreignKey: Field, options?: Opts) => RelationDef<"belongsTo", Target, Field, Opts>;
|
|
147
|
+
/**
|
|
148
|
+
* This model has many of another model.
|
|
149
|
+
* e.g., Project has many Tasks (via Task.projectId)
|
|
150
|
+
*
|
|
151
|
+
* At runtime, generates a getter on the parent model that returns
|
|
152
|
+
* all child models matching the FK via ObjectPool.getByForeignKey.
|
|
153
|
+
* The FK index on the child model is auto-registered.
|
|
154
|
+
*
|
|
155
|
+
* ```ts
|
|
156
|
+
* slides: relation.hasMany('slideLayers', 'slideId'),
|
|
157
|
+
* // → deck.slides returns all SlideLayer[] where slideId === deck.id
|
|
158
|
+
*
|
|
159
|
+
* slides: relation.hasMany('slideLayers', 'slideId', { orderBy: 'zIndex' }),
|
|
160
|
+
* // → deck.slides returns SlideLayer[] sorted by zIndex ascending
|
|
161
|
+
* ```
|
|
162
|
+
*/
|
|
163
|
+
readonly hasMany: <Target extends string, Field extends string>(target: Target, foreignKey: Field, options?: {
|
|
164
|
+
orderBy?: string;
|
|
165
|
+
}) => RelationDef<"hasMany", Target, Field>;
|
|
166
|
+
/**
|
|
167
|
+
* This model has one of another model.
|
|
168
|
+
* e.g., User has one Profile (via Profile.userId)
|
|
169
|
+
*/
|
|
170
|
+
readonly hasOne: <Target extends string, Field extends string>(target: Target, foreignKey: Field) => RelationDef<"hasOne", Target, Field>;
|
|
171
|
+
};
|
|
172
|
+
export {};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Relation Definitions
|
|
3
|
+
*
|
|
4
|
+
* Declarative relations between models. Used for:
|
|
5
|
+
* - FK index registration (ObjectPool.registerForeignKey)
|
|
6
|
+
* - Model create priority derivation (parents before children)
|
|
7
|
+
* - Query include/join support
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* import { relation } from '@ablo/sync-engine/schema';
|
|
11
|
+
*
|
|
12
|
+
* const taskRelations = {
|
|
13
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
14
|
+
* assignee: relation.belongsTo('users', 'assigneeId'),
|
|
15
|
+
* comments: relation.hasMany('comments', 'taskId'),
|
|
16
|
+
* };
|
|
17
|
+
*/
|
|
18
|
+
// ── Internal relation builder ─────────────────────────────────────────────
|
|
19
|
+
class RelationBuilder {
|
|
20
|
+
type;
|
|
21
|
+
target;
|
|
22
|
+
foreignKey;
|
|
23
|
+
/**
|
|
24
|
+
* Starts out identical to {@link foreignKey}. `defineSchema` overwrites
|
|
25
|
+
* this when a `casing` option is set — it's declared as a mutable
|
|
26
|
+
* (non-readonly on the implementation side) so the schema builder can
|
|
27
|
+
* resolve it once at build time without allocating a new object per
|
|
28
|
+
* relation. Consumers see it typed as `readonly` on {@link RelationDef}.
|
|
29
|
+
*/
|
|
30
|
+
foreignKeyColumn;
|
|
31
|
+
options;
|
|
32
|
+
/**
|
|
33
|
+
* Stashed by `hasMany` when the caller provides `{ orderBy }`. Read
|
|
34
|
+
* back in `createSyncEngine` to install the sort comparator on the
|
|
35
|
+
* generated relation getter. Declared on the builder so both writer
|
|
36
|
+
* and reader stay type-safe — no `as unknown as Record<...>` smuggle.
|
|
37
|
+
*/
|
|
38
|
+
_orderBy;
|
|
39
|
+
constructor(type, target, foreignKey, options) {
|
|
40
|
+
this.type = type;
|
|
41
|
+
this.target = target;
|
|
42
|
+
this.foreignKey = foreignKey;
|
|
43
|
+
this.foreignKeyColumn = foreignKey;
|
|
44
|
+
this.options = (options ?? {});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
// ── Public relation factories ─────────────────────────────────────────────
|
|
48
|
+
export const relation = {
|
|
49
|
+
/**
|
|
50
|
+
* This model belongs to another model via a foreign key.
|
|
51
|
+
* e.g., Task belongs to Project via projectId
|
|
52
|
+
*
|
|
53
|
+
* ```ts
|
|
54
|
+
* // Simple reference (no options)
|
|
55
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
56
|
+
*
|
|
57
|
+
* // Register an FK index for O(1) child lookups
|
|
58
|
+
* slide: relation.belongsTo('slides', 'slideId', { index: true }),
|
|
59
|
+
*
|
|
60
|
+
* // Auto-populate the parent on delta arrival
|
|
61
|
+
* team: relation.belongsTo('teams', 'teamId', { enrich: true }),
|
|
62
|
+
*
|
|
63
|
+
* // Both
|
|
64
|
+
* parent: relation.belongsTo('threads', 'parentId', { index: true, enrich: true }),
|
|
65
|
+
*
|
|
66
|
+
* // Mark the soft side of a cycle so the priority computer breaks
|
|
67
|
+
* // the cycle deterministically instead of tying the two models.
|
|
68
|
+
* deck: relation.belongsTo('slideDecks', 'deckId', { defer: true }),
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
belongsTo(target, foreignKey, options) {
|
|
72
|
+
return new RelationBuilder('belongsTo', target, foreignKey, options ?? {});
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* This model has many of another model.
|
|
76
|
+
* e.g., Project has many Tasks (via Task.projectId)
|
|
77
|
+
*
|
|
78
|
+
* At runtime, generates a getter on the parent model that returns
|
|
79
|
+
* all child models matching the FK via ObjectPool.getByForeignKey.
|
|
80
|
+
* The FK index on the child model is auto-registered.
|
|
81
|
+
*
|
|
82
|
+
* ```ts
|
|
83
|
+
* slides: relation.hasMany('slideLayers', 'slideId'),
|
|
84
|
+
* // → deck.slides returns all SlideLayer[] where slideId === deck.id
|
|
85
|
+
*
|
|
86
|
+
* slides: relation.hasMany('slideLayers', 'slideId', { orderBy: 'zIndex' }),
|
|
87
|
+
* // → deck.slides returns SlideLayer[] sorted by zIndex ascending
|
|
88
|
+
* ```
|
|
89
|
+
*/
|
|
90
|
+
hasMany(target, foreignKey, options) {
|
|
91
|
+
const builder = new RelationBuilder('hasMany', target, foreignKey);
|
|
92
|
+
if (options?.orderBy) {
|
|
93
|
+
builder._orderBy = options.orderBy;
|
|
94
|
+
}
|
|
95
|
+
return builder;
|
|
96
|
+
},
|
|
97
|
+
/**
|
|
98
|
+
* This model has one of another model.
|
|
99
|
+
* e.g., User has one Profile (via Profile.userId)
|
|
100
|
+
*/
|
|
101
|
+
hasOne(target, foreignKey) {
|
|
102
|
+
return new RelationBuilder('hasOne', target, foreignKey);
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Definition + Type Inference
|
|
3
|
+
*
|
|
4
|
+
* defineSchema() wraps your models. Types are inferred via Zod — no custom type system.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* import { z } from 'zod';
|
|
8
|
+
* import { defineSchema, model, relation } from '@ablo/sync-engine/schema';
|
|
9
|
+
*
|
|
10
|
+
* 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
|
+
import { z } from 'zod';
|
|
23
|
+
import type { ModelDef, RelationRecord } from './model.js';
|
|
24
|
+
import type { RelationDef } from './relation.js';
|
|
25
|
+
/** The set of built-in casing conventions supported by `defineSchema`. */
|
|
26
|
+
export type CasingConvention = 'snake_case' | 'camelCase';
|
|
27
|
+
/** Plug point for custom conventions (e.g. mixed legacy databases). */
|
|
28
|
+
export type CasingFn = (jsField: string) => string;
|
|
29
|
+
/** `defineSchema`'s casing option. Identity when unset. */
|
|
30
|
+
export type Casing = CasingConvention | CasingFn;
|
|
31
|
+
/**
|
|
32
|
+
* The identity shape passed to {@link IdentityRole.extract}. Free-form
|
|
33
|
+
* `Record<string, unknown>` because each schema chooses what fields
|
|
34
|
+
* its roles read — Ablo's roles read `organizationId`/`userId`/`teamIds`;
|
|
35
|
+
* a tenant model with `regionId`/`customerId` is equally valid. The
|
|
36
|
+
* server hands its resolved identity (whatever shape its AuthProvider
|
|
37
|
+
* returns) straight to `extract` without translation.
|
|
38
|
+
*/
|
|
39
|
+
export type IdentityContext = Record<string, unknown>;
|
|
40
|
+
/**
|
|
41
|
+
* Open registration of an identity-anchored sync-group. Each role
|
|
42
|
+
* declares (a) a free-form `kind` label (purely for diagnostics), (b)
|
|
43
|
+
* a `template` containing a single `{id}` placeholder, and (c) an
|
|
44
|
+
* `extract` function that pulls the relevant ids out of an arbitrary
|
|
45
|
+
* identity context.
|
|
46
|
+
*
|
|
47
|
+
* No closed enum. A consumer whose identity shape is
|
|
48
|
+
* `{ regionId, customerId }` registers:
|
|
49
|
+
*
|
|
50
|
+
* ```ts
|
|
51
|
+
* defineSchema({ ... }, {
|
|
52
|
+
* identityRoles: [
|
|
53
|
+
* { kind: 'region', template: 'region:{id}',
|
|
54
|
+
* extract: (i) => i.regionId ? [String(i.regionId)] : [] },
|
|
55
|
+
* { kind: 'customer', template: 'customer:{id}',
|
|
56
|
+
* extract: (i) => i.customerId ? [String(i.customerId)] : [] },
|
|
57
|
+
* ],
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* The server's `composeIdentitySyncGroups` walks every registered role
|
|
62
|
+
* and produces the union — no hardcoded `org:` / `user:` / `team:`
|
|
63
|
+
* anywhere in the engine.
|
|
64
|
+
*/
|
|
65
|
+
export interface IdentityRole {
|
|
66
|
+
/** Free-form label for diagnostics/logging. Not parsed. */
|
|
67
|
+
readonly kind: string;
|
|
68
|
+
/**
|
|
69
|
+
* Sync-group template with a single `{id}` placeholder. Substituted
|
|
70
|
+
* once per id returned by `extract`. Example: `'org:{id}'` →
|
|
71
|
+
* `'org:abc-123'`.
|
|
72
|
+
*/
|
|
73
|
+
readonly template: string;
|
|
74
|
+
/**
|
|
75
|
+
* Extract zero or more ids from the resolved identity context. Pure
|
|
76
|
+
* function — must not allocate persistent state, must be safe to
|
|
77
|
+
* call once per request. Return an empty array when the role
|
|
78
|
+
* doesn't apply to this identity.
|
|
79
|
+
*/
|
|
80
|
+
readonly extract: (identity: IdentityContext) => readonly string[];
|
|
81
|
+
}
|
|
82
|
+
/** Options for `defineSchema`. */
|
|
83
|
+
export interface DefineSchemaOptions {
|
|
84
|
+
/**
|
|
85
|
+
* How to translate camelCase JS field names into database column
|
|
86
|
+
* identifiers. Applied once, at schema build, to every relation's
|
|
87
|
+
* `foreignKey` to produce `foreignKeyColumn`. Consumers whose DB
|
|
88
|
+
* columns already match their JS field names can omit this — the
|
|
89
|
+
* default is identity (no transform).
|
|
90
|
+
*
|
|
91
|
+
* Accepts a named convention or a custom function:
|
|
92
|
+
*
|
|
93
|
+
* ```ts
|
|
94
|
+
* defineSchema({ ... }, { casing: 'snake_case' })
|
|
95
|
+
* defineSchema({ ... }, { casing: (key) => key.toUpperCase() })
|
|
96
|
+
* ```
|
|
97
|
+
*/
|
|
98
|
+
readonly casing?: Casing;
|
|
99
|
+
/**
|
|
100
|
+
* Identity-anchored sync-group roles. Server's
|
|
101
|
+
* `composeIdentitySyncGroups` iterates these to build the
|
|
102
|
+
* participant's allowed-set, replacing the prior hardcoded
|
|
103
|
+
* `org:/user:/team:` triad in `@ablo/schema`. See {@link IdentityRole}
|
|
104
|
+
* for the open-registration shape — no closed enum, consumer fully
|
|
105
|
+
* controls both the template string and the extractor function.
|
|
106
|
+
*
|
|
107
|
+
* Leave unset for schemas that don't need identity-derived scoping
|
|
108
|
+
* (e.g. fully public read models). When unset, `composeIdentitySyncGroups`
|
|
109
|
+
* returns `[]` and consumers fall back to whatever explicit
|
|
110
|
+
* `syncGroups` the AuthProvider attaches to the Identity.
|
|
111
|
+
*/
|
|
112
|
+
readonly identityRoles?: readonly IdentityRole[];
|
|
113
|
+
}
|
|
114
|
+
/** A record of model names → model definitions */
|
|
115
|
+
export type SchemaRecord = Record<string, ModelDef>;
|
|
116
|
+
/** Base fields every synced model gets automatically */
|
|
117
|
+
declare const baseFieldsSchema: z.ZodObject<{
|
|
118
|
+
id: z.ZodString;
|
|
119
|
+
createdAt: z.ZodDate;
|
|
120
|
+
updatedAt: z.ZodDate;
|
|
121
|
+
organizationId: z.ZodOptional<z.ZodString>;
|
|
122
|
+
createdBy: z.ZodOptional<z.ZodString>;
|
|
123
|
+
}, z.core.$strip>;
|
|
124
|
+
/** The base fields type — pure data columns. */
|
|
125
|
+
export type BaseModelFields = z.infer<typeof baseFieldsSchema>;
|
|
126
|
+
/**
|
|
127
|
+
* Methods every dynamic model class inherits from `Model`. Intersected
|
|
128
|
+
* into `InferModel` (the read-side type) but NOT into `InferCreate` /
|
|
129
|
+
* `UpdatePatch` — methods aren't legal create/update inputs.
|
|
130
|
+
*/
|
|
131
|
+
export interface BaseModelMethods {
|
|
132
|
+
/** Wire-format model name (e.g. `'Slide'`, `'Comment'`). */
|
|
133
|
+
getModelName(): string;
|
|
134
|
+
/** Plain-object serialization suitable for sending over the wire. */
|
|
135
|
+
toJSON(): Record<string, unknown>;
|
|
136
|
+
}
|
|
137
|
+
/** The schema object returned by defineSchema() */
|
|
138
|
+
export interface Schema<S extends SchemaRecord = SchemaRecord> {
|
|
139
|
+
/** The raw model definitions */
|
|
140
|
+
readonly models: S;
|
|
141
|
+
/** Zod schemas with base fields merged in */
|
|
142
|
+
readonly validators: {
|
|
143
|
+
readonly [K in keyof S]: S[K] extends ModelDef<infer Shape> ? z.ZodObject<Shape & typeof baseFieldsSchema.shape> : never;
|
|
144
|
+
};
|
|
145
|
+
/**
|
|
146
|
+
* Identity-anchored sync-group roles registered via
|
|
147
|
+
* `defineSchema({...}, { identityRoles })`. Empty array when unset.
|
|
148
|
+
* Server's `composeIdentitySyncGroups(identity, schema)` reads this
|
|
149
|
+
* to derive a participant's allowed sync-group set.
|
|
150
|
+
*/
|
|
151
|
+
readonly identityRoles: readonly IdentityRole[];
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Infer the full model type from a schema.
|
|
155
|
+
* Includes base fields (id, createdAt, updatedAt, etc.)
|
|
156
|
+
*
|
|
157
|
+
* ```ts
|
|
158
|
+
* type Task = InferModel<typeof schema, 'tasks'>;
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export type InferModel<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape, infer R, infer C> ? z.infer<z.ZodObject<Shape>> & BaseModelFields & BaseModelMethods & InferComputed<C> & InferRelations<S, R> : never;
|
|
162
|
+
/**
|
|
163
|
+
* Infer relation accessor types from a model's relations record.
|
|
164
|
+
*
|
|
165
|
+
* The dynamic class installs prototype getters for each declared relation in
|
|
166
|
+
* `createSyncEngine.ts` (`hasMany` → `store.getByForeignKey(...)`, `belongsTo`
|
|
167
|
+
* → pool lookup by FK). This type mirrors those installations so callers can
|
|
168
|
+
* write `slide.layers` and `slide.deck` without manual casts.
|
|
169
|
+
*
|
|
170
|
+
* - `hasMany` → `InferModel<S, Target>[]`
|
|
171
|
+
* - `belongsTo` / `hasOne` → `InferModel<S, Target> | undefined` (undefined
|
|
172
|
+
* when the FK is unset or the parent isn't in the pool yet)
|
|
173
|
+
*
|
|
174
|
+
* Kept as `readonly` because the accessors are prototype-level getters with
|
|
175
|
+
* no setter — writing to `slide.layers = [...]` would be a no-op at runtime.
|
|
176
|
+
*/
|
|
177
|
+
export type InferRelations<S extends Schema, R extends RelationRecord> = string extends keyof R ? unknown : {
|
|
178
|
+
readonly [K in keyof R]: R[K] extends RelationDef<infer Type, infer Target, infer _F, infer _O> ? Target extends keyof S['models'] ? Type extends 'hasMany' ? InferModel<S, Target>[] : Type extends 'hasOne' | 'belongsTo' ? InferModel<S, Target> | undefined : never : never : never;
|
|
179
|
+
};
|
|
180
|
+
/**
|
|
181
|
+
* Infer the return types of computed getters.
|
|
182
|
+
* Maps each computed function's return type into a readonly property.
|
|
183
|
+
*
|
|
184
|
+
* ```ts
|
|
185
|
+
* // Given: computed: { displayTitle: (self) => self.title || 'Untitled' }
|
|
186
|
+
* // Infers: { readonly displayTitle: string }
|
|
187
|
+
* ```
|
|
188
|
+
*/
|
|
189
|
+
export type InferComputed<C> = string extends keyof C ? unknown : {
|
|
190
|
+
readonly [K in keyof C]: C[K] extends (...args: any[]) => infer R ? R : never;
|
|
191
|
+
};
|
|
192
|
+
/**
|
|
193
|
+
* Infer the create input type. Only schema-defined fields are accepted —
|
|
194
|
+
* base fields (id, createdAt, updatedAt) are auto-generated by the SDK
|
|
195
|
+
* and cannot be passed by the consumer.
|
|
196
|
+
*
|
|
197
|
+
* The only exception is `id`: consumers can optionally provide one for
|
|
198
|
+
* client-generated IDs (useful for optimistic UI that needs to reference
|
|
199
|
+
* the entity before the server confirms).
|
|
200
|
+
*
|
|
201
|
+
* ```ts
|
|
202
|
+
* type CreateTask = InferCreate<typeof schema, 'tasks'>;
|
|
203
|
+
* // { title: string; status?: 'todo' | 'doing' | 'done'; id?: string }
|
|
204
|
+
* // createdAt, updatedAt are NOT accepted — they're auto-generated
|
|
205
|
+
* ```
|
|
206
|
+
*/
|
|
207
|
+
export type InferCreate<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ? z.input<z.ZodObject<Shape>> & Partial<BaseModelFields> : never;
|
|
208
|
+
/**
|
|
209
|
+
* Extract all model names from a schema.
|
|
210
|
+
*/
|
|
211
|
+
export type InferModelNames<S extends Schema> = keyof S['models'] & string;
|
|
212
|
+
/**
|
|
213
|
+
* The value type for inserting a new row. Same shape as {@link InferCreate}:
|
|
214
|
+
* consumer-writable fields + optional `id` for client-generated IDs.
|
|
215
|
+
*
|
|
216
|
+
* Matches Zero's `InsertValue<TableSchema>` from `zql/src/mutate/crud.ts`.
|
|
217
|
+
*/
|
|
218
|
+
export type InsertValue<S extends Schema, ModelName extends keyof S['models']> = InferCreate<S, ModelName>;
|
|
219
|
+
/**
|
|
220
|
+
* The value type for upserting (insert or overwrite). Same shape as
|
|
221
|
+
* {@link InsertValue} — a full row. If a row with the same `id` exists,
|
|
222
|
+
* it gets overwritten.
|
|
223
|
+
*/
|
|
224
|
+
export type UpsertValue<S extends Schema, ModelName extends keyof S['models']> = InsertValue<S, ModelName>;
|
|
225
|
+
/**
|
|
226
|
+
* The value type for updating an existing row. `id` is required (identifies
|
|
227
|
+
* the row to update); all other fields are optional (only provided fields
|
|
228
|
+
* are changed).
|
|
229
|
+
*
|
|
230
|
+
* Matches Zero's `UpdateValue<TableSchema>` from `zql/src/mutate/crud.ts`.
|
|
231
|
+
*/
|
|
232
|
+
export type UpdateValue<S extends Schema, ModelName extends keyof S['models']> = S['models'][ModelName] extends ModelDef<infer Shape> ? {
|
|
233
|
+
id: string;
|
|
234
|
+
} & Partial<z.input<z.ZodObject<Shape>>> : never;
|
|
235
|
+
/**
|
|
236
|
+
* The value type for deleting a row. Just the primary key.
|
|
237
|
+
*
|
|
238
|
+
* Matches Zero's `DeleteID<TableSchema>` from `zql/src/mutate/crud.ts`.
|
|
239
|
+
*/
|
|
240
|
+
export type DeleteId<S extends Schema, ModelName extends keyof S['models']> = {
|
|
241
|
+
id: string;
|
|
242
|
+
};
|
|
243
|
+
export declare function defineSchema<const S extends SchemaRecord>(models: S, options?: DefineSchemaOptions): Schema<S>;
|
|
244
|
+
/**
|
|
245
|
+
* Compose the canonical sync-group set this identity is allowed to
|
|
246
|
+
* subscribe to, derived purely from the schema's registered
|
|
247
|
+
* {@link IdentityRole}s. Walks every role, calls its `extract` against
|
|
248
|
+
* the identity context, and substitutes each id into the role's
|
|
249
|
+
* `template`. Output is stable, deduped, and never includes a literal
|
|
250
|
+
* convention string from this function itself — the convention lives
|
|
251
|
+
* 100% in the consumer's schema declaration.
|
|
252
|
+
*
|
|
253
|
+
* Returns `[]` when the schema has no identity roles registered or
|
|
254
|
+
* when no role's extractor produces an id. Caller decides what to do
|
|
255
|
+
* with `[]`; the server's intersect-with-requested logic treats it as
|
|
256
|
+
* "no scope" rather than "match everything."
|
|
257
|
+
*/
|
|
258
|
+
export declare function composeIdentitySyncGroups(identity: IdentityContext, schema: Pick<Schema, 'identityRoles'>): readonly string[];
|
|
259
|
+
export {};
|