@abloatai/ablo 0.5.1 → 0.7.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 +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema diff + migration planning — the pure core of the managed-migration loop.
|
|
3
|
+
*
|
|
4
|
+
* Given two serialized schemas (the active one and the one being pushed), produce
|
|
5
|
+
* an ordered list of {@link MigrationStep}s describing how to evolve the database,
|
|
6
|
+
* and a {@link MigrationClassification} splitting the risky parts into *warnings*
|
|
7
|
+
* (execute but may lose/risk data) and *unexecutable* steps (fail on a non-empty
|
|
8
|
+
* table without a backfill/default). SQL emission and execution live elsewhere
|
|
9
|
+
* (server-side, where the type map + RLS live); this module is intentionally pure
|
|
10
|
+
* and DB-free so it is exhaustively unit-testable and reusable by the CLI.
|
|
11
|
+
*
|
|
12
|
+
* Design borrowed from mature tools:
|
|
13
|
+
* - **Drizzle Kit**: keep the differ pure and inject RENAME decisions as data
|
|
14
|
+
* (the {@link RenameHints} resolver seam) rather than guessing — the same
|
|
15
|
+
* engine is then headless-testable and drivable by an interactive prompt.
|
|
16
|
+
* - **Prisma migration engine**: a two-tier destructive classification
|
|
17
|
+
* (warning vs unexecutable) and a type-change sub-tier
|
|
18
|
+
* (safe / risky / not-castable) that decides in-place `ALTER TYPE` vs a
|
|
19
|
+
* lossy drop-and-recreate.
|
|
20
|
+
* - **Atlas**: a single `alter_field` step carrying *which* facets changed
|
|
21
|
+
* (type / nullability / enum / index) instead of N discrete alter steps.
|
|
22
|
+
*
|
|
23
|
+
* Step ordering is the expand→contract sequence (add before drop, widen before
|
|
24
|
+
* narrow): create models → rename → add columns (always nullable) → alter →
|
|
25
|
+
* drop columns → drop models. NOT NULL is never set on add — it is an
|
|
26
|
+
* `alter_field` nullability change that a backfill must precede.
|
|
27
|
+
*/
|
|
28
|
+
// ── Cast safety matrix ────────────────────────────────────────────────────────
|
|
29
|
+
// Keyed `${from}->${to}` over the 6 sync field types. Targets that map to TEXT
|
|
30
|
+
// (`string`) accept any scalar losslessly; tightening into an `enum` adds a CHECK
|
|
31
|
+
// that existing rows may violate (risky); narrowing into number/bool/date/json
|
|
32
|
+
// is risky (USING cast can fail per-row) or impossible (notCastable).
|
|
33
|
+
const CAST = {
|
|
34
|
+
// → string (TEXT): always safe
|
|
35
|
+
'number->string': 'safe', 'boolean->string': 'safe', 'date->string': 'safe',
|
|
36
|
+
'enum->string': 'safe', 'json->string': 'safe',
|
|
37
|
+
// → enum (TEXT + CHECK): constraint over existing data is risky
|
|
38
|
+
'string->enum': 'risky', 'number->enum': 'risky', 'boolean->enum': 'risky',
|
|
39
|
+
'date->enum': 'risky', 'json->enum': 'notCastable',
|
|
40
|
+
// → number (DOUBLE PRECISION)
|
|
41
|
+
'string->number': 'risky', 'enum->number': 'risky', 'boolean->number': 'notCastable',
|
|
42
|
+
'date->number': 'notCastable', 'json->number': 'notCastable',
|
|
43
|
+
// → boolean
|
|
44
|
+
'string->boolean': 'risky', 'enum->boolean': 'risky', 'number->boolean': 'risky',
|
|
45
|
+
'date->boolean': 'notCastable', 'json->boolean': 'notCastable',
|
|
46
|
+
// → date (TIMESTAMPTZ)
|
|
47
|
+
'string->date': 'risky', 'enum->date': 'risky', 'number->date': 'notCastable',
|
|
48
|
+
'boolean->date': 'notCastable', 'json->date': 'notCastable',
|
|
49
|
+
// → json (JSONB)
|
|
50
|
+
'string->json': 'risky', 'enum->json': 'risky', 'number->json': 'notCastable',
|
|
51
|
+
'boolean->json': 'notCastable', 'date->json': 'notCastable',
|
|
52
|
+
};
|
|
53
|
+
export function classifyCast(from, to) {
|
|
54
|
+
if (from === to)
|
|
55
|
+
return 'safe';
|
|
56
|
+
return CAST[`${from}->${to}`] ?? 'notCastable';
|
|
57
|
+
}
|
|
58
|
+
// ── Diff ──────────────────────────────────────────────────────────────────────
|
|
59
|
+
function diffEnumValues(from, to) {
|
|
60
|
+
const a = new Set(from ?? []);
|
|
61
|
+
const b = new Set(to ?? []);
|
|
62
|
+
const added = [...b].filter((v) => !a.has(v));
|
|
63
|
+
const removed = [...a].filter((v) => !b.has(v));
|
|
64
|
+
if (added.length === 0 && removed.length === 0)
|
|
65
|
+
return undefined;
|
|
66
|
+
return { added, removed };
|
|
67
|
+
}
|
|
68
|
+
function camelToSnake(identifier) {
|
|
69
|
+
return identifier.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
70
|
+
}
|
|
71
|
+
function columnNameOf(fieldName, meta) {
|
|
72
|
+
return meta.column ?? camelToSnake(fieldName);
|
|
73
|
+
}
|
|
74
|
+
function diffField(prevFieldName, nextFieldName, prev, next) {
|
|
75
|
+
const changes = {};
|
|
76
|
+
const prevColumn = columnNameOf(prevFieldName, prev);
|
|
77
|
+
const nextColumn = columnNameOf(nextFieldName, next);
|
|
78
|
+
if (prevColumn !== nextColumn) {
|
|
79
|
+
changes.column = { from: prevColumn, to: nextColumn };
|
|
80
|
+
}
|
|
81
|
+
if (prev.type !== next.type) {
|
|
82
|
+
changes.type = { from: prev.type, to: next.type, cast: classifyCast(prev.type, next.type) };
|
|
83
|
+
}
|
|
84
|
+
if (prev.isOptional !== next.isOptional) {
|
|
85
|
+
changes.nullability = { fromOptional: prev.isOptional, toOptional: next.isOptional };
|
|
86
|
+
}
|
|
87
|
+
// Enum value drift only matters while the field is (still) an enum; a type
|
|
88
|
+
// change away from enum is already captured by `type`.
|
|
89
|
+
if (prev.type === 'enum' && next.type === 'enum') {
|
|
90
|
+
const ev = diffEnumValues(prev.enumValues, next.enumValues);
|
|
91
|
+
if (ev)
|
|
92
|
+
changes.enumValues = ev;
|
|
93
|
+
}
|
|
94
|
+
if (prev.isIndexed !== next.isIndexed) {
|
|
95
|
+
changes.indexed = { from: prev.isIndexed, to: next.isIndexed };
|
|
96
|
+
}
|
|
97
|
+
return Object.keys(changes).length === 0 ? null : changes;
|
|
98
|
+
}
|
|
99
|
+
function tableNameOf(model, key) {
|
|
100
|
+
return model.tableName ?? key;
|
|
101
|
+
}
|
|
102
|
+
function diffModelFields(model, prev, next, fieldRenames) {
|
|
103
|
+
const steps = [];
|
|
104
|
+
const renameByNewName = new Map(fieldRenames.map((r) => [r.to, r.from]));
|
|
105
|
+
const renamedFromNames = new Set(fieldRenames.map((r) => r.from));
|
|
106
|
+
// Renames first (so subsequent alter steps reference the new name).
|
|
107
|
+
for (const { from, to } of fieldRenames) {
|
|
108
|
+
if (from in prev.fields && to in next.fields) {
|
|
109
|
+
steps.push({ kind: 'rename_field', model, from, to });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Added (present in next, not in prev, and not the target of a rename).
|
|
113
|
+
for (const [name, meta] of Object.entries(next.fields)) {
|
|
114
|
+
if (name in prev.fields)
|
|
115
|
+
continue;
|
|
116
|
+
if (renameByNewName.has(name))
|
|
117
|
+
continue;
|
|
118
|
+
steps.push({ kind: 'add_field', model, field: name, meta });
|
|
119
|
+
}
|
|
120
|
+
// Altered: every field present in both (directly or via rename).
|
|
121
|
+
for (const [name, nextMeta] of Object.entries(next.fields)) {
|
|
122
|
+
const prevName = renameByNewName.get(name) ?? name;
|
|
123
|
+
const prevMeta = prev.fields[prevName];
|
|
124
|
+
if (!prevMeta)
|
|
125
|
+
continue;
|
|
126
|
+
const changes = diffField(prevName, name, prevMeta, nextMeta);
|
|
127
|
+
if (changes?.column && renameByNewName.has(name)) {
|
|
128
|
+
// A hinted logical field rename already emits `rename_field`, whose
|
|
129
|
+
// lowering renames the physical column when needed. Do not emit a
|
|
130
|
+
// second `alter_field.column` for the same transition.
|
|
131
|
+
delete changes.column;
|
|
132
|
+
}
|
|
133
|
+
if (changes && Object.keys(changes).length > 0) {
|
|
134
|
+
steps.push({ kind: 'alter_field', model, field: name, changes });
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Dropped (present in prev, not in next, and not renamed away).
|
|
138
|
+
for (const name of Object.keys(prev.fields)) {
|
|
139
|
+
if (name in next.fields)
|
|
140
|
+
continue;
|
|
141
|
+
if (renamedFromNames.has(name))
|
|
142
|
+
continue;
|
|
143
|
+
steps.push({ kind: 'drop_field', model, field: name });
|
|
144
|
+
}
|
|
145
|
+
return steps;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Diff two serialized schemas into an ordered, expand→contract migration plan.
|
|
149
|
+
* `prev` is the active schema (`null` for a first push → all creates). Rename
|
|
150
|
+
* decisions are supplied via {@link RenameHints}; anything not hinted reads as
|
|
151
|
+
* drop+add.
|
|
152
|
+
*/
|
|
153
|
+
export function diffSchema(prev, next, hints = {}) {
|
|
154
|
+
if (!prev) {
|
|
155
|
+
// First push: every model is created, with its fields carried in the
|
|
156
|
+
// create (no per-field add steps — the table is born with them).
|
|
157
|
+
return Object.entries(next.models).map(([model, def]) => ({
|
|
158
|
+
kind: 'create_model',
|
|
159
|
+
model,
|
|
160
|
+
tableName: tableNameOf(def, model),
|
|
161
|
+
}));
|
|
162
|
+
}
|
|
163
|
+
const modelRenames = hints.models ?? [];
|
|
164
|
+
const renameByNewModel = new Map(modelRenames.map((r) => [r.to, r.from]));
|
|
165
|
+
const renamedFromModels = new Set(modelRenames.map((r) => r.from));
|
|
166
|
+
const fieldHints = hints.fields ?? [];
|
|
167
|
+
const creates = [];
|
|
168
|
+
const renames = [];
|
|
169
|
+
const fieldSteps = [];
|
|
170
|
+
const drops = [];
|
|
171
|
+
// New + renamed models, and per-model field diffs.
|
|
172
|
+
for (const [model, nextDef] of Object.entries(next.models)) {
|
|
173
|
+
const prevModelKey = renameByNewModel.get(model) ?? model;
|
|
174
|
+
const prevDef = prev.models[prevModelKey];
|
|
175
|
+
if (!prevDef) {
|
|
176
|
+
creates.push({ kind: 'create_model', model, tableName: tableNameOf(nextDef, model) });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
if (renameByNewModel.has(model)) {
|
|
180
|
+
renames.push({ kind: 'rename_model', from: prevModelKey, to: model });
|
|
181
|
+
}
|
|
182
|
+
const myFieldRenames = fieldHints
|
|
183
|
+
.filter((f) => f.model === model)
|
|
184
|
+
.map((f) => ({ from: f.from, to: f.to }));
|
|
185
|
+
fieldSteps.push(...diffModelFields(model, prevDef, nextDef, myFieldRenames));
|
|
186
|
+
}
|
|
187
|
+
// Dropped models (in prev, not in next, not renamed away).
|
|
188
|
+
for (const [model, prevDef] of Object.entries(prev.models)) {
|
|
189
|
+
if (model in next.models)
|
|
190
|
+
continue;
|
|
191
|
+
if (renamedFromModels.has(model))
|
|
192
|
+
continue;
|
|
193
|
+
drops.push({ kind: 'drop_model', model, tableName: tableNameOf(prevDef, model) });
|
|
194
|
+
}
|
|
195
|
+
// Expand → contract ordering. Within fieldSteps the per-model helper already
|
|
196
|
+
// emits rename → add → alter → drop_field, which preserves the same invariant.
|
|
197
|
+
return [...creates, ...renames, ...fieldSteps, ...drops];
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Classify a plan's steps into Prisma-style warnings vs unexecutable. The IR
|
|
201
|
+
* carries no per-field default, so a non-optional `add_field` is conservatively
|
|
202
|
+
* unexecutable (a backfill or default resolves it) — we cannot prove a default
|
|
203
|
+
* exists. Classification is rule-based (schema-derived); the runtime layer can
|
|
204
|
+
* downgrade a signal to a no-op when the target table is empty.
|
|
205
|
+
*/
|
|
206
|
+
export function classifyMigration(steps) {
|
|
207
|
+
const warnings = [];
|
|
208
|
+
const unexecutable = [];
|
|
209
|
+
for (const step of steps) {
|
|
210
|
+
switch (step.kind) {
|
|
211
|
+
case 'drop_model':
|
|
212
|
+
warnings.push({ code: 'drop_model', model: step.model, detail: `drops table for "${step.model}" (data loss)` });
|
|
213
|
+
break;
|
|
214
|
+
case 'drop_field':
|
|
215
|
+
warnings.push({ code: 'drop_field', model: step.model, field: step.field, detail: `drops column "${step.field}" (data loss)` });
|
|
216
|
+
break;
|
|
217
|
+
case 'add_field':
|
|
218
|
+
if (!step.meta.isOptional) {
|
|
219
|
+
unexecutable.push({
|
|
220
|
+
code: 'required_field_added',
|
|
221
|
+
model: step.model,
|
|
222
|
+
field: step.field,
|
|
223
|
+
detail: `adds required column "${step.field}" — needs a default or backfill on a non-empty table`,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
break;
|
|
227
|
+
case 'alter_field': {
|
|
228
|
+
const { changes } = step;
|
|
229
|
+
if (changes.nullability && changes.nullability.fromOptional && !changes.nullability.toOptional) {
|
|
230
|
+
unexecutable.push({
|
|
231
|
+
code: 'made_required',
|
|
232
|
+
model: step.model,
|
|
233
|
+
field: step.field,
|
|
234
|
+
detail: `makes "${step.field}" required — fails if existing rows are NULL`,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (changes.type) {
|
|
238
|
+
if (changes.type.cast === 'risky') {
|
|
239
|
+
warnings.push({ code: 'risky_cast', model: step.model, field: step.field, detail: `${changes.type.from} → ${changes.type.to} may fail per-row` });
|
|
240
|
+
}
|
|
241
|
+
else if (changes.type.cast === 'notCastable') {
|
|
242
|
+
warnings.push({ code: 'lossy_recreate', model: step.model, field: step.field, detail: `${changes.type.from} → ${changes.type.to} requires drop-and-recreate (data loss)` });
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (changes.enumValues && changes.enumValues.removed.length > 0) {
|
|
246
|
+
warnings.push({
|
|
247
|
+
code: 'enum_value_removed',
|
|
248
|
+
model: step.model,
|
|
249
|
+
field: step.field,
|
|
250
|
+
detail: `removes enum value(s) ${changes.enumValues.removed.join(', ')} — rows using them violate the new CHECK`,
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
// create_model, rename_model, rename_field, add optional field: non-destructive.
|
|
256
|
+
default:
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return { warnings, unexecutable };
|
|
261
|
+
}
|
|
262
|
+
/** Convenience: a plan is safe to auto-apply iff it has no unexecutable steps. */
|
|
263
|
+
export function isAutoApplicable(classification) {
|
|
264
|
+
return classification.unexecutable.length === 0;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Does a provided backfill resolve this blocker? Only the two row-dependent
|
|
268
|
+
* blockers (`required_field_added`, `made_required`) are backfill-resolvable; a
|
|
269
|
+
* data-loss *warning* is not — that always needs `force`.
|
|
270
|
+
*/
|
|
271
|
+
export function isBlockerResolved(signal, backfills) {
|
|
272
|
+
if (signal.code !== 'required_field_added' && signal.code !== 'made_required')
|
|
273
|
+
return false;
|
|
274
|
+
return backfills.some((b) => b.model === signal.model && b.field === signal.field);
|
|
275
|
+
}
|
|
276
|
+
/** The unexecutable signals NOT covered by a supplied backfill. Empty → the push
|
|
277
|
+
* can proceed (modulo the separate `warnings`/`force` gate). */
|
|
278
|
+
export function unresolvedBlockers(classification, backfills) {
|
|
279
|
+
return classification.unexecutable.filter((s) => !isBlockerResolved(s, backfills));
|
|
280
|
+
}
|
package/dist/schema/field.d.ts
CHANGED
|
@@ -31,6 +31,11 @@ export interface FieldMeta {
|
|
|
31
31
|
isOptional: boolean;
|
|
32
32
|
/** Whether the field was marked indexed via `.indexed()`. */
|
|
33
33
|
isIndexed: boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Physical database column name override. When absent, SQL layers derive
|
|
36
|
+
* the column from the field name using the active casing convention.
|
|
37
|
+
*/
|
|
38
|
+
column?: string;
|
|
34
39
|
/** For enums: the allowed values. */
|
|
35
40
|
enumValues?: readonly string[];
|
|
36
41
|
}
|
|
@@ -73,27 +78,21 @@ export declare function inferFieldMetaFromZod(schema: z.ZodType): FieldMeta;
|
|
|
73
78
|
* `model.ts:inferMetaFromZod`.
|
|
74
79
|
*/
|
|
75
80
|
export declare function resolveFieldMeta(schema: z.ZodType): FieldMeta;
|
|
81
|
+
export type FieldBuilder<T extends z.ZodType> = T & {
|
|
82
|
+
indexed(): FieldBuilder<T>;
|
|
83
|
+
from(column: string): FieldBuilder<T>;
|
|
84
|
+
};
|
|
76
85
|
export declare const field: {
|
|
77
86
|
/** String field */
|
|
78
|
-
readonly string: () => z.ZodString
|
|
79
|
-
indexed(): z.ZodString;
|
|
80
|
-
};
|
|
87
|
+
readonly string: () => FieldBuilder<z.ZodString>;
|
|
81
88
|
/** Number field */
|
|
82
|
-
readonly number: () => z.ZodNumber
|
|
83
|
-
indexed(): z.ZodNumber;
|
|
84
|
-
};
|
|
89
|
+
readonly number: () => FieldBuilder<z.ZodNumber>;
|
|
85
90
|
/** Boolean field */
|
|
86
|
-
readonly boolean: () => z.ZodBoolean
|
|
87
|
-
indexed(): z.ZodBoolean;
|
|
88
|
-
};
|
|
91
|
+
readonly boolean: () => FieldBuilder<z.ZodBoolean>;
|
|
89
92
|
/** Date field */
|
|
90
|
-
readonly date: () => z.ZodDate
|
|
91
|
-
indexed(): z.ZodDate;
|
|
92
|
-
};
|
|
93
|
+
readonly date: () => FieldBuilder<z.ZodDate>;
|
|
93
94
|
/** Enum field with constrained string values */
|
|
94
|
-
readonly enum: <const T extends readonly [string, ...string[]]>(values: T) => z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never
|
|
95
|
-
indexed(): z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_2 ? { [k in keyof T_2]: T_2[k]; } : never>;
|
|
96
|
-
};
|
|
95
|
+
readonly enum: <const T extends readonly [string, ...string[]]>(values: T) => FieldBuilder<z.ZodEnum<{ [k_1 in T[number]]: k_1; } extends infer T_1 ? { [k in keyof T_1]: T_1[k]; } : never>>;
|
|
97
96
|
/**
|
|
98
97
|
* JSON field. Three call shapes:
|
|
99
98
|
*
|
|
@@ -124,11 +123,9 @@ export declare const field: {
|
|
|
124
123
|
* deck.metadataJson.icon // 'presentation' (typed, with default)
|
|
125
124
|
* ```
|
|
126
125
|
*/
|
|
127
|
-
readonly json: <T extends z.ZodType = z.ZodUnknown>(schemaOrShape?: T | z.ZodRawShape) => z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown
|
|
128
|
-
indexed(): z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>;
|
|
129
|
-
};
|
|
126
|
+
readonly json: <T extends z.ZodType = z.ZodUnknown>(schemaOrShape?: T | z.ZodRawShape) => FieldBuilder<z.ZodType<unknown, unknown, z.core.$ZodTypeInternals<unknown, unknown>>>;
|
|
130
127
|
/** Indexed string field (shorthand for `field.string().indexed()`). */
|
|
131
|
-
readonly id: () => z.ZodString
|
|
128
|
+
readonly id: () => FieldBuilder<z.ZodString>;
|
|
132
129
|
};
|
|
133
130
|
/** Mark a Zod schema as indexed for fast lookups (function form). */
|
|
134
131
|
export declare function indexed<T extends z.ZodType>(schema: T): T;
|
package/dist/schema/field.js
CHANGED
|
@@ -142,7 +142,15 @@ export function inferFieldMetaFromZod(schema) {
|
|
|
142
142
|
current instanceof z.ZodArray ||
|
|
143
143
|
current instanceof z.ZodRecord ||
|
|
144
144
|
current instanceof z.ZodUnion ||
|
|
145
|
-
current instanceof z.ZodUnknown
|
|
145
|
+
current instanceof z.ZodUnknown ||
|
|
146
|
+
// `z.custom<T>()` is an opaque, structurally-uninspectable blob —
|
|
147
|
+
// by construction the engine can't see its shape, which is exactly
|
|
148
|
+
// the JSON-blob pattern (ProseMirror docs, LayerData/LayerStyle maps,
|
|
149
|
+
// chart specs). Classifying it as `'string'` (the fallthrough default)
|
|
150
|
+
// gave these fields deep MobX observability instead of the intended
|
|
151
|
+
// `observable.ref` (see Ablo.ts registerProperty), producing the
|
|
152
|
+
// microtask-storm + nested-reactivity-drift bug on live updates.
|
|
153
|
+
current instanceof z.ZodCustom) {
|
|
146
154
|
type = 'json';
|
|
147
155
|
}
|
|
148
156
|
return { type, isOptional, isIndexed: false, enumValues };
|
|
@@ -169,39 +177,44 @@ export function resolveFieldMeta(schema) {
|
|
|
169
177
|
return attached;
|
|
170
178
|
return inferFieldMetaFromZod(schema);
|
|
171
179
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
/** Add
|
|
178
|
-
function
|
|
179
|
-
const described = schema.describe(encodeMeta(
|
|
180
|
-
described.indexed = () => {
|
|
181
|
-
|
|
180
|
+
function assertColumnName(column) {
|
|
181
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]{0,62}$/.test(column)) {
|
|
182
|
+
throw new Error(`field.from(): invalid column identifier ${JSON.stringify(column)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/** Add sync-engine chain methods to a Zod schema without disturbing its type. */
|
|
186
|
+
function withFieldMethods(schema, meta) {
|
|
187
|
+
const described = schema.describe(encodeMeta(meta));
|
|
188
|
+
described.indexed = () => withFieldMethods(schema, { ...meta, isIndexed: true });
|
|
189
|
+
described.from = (column) => {
|
|
190
|
+
assertColumnName(column);
|
|
191
|
+
return withFieldMethods(schema, { ...meta, column });
|
|
182
192
|
};
|
|
183
193
|
return described;
|
|
184
194
|
}
|
|
195
|
+
function buildField(schema, baseMeta) {
|
|
196
|
+
return withFieldMethods(schema, { ...baseMeta, isIndexed: false });
|
|
197
|
+
}
|
|
185
198
|
export const field = {
|
|
186
199
|
/** String field */
|
|
187
200
|
string() {
|
|
188
|
-
return
|
|
201
|
+
return buildField(z.string(), { type: 'string' });
|
|
189
202
|
},
|
|
190
203
|
/** Number field */
|
|
191
204
|
number() {
|
|
192
|
-
return
|
|
205
|
+
return buildField(z.number(), { type: 'number' });
|
|
193
206
|
},
|
|
194
207
|
/** Boolean field */
|
|
195
208
|
boolean() {
|
|
196
|
-
return
|
|
209
|
+
return buildField(z.boolean(), { type: 'boolean' });
|
|
197
210
|
},
|
|
198
211
|
/** Date field */
|
|
199
212
|
date() {
|
|
200
|
-
return
|
|
213
|
+
return buildField(z.date(), { type: 'date' });
|
|
201
214
|
},
|
|
202
215
|
/** Enum field with constrained string values */
|
|
203
216
|
enum(values) {
|
|
204
|
-
return
|
|
217
|
+
return buildField(z.enum(values), { type: 'enum', enumValues: values });
|
|
205
218
|
},
|
|
206
219
|
/**
|
|
207
220
|
* JSON field. Three call shapes:
|
|
@@ -245,7 +258,7 @@ export const field = {
|
|
|
245
258
|
// Plain object shape → wrap in z.object() for the sub-property pattern
|
|
246
259
|
inner = z.object(schemaOrShape);
|
|
247
260
|
}
|
|
248
|
-
return
|
|
261
|
+
return buildField(inner, { type: 'json' });
|
|
249
262
|
},
|
|
250
263
|
/** Indexed string field (shorthand for `field.string().indexed()`). */
|
|
251
264
|
id() {
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → TypeScript type emission — the `generate` half of the loop (the
|
|
3
|
+
* counterpart to `diff`/migrate). Lowers a serialized schema to a `.ts` file of
|
|
4
|
+
* row interfaces + an `AbloSchema` map, so a consumer's app is typed against the
|
|
5
|
+
* exact schema that the database and sync layer enforce. Pure (returns source
|
|
6
|
+
* text); the CLI writes it to disk.
|
|
7
|
+
*
|
|
8
|
+
* This is the SDK's front door: the developer writes ONE `defineSchema`, pushes
|
|
9
|
+
* it (which migrates the DB), and generates types FROM the same schema — so the
|
|
10
|
+
* types they code against, the rows the DB stores, and the entities sync moves
|
|
11
|
+
* are provably the same thing.
|
|
12
|
+
*
|
|
13
|
+
* v1 emits the row shape (base columns + declared fields, enums as literal
|
|
14
|
+
* unions). Relations are resolved by the runtime SDK's typed accessors and are
|
|
15
|
+
* not expanded here.
|
|
16
|
+
*/
|
|
17
|
+
import type { SchemaJSON } from './serialize.js';
|
|
18
|
+
/** Emit a TypeScript module of row interfaces + the `AbloSchema` map. */
|
|
19
|
+
export declare function generateTypes(schema: SchemaJSON): string;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → TypeScript type emission — the `generate` half of the loop (the
|
|
3
|
+
* counterpart to `diff`/migrate). Lowers a serialized schema to a `.ts` file of
|
|
4
|
+
* row interfaces + an `AbloSchema` map, so a consumer's app is typed against the
|
|
5
|
+
* exact schema that the database and sync layer enforce. Pure (returns source
|
|
6
|
+
* text); the CLI writes it to disk.
|
|
7
|
+
*
|
|
8
|
+
* This is the SDK's front door: the developer writes ONE `defineSchema`, pushes
|
|
9
|
+
* it (which migrates the DB), and generates types FROM the same schema — so the
|
|
10
|
+
* types they code against, the rows the DB stores, and the entities sync moves
|
|
11
|
+
* are provably the same thing.
|
|
12
|
+
*
|
|
13
|
+
* v1 emits the row shape (base columns + declared fields, enums as literal
|
|
14
|
+
* unions). Relations are resolved by the runtime SDK's typed accessors and are
|
|
15
|
+
* not expanded here.
|
|
16
|
+
*/
|
|
17
|
+
/** The base columns every model carries (mirrors `baseFieldsSchema`). */
|
|
18
|
+
const BASE_FIELDS = ['id', 'createdAt', 'updatedAt', 'organizationId', 'createdBy'];
|
|
19
|
+
function tsType(meta) {
|
|
20
|
+
switch (meta.type) {
|
|
21
|
+
case 'string':
|
|
22
|
+
return 'string';
|
|
23
|
+
case 'number':
|
|
24
|
+
return 'number';
|
|
25
|
+
case 'boolean':
|
|
26
|
+
return 'boolean';
|
|
27
|
+
case 'date':
|
|
28
|
+
// `baseFieldsSchema` uses `z.date()` and the driver hydrates timestamptz
|
|
29
|
+
// to Date — declared date fields match.
|
|
30
|
+
return 'Date';
|
|
31
|
+
case 'json':
|
|
32
|
+
return 'unknown';
|
|
33
|
+
case 'enum':
|
|
34
|
+
return meta.enumValues && meta.enumValues.length > 0
|
|
35
|
+
? meta.enumValues.map((v) => `'${v.replace(/'/g, "\\'")}'`).join(' | ')
|
|
36
|
+
: 'string';
|
|
37
|
+
default:
|
|
38
|
+
return 'unknown';
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** A valid PascalCase TS identifier from a model's typename (or its key). */
|
|
42
|
+
function interfaceName(model, key) {
|
|
43
|
+
const raw = model.typename && model.typename.trim() ? model.typename : key;
|
|
44
|
+
const id = raw
|
|
45
|
+
.replace(/[^A-Za-z0-9]+/g, ' ')
|
|
46
|
+
.trim()
|
|
47
|
+
.split(/\s+/)
|
|
48
|
+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
|
|
49
|
+
.join('');
|
|
50
|
+
return /^[A-Za-z_]/.test(id) ? id : `Model${id}`;
|
|
51
|
+
}
|
|
52
|
+
/** Emit a TypeScript module of row interfaces + the `AbloSchema` map. */
|
|
53
|
+
export function generateTypes(schema) {
|
|
54
|
+
const lines = [
|
|
55
|
+
'// Generated by `ablo generate` — do not edit by hand.',
|
|
56
|
+
'// Re-run `ablo generate` after pushing a schema change.',
|
|
57
|
+
'',
|
|
58
|
+
];
|
|
59
|
+
const nameByKey = new Map();
|
|
60
|
+
for (const [key, model] of Object.entries(schema.models)) {
|
|
61
|
+
nameByKey.set(key, interfaceName(model, key));
|
|
62
|
+
}
|
|
63
|
+
for (const [key, model] of Object.entries(schema.models)) {
|
|
64
|
+
lines.push(`export interface ${nameByKey.get(key)} {`);
|
|
65
|
+
lines.push(' id: string;');
|
|
66
|
+
lines.push(' createdAt: Date;');
|
|
67
|
+
lines.push(' updatedAt: Date;');
|
|
68
|
+
lines.push(' organizationId?: string;');
|
|
69
|
+
lines.push(' createdBy?: string;');
|
|
70
|
+
for (const [fieldName, meta] of Object.entries(model.fields)) {
|
|
71
|
+
// A model that redeclares a base column doesn't double-emit it.
|
|
72
|
+
if (BASE_FIELDS.includes(fieldName))
|
|
73
|
+
continue;
|
|
74
|
+
lines.push(` ${fieldName}${meta.isOptional ? '?' : ''}: ${tsType(meta)};`);
|
|
75
|
+
}
|
|
76
|
+
lines.push('}');
|
|
77
|
+
lines.push('');
|
|
78
|
+
}
|
|
79
|
+
// The model map — parameterize the Ablo client with this for typed access.
|
|
80
|
+
lines.push('export interface AbloSchema {');
|
|
81
|
+
for (const key of Object.keys(schema.models)) {
|
|
82
|
+
lines.push(` ${JSON.stringify(key)}: ${nameByKey.get(key)};`);
|
|
83
|
+
}
|
|
84
|
+
lines.push('}');
|
|
85
|
+
lines.push('');
|
|
86
|
+
return lines.join('\n');
|
|
87
|
+
}
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -21,9 +21,15 @@
|
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
23
|
export { z } from 'zod';
|
|
24
|
-
export { field, indexed, getFieldMeta, type FieldMeta } from './field.js';
|
|
24
|
+
export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
|
|
25
25
|
export { relation, type RelationDef, type RelationType } from './relation.js';
|
|
26
|
-
export {
|
|
26
|
+
export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type TenancyInput, } from './tenancy.js';
|
|
27
|
+
export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, } from './model.js';
|
|
27
28
|
export { mutable, readOnly, type SugarOptions } from './sugar.js';
|
|
28
|
-
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, type IdentityRole, type IdentityContext, } from './schema.js';
|
|
29
|
+
export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
30
|
+
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
31
|
+
export { selectModels } from './select.js';
|
|
32
|
+
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
|
|
33
|
+
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, type BackfillValue, type MigrationStep, type FieldChanges, type FieldColumnChange, type FieldTypeChange, type NullabilityChange, type EnumValuesChange, type IndexChange, type CastSafety, type FieldType, type RenameHints, type MigrationSignal, type MigrationClassification, type WarningCode, type BlockerCode, } from './diff.js';
|
|
34
|
+
export { generateTypes } from './generate.js';
|
|
29
35
|
export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -26,13 +26,25 @@ export { z } from 'zod';
|
|
|
26
26
|
export { field, indexed, getFieldMeta } from './field.js';
|
|
27
27
|
// Relation builders
|
|
28
28
|
export { relation } from './relation.js';
|
|
29
|
+
// Tenancy — the single source of truth for how a model's rows are tenant-scoped.
|
|
30
|
+
export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
|
|
29
31
|
// Model builder
|
|
30
|
-
export { model, } from './model.js';
|
|
32
|
+
export { model, scopeKindOf, } from './model.js';
|
|
31
33
|
// Intent-first shorthand: `mutable.lazy({...})` and friends. Read the
|
|
32
34
|
// safety posture and load shape off the verb tokens; everything else
|
|
33
35
|
// falls back to sensible defaults. See sugar.ts for the full pattern.
|
|
34
36
|
export { mutable, readOnly } from './sugar.js';
|
|
35
37
|
// Schema definition + type inference
|
|
36
|
-
export { defineSchema, composeIdentitySyncGroups, } from './schema.js';
|
|
38
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
39
|
+
// Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
|
|
40
|
+
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
41
|
+
// Schema projection — derive an app's subset from one canonical schema.
|
|
42
|
+
export { selectModels } from './select.js';
|
|
43
|
+
// Schema → Postgres DDL (pure; shared by the hosted server and the CLI)
|
|
44
|
+
export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, q, sqlType, } from './ddl.js';
|
|
45
|
+
// Schema diff + migration planning (pure; SQL emission lowered by ddl.ts)
|
|
46
|
+
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, } from './diff.js';
|
|
47
|
+
// Schema → TypeScript type emission (the `generate` half; pure)
|
|
48
|
+
export { generateTypes } from './generate.js';
|
|
37
49
|
// Query definition DSL + type inference
|
|
38
50
|
export { query, defineQueries, } from './queries.js';
|