@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.
Files changed (278) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +12 -0
  4. package/README.md +230 -0
  5. package/dist/BaseSyncedStore.d.ts +709 -0
  6. package/dist/BaseSyncedStore.js +1843 -0
  7. package/dist/Database.d.ts +344 -0
  8. package/dist/Database.js +1259 -0
  9. package/dist/LazyReferenceCollection.d.ts +181 -0
  10. package/dist/LazyReferenceCollection.js +460 -0
  11. package/dist/Model.d.ts +339 -0
  12. package/dist/Model.js +715 -0
  13. package/dist/ModelRegistry.d.ts +200 -0
  14. package/dist/ModelRegistry.js +535 -0
  15. package/dist/NetworkMonitor.d.ts +27 -0
  16. package/dist/NetworkMonitor.js +73 -0
  17. package/dist/ObjectPool.d.ts +202 -0
  18. package/dist/ObjectPool.js +1106 -0
  19. package/dist/SyncClient.d.ts +489 -0
  20. package/dist/SyncClient.js +1555 -0
  21. package/dist/SyncEngineContext.d.ts +46 -0
  22. package/dist/SyncEngineContext.js +74 -0
  23. package/dist/adapters/alwaysOnline.d.ts +16 -0
  24. package/dist/adapters/alwaysOnline.js +19 -0
  25. package/dist/adapters/inMemoryStorage.d.ts +30 -0
  26. package/dist/adapters/inMemoryStorage.js +94 -0
  27. package/dist/agent/Agent.d.ts +358 -0
  28. package/dist/agent/Agent.js +500 -0
  29. package/dist/agent/index.d.ts +115 -0
  30. package/dist/agent/index.js +128 -0
  31. package/dist/agent/session.d.ts +90 -0
  32. package/dist/agent/session.js +156 -0
  33. package/dist/agent/types.d.ts +73 -0
  34. package/dist/agent/types.js +10 -0
  35. package/dist/ai-sdk/coordination-context.d.ts +51 -0
  36. package/dist/ai-sdk/coordination-context.js +107 -0
  37. package/dist/ai-sdk/index.d.ts +68 -0
  38. package/dist/ai-sdk/index.js +68 -0
  39. package/dist/ai-sdk/intent-broadcast.d.ts +77 -0
  40. package/dist/ai-sdk/intent-broadcast.js +72 -0
  41. package/dist/ai-sdk/wrap.d.ts +67 -0
  42. package/dist/ai-sdk/wrap.js +45 -0
  43. package/dist/api/index.d.ts +10 -0
  44. package/dist/api/index.js +9 -0
  45. package/dist/auth/index.d.ts +137 -0
  46. package/dist/auth/index.js +246 -0
  47. package/dist/client/Ablo.d.ts +835 -0
  48. package/dist/client/Ablo.js +1440 -0
  49. package/dist/client/ApiClient.d.ts +200 -0
  50. package/dist/client/ApiClient.js +659 -0
  51. package/dist/client/auth.d.ts +79 -0
  52. package/dist/client/auth.js +81 -0
  53. package/dist/client/createInternalComponents.d.ts +44 -0
  54. package/dist/client/createInternalComponents.js +88 -0
  55. package/dist/client/createModelProxy.d.ts +152 -0
  56. package/dist/client/createModelProxy.js +199 -0
  57. package/dist/client/identity.d.ts +63 -0
  58. package/dist/client/identity.js +156 -0
  59. package/dist/client/index.d.ts +36 -0
  60. package/dist/client/index.js +33 -0
  61. package/dist/client/persistence.d.ts +7 -0
  62. package/dist/client/persistence.js +11 -0
  63. package/dist/client/validateAbloOptions.d.ts +42 -0
  64. package/dist/client/validateAbloOptions.js +43 -0
  65. package/dist/config/index.d.ts +10 -0
  66. package/dist/config/index.js +12 -0
  67. package/dist/context.d.ts +27 -0
  68. package/dist/context.js +58 -0
  69. package/dist/core/DatabaseManager.d.ts +108 -0
  70. package/dist/core/DatabaseManager.js +361 -0
  71. package/dist/core/QueryProcessor.d.ts +77 -0
  72. package/dist/core/QueryProcessor.js +262 -0
  73. package/dist/core/QueryView.d.ts +64 -0
  74. package/dist/core/QueryView.js +219 -0
  75. package/dist/core/StoreManager.d.ts +131 -0
  76. package/dist/core/StoreManager.js +334 -0
  77. package/dist/core/ViewRegistry.d.ts +20 -0
  78. package/dist/core/ViewRegistry.js +55 -0
  79. package/dist/core/index.d.ts +34 -0
  80. package/dist/core/index.js +59 -0
  81. package/dist/core/openIDBWithTimeout.d.ts +27 -0
  82. package/dist/core/openIDBWithTimeout.js +63 -0
  83. package/dist/core/query-utils.d.ts +37 -0
  84. package/dist/core/query-utils.js +60 -0
  85. package/dist/errors.d.ts +235 -0
  86. package/dist/errors.js +243 -0
  87. package/dist/index.d.ts +41 -0
  88. package/dist/index.js +82 -0
  89. package/dist/interfaces/headless.d.ts +95 -0
  90. package/dist/interfaces/headless.js +41 -0
  91. package/dist/interfaces/index.d.ts +321 -0
  92. package/dist/interfaces/index.js +8 -0
  93. package/dist/mutators/RecordingTransaction.d.ts +36 -0
  94. package/dist/mutators/RecordingTransaction.js +216 -0
  95. package/dist/mutators/Transaction.d.ts +48 -0
  96. package/dist/mutators/Transaction.js +64 -0
  97. package/dist/mutators/UndoManager.d.ts +114 -0
  98. package/dist/mutators/UndoManager.js +143 -0
  99. package/dist/mutators/defineMutators.d.ts +55 -0
  100. package/dist/mutators/defineMutators.js +28 -0
  101. package/dist/policy/index.d.ts +19 -0
  102. package/dist/policy/index.js +18 -0
  103. package/dist/policy/types.d.ts +74 -0
  104. package/dist/policy/types.js +17 -0
  105. package/dist/principal.d.ts +44 -0
  106. package/dist/principal.js +49 -0
  107. package/dist/query/client.d.ts +43 -0
  108. package/dist/query/client.js +84 -0
  109. package/dist/query/index.d.ts +6 -0
  110. package/dist/query/index.js +5 -0
  111. package/dist/query/types.d.ts +143 -0
  112. package/dist/query/types.js +36 -0
  113. package/dist/react/AbloProvider.d.ts +205 -0
  114. package/dist/react/AbloProvider.js +398 -0
  115. package/dist/react/ClientSideSuspense.d.ts +36 -0
  116. package/dist/react/ClientSideSuspense.js +17 -0
  117. package/dist/react/DefaultFallback.d.ts +24 -0
  118. package/dist/react/DefaultFallback.js +43 -0
  119. package/dist/react/SyncGroupProvider.d.ts +19 -0
  120. package/dist/react/SyncGroupProvider.js +44 -0
  121. package/dist/react/context.d.ts +161 -0
  122. package/dist/react/context.js +35 -0
  123. package/dist/react/index.d.ts +64 -0
  124. package/dist/react/index.js +73 -0
  125. package/dist/react/internalContext.d.ts +35 -0
  126. package/dist/react/internalContext.js +3 -0
  127. package/dist/react/useAblo.d.ts +72 -0
  128. package/dist/react/useAblo.js +63 -0
  129. package/dist/react/useCurrentUserId.d.ts +21 -0
  130. package/dist/react/useCurrentUserId.js +33 -0
  131. package/dist/react/useErrorListener.d.ts +20 -0
  132. package/dist/react/useErrorListener.js +39 -0
  133. package/dist/react/useIntent.d.ts +29 -0
  134. package/dist/react/useIntent.js +42 -0
  135. package/dist/react/useMutate.d.ts +83 -0
  136. package/dist/react/useMutate.js +122 -0
  137. package/dist/react/useMutationFailureListener.d.ts +26 -0
  138. package/dist/react/useMutationFailureListener.js +38 -0
  139. package/dist/react/useMutators.d.ts +56 -0
  140. package/dist/react/useMutators.js +66 -0
  141. package/dist/react/usePresence.d.ts +32 -0
  142. package/dist/react/usePresence.js +41 -0
  143. package/dist/react/useQuery.d.ts +123 -0
  144. package/dist/react/useQuery.js +145 -0
  145. package/dist/react/useReactive.d.ts +35 -0
  146. package/dist/react/useReactive.js +111 -0
  147. package/dist/react/useReader.d.ts +69 -0
  148. package/dist/react/useReader.js +73 -0
  149. package/dist/react/useSyncStatus.d.ts +61 -0
  150. package/dist/react/useSyncStatus.js +76 -0
  151. package/dist/react/useUndoScope.d.ts +36 -0
  152. package/dist/react/useUndoScope.js +73 -0
  153. package/dist/realtime/index.d.ts +10 -0
  154. package/dist/realtime/index.js +9 -0
  155. package/dist/schema/field.d.ts +134 -0
  156. package/dist/schema/field.js +264 -0
  157. package/dist/schema/index.d.ts +29 -0
  158. package/dist/schema/index.js +38 -0
  159. package/dist/schema/model.d.ts +326 -0
  160. package/dist/schema/model.js +89 -0
  161. package/dist/schema/queries.d.ts +203 -0
  162. package/dist/schema/queries.js +145 -0
  163. package/dist/schema/relation.d.ts +172 -0
  164. package/dist/schema/relation.js +104 -0
  165. package/dist/schema/schema.d.ts +259 -0
  166. package/dist/schema/schema.js +188 -0
  167. package/dist/schema/sugar.d.ts +129 -0
  168. package/dist/schema/sugar.js +94 -0
  169. package/dist/source/index.d.ts +423 -0
  170. package/dist/source/index.js +320 -0
  171. package/dist/source/pushQueue.d.ts +112 -0
  172. package/dist/source/pushQueue.js +249 -0
  173. package/dist/stores/ObjectStore.d.ts +103 -0
  174. package/dist/stores/ObjectStore.js +371 -0
  175. package/dist/stores/ObjectStoreContract.d.ts +39 -0
  176. package/dist/stores/ObjectStoreContract.js +1 -0
  177. package/dist/stores/SyncActionStore.d.ts +101 -0
  178. package/dist/stores/SyncActionStore.js +481 -0
  179. package/dist/sync/BootstrapHelper.d.ts +127 -0
  180. package/dist/sync/BootstrapHelper.js +434 -0
  181. package/dist/sync/ConnectionManager.d.ts +136 -0
  182. package/dist/sync/ConnectionManager.js +465 -0
  183. package/dist/sync/HydrationCoordinator.d.ts +137 -0
  184. package/dist/sync/HydrationCoordinator.js +468 -0
  185. package/dist/sync/NetworkProbe.d.ts +43 -0
  186. package/dist/sync/NetworkProbe.js +113 -0
  187. package/dist/sync/OfflineFlush.d.ts +9 -0
  188. package/dist/sync/OfflineFlush.js +22 -0
  189. package/dist/sync/OfflineTransactionStore.d.ts +37 -0
  190. package/dist/sync/OfflineTransactionStore.js +263 -0
  191. package/dist/sync/SyncWebSocket.d.ts +663 -0
  192. package/dist/sync/SyncWebSocket.js +1336 -0
  193. package/dist/sync/createIntentStream.d.ts +33 -0
  194. package/dist/sync/createIntentStream.js +243 -0
  195. package/dist/sync/createPresenceStream.d.ts +46 -0
  196. package/dist/sync/createPresenceStream.js +192 -0
  197. package/dist/sync/createSnapshot.d.ts +33 -0
  198. package/dist/sync/createSnapshot.js +124 -0
  199. package/dist/sync/participants.d.ts +114 -0
  200. package/dist/sync/participants.js +336 -0
  201. package/dist/sync/schemas.d.ts +79 -0
  202. package/dist/sync/schemas.js +78 -0
  203. package/dist/testing/fixtures/bootstrap.d.ts +45 -0
  204. package/dist/testing/fixtures/bootstrap.js +53 -0
  205. package/dist/testing/fixtures/deltas.d.ts +86 -0
  206. package/dist/testing/fixtures/deltas.js +139 -0
  207. package/dist/testing/fixtures/models.d.ts +82 -0
  208. package/dist/testing/fixtures/models.js +270 -0
  209. package/dist/testing/helpers/react-wrapper.d.ts +66 -0
  210. package/dist/testing/helpers/react-wrapper.js +64 -0
  211. package/dist/testing/helpers/sync-engine-harness.d.ts +55 -0
  212. package/dist/testing/helpers/sync-engine-harness.js +70 -0
  213. package/dist/testing/helpers/wait.d.ts +25 -0
  214. package/dist/testing/helpers/wait.js +44 -0
  215. package/dist/testing/index.d.ts +21 -0
  216. package/dist/testing/index.js +32 -0
  217. package/dist/testing/mocks/MockMutationExecutor.d.ts +65 -0
  218. package/dist/testing/mocks/MockMutationExecutor.js +139 -0
  219. package/dist/testing/mocks/MockNetworkMonitor.d.ts +20 -0
  220. package/dist/testing/mocks/MockNetworkMonitor.js +46 -0
  221. package/dist/testing/mocks/MockSyncContext.d.ts +64 -0
  222. package/dist/testing/mocks/MockSyncContext.js +100 -0
  223. package/dist/testing/mocks/MockSyncStore.d.ts +88 -0
  224. package/dist/testing/mocks/MockSyncStore.js +171 -0
  225. package/dist/testing/mocks/MockWebSocket.d.ts +66 -0
  226. package/dist/testing/mocks/MockWebSocket.js +117 -0
  227. package/dist/transactions/OptimisticEchoTracker.d.ts +82 -0
  228. package/dist/transactions/OptimisticEchoTracker.js +104 -0
  229. package/dist/transactions/TransactionQueue.d.ts +499 -0
  230. package/dist/transactions/TransactionQueue.js +1895 -0
  231. package/dist/transactions/index.d.ts +16 -0
  232. package/dist/transactions/index.js +7 -0
  233. package/dist/transactions/mutation-error-handler.d.ts +5 -0
  234. package/dist/transactions/mutation-error-handler.js +39 -0
  235. package/dist/types/global.d.ts +107 -0
  236. package/dist/types/global.js +38 -0
  237. package/dist/types/index.d.ts +241 -0
  238. package/dist/types/index.js +70 -0
  239. package/dist/types/streams.d.ts +495 -0
  240. package/dist/types/streams.js +11 -0
  241. package/dist/utils/asyncIterator.d.ts +41 -0
  242. package/dist/utils/asyncIterator.js +142 -0
  243. package/dist/utils/duration.d.ts +28 -0
  244. package/dist/utils/duration.js +47 -0
  245. package/dist/utils/mobx-setup.d.ts +42 -0
  246. package/dist/utils/mobx-setup.js +381 -0
  247. package/docs/api-keys.md +24 -0
  248. package/docs/api.md +230 -0
  249. package/docs/audit.md +81 -0
  250. package/docs/capabilities.md +163 -0
  251. package/docs/client-behavior.md +202 -0
  252. package/docs/data-sources.md +214 -0
  253. package/docs/examples/agent-human.md +84 -0
  254. package/docs/examples/ai-sdk-tool.md +92 -0
  255. package/docs/examples/existing-python-backend.md +249 -0
  256. package/docs/examples/nextjs.md +88 -0
  257. package/docs/examples/server-agent.md +86 -0
  258. package/docs/guarantees.md +148 -0
  259. package/docs/index.md +97 -0
  260. package/docs/integration-guide.md +493 -0
  261. package/docs/interaction-model.md +140 -0
  262. package/docs/mcp/claude-code.md +43 -0
  263. package/docs/mcp/cursor.md +53 -0
  264. package/docs/mcp/windsurf.md +46 -0
  265. package/docs/mcp.md +59 -0
  266. package/docs/quickstart.md +152 -0
  267. package/docs/react.md +115 -0
  268. package/docs/roadmap.md +45 -0
  269. package/examples/README.md +54 -0
  270. package/examples/data-source/README.md +102 -0
  271. package/examples/data-source/ablo-driver.ts +89 -0
  272. package/examples/data-source/customer-server.ts +208 -0
  273. package/examples/data-source/run.ts +101 -0
  274. package/examples/data-source/schema.ts +25 -0
  275. package/examples/quickstart.ts +54 -0
  276. package/examples/tsconfig.json +16 -0
  277. package/llms.txt +143 -0
  278. 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 {};