@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,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Model Definition
|
|
3
|
+
*
|
|
4
|
+
* A model is a Zod object schema + optional relations.
|
|
5
|
+
* Types are inferred directly from Zod — no custom type system.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { z } from 'zod';
|
|
9
|
+
* import { model, relation } from '@ablo/sync-engine/schema';
|
|
10
|
+
*
|
|
11
|
+
* const 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
|
+
import { z } from 'zod';
|
|
20
|
+
import type { RelationDef } from './relation.js';
|
|
21
|
+
import { type FieldMeta } from './field.js';
|
|
22
|
+
/**
|
|
23
|
+
* Controls when model data is loaded from the server.
|
|
24
|
+
*
|
|
25
|
+
* - `'instant'` — loaded during bootstrap (appears immediately on page load)
|
|
26
|
+
* - `'lazy'` — loaded on first access (e.g., when you navigate to a page that needs it)
|
|
27
|
+
* - `'manual'` — only loaded when you explicitly call sync.model.load()
|
|
28
|
+
*/
|
|
29
|
+
export type LoadStrategy = 'instant' | 'lazy' | 'manual';
|
|
30
|
+
/** A record of relation definitions */
|
|
31
|
+
export type RelationRecord = Record<string, RelationDef>;
|
|
32
|
+
/**
|
|
33
|
+
* Persistence hints for IndexedDB write-through and hydration.
|
|
34
|
+
*
|
|
35
|
+
* The sync engine's generic loader uses these to route incoming rows to
|
|
36
|
+
* the right client-side store without the consumer wiring each model by
|
|
37
|
+
* hand. `store` defaults to the model's {@link ModelDef.typename} (which
|
|
38
|
+
* itself defaults to the schema key), so consumers only set this when
|
|
39
|
+
* the IDB store name diverges from the typename.
|
|
40
|
+
*/
|
|
41
|
+
export interface PersistOptions {
|
|
42
|
+
/**
|
|
43
|
+
* Name of the IndexedDB object store that backs this model.
|
|
44
|
+
* Defaults to the model's {@link ModelDef.typename}.
|
|
45
|
+
*/
|
|
46
|
+
store?: string;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Describes how to scope a table's rows via a parent table. See
|
|
50
|
+
* {@link ModelOptions.scopedVia} for semantics and code emitted.
|
|
51
|
+
*/
|
|
52
|
+
export interface ScopedViaRef {
|
|
53
|
+
/** Column on THIS table that points at `parentKey` (e.g. `'team_id'`). */
|
|
54
|
+
localKey: string;
|
|
55
|
+
/** Parent table name (e.g. `'team'`, `'member'`). */
|
|
56
|
+
parentTable: string;
|
|
57
|
+
/** Column on the parent that `localKey` references. Default: `'id'`. */
|
|
58
|
+
parentKey?: string;
|
|
59
|
+
/** Column on the parent holding the tenant id. Default: `'organization_id'`. */
|
|
60
|
+
parentOrgColumn?: string;
|
|
61
|
+
}
|
|
62
|
+
/** Options for model() */
|
|
63
|
+
export interface ModelOptions {
|
|
64
|
+
/** When to load this model's data. Default: 'instant' */
|
|
65
|
+
load?: LoadStrategy;
|
|
66
|
+
/** Max records to bootstrap. Default: unlimited. Only applies to 'instant' strategy. */
|
|
67
|
+
bootstrapLimit?: number;
|
|
68
|
+
/** Order to sort by during bootstrap (e.g., 'created_at DESC'). */
|
|
69
|
+
bootstrapOrderBy?: string;
|
|
70
|
+
/**
|
|
71
|
+
* The GraphQL/wire `__typename` value for this model.
|
|
72
|
+
*
|
|
73
|
+
* Used by the generic loader + hydration pipeline to stamp `__typename`
|
|
74
|
+
* on raw rows before `pool.createFromData(...)`, and to look up the
|
|
75
|
+
* matching class in the model registry. Defaults to the schema key
|
|
76
|
+
* (e.g., `tasks` → `'tasks'`). Provide explicitly when the wire shape
|
|
77
|
+
* uses a different casing (e.g., schema key `slideLayer` → typename
|
|
78
|
+
* `'SlideLayer'`).
|
|
79
|
+
*
|
|
80
|
+
* This is the single source of truth for "what identifies this model
|
|
81
|
+
* on the wire." Every other layer (IDB store name, query.returns
|
|
82
|
+
* references, delta routing) resolves through this value.
|
|
83
|
+
*/
|
|
84
|
+
typename?: string;
|
|
85
|
+
/**
|
|
86
|
+
* IndexedDB persistence hints. See {@link PersistOptions}.
|
|
87
|
+
*/
|
|
88
|
+
persist?: PersistOptions;
|
|
89
|
+
/**
|
|
90
|
+
* The actual database table name. Defaults to snake_case of the model
|
|
91
|
+
* name if not provided. Used by the bootstrap query builder to know
|
|
92
|
+
* which table to SELECT from — without this, the server has to guess
|
|
93
|
+
* via a naming convention that may not match the Prisma @@map directive.
|
|
94
|
+
*/
|
|
95
|
+
tableName?: string;
|
|
96
|
+
/**
|
|
97
|
+
* Whether this model's table has an organization_id column.
|
|
98
|
+
* Default: true. When false, the bootstrap query omits the
|
|
99
|
+
* `WHERE organization_id = $1` clause for this model.
|
|
100
|
+
*/
|
|
101
|
+
orgScoped?: boolean;
|
|
102
|
+
/**
|
|
103
|
+
* Scope rows via a parent table when THIS table has no
|
|
104
|
+
* `organization_id` column of its own, but rows still belong to a
|
|
105
|
+
* tenant via a foreign key (e.g. `memberRoles.member_id → member.id`,
|
|
106
|
+
* `teamMember.team_id → team.id`, `users.id ← member.user_id`).
|
|
107
|
+
*
|
|
108
|
+
* Emits, in place of the missing `organization_id = $1` clause:
|
|
109
|
+
*
|
|
110
|
+
* WHERE <table>.<localKey> IN
|
|
111
|
+
* (SELECT <parentKey> FROM <parentTable> WHERE <parentOrgColumn> = $1)
|
|
112
|
+
*
|
|
113
|
+
* Use this INSTEAD of `orgScoped: false` for any `load: 'instant'`
|
|
114
|
+
* model whose rows would otherwise leak cross-tenant on bootstrap —
|
|
115
|
+
* dropping the filter entirely exposes the entire DB to every client.
|
|
116
|
+
*
|
|
117
|
+
* Identifiers must match the regular `[a-zA-Z_][a-zA-Z0-9_]*` shape;
|
|
118
|
+
* the SQL compiler validates them to keep this away from injection
|
|
119
|
+
* paths.
|
|
120
|
+
*/
|
|
121
|
+
scopedVia?: ScopedViaRef;
|
|
122
|
+
/**
|
|
123
|
+
* Template for the sync group this entity lives in. When set, the
|
|
124
|
+
* participant APIs can derive a transport scope from the entity id.
|
|
125
|
+
* The single `{id}` placeholder is substituted with the scope id.
|
|
126
|
+
*
|
|
127
|
+
* Example: `syncGroupFormat: 'matter:{id}'` + `scope: { matters: 'acme-q3' }`
|
|
128
|
+
* yields a capability restricted to `sync_group: ['matter:acme-q3']`.
|
|
129
|
+
*
|
|
130
|
+
* Leave unset for entities that aren't directly scopable (nested
|
|
131
|
+
* children whose access derives from their parent — e.g. a `redline`
|
|
132
|
+
* inside a `document` inside a `matter`).
|
|
133
|
+
*/
|
|
134
|
+
syncGroupFormat?: string;
|
|
135
|
+
/**
|
|
136
|
+
* Whether clients may issue CREATE/UPDATE/DELETE mutations for this
|
|
137
|
+
* model via the `commit` wire protocol. Default: false.
|
|
138
|
+
*
|
|
139
|
+
* Safety-by-default: a newly-declared schema entity is read-only from
|
|
140
|
+
* the client side until the author explicitly opts into wire mutability.
|
|
141
|
+
* Prevents the class of bug where adding a new entity to the schema
|
|
142
|
+
* silently exposes it as a write surface (the 2026-04-20 `AgentJob`
|
|
143
|
+
* incident) OR where internal tables (`sync_deltas`, `presences`,
|
|
144
|
+
* digest/ingestion tables) become writable by accident.
|
|
145
|
+
*
|
|
146
|
+
* The server's `buildModelMap` (src/server/commit.ts) derives
|
|
147
|
+
* the mutation allowlist from this flag — no parallel hardcoded list.
|
|
148
|
+
*/
|
|
149
|
+
mutable?: boolean;
|
|
150
|
+
/**
|
|
151
|
+
* Defer MobX observability setup until the model is first accessed
|
|
152
|
+
* by an observer component. Default: false (observe immediately).
|
|
153
|
+
*
|
|
154
|
+
* Use for models that are created in bulk (e.g., during import or
|
|
155
|
+
* batch bootstrap) where most instances are never rendered. The
|
|
156
|
+
* model's constructor skips makeObservable(); instead, consuming
|
|
157
|
+
* code calls model.makeObservable() when the model enters the
|
|
158
|
+
* render tree. This matches Ablo's SlideLayer.ensureObservable()
|
|
159
|
+
* pattern and avoids ~10ms of MobX setup overhead per instance
|
|
160
|
+
* when creating hundreds of models that never get observed.
|
|
161
|
+
*/
|
|
162
|
+
lazyObservable?: boolean;
|
|
163
|
+
/**
|
|
164
|
+
* Computed getters installed on the dynamic model class prototype.
|
|
165
|
+
*
|
|
166
|
+
* Each key becomes a getter on the model instance. The function receives
|
|
167
|
+
* `self` (the model instance) and returns the computed value. These replace
|
|
168
|
+
* hand-coded getter methods on legacy Model subclasses.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* model({ title: z.string(), metadata: z.string() }, {}, {
|
|
172
|
+
* computed: {
|
|
173
|
+
* displayTitle: (self) => self.title || `Untitled`,
|
|
174
|
+
* metadataObject: (self) => {
|
|
175
|
+
* try { return JSON.parse(self.metadata || '{}'); }
|
|
176
|
+
* catch { return {}; }
|
|
177
|
+
* },
|
|
178
|
+
* },
|
|
179
|
+
* })
|
|
180
|
+
*/
|
|
181
|
+
computed?: ComputedRecord;
|
|
182
|
+
/**
|
|
183
|
+
* Fields to back-fill from the sync client identity when missing
|
|
184
|
+
* during IndexedDB self-healing.
|
|
185
|
+
*
|
|
186
|
+
* Healing runs on every row loaded from IDB at hydration time and on
|
|
187
|
+
* every delta merge. If the row is missing one of these fields, the
|
|
188
|
+
* engine writes the corresponding identity value (`organizationId` /
|
|
189
|
+
* `userId` from `SyncClient.initialize`) into the row before passing
|
|
190
|
+
* it to the ObjectPool. Without this, rows from a past version that
|
|
191
|
+
* didn't write the field would surface as `undefined` and break any
|
|
192
|
+
* code that assumes the field is set.
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* autoFill: [
|
|
196
|
+
* { field: 'organizationId', from: 'organizationId' },
|
|
197
|
+
* { field: 'createdBy', from: 'userId' },
|
|
198
|
+
* ]
|
|
199
|
+
*/
|
|
200
|
+
autoFill?: readonly AutoFillRule[];
|
|
201
|
+
/**
|
|
202
|
+
* Fields whose absence makes a stored row "orphaned" — corrupt
|
|
203
|
+
* enough that the engine should drop it instead of loading it.
|
|
204
|
+
*
|
|
205
|
+
* Healing returns `null` for the row when any listed field is
|
|
206
|
+
* missing, which causes the caller to skip pool insertion for that
|
|
207
|
+
* record. Use for foreign keys whose absence would crash dependent
|
|
208
|
+
* code (e.g. a `SlideLayer` with no `slideId` can't render anywhere).
|
|
209
|
+
*
|
|
210
|
+
* @example requiredFields: ['slideId']
|
|
211
|
+
*/
|
|
212
|
+
requiredFields?: readonly string[];
|
|
213
|
+
}
|
|
214
|
+
/** Base type for computed getter records. Preserves return types via inference. */
|
|
215
|
+
export type ComputedRecord = Record<string, (self: any) => any>;
|
|
216
|
+
/**
|
|
217
|
+
* Identity sources the sync engine can pull from when auto-filling a
|
|
218
|
+
* record's missing field during IndexedDB self-healing.
|
|
219
|
+
*
|
|
220
|
+
* - `'organizationId'` — the org id passed to `SyncClient.initialize`
|
|
221
|
+
* - `'userId'` — the user id passed to `SyncClient.initialize`
|
|
222
|
+
*/
|
|
223
|
+
export type AutoFillSource = 'organizationId' | 'userId';
|
|
224
|
+
/**
|
|
225
|
+
* Declaration of a field that should be back-filled from the connected
|
|
226
|
+
* sync identity if missing from a stored row.
|
|
227
|
+
*
|
|
228
|
+
* Used by `SyncClient.healModelRecord` to repair pre-existing IDB rows
|
|
229
|
+
* that were written without `organizationId` / `createdBy` due to past
|
|
230
|
+
* bugs in delta merging. Declared per-model so the engine itself stays
|
|
231
|
+
* product-neutral.
|
|
232
|
+
*/
|
|
233
|
+
export interface AutoFillRule {
|
|
234
|
+
/** Field name on the model (e.g. `'organizationId'`, `'createdBy'`). */
|
|
235
|
+
field: string;
|
|
236
|
+
/** Where to read the replacement value from on the sync client. */
|
|
237
|
+
from: AutoFillSource;
|
|
238
|
+
}
|
|
239
|
+
/** A complete model definition: Zod shape + fields metadata + relations + options */
|
|
240
|
+
export interface ModelDef<Shape extends z.ZodRawShape = z.ZodRawShape, R extends RelationRecord = RelationRecord, C extends ComputedRecord = ComputedRecord> {
|
|
241
|
+
/** The Zod object schema for this model's fields */
|
|
242
|
+
readonly schema: z.ZodObject<Shape>;
|
|
243
|
+
/** The raw shape (for type inference) */
|
|
244
|
+
readonly shape: Shape;
|
|
245
|
+
/**
|
|
246
|
+
* Runtime metadata for each field, keyed by field name.
|
|
247
|
+
*
|
|
248
|
+
* Populated automatically from `field.*()` builders. Fields defined
|
|
249
|
+
* with raw Zod (e.g., `z.string()`) get a fallback metadata entry
|
|
250
|
+
* with type inferred from Zod's `_def.typeName`.
|
|
251
|
+
*
|
|
252
|
+
* Used by the CLI (`npx ablo migrate`), admin panels, and any tooling
|
|
253
|
+
* that needs to introspect the schema without parsing Zod internals.
|
|
254
|
+
*/
|
|
255
|
+
readonly fields: Record<string, FieldMeta>;
|
|
256
|
+
/** Relations to other models */
|
|
257
|
+
readonly relations: R;
|
|
258
|
+
/** Load strategy */
|
|
259
|
+
readonly load: LoadStrategy;
|
|
260
|
+
/** Max records to bootstrap */
|
|
261
|
+
readonly bootstrapLimit?: number;
|
|
262
|
+
/** Sort order for bootstrap */
|
|
263
|
+
readonly bootstrapOrderBy?: string;
|
|
264
|
+
/**
|
|
265
|
+
* The GraphQL/wire `__typename` value for this model. When unset in
|
|
266
|
+
* {@link ModelOptions}, this falls back to the schema key at schema
|
|
267
|
+
* assembly time (see `defineSchema`).
|
|
268
|
+
*/
|
|
269
|
+
readonly typename?: string;
|
|
270
|
+
/** IndexedDB persistence hints. See {@link PersistOptions}. */
|
|
271
|
+
readonly persist?: PersistOptions;
|
|
272
|
+
/** The actual database table name from Prisma @@map. See {@link ModelOptions.tableName}. */
|
|
273
|
+
readonly tableName?: string;
|
|
274
|
+
/** Whether the table has organization_id. See {@link ModelOptions.orgScoped}. */
|
|
275
|
+
readonly orgScoped?: boolean;
|
|
276
|
+
/** Parent-table scoping for rows with no organization_id. See {@link ModelOptions.scopedVia}. */
|
|
277
|
+
readonly scopedVia?: ScopedViaRef;
|
|
278
|
+
/** Template for participant scope derivation. See {@link ModelOptions.syncGroupFormat}. */
|
|
279
|
+
readonly syncGroupFormat?: string;
|
|
280
|
+
/** Whether wire-level CREATE/UPDATE/DELETE is allowed. See {@link ModelOptions.mutable}. */
|
|
281
|
+
readonly mutable?: boolean;
|
|
282
|
+
/** Defer MobX setup until first observer access. See {@link ModelOptions.lazyObservable}. */
|
|
283
|
+
readonly lazyObservable?: boolean;
|
|
284
|
+
/** Computed getters for the dynamic model class. See {@link ModelOptions.computed}. */
|
|
285
|
+
readonly computed?: C;
|
|
286
|
+
/** Auto-fill rules for IDB self-healing. See {@link ModelOptions.autoFill}. */
|
|
287
|
+
readonly autoFill?: readonly AutoFillRule[];
|
|
288
|
+
/** Fields whose absence orphans a row. See {@link ModelOptions.requiredFields}. */
|
|
289
|
+
readonly requiredFields?: readonly string[];
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Define a model with a Zod shape and optional relations.
|
|
293
|
+
*
|
|
294
|
+
* ```ts
|
|
295
|
+
* import { z } from 'zod';
|
|
296
|
+
* import { model, relation } from '@ablo/sync-engine/schema';
|
|
297
|
+
*
|
|
298
|
+
* const tasks = model({
|
|
299
|
+
* title: z.string(),
|
|
300
|
+
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
301
|
+
* priority: z.number().default(0),
|
|
302
|
+
* projectId: z.string().optional(),
|
|
303
|
+
* }, {
|
|
304
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
305
|
+
* });
|
|
306
|
+
* ```
|
|
307
|
+
*/
|
|
308
|
+
/**
|
|
309
|
+
* Define a model with fields, optional relations, and load strategy.
|
|
310
|
+
*
|
|
311
|
+
* ```ts
|
|
312
|
+
* // Loaded at bootstrap (default)
|
|
313
|
+
* const tasks = model({ title: z.string() });
|
|
314
|
+
*
|
|
315
|
+
* // Loaded on first access (lazy)
|
|
316
|
+
* const slideLayers = model({ slideId: z.string(), type: z.string() }, {
|
|
317
|
+
* slide: relation.belongsTo('slides', 'slideId'),
|
|
318
|
+
* }, { load: 'lazy' });
|
|
319
|
+
*
|
|
320
|
+
* // Only loaded when explicitly requested
|
|
321
|
+
* const auditLogs = model({ action: z.string() }, {}, { load: 'manual' });
|
|
322
|
+
* ```
|
|
323
|
+
*/
|
|
324
|
+
export declare function model<Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, relations?: R, options?: ModelOptions & {
|
|
325
|
+
computed?: C;
|
|
326
|
+
}): ModelDef<Shape, R, C>;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema Model Definition
|
|
3
|
+
*
|
|
4
|
+
* A model is a Zod object schema + optional relations.
|
|
5
|
+
* Types are inferred directly from Zod — no custom type system.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { z } from 'zod';
|
|
9
|
+
* import { model, relation } from '@ablo/sync-engine/schema';
|
|
10
|
+
*
|
|
11
|
+
* const 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
|
+
import { z } from 'zod';
|
|
20
|
+
import { getFieldMeta, inferFieldMetaFromZod } from './field.js';
|
|
21
|
+
// ── Model factory ─────────────────────────────────────────────────────────
|
|
22
|
+
/**
|
|
23
|
+
* Define a model with a Zod shape and optional relations.
|
|
24
|
+
*
|
|
25
|
+
* ```ts
|
|
26
|
+
* import { z } from 'zod';
|
|
27
|
+
* import { model, relation } from '@ablo/sync-engine/schema';
|
|
28
|
+
*
|
|
29
|
+
* const tasks = model({
|
|
30
|
+
* title: z.string(),
|
|
31
|
+
* status: z.enum(['todo', 'doing', 'done']).default('todo'),
|
|
32
|
+
* priority: z.number().default(0),
|
|
33
|
+
* projectId: z.string().optional(),
|
|
34
|
+
* }, {
|
|
35
|
+
* project: relation.belongsTo('projects', 'projectId'),
|
|
36
|
+
* });
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
/**
|
|
40
|
+
* Define a model with fields, optional relations, and load strategy.
|
|
41
|
+
*
|
|
42
|
+
* ```ts
|
|
43
|
+
* // Loaded at bootstrap (default)
|
|
44
|
+
* const tasks = model({ title: z.string() });
|
|
45
|
+
*
|
|
46
|
+
* // Loaded on first access (lazy)
|
|
47
|
+
* const slideLayers = model({ slideId: z.string(), type: z.string() }, {
|
|
48
|
+
* slide: relation.belongsTo('slides', 'slideId'),
|
|
49
|
+
* }, { load: 'lazy' });
|
|
50
|
+
*
|
|
51
|
+
* // Only loaded when explicitly requested
|
|
52
|
+
* const auditLogs = model({ action: z.string() }, {}, { load: 'manual' });
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function model(shape, relations, options) {
|
|
56
|
+
// Build the fields metadata record by walking the Zod shape.
|
|
57
|
+
// Fields built with `field.*()` have structured metadata; fields built
|
|
58
|
+
// with raw Zod get a fallback derived from the Zod typeName.
|
|
59
|
+
const fields = {};
|
|
60
|
+
for (const [name, zodType] of Object.entries(shape)) {
|
|
61
|
+
const meta = getFieldMeta(zodType);
|
|
62
|
+
if (meta) {
|
|
63
|
+
fields[name] = meta;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
fields[name] = inferFieldMetaFromZod(zodType);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
schema: z.object(shape),
|
|
71
|
+
shape,
|
|
72
|
+
fields,
|
|
73
|
+
relations: (relations ?? {}),
|
|
74
|
+
load: options?.load ?? 'instant',
|
|
75
|
+
bootstrapLimit: options?.bootstrapLimit,
|
|
76
|
+
bootstrapOrderBy: options?.bootstrapOrderBy,
|
|
77
|
+
typename: options?.typename,
|
|
78
|
+
persist: options?.persist,
|
|
79
|
+
tableName: options?.tableName,
|
|
80
|
+
orgScoped: options?.orgScoped,
|
|
81
|
+
scopedVia: options?.scopedVia,
|
|
82
|
+
syncGroupFormat: options?.syncGroupFormat,
|
|
83
|
+
mutable: options?.mutable,
|
|
84
|
+
lazyObservable: options?.lazyObservable,
|
|
85
|
+
computed: options?.computed,
|
|
86
|
+
autoFill: options?.autoFill,
|
|
87
|
+
requiredFields: options?.requiredFields,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
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 { z } from 'zod';
|
|
60
|
+
import type { Schema, InferModel, InferModelNames } from './schema.js';
|
|
61
|
+
/**
|
|
62
|
+
* A single query definition: a zod input shape, the schema key of the
|
|
63
|
+
* model it returns, and (after assembly) the name it was registered
|
|
64
|
+
* under.
|
|
65
|
+
*
|
|
66
|
+
* `name` is filled in by `defineQueries()` from the record key. It is
|
|
67
|
+
* optional on the type so `query()` can return a value without one —
|
|
68
|
+
* downstream code should only read `name` after the query has been
|
|
69
|
+
* passed through `defineQueries()`.
|
|
70
|
+
*/
|
|
71
|
+
export interface QueryDef<TInput extends z.ZodType = z.ZodType, TReturns extends string = string> {
|
|
72
|
+
/** Zod schema for the query's input arguments. */
|
|
73
|
+
readonly input: TInput;
|
|
74
|
+
/**
|
|
75
|
+
* The schema key of the model this query returns. Narrowed by
|
|
76
|
+
* `defineQueries()` to `InferModelNames<S>` via the `Q` generic, so
|
|
77
|
+
* a query whose `returns` does not match a known model fails at
|
|
78
|
+
* compile time.
|
|
79
|
+
*/
|
|
80
|
+
readonly returns: TReturns;
|
|
81
|
+
/**
|
|
82
|
+
* Name under which the query is registered. Populated by
|
|
83
|
+
* `defineQueries()` from the record key — do not set directly.
|
|
84
|
+
* Present so wire-dispatch code (`client.runNamed(queryDef.name,
|
|
85
|
+
* ...)`) and the Go registry lookup can read it straight off the
|
|
86
|
+
* def without needing the surrounding `Queries` object.
|
|
87
|
+
*/
|
|
88
|
+
readonly name?: string;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The raw spec accepted by `query()`. Internal type — consumers never
|
|
92
|
+
* reference this directly, they call `query({ input, returns })`.
|
|
93
|
+
*/
|
|
94
|
+
interface QuerySpec<TInput extends z.ZodType, TReturns extends string> {
|
|
95
|
+
readonly input: TInput;
|
|
96
|
+
readonly returns: TReturns;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Define a query.
|
|
100
|
+
*
|
|
101
|
+
* `TReturns` is a `const` generic so TypeScript preserves the literal
|
|
102
|
+
* type of the `returns` value at call time (e.g., `'slideLayer'`
|
|
103
|
+
* instead of widening to `string`). This is what lets `defineQueries`
|
|
104
|
+
* type-check `returns` against the schema's model keys without
|
|
105
|
+
* requiring the consumer to write `as const` manually.
|
|
106
|
+
*
|
|
107
|
+
* ```ts
|
|
108
|
+
* const slideLayersByDeck = query({
|
|
109
|
+
* input: z.object({ deckId: z.string() }),
|
|
110
|
+
* returns: 'slideLayer',
|
|
111
|
+
* });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export declare function query<TInput extends z.ZodType, const TReturns extends string>(spec: QuerySpec<TInput, TReturns>): QueryDef<TInput, TReturns>;
|
|
115
|
+
/**
|
|
116
|
+
* A record of query names → query definitions, parameterized by a
|
|
117
|
+
* schema so each query's `returns` must reference a known model.
|
|
118
|
+
*
|
|
119
|
+
* This is the constraint type used by `defineQueries()` — it's what
|
|
120
|
+
* turns "any string" into "a model in this schema" without touching
|
|
121
|
+
* the `query()` factory itself.
|
|
122
|
+
*/
|
|
123
|
+
export type QueryRecord<S extends Schema> = Record<string, QueryDef<z.ZodType, InferModelNames<S>>>;
|
|
124
|
+
/**
|
|
125
|
+
* The object returned by `defineQueries()`. Holds a reference back to
|
|
126
|
+
* the schema (so the generic loader can resolve `queryDef.returns` to
|
|
127
|
+
* a `ModelDef` at runtime via `schema.models[def.returns]`) and the
|
|
128
|
+
* resolved record of queries, each with its `name` field filled in.
|
|
129
|
+
*/
|
|
130
|
+
export interface Queries<S extends Schema, Q extends QueryRecord<S>> {
|
|
131
|
+
readonly schema: S;
|
|
132
|
+
readonly queries: Q;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Infer the input type of a query from its zod schema.
|
|
136
|
+
*
|
|
137
|
+
* ```ts
|
|
138
|
+
* type Input = InferQueryInput<typeof queries.queries.slideLayersByDeck>;
|
|
139
|
+
* // { deckId: string }
|
|
140
|
+
* ```
|
|
141
|
+
*/
|
|
142
|
+
export type InferQueryInput<Q extends QueryDef> = z.infer<Q['input']>;
|
|
143
|
+
/**
|
|
144
|
+
* Infer the result type of a query by looking up its `returns` model
|
|
145
|
+
* in the schema.
|
|
146
|
+
*
|
|
147
|
+
* Returns `Array<InferModel<S, returns>>` — all queries return a
|
|
148
|
+
* collection at this layer. Single-entity fetches use
|
|
149
|
+
* `ids: [singleId]` and the caller picks `result[0]`; this avoids a
|
|
150
|
+
* second `scope: 'single'` shape in the DSL for a case that can be
|
|
151
|
+
* expressed with the collection form.
|
|
152
|
+
*
|
|
153
|
+
* ```ts
|
|
154
|
+
* type Result = InferQueryResult<
|
|
155
|
+
* typeof schema,
|
|
156
|
+
* typeof queries.queries.slideLayersByDeck
|
|
157
|
+
* >;
|
|
158
|
+
* // Array<SlideLayer>
|
|
159
|
+
* ```
|
|
160
|
+
*/
|
|
161
|
+
export type InferQueryResult<S extends Schema, Q extends QueryDef> = Q extends QueryDef<z.ZodType, infer R> ? R extends InferModelNames<S> ? Array<InferModel<S, R>> : never : never;
|
|
162
|
+
/**
|
|
163
|
+
* Define a typed query set against a schema.
|
|
164
|
+
*
|
|
165
|
+
* Each entry's `returns` field is constrained to the schema's model
|
|
166
|
+
* names at compile time via the `Q` generic bound to `QueryRecord<S>`.
|
|
167
|
+
* A query whose `returns` does not match a known model will fail at
|
|
168
|
+
* the `defineQueries` call site with a TypeScript error, not at
|
|
169
|
+
* runtime.
|
|
170
|
+
*
|
|
171
|
+
* The factory also performs a runtime validation of the same
|
|
172
|
+
* invariant. This catches the edge case where the schema and the
|
|
173
|
+
* queries live in separate modules that drift out of sync — for
|
|
174
|
+
* example, when a developer removes a model from the schema without
|
|
175
|
+
* updating the queries that referenced it. The runtime error points
|
|
176
|
+
* at the specific offending query and lists the available models,
|
|
177
|
+
* which is a nicer failure mode than a generic "property does not
|
|
178
|
+
* exist" error deep inside the loader.
|
|
179
|
+
*
|
|
180
|
+
* Each resolved query gets its `name` populated from the record key:
|
|
181
|
+
* `queries.slideLayersByDeck.name === 'slideLayersByDeck'`. Wire
|
|
182
|
+
* dispatch, the Go registry, and the loader orchestrator all read
|
|
183
|
+
* `queryDef.name` directly rather than re-deriving it.
|
|
184
|
+
*
|
|
185
|
+
* ```ts
|
|
186
|
+
* const schema = defineSchema({
|
|
187
|
+
* slideLayer: model(
|
|
188
|
+
* { slideId: z.string() },
|
|
189
|
+
* {},
|
|
190
|
+
* { load: 'lazy', typename: 'SlideLayer', persist: {} },
|
|
191
|
+
* ),
|
|
192
|
+
* });
|
|
193
|
+
*
|
|
194
|
+
* const queries = defineQueries(schema, {
|
|
195
|
+
* slideLayersByDeck: query({
|
|
196
|
+
* input: z.object({ deckId: z.string() }),
|
|
197
|
+
* returns: 'slideLayer', // ← type-checked
|
|
198
|
+
* }),
|
|
199
|
+
* });
|
|
200
|
+
* ```
|
|
201
|
+
*/
|
|
202
|
+
export declare function defineQueries<S extends Schema, const Q extends QueryRecord<S>>(schema: S, queries: Q): Queries<S, Q>;
|
|
203
|
+
export {};
|