@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,188 @@
|
|
|
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 { AbloValidationError } from '../errors.js';
|
|
24
|
+
function resolveCasing(fn) {
|
|
25
|
+
if (fn === undefined)
|
|
26
|
+
return (x) => x;
|
|
27
|
+
if (typeof fn === 'function')
|
|
28
|
+
return fn;
|
|
29
|
+
switch (fn) {
|
|
30
|
+
case 'snake_case':
|
|
31
|
+
return camelToSnake;
|
|
32
|
+
case 'camelCase':
|
|
33
|
+
return (x) => x;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Pure camelCase → snake_case. Matches postgres.fromCamel semantics but
|
|
37
|
+
* kept local so the SDK stays free of any driver dependency — consumers
|
|
38
|
+
* using Prisma/Drizzle/raw pg should all get the same result. */
|
|
39
|
+
function camelToSnake(identifier) {
|
|
40
|
+
return identifier.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
41
|
+
}
|
|
42
|
+
/** Base fields every synced model gets automatically */
|
|
43
|
+
const baseFieldsSchema = z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
createdAt: z.date(),
|
|
46
|
+
updatedAt: z.date(),
|
|
47
|
+
organizationId: z.string().optional(),
|
|
48
|
+
createdBy: z.string().optional(),
|
|
49
|
+
});
|
|
50
|
+
// ── Factory ───────────────────────────────────────────────────────────────
|
|
51
|
+
/**
|
|
52
|
+
* Define a sync engine schema.
|
|
53
|
+
*
|
|
54
|
+
* ```ts
|
|
55
|
+
* const schema = defineSchema({
|
|
56
|
+
* tasks: model({ title: z.string(), status: z.string().default('todo') }),
|
|
57
|
+
* projects: model({ name: z.string() }),
|
|
58
|
+
* });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
/**
|
|
62
|
+
* Lowercase-first camelCase round-trip check. Must match the convention used
|
|
63
|
+
* by `postgres.camel` in the client driver (porsager/postgres), which is:
|
|
64
|
+
*
|
|
65
|
+
* `content_json` → `contentJson` (snake → camel)
|
|
66
|
+
* `contentJson` → `content_json` (camel → snake)
|
|
67
|
+
* `contentJSON` → `content_j_s_o_n` (BROKEN — doesn't round-trip)
|
|
68
|
+
*
|
|
69
|
+
* Any field name that doesn't round-trip under this pair of transforms will
|
|
70
|
+
* silently fail to populate on the client: the wire delivers one casing,
|
|
71
|
+
* the dynamic class's constructor reads another, and the field lands as
|
|
72
|
+
* `undefined`. We catch it here so schema authors see an error at
|
|
73
|
+
* definition time rather than at runtime.
|
|
74
|
+
*
|
|
75
|
+
* Rule: a standard camelCase identifier has runs of one uppercase letter
|
|
76
|
+
* followed by lowercase letters — never two consecutive uppercase letters.
|
|
77
|
+
* `contentJSON` has `JSON` all-uppercase, so we reject it.
|
|
78
|
+
*/
|
|
79
|
+
function assertRoundTrippableCamelCase(modelName, fieldName) {
|
|
80
|
+
// Base fields merged in by defineSchema are already validated; skip.
|
|
81
|
+
if (fieldName === 'id')
|
|
82
|
+
return;
|
|
83
|
+
// Leading-lowercase constraint: fields must be camelCase, not PascalCase.
|
|
84
|
+
// PascalCase is reserved for typenames.
|
|
85
|
+
if (fieldName[0] >= 'A' && fieldName[0] <= 'Z') {
|
|
86
|
+
throw new AbloValidationError(`[defineSchema] ${modelName}.${fieldName}: field names must start lowercase ` +
|
|
87
|
+
`(camelCase). Use "${fieldName[0].toLowerCase()}${fieldName.slice(1)}" instead.`, { code: 'schema_field_not_camelcase' });
|
|
88
|
+
}
|
|
89
|
+
// Two-consecutive-uppercase check. The classic failure mode is
|
|
90
|
+
// `contentJSON`, `contentHTML`, `myURLParam`, etc. These don't round-trip
|
|
91
|
+
// through `postgres.camel` — the snake_case intermediate would be
|
|
92
|
+
// `content_j_s_o_n`, which is not a column that exists.
|
|
93
|
+
for (let i = 0; i < fieldName.length - 1; i++) {
|
|
94
|
+
const a = fieldName[i];
|
|
95
|
+
const b = fieldName[i + 1];
|
|
96
|
+
const aUpper = a >= 'A' && a <= 'Z';
|
|
97
|
+
const bUpper = b >= 'A' && b <= 'Z';
|
|
98
|
+
if (aUpper && bUpper) {
|
|
99
|
+
throw new AbloValidationError(`[defineSchema] ${modelName}.${fieldName}: two consecutive uppercase ` +
|
|
100
|
+
`letters ("${a}${b}") will not round-trip through the ` +
|
|
101
|
+
`snake_case ↔ camelCase transform used by the sync driver. ` +
|
|
102
|
+
`The wire delivers camelCase (lowercase after the first letter of ` +
|
|
103
|
+
`each word); a field named "${fieldName}" would never receive its ` +
|
|
104
|
+
`value and read as undefined on the client. Use standard ` +
|
|
105
|
+
`camelCase (e.g. "contentJson" instead of "contentJSON").`, { code: 'schema_field_consecutive_caps' });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function defineSchema(models, options) {
|
|
110
|
+
// Build validators with base fields merged in, and resolve defaults for
|
|
111
|
+
// `typename` and `persist.store` so downstream code (the generic loader,
|
|
112
|
+
// the hydration pipeline, the Go named-query registry) can rely on these
|
|
113
|
+
// fields being set without re-deriving them at every call site.
|
|
114
|
+
//
|
|
115
|
+
// Defaults:
|
|
116
|
+
// typename ← schema key (e.g. `slideLayer` → `'slideLayer'`)
|
|
117
|
+
// persist.store ← typename (only resolved when `persist` was provided)
|
|
118
|
+
//
|
|
119
|
+
// A consumer that passes `typename: 'SlideLayer'` explicitly (common when
|
|
120
|
+
// the wire shape uses PascalCase while the schema key is camelCase) keeps
|
|
121
|
+
// that value — the fallback only fires when the field is unset.
|
|
122
|
+
//
|
|
123
|
+
// The models record is rebuilt rather than mutated in place because
|
|
124
|
+
// `ModelDef`'s fields are `readonly`. The rebuild is a shallow spread per
|
|
125
|
+
// entry, so the inferred shape/relations/fields metadata references are
|
|
126
|
+
// preserved (no type inference regression at consumer call sites).
|
|
127
|
+
const validators = {};
|
|
128
|
+
const resolvedModels = {};
|
|
129
|
+
const casing = resolveCasing(options?.casing);
|
|
130
|
+
for (const [name, def] of Object.entries(models)) {
|
|
131
|
+
// Catch round-trip-hostile field names at definition time. Deferring
|
|
132
|
+
// this check to runtime means every affected field silently reads
|
|
133
|
+
// `undefined` on the client — and the author only notices when a UI
|
|
134
|
+
// that depends on the field goes blank. Throwing here makes the
|
|
135
|
+
// failure immediate and unambiguous.
|
|
136
|
+
for (const fieldName of Object.keys(def.shape)) {
|
|
137
|
+
assertRoundTrippableCamelCase(name, fieldName);
|
|
138
|
+
}
|
|
139
|
+
validators[name] = baseFieldsSchema.merge(def.schema);
|
|
140
|
+
// Resolve every relation's `foreignKeyColumn` once, now. The builder
|
|
141
|
+
// constructs each RelationDef with `foreignKeyColumn = foreignKey`
|
|
142
|
+
// (identity) so this is a no-op when `casing` is unset — existing
|
|
143
|
+
// consumers get the same behavior they had before the option landed.
|
|
144
|
+
// When `casing: 'snake_case'` is set, every FK flips to its
|
|
145
|
+
// snake_case DB column name here and nowhere else. Server-side SQL
|
|
146
|
+
// compilers read the resolved value directly.
|
|
147
|
+
for (const relName of Object.keys(def.relations)) {
|
|
148
|
+
const rel = def.relations[relName];
|
|
149
|
+
rel.foreignKeyColumn = casing(rel.foreignKey);
|
|
150
|
+
}
|
|
151
|
+
const typename = def.typename ?? name;
|
|
152
|
+
const persist = def.persist
|
|
153
|
+
? { ...def.persist, store: def.persist.store ?? typename }
|
|
154
|
+
: undefined;
|
|
155
|
+
resolvedModels[name] = { ...def, typename, persist };
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
// Cast back to S: we only added values to optional fields that were
|
|
159
|
+
// already part of ModelDef, so the shape is structurally unchanged.
|
|
160
|
+
models: resolvedModels,
|
|
161
|
+
validators: validators,
|
|
162
|
+
identityRoles: options?.identityRoles ?? [],
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Compose the canonical sync-group set this identity is allowed to
|
|
167
|
+
* subscribe to, derived purely from the schema's registered
|
|
168
|
+
* {@link IdentityRole}s. Walks every role, calls its `extract` against
|
|
169
|
+
* the identity context, and substitutes each id into the role's
|
|
170
|
+
* `template`. Output is stable, deduped, and never includes a literal
|
|
171
|
+
* convention string from this function itself — the convention lives
|
|
172
|
+
* 100% in the consumer's schema declaration.
|
|
173
|
+
*
|
|
174
|
+
* Returns `[]` when the schema has no identity roles registered or
|
|
175
|
+
* when no role's extractor produces an id. Caller decides what to do
|
|
176
|
+
* with `[]`; the server's intersect-with-requested logic treats it as
|
|
177
|
+
* "no scope" rather than "match everything."
|
|
178
|
+
*/
|
|
179
|
+
export function composeIdentitySyncGroups(identity, schema) {
|
|
180
|
+
const out = new Set();
|
|
181
|
+
for (const role of schema.identityRoles) {
|
|
182
|
+
for (const id of role.extract(identity)) {
|
|
183
|
+
if (id)
|
|
184
|
+
out.add(role.template.replace('{id}', id));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return Array.from(out);
|
|
188
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent-first shorthand for `model(...)` — the Modal-inspired DX layer.
|
|
3
|
+
*
|
|
4
|
+
* The factory verbs encode the two orthogonal axes that matter for
|
|
5
|
+
* safety and bootstrap behavior:
|
|
6
|
+
*
|
|
7
|
+
* - **Writability** (axis 1): `mutable.*` means clients may send
|
|
8
|
+
* CREATE/UPDATE/DELETE over the `commit` wire protocol.
|
|
9
|
+
* `readOnly.*` means the model is server-managed — deltas stream
|
|
10
|
+
* to clients but clients cannot mutate.
|
|
11
|
+
* - **Load strategy** (axis 2): `.instant` loads at bootstrap,
|
|
12
|
+
* `.lazy` loads on first access, `.manual` requires explicit
|
|
13
|
+
* queries.
|
|
14
|
+
*
|
|
15
|
+
* The two-token form (`mutable.lazy({...})`) reads the safety intent
|
|
16
|
+
* in the first token and the load shape in the second — you know both
|
|
17
|
+
* key facts about the entity before scanning its fields.
|
|
18
|
+
*
|
|
19
|
+
* This is additive: the original `model(...)` factory keeps working.
|
|
20
|
+
* New entities should prefer the verbs; existing entities can migrate
|
|
21
|
+
* entity-by-entity.
|
|
22
|
+
*
|
|
23
|
+
* Example:
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Before — 7 options to read before the fields make sense
|
|
26
|
+
* tasks: model({ title: z.string() }, { ... }, {
|
|
27
|
+
* typename: 'Task', tableName: 'tasks', mutable: true,
|
|
28
|
+
* load: 'lazy', lazyObservable: true, computed: tasksComputed,
|
|
29
|
+
* }),
|
|
30
|
+
*
|
|
31
|
+
* // After — intent reads off the verb; options carry only the
|
|
32
|
+
* // fields that actually diverge from defaults
|
|
33
|
+
* tasks: mutable.lazy({ title: z.string() }, {
|
|
34
|
+
* typename: 'Task', tableName: 'tasks',
|
|
35
|
+
* relations: { ... },
|
|
36
|
+
* computed: tasksComputed,
|
|
37
|
+
* }),
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
import type { z } from 'zod';
|
|
41
|
+
import { type ModelDef, type ModelOptions, type ComputedRecord, type RelationRecord } from './model.js';
|
|
42
|
+
/**
|
|
43
|
+
* Options accepted by every sugar verb. A strict subset of
|
|
44
|
+
* {@link ModelOptions} — anything the verb infers (`mutable`, `load`,
|
|
45
|
+
* `lazyObservable`) is deliberately absent so the call site can't
|
|
46
|
+
* contradict its verb.
|
|
47
|
+
*/
|
|
48
|
+
export interface SugarOptions<R extends RelationRecord = RelationRecord, C extends ComputedRecord = ComputedRecord> {
|
|
49
|
+
/** Relations to other models. Same shape as `model()`'s second arg. */
|
|
50
|
+
relations?: R;
|
|
51
|
+
/** Computed getters installed on the model class prototype. */
|
|
52
|
+
computed?: C;
|
|
53
|
+
/**
|
|
54
|
+
* Wire `__typename` (PascalCase, e.g. `'Task'`). Defaults to the schema
|
|
55
|
+
* key via `defineSchema` — override when the wire shape differs from
|
|
56
|
+
* the camelCase schema key.
|
|
57
|
+
*/
|
|
58
|
+
typename?: string;
|
|
59
|
+
/**
|
|
60
|
+
* Actual Postgres table name. Override when Prisma's `@@map` diverges
|
|
61
|
+
* from the naive snake_case of the typename (e.g. `Member` maps to
|
|
62
|
+
* `'member'` singular, not `'members'`).
|
|
63
|
+
*/
|
|
64
|
+
tableName?: string;
|
|
65
|
+
/**
|
|
66
|
+
* Whether the table has an `organization_id` column. Default: `true`.
|
|
67
|
+
* Set `false` for system-scoped tables (subscriptions, teams, etc.).
|
|
68
|
+
*/
|
|
69
|
+
orgScoped?: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* Scope rows via a parent table when this table has no
|
|
72
|
+
* `organization_id` column. See {@link ModelOptions.scopedVia}.
|
|
73
|
+
*/
|
|
74
|
+
scopedVia?: ModelOptions['scopedVia'];
|
|
75
|
+
/**
|
|
76
|
+
* Template for participant scope derivation (e.g. `'matter:{id}'`).
|
|
77
|
+
* Omit for nested children whose access cascades from a parent.
|
|
78
|
+
*/
|
|
79
|
+
syncGroupFormat?: string;
|
|
80
|
+
/** Max rows loaded during bootstrap. Only applies to `.instant`. */
|
|
81
|
+
bootstrapLimit?: number;
|
|
82
|
+
/** Bootstrap sort order (e.g. `'created_at DESC'`). */
|
|
83
|
+
bootstrapOrderBy?: string;
|
|
84
|
+
/** IndexedDB persistence hints — see {@link ModelOptions.persist}. */
|
|
85
|
+
persist?: ModelOptions['persist'];
|
|
86
|
+
/**
|
|
87
|
+
* Defer MobX observability to first access. Override the verb's
|
|
88
|
+
* default when a `.lazy` model is small enough that eager MobX setup
|
|
89
|
+
* is fine, or a `.instant` model is hot enough to justify deferral.
|
|
90
|
+
*/
|
|
91
|
+
lazyObservable?: boolean;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Client-writable entities. `mutable.*` is the opt-in signal for wire
|
|
95
|
+
* mutations via `commit` — equivalent to setting
|
|
96
|
+
* `{ mutable: true, load: X }` on `model()`.
|
|
97
|
+
*
|
|
98
|
+
* Pick the load suffix by data-access pattern:
|
|
99
|
+
* - `.instant` — small, always-needed (Theme, Layout, StatusGroup)
|
|
100
|
+
* - `.lazy` — large collections fetched on first query
|
|
101
|
+
* (SlideLayer, Message, Task)
|
|
102
|
+
* - `.manual` — never auto-loaded; explicit queries only
|
|
103
|
+
*/
|
|
104
|
+
export declare const mutable: {
|
|
105
|
+
instant: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
106
|
+
lazy: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
107
|
+
manual: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Server-managed entities. `readOnly.*` means clients subscribe to
|
|
111
|
+
* deltas but cannot emit mutations — any `commit` op for this model
|
|
112
|
+
* is rejected at the server with "Unknown model."
|
|
113
|
+
*
|
|
114
|
+
* Use for:
|
|
115
|
+
* - Server-written state: `sync_deltas`, `presence`, version vectors
|
|
116
|
+
* - Ingestion pipelines: digest entries, filing jobs
|
|
117
|
+
* - Audit surfaces: anything where clients watch but only the server
|
|
118
|
+
* writes
|
|
119
|
+
*/
|
|
120
|
+
export declare const readOnly: {
|
|
121
|
+
instant: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
122
|
+
lazy: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
123
|
+
/**
|
|
124
|
+
* Internal-only: never auto-loaded, never written by clients. The
|
|
125
|
+
* strongest safety posture — use for tables the SDK must know about
|
|
126
|
+
* (for type inference) but that should never flow over the wire.
|
|
127
|
+
*/
|
|
128
|
+
internal: <Shape extends z.ZodRawShape, R extends RelationRecord = Record<string, never>, C extends ComputedRecord = Record<string, never>>(shape: Shape, opts?: SugarOptions<R, C>) => ModelDef<Shape, R, C>;
|
|
129
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Intent-first shorthand for `model(...)` — the Modal-inspired DX layer.
|
|
3
|
+
*
|
|
4
|
+
* The factory verbs encode the two orthogonal axes that matter for
|
|
5
|
+
* safety and bootstrap behavior:
|
|
6
|
+
*
|
|
7
|
+
* - **Writability** (axis 1): `mutable.*` means clients may send
|
|
8
|
+
* CREATE/UPDATE/DELETE over the `commit` wire protocol.
|
|
9
|
+
* `readOnly.*` means the model is server-managed — deltas stream
|
|
10
|
+
* to clients but clients cannot mutate.
|
|
11
|
+
* - **Load strategy** (axis 2): `.instant` loads at bootstrap,
|
|
12
|
+
* `.lazy` loads on first access, `.manual` requires explicit
|
|
13
|
+
* queries.
|
|
14
|
+
*
|
|
15
|
+
* The two-token form (`mutable.lazy({...})`) reads the safety intent
|
|
16
|
+
* in the first token and the load shape in the second — you know both
|
|
17
|
+
* key facts about the entity before scanning its fields.
|
|
18
|
+
*
|
|
19
|
+
* This is additive: the original `model(...)` factory keeps working.
|
|
20
|
+
* New entities should prefer the verbs; existing entities can migrate
|
|
21
|
+
* entity-by-entity.
|
|
22
|
+
*
|
|
23
|
+
* Example:
|
|
24
|
+
* ```ts
|
|
25
|
+
* // Before — 7 options to read before the fields make sense
|
|
26
|
+
* tasks: model({ title: z.string() }, { ... }, {
|
|
27
|
+
* typename: 'Task', tableName: 'tasks', mutable: true,
|
|
28
|
+
* load: 'lazy', lazyObservable: true, computed: tasksComputed,
|
|
29
|
+
* }),
|
|
30
|
+
*
|
|
31
|
+
* // After — intent reads off the verb; options carry only the
|
|
32
|
+
* // fields that actually diverge from defaults
|
|
33
|
+
* tasks: mutable.lazy({ title: z.string() }, {
|
|
34
|
+
* typename: 'Task', tableName: 'tasks',
|
|
35
|
+
* relations: { ... },
|
|
36
|
+
* computed: tasksComputed,
|
|
37
|
+
* }),
|
|
38
|
+
* ```
|
|
39
|
+
*/
|
|
40
|
+
import { model, } from './model.js';
|
|
41
|
+
/** Internal helper — builds a ModelDef with baseline safety+load flags applied. */
|
|
42
|
+
function build(shape, opts, baseline) {
|
|
43
|
+
return model(shape, (opts?.relations ?? {}), {
|
|
44
|
+
mutable: baseline.mutable,
|
|
45
|
+
load: baseline.load,
|
|
46
|
+
lazyObservable: opts?.lazyObservable ?? baseline.lazyObservable,
|
|
47
|
+
typename: opts?.typename,
|
|
48
|
+
tableName: opts?.tableName,
|
|
49
|
+
orgScoped: opts?.orgScoped,
|
|
50
|
+
scopedVia: opts?.scopedVia,
|
|
51
|
+
syncGroupFormat: opts?.syncGroupFormat,
|
|
52
|
+
bootstrapLimit: opts?.bootstrapLimit,
|
|
53
|
+
bootstrapOrderBy: opts?.bootstrapOrderBy,
|
|
54
|
+
persist: opts?.persist,
|
|
55
|
+
computed: opts?.computed,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Client-writable entities. `mutable.*` is the opt-in signal for wire
|
|
60
|
+
* mutations via `commit` — equivalent to setting
|
|
61
|
+
* `{ mutable: true, load: X }` on `model()`.
|
|
62
|
+
*
|
|
63
|
+
* Pick the load suffix by data-access pattern:
|
|
64
|
+
* - `.instant` — small, always-needed (Theme, Layout, StatusGroup)
|
|
65
|
+
* - `.lazy` — large collections fetched on first query
|
|
66
|
+
* (SlideLayer, Message, Task)
|
|
67
|
+
* - `.manual` — never auto-loaded; explicit queries only
|
|
68
|
+
*/
|
|
69
|
+
export const mutable = {
|
|
70
|
+
instant: (shape, opts) => build(shape, opts, { mutable: true, load: 'instant', lazyObservable: false }),
|
|
71
|
+
lazy: (shape, opts) => build(shape, opts, { mutable: true, load: 'lazy', lazyObservable: true }),
|
|
72
|
+
manual: (shape, opts) => build(shape, opts, { mutable: true, load: 'manual', lazyObservable: true }),
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* Server-managed entities. `readOnly.*` means clients subscribe to
|
|
76
|
+
* deltas but cannot emit mutations — any `commit` op for this model
|
|
77
|
+
* is rejected at the server with "Unknown model."
|
|
78
|
+
*
|
|
79
|
+
* Use for:
|
|
80
|
+
* - Server-written state: `sync_deltas`, `presence`, version vectors
|
|
81
|
+
* - Ingestion pipelines: digest entries, filing jobs
|
|
82
|
+
* - Audit surfaces: anything where clients watch but only the server
|
|
83
|
+
* writes
|
|
84
|
+
*/
|
|
85
|
+
export const readOnly = {
|
|
86
|
+
instant: (shape, opts) => build(shape, opts, { mutable: false, load: 'instant', lazyObservable: false }),
|
|
87
|
+
lazy: (shape, opts) => build(shape, opts, { mutable: false, load: 'lazy', lazyObservable: true }),
|
|
88
|
+
/**
|
|
89
|
+
* Internal-only: never auto-loaded, never written by clients. The
|
|
90
|
+
* strongest safety posture — use for tables the SDK must know about
|
|
91
|
+
* (for type inference) but that should never flow over the wire.
|
|
92
|
+
*/
|
|
93
|
+
internal: (shape, opts) => build(shape, opts, { mutable: false, load: 'manual', lazyObservable: true }),
|
|
94
|
+
};
|