@abloatai/ablo 0.6.0 → 0.8.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 +77 -0
- package/README.md +95 -57
- package/dist/BaseSyncedStore.d.ts +1 -1
- package/dist/BaseSyncedStore.js +8 -4
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +3 -2
- package/dist/auth/index.js +39 -11
- package/dist/client/Ablo.d.ts +112 -3
- package/dist/client/Ablo.js +144 -10
- package/dist/client/ApiClient.d.ts +32 -0
- package/dist/client/ApiClient.js +76 -44
- package/dist/client/auth.d.ts +11 -1
- package/dist/client/auth.js +21 -2
- package/dist/client/createModelProxy.d.ts +120 -53
- package/dist/client/createModelProxy.js +66 -31
- package/dist/client/identity.js +14 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +57 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- 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/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +286 -0
- package/dist/errorCodes.js +284 -0
- package/dist/errors.d.ts +103 -7
- package/dist/errors.js +192 -41
- package/dist/index.d.ts +11 -6
- package/dist/index.js +10 -6
- package/dist/keys/index.d.ts +61 -0
- package/dist/keys/index.js +151 -0
- 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/client.js +19 -8
- package/dist/react/AbloProvider.d.ts +37 -0
- package/dist/react/AbloProvider.js +107 -4
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +6 -0
- package/dist/schema/diff.js +21 -3
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/index.d.ts +7 -4
- package/dist/schema/index.js +9 -3
- 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 +2 -112
- package/dist/schema/schema.js +50 -62
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +16 -12
- package/dist/schema/serialize.js +16 -12
- 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/BootstrapHelper.js +46 -27
- package/dist/sync/ConnectionManager.d.ts +3 -1
- package/dist/sync/ConnectionManager.js +37 -1
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +26 -19
- package/dist/sync/NetworkProbe.d.ts +8 -0
- package/dist/sync/NetworkProbe.js +24 -2
- package/dist/sync/SyncWebSocket.d.ts +1 -1
- package/dist/sync/SyncWebSocket.js +43 -53
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +46 -1
- package/dist/sync/participants.js +10 -16
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +53 -33
- package/docs/api-keys.md +47 -3
- package/docs/api.md +103 -57
- package/docs/audit.md +16 -9
- package/docs/cli.md +222 -0
- package/docs/client-behavior.md +35 -21
- package/docs/coordination.md +74 -36
- package/docs/data-sources.md +23 -21
- package/docs/examples/agent-human.md +72 -28
- package/docs/examples/ai-sdk-tool.md +14 -11
- package/docs/examples/existing-python-backend.md +30 -19
- package/docs/examples/nextjs.md +21 -8
- package/docs/examples/scoped-agent.md +93 -0
- package/docs/examples/server-agent.md +27 -5
- package/docs/guarantees.md +29 -17
- package/docs/identity.md +198 -121
- package/docs/index.md +35 -18
- package/docs/integration-guide.md +79 -83
- package/docs/interaction-model.md +40 -25
- package/docs/mcp/claude-code.md +9 -17
- package/docs/mcp/cursor.md +6 -24
- package/docs/mcp/windsurf.md +6 -19
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +31 -39
- package/docs/react.md +18 -14
- package/docs/roadmap.md +15 -3
- package/docs/schema-contract.md +109 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +6 -2
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +27 -16
- package/package.json +13 -1
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → Postgres DDL — the one pure SQL emitter shared by every consumer.
|
|
3
|
+
*
|
|
4
|
+
* `defineSchema(...)` (serialized to {@link SchemaJSON}) is the single source of
|
|
5
|
+
* truth; this module lowers it to ordered DDL strings. Both the hosted server
|
|
6
|
+
* (which applies it to Ablo-managed Postgres on `schema push`) and the
|
|
7
|
+
* `ablo migrate` CLI (which applies it to a customer's own Postgres) call these
|
|
8
|
+
* generators, so the SQL — column types, RLS, enum checks — is identical no
|
|
9
|
+
* matter who runs it. There is no second type map.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure (returns strings; no DB, no I/O); the execution side
|
|
12
|
+
* (transaction + advisory lock) lives with each consumer because it's coupled
|
|
13
|
+
* to that consumer's Postgres client and error type.
|
|
14
|
+
*
|
|
15
|
+
* - `generateProvisionPlan` — additive + idempotent (CREATE/ADD … IF NOT
|
|
16
|
+
* EXISTS + RLS). Never loses data. The "create my tables" primitive.
|
|
17
|
+
* - `generateMigrationPlan` — the destructive-aware counterpart driven by the
|
|
18
|
+
* {@link diffSchema} step list (drops, renames, type casts, backfills).
|
|
19
|
+
*/
|
|
20
|
+
import type { SchemaJSON, ModelJSON } from './serialize.js';
|
|
21
|
+
import type { MigrationStep, BackfillValue } from './diff.js';
|
|
22
|
+
export interface ProvisionPlan {
|
|
23
|
+
/** The Postgres schema the tables live in (`app_<id>` or `public`). */
|
|
24
|
+
readonly appSchema: string;
|
|
25
|
+
/** Ordered, idempotent DDL statements. Safe to run repeatedly. */
|
|
26
|
+
readonly statements: readonly string[];
|
|
27
|
+
}
|
|
28
|
+
export interface MigrationPlan {
|
|
29
|
+
/** The app Postgres schema the DDL targets (`app_<id>` or `public`). */
|
|
30
|
+
readonly appSchema: string;
|
|
31
|
+
/** Ordered DDL statements (expand → contract). */
|
|
32
|
+
readonly statements: readonly string[];
|
|
33
|
+
}
|
|
34
|
+
/** Per-app schema name for an app (organization) id. */
|
|
35
|
+
export declare function appSchemaName(organizationId: string): string;
|
|
36
|
+
export declare function camelToSnake(identifier: string): string;
|
|
37
|
+
/** Quote an identifier (defense-in-depth; inputs are already slug/snake). */
|
|
38
|
+
export declare function q(identifier: string): string;
|
|
39
|
+
export declare function sqlType(fieldType: ModelJSON['fields'][string]['type']): string;
|
|
40
|
+
/**
|
|
41
|
+
* Build the additive, idempotent provisioning plan for an app. Pure — no DB
|
|
42
|
+
* access.
|
|
43
|
+
*
|
|
44
|
+
* `targetSchema` is where the tables live: the app's schema `app_<id>` on the
|
|
45
|
+
* shared tier, or `public` on a dedicated tenant's own database (where the DB
|
|
46
|
+
* itself is the isolation boundary). For `public` the `CREATE SCHEMA` is
|
|
47
|
+
* skipped (it always exists).
|
|
48
|
+
*/
|
|
49
|
+
export declare function generateProvisionPlan(schema: SchemaJSON, targetSchema: string): ProvisionPlan;
|
|
50
|
+
/**
|
|
51
|
+
* Lower an ordered migration step list to DDL. `next` is the schema being pushed
|
|
52
|
+
* (the target column shapes are read from it), `prev` the active one (used to
|
|
53
|
+
* resolve the *old* table name on a model rename).
|
|
54
|
+
*/
|
|
55
|
+
export declare function generateMigrationPlan(steps: readonly MigrationStep[], opts: {
|
|
56
|
+
readonly prev: SchemaJSON | null;
|
|
57
|
+
readonly next: SchemaJSON;
|
|
58
|
+
readonly targetSchema: string;
|
|
59
|
+
/** Constant seed values that let a required-field add / made-required step
|
|
60
|
+
* set NOT NULL on a non-empty table. Keyed by (model, field). */
|
|
61
|
+
readonly backfills?: readonly BackfillValue[];
|
|
62
|
+
}): MigrationPlan;
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Schema → Postgres DDL — the one pure SQL emitter shared by every consumer.
|
|
3
|
+
*
|
|
4
|
+
* `defineSchema(...)` (serialized to {@link SchemaJSON}) is the single source of
|
|
5
|
+
* truth; this module lowers it to ordered DDL strings. Both the hosted server
|
|
6
|
+
* (which applies it to Ablo-managed Postgres on `schema push`) and the
|
|
7
|
+
* `ablo migrate` CLI (which applies it to a customer's own Postgres) call these
|
|
8
|
+
* generators, so the SQL — column types, RLS, enum checks — is identical no
|
|
9
|
+
* matter who runs it. There is no second type map.
|
|
10
|
+
*
|
|
11
|
+
* Everything here is pure (returns strings; no DB, no I/O); the execution side
|
|
12
|
+
* (transaction + advisory lock) lives with each consumer because it's coupled
|
|
13
|
+
* to that consumer's Postgres client and error type.
|
|
14
|
+
*
|
|
15
|
+
* - `generateProvisionPlan` — additive + idempotent (CREATE/ADD … IF NOT
|
|
16
|
+
* EXISTS + RLS). Never loses data. The "create my tables" primitive.
|
|
17
|
+
* - `generateMigrationPlan` — the destructive-aware counterpart driven by the
|
|
18
|
+
* {@link diffSchema} step list (drops, renames, type casts, backfills).
|
|
19
|
+
*/
|
|
20
|
+
import { resolveTenancy, tenancyColumn } from './tenancy.js';
|
|
21
|
+
// ── Identifier safety ────────────────────────────────────────────────────────
|
|
22
|
+
/** Postgres unquoted-identifier-safe slug: lowercase `[a-z0-9_]`, ≤50 chars. */
|
|
23
|
+
function slug(raw) {
|
|
24
|
+
const s = raw.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
|
|
25
|
+
return s.slice(0, 50) || 'x';
|
|
26
|
+
}
|
|
27
|
+
/** Per-app schema name for an app (organization) id. */
|
|
28
|
+
export function appSchemaName(organizationId) {
|
|
29
|
+
return `app_${slug(organizationId)}`;
|
|
30
|
+
}
|
|
31
|
+
export function camelToSnake(identifier) {
|
|
32
|
+
return identifier.replace(/[A-Z]/g, (ch) => `_${ch.toLowerCase()}`);
|
|
33
|
+
}
|
|
34
|
+
/** Quote an identifier (defense-in-depth; inputs are already slug/snake). */
|
|
35
|
+
export function q(identifier) {
|
|
36
|
+
return `"${identifier.replace(/"/g, '""')}"`;
|
|
37
|
+
}
|
|
38
|
+
// ── Field type mapping ───────────────────────────────────────────────────────
|
|
39
|
+
export function sqlType(fieldType) {
|
|
40
|
+
switch (fieldType) {
|
|
41
|
+
case 'string':
|
|
42
|
+
case 'enum':
|
|
43
|
+
return 'TEXT';
|
|
44
|
+
case 'number':
|
|
45
|
+
// DOUBLE PRECISION, not INTEGER — a Zod `number` may be fractional and
|
|
46
|
+
// truncating to INTEGER is silent data loss.
|
|
47
|
+
return 'DOUBLE PRECISION';
|
|
48
|
+
case 'boolean':
|
|
49
|
+
return 'BOOLEAN';
|
|
50
|
+
case 'date':
|
|
51
|
+
return 'TIMESTAMPTZ';
|
|
52
|
+
case 'json':
|
|
53
|
+
default:
|
|
54
|
+
return 'JSONB';
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const BASE_COLUMNS = new Set(['id', 'organization_id', 'created_by', 'created_at', 'updated_at']);
|
|
58
|
+
// ── Provisioning (additive, idempotent) ─────────────────────────────────────
|
|
59
|
+
/**
|
|
60
|
+
* Build the additive, idempotent provisioning plan for an app. Pure — no DB
|
|
61
|
+
* access.
|
|
62
|
+
*
|
|
63
|
+
* `targetSchema` is where the tables live: the app's schema `app_<id>` on the
|
|
64
|
+
* shared tier, or `public` on a dedicated tenant's own database (where the DB
|
|
65
|
+
* itself is the isolation boundary). For `public` the `CREATE SCHEMA` is
|
|
66
|
+
* skipped (it always exists).
|
|
67
|
+
*/
|
|
68
|
+
export function generateProvisionPlan(schema, targetSchema) {
|
|
69
|
+
const appSchema = targetSchema;
|
|
70
|
+
const qs = q(appSchema);
|
|
71
|
+
const statements = appSchema === 'public' ? [] : [`CREATE SCHEMA IF NOT EXISTS ${qs};`];
|
|
72
|
+
for (const [key, model] of Object.entries(schema.models)) {
|
|
73
|
+
// Default the physical table to the model key when `tableName` is omitted —
|
|
74
|
+
// same fallback the migration path uses (`tableOfModel: m.tableName ?? key`).
|
|
75
|
+
// Without this, a schema that doesn't set `tableName` (e.g. the `ablo init`
|
|
76
|
+
// starter) provisions zero tables.
|
|
77
|
+
const table = model.tableName ?? key;
|
|
78
|
+
const qt = `${qs}.${q(table)}`;
|
|
79
|
+
// Base columns are schema-driven, not blanket. `organization_id` (and its
|
|
80
|
+
// index + tenant-isolation RLS below) is emitted only for org-scoped models.
|
|
81
|
+
// A model that declares `orgScoped: false` (users, organizations, and other
|
|
82
|
+
// tables scoped via a FK / app layer) genuinely has no `organization_id`
|
|
83
|
+
// column — forcing one would add a NOT NULL column that fails on existing
|
|
84
|
+
// rows and contradicts the model's own declaration.
|
|
85
|
+
// Tenancy column: present only for column-scoped models, with the
|
|
86
|
+
// configured name (default `organization_id`). `parent`/`none` tenancy emit
|
|
87
|
+
// no tenancy column — they're scoped via a parent FK or not at all.
|
|
88
|
+
const orgCol = tenancyColumn(resolveTenancy(model));
|
|
89
|
+
const baseColumns = [
|
|
90
|
+
` ${q('id')} TEXT PRIMARY KEY,`,
|
|
91
|
+
...(orgCol ? [` ${q(orgCol)} TEXT NOT NULL,`] : []),
|
|
92
|
+
` ${q('created_by')} TEXT,`,
|
|
93
|
+
` ${q('created_at')} TIMESTAMPTZ NOT NULL DEFAULT NOW(),`,
|
|
94
|
+
` ${q('updated_at')} TIMESTAMPTZ NOT NULL DEFAULT NOW()`,
|
|
95
|
+
];
|
|
96
|
+
statements.push(`CREATE TABLE IF NOT EXISTS ${qt} (\n${baseColumns.join('\n')}\n);`);
|
|
97
|
+
for (const [fieldName, meta] of Object.entries(model.fields)) {
|
|
98
|
+
const col = meta.column ?? camelToSnake(fieldName);
|
|
99
|
+
if (BASE_COLUMNS.has(col) || col === orgCol)
|
|
100
|
+
continue;
|
|
101
|
+
statements.push(`ALTER TABLE ${qt} ADD COLUMN IF NOT EXISTS ${q(col)} ${sqlType(meta.type)};`);
|
|
102
|
+
if (meta.type === 'enum' && meta.enumValues && meta.enumValues.length > 0) {
|
|
103
|
+
const cname = `${table}_${col}_enum`;
|
|
104
|
+
const allowed = meta.enumValues.map((v) => `'${v.replace(/'/g, "''")}'`).join(', ');
|
|
105
|
+
statements.push(`DO $$ BEGIN\n` +
|
|
106
|
+
` IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = '${cname}') THEN\n` +
|
|
107
|
+
` ALTER TABLE ${qt} ADD CONSTRAINT ${q(cname)} CHECK (${q(col)} IN (${allowed}));\n` +
|
|
108
|
+
` END IF;\n` +
|
|
109
|
+
`END $$;`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Org index + tenant-isolation RLS only where there's an `organization_id`
|
|
113
|
+
// to isolate on. Non-org-scoped tables rely on FK/app-layer scoping.
|
|
114
|
+
if (orgCol) {
|
|
115
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${q(`${table}_${orgCol}_idx`)} ON ${qt} (${q(orgCol)});`);
|
|
116
|
+
statements.push(`ALTER TABLE ${qt} ENABLE ROW LEVEL SECURITY;`);
|
|
117
|
+
statements.push(`ALTER TABLE ${qt} FORCE ROW LEVEL SECURITY;`);
|
|
118
|
+
const policy = `${table}_tenant_isolation`;
|
|
119
|
+
const predicate = `${q(orgCol)} = current_setting('app.current_org_id', true)`;
|
|
120
|
+
statements.push(`DROP POLICY IF EXISTS ${q(policy)} ON ${qt};`);
|
|
121
|
+
statements.push(`CREATE POLICY ${q(policy)} ON ${qt}\n USING (${predicate})\n WITH CHECK (${predicate});`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
return { appSchema, statements };
|
|
125
|
+
}
|
|
126
|
+
// ── Migration (destructive-aware, diff-driven) ──────────────────────────────
|
|
127
|
+
function enumCheckStatements(table, col, qt, values) {
|
|
128
|
+
const cname = `${table}_${col}_enum`;
|
|
129
|
+
const stmts = [`ALTER TABLE ${qt} DROP CONSTRAINT IF EXISTS ${q(cname)};`];
|
|
130
|
+
if (values.length > 0) {
|
|
131
|
+
const allowed = values.map((v) => `'${v.replace(/'/g, "''")}'`).join(', ');
|
|
132
|
+
stmts.push(`ALTER TABLE ${qt} ADD CONSTRAINT ${q(cname)} CHECK (${q(col)} IN (${allowed}));`);
|
|
133
|
+
}
|
|
134
|
+
return stmts;
|
|
135
|
+
}
|
|
136
|
+
function indexName(table, col) {
|
|
137
|
+
return `${table}_${col}_idx`;
|
|
138
|
+
}
|
|
139
|
+
function columnNameOf(fieldName, meta) {
|
|
140
|
+
return meta?.column ?? camelToSnake(fieldName);
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Encode a constant backfill value as a typed SQL literal. Inputs are operator-
|
|
144
|
+
* supplied (via the authed push), but we still encode by the field's declared
|
|
145
|
+
* type and escape strings rather than interpolate raw — defense-in-depth.
|
|
146
|
+
*/
|
|
147
|
+
function sqlLiteral(value, fieldType) {
|
|
148
|
+
switch (fieldType) {
|
|
149
|
+
case 'number':
|
|
150
|
+
if (typeof value !== 'number' || !Number.isFinite(value)) {
|
|
151
|
+
throw new Error(`backfill for a number field must be a finite number, got ${JSON.stringify(value)}`);
|
|
152
|
+
}
|
|
153
|
+
return String(value);
|
|
154
|
+
case 'boolean':
|
|
155
|
+
return value ? 'TRUE' : 'FALSE';
|
|
156
|
+
case 'date':
|
|
157
|
+
return `'${String(value).replace(/'/g, "''")}'::timestamptz`;
|
|
158
|
+
case 'json':
|
|
159
|
+
return `'${JSON.stringify(value).replace(/'/g, "''")}'::jsonb`;
|
|
160
|
+
case 'string':
|
|
161
|
+
case 'enum':
|
|
162
|
+
default:
|
|
163
|
+
return `'${String(value).replace(/'/g, "''")}'`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Lower an ordered migration step list to DDL. `next` is the schema being pushed
|
|
168
|
+
* (the target column shapes are read from it), `prev` the active one (used to
|
|
169
|
+
* resolve the *old* table name on a model rename).
|
|
170
|
+
*/
|
|
171
|
+
export function generateMigrationPlan(steps, opts) {
|
|
172
|
+
const { prev, next, targetSchema, backfills = [] } = opts;
|
|
173
|
+
const qs = q(targetSchema);
|
|
174
|
+
const statements = [];
|
|
175
|
+
const qtFor = (table) => `${qs}.${q(table)}`;
|
|
176
|
+
const tableOfModel = (schema, key) => {
|
|
177
|
+
const m = schema?.models[key];
|
|
178
|
+
if (!m)
|
|
179
|
+
return null;
|
|
180
|
+
return m.tableName ?? key;
|
|
181
|
+
};
|
|
182
|
+
const backfillFor = (model, field) => backfills.find((b) => b.model === model && b.field === field);
|
|
183
|
+
for (const step of steps) {
|
|
184
|
+
switch (step.kind) {
|
|
185
|
+
case 'create_model': {
|
|
186
|
+
// Reuse the provisioner for the full table (base cols + fields + enum
|
|
187
|
+
// checks + RLS), minus its `CREATE SCHEMA` (the schema already exists
|
|
188
|
+
// mid-migration).
|
|
189
|
+
const def = next.models[step.model];
|
|
190
|
+
if (!def)
|
|
191
|
+
break;
|
|
192
|
+
const sub = { v: next.v, models: { [step.model]: def }, identityRoles: next.identityRoles };
|
|
193
|
+
for (const s of generateProvisionPlan(sub, targetSchema).statements) {
|
|
194
|
+
if (!s.startsWith('CREATE SCHEMA'))
|
|
195
|
+
statements.push(s);
|
|
196
|
+
}
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
case 'drop_model':
|
|
200
|
+
statements.push(`DROP TABLE IF EXISTS ${qtFor(step.tableName)};`);
|
|
201
|
+
break;
|
|
202
|
+
case 'rename_model': {
|
|
203
|
+
const fromTable = tableOfModel(prev, step.from);
|
|
204
|
+
const toTable = tableOfModel(next, step.to);
|
|
205
|
+
// A logical model rename only needs SQL when the physical table name
|
|
206
|
+
// actually changes; if tableName is unchanged the rename is metadata.
|
|
207
|
+
if (fromTable && toTable && fromTable !== toTable) {
|
|
208
|
+
statements.push(`ALTER TABLE ${qtFor(fromTable)} RENAME TO ${q(toTable)};`);
|
|
209
|
+
}
|
|
210
|
+
break;
|
|
211
|
+
}
|
|
212
|
+
case 'add_field': {
|
|
213
|
+
const table = tableOfModel(next, step.model);
|
|
214
|
+
if (!table)
|
|
215
|
+
break;
|
|
216
|
+
const qt = qtFor(table);
|
|
217
|
+
const col = columnNameOf(step.field, step.meta);
|
|
218
|
+
// Added nullable first (the column is born NULL on every existing row).
|
|
219
|
+
statements.push(`ALTER TABLE ${qt} ADD COLUMN IF NOT EXISTS ${q(col)} ${sqlType(step.meta.type)};`);
|
|
220
|
+
if (step.meta.type === 'enum' && step.meta.enumValues?.length) {
|
|
221
|
+
statements.push(...enumCheckStatements(table, col, qt, step.meta.enumValues));
|
|
222
|
+
}
|
|
223
|
+
// Backfill + enforce NOT NULL only with a supplied seed value. Without
|
|
224
|
+
// one, a required field stays nullable (gated `unexecutable` upstream).
|
|
225
|
+
const addBf = backfillFor(step.model, step.field);
|
|
226
|
+
if (addBf !== undefined) {
|
|
227
|
+
statements.push(`UPDATE ${qt} SET ${q(col)} = ${sqlLiteral(addBf.value, step.meta.type)} WHERE ${q(col)} IS NULL;`);
|
|
228
|
+
if (!step.meta.isOptional) {
|
|
229
|
+
statements.push(`ALTER TABLE ${qt} ALTER COLUMN ${q(col)} SET NOT NULL;`);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
if (step.meta.isIndexed) {
|
|
233
|
+
statements.push(`CREATE INDEX IF NOT EXISTS ${q(indexName(table, col))} ON ${qt} (${q(col)});`);
|
|
234
|
+
}
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'drop_field': {
|
|
238
|
+
const table = tableOfModel(next, step.model);
|
|
239
|
+
if (!table)
|
|
240
|
+
break;
|
|
241
|
+
const prevMeta = prev?.models[step.model]?.fields[step.field];
|
|
242
|
+
statements.push(`ALTER TABLE ${qtFor(table)} DROP COLUMN IF EXISTS ${q(columnNameOf(step.field, prevMeta))};`);
|
|
243
|
+
break;
|
|
244
|
+
}
|
|
245
|
+
case 'rename_field': {
|
|
246
|
+
const table = tableOfModel(next, step.model);
|
|
247
|
+
if (!table)
|
|
248
|
+
break;
|
|
249
|
+
const prevMeta = prev?.models[step.model]?.fields[step.from];
|
|
250
|
+
const nextMeta = next.models[step.model]?.fields[step.to];
|
|
251
|
+
const fromCol = columnNameOf(step.from, prevMeta);
|
|
252
|
+
const toCol = columnNameOf(step.to, nextMeta);
|
|
253
|
+
if (fromCol === toCol)
|
|
254
|
+
break;
|
|
255
|
+
statements.push(`ALTER TABLE ${qtFor(table)} RENAME COLUMN ${q(fromCol)} TO ${q(toCol)};`);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'alter_field': {
|
|
259
|
+
const table = tableOfModel(next, step.model);
|
|
260
|
+
if (!table)
|
|
261
|
+
break;
|
|
262
|
+
const qt = qtFor(table);
|
|
263
|
+
const nextMeta = next.models[step.model]?.fields[step.field];
|
|
264
|
+
let col = columnNameOf(step.field, nextMeta);
|
|
265
|
+
const ch = step.changes;
|
|
266
|
+
// 0. Physical column rename. Subsequent alterations must address
|
|
267
|
+
// the new name.
|
|
268
|
+
if (ch.column) {
|
|
269
|
+
statements.push(`ALTER TABLE ${qt} RENAME COLUMN ${q(ch.column.from)} TO ${q(ch.column.to)};`);
|
|
270
|
+
col = ch.column.to;
|
|
271
|
+
}
|
|
272
|
+
// 1. Type — in-place cast or lossy drop-and-recreate.
|
|
273
|
+
if (ch.type) {
|
|
274
|
+
const target = sqlType(ch.type.to);
|
|
275
|
+
if (ch.type.cast === 'notCastable') {
|
|
276
|
+
statements.push(`ALTER TABLE ${qt} DROP COLUMN IF EXISTS ${q(col)};`);
|
|
277
|
+
statements.push(`ALTER TABLE ${qt} ADD COLUMN IF NOT EXISTS ${q(col)} ${target};`);
|
|
278
|
+
}
|
|
279
|
+
else {
|
|
280
|
+
statements.push(`ALTER TABLE ${qt} ALTER COLUMN ${q(col)} TYPE ${target} USING ${q(col)}::${target};`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// 2. Enum CHECK — drop when leaving enum; (re)build when arriving at or
|
|
284
|
+
// re-valuing an enum. Reads the full target value set from `next`.
|
|
285
|
+
if (ch.type?.from === 'enum' && nextMeta?.type !== 'enum') {
|
|
286
|
+
statements.push(`ALTER TABLE ${qt} DROP CONSTRAINT IF EXISTS ${q(`${table}_${col}_enum`)};`);
|
|
287
|
+
}
|
|
288
|
+
else if (nextMeta?.type === 'enum' && (ch.enumValues || ch.type)) {
|
|
289
|
+
statements.push(...enumCheckStatements(table, col, qt, nextMeta.enumValues ?? []));
|
|
290
|
+
}
|
|
291
|
+
// 3. Nullability. DROP NOT NULL is always safe. SET NOT NULL is gated
|
|
292
|
+
// upstream (unexecutable on a table with NULLs); a supplied backfill
|
|
293
|
+
// seeds the existing NULLs first so the constraint can take.
|
|
294
|
+
if (ch.nullability) {
|
|
295
|
+
if (ch.nullability.toOptional) {
|
|
296
|
+
statements.push(`ALTER TABLE ${qt} ALTER COLUMN ${q(col)} DROP NOT NULL;`);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
const bf = backfillFor(step.model, step.field);
|
|
300
|
+
if (bf !== undefined && nextMeta) {
|
|
301
|
+
statements.push(`UPDATE ${qt} SET ${q(col)} = ${sqlLiteral(bf.value, nextMeta.type)} WHERE ${q(col)} IS NULL;`);
|
|
302
|
+
}
|
|
303
|
+
statements.push(`ALTER TABLE ${qt} ALTER COLUMN ${q(col)} SET NOT NULL;`);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// 4. Index.
|
|
307
|
+
if (ch.indexed) {
|
|
308
|
+
statements.push(ch.indexed.to
|
|
309
|
+
? `CREATE INDEX IF NOT EXISTS ${q(indexName(table, col))} ON ${qt} (${q(col)});`
|
|
310
|
+
: `DROP INDEX IF EXISTS ${qs}.${q(indexName(table, col))};`);
|
|
311
|
+
}
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return { appSchema: targetSchema, statements };
|
|
317
|
+
}
|
package/dist/schema/diff.d.ts
CHANGED
|
@@ -50,8 +50,14 @@ export interface IndexChange {
|
|
|
50
50
|
readonly from: boolean;
|
|
51
51
|
readonly to: boolean;
|
|
52
52
|
}
|
|
53
|
+
/** Physical column-name transition for a stable logical field. */
|
|
54
|
+
export interface FieldColumnChange {
|
|
55
|
+
readonly from: string;
|
|
56
|
+
readonly to: string;
|
|
57
|
+
}
|
|
53
58
|
/** The facets of a single column that changed (Atlas-style bitmask, as data). */
|
|
54
59
|
export interface FieldChanges {
|
|
60
|
+
readonly column?: FieldColumnChange;
|
|
55
61
|
readonly type?: FieldTypeChange;
|
|
56
62
|
readonly nullability?: NullabilityChange;
|
|
57
63
|
readonly enumValues?: EnumValuesChange;
|
package/dist/schema/diff.js
CHANGED
|
@@ -65,8 +65,19 @@ function diffEnumValues(from, to) {
|
|
|
65
65
|
return undefined;
|
|
66
66
|
return { added, removed };
|
|
67
67
|
}
|
|
68
|
-
function
|
|
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) {
|
|
69
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
|
+
}
|
|
70
81
|
if (prev.type !== next.type) {
|
|
71
82
|
changes.type = { from: prev.type, to: next.type, cast: classifyCast(prev.type, next.type) };
|
|
72
83
|
}
|
|
@@ -112,9 +123,16 @@ function diffModelFields(model, prev, next, fieldRenames) {
|
|
|
112
123
|
const prevMeta = prev.fields[prevName];
|
|
113
124
|
if (!prevMeta)
|
|
114
125
|
continue;
|
|
115
|
-
const changes = diffField(prevMeta, nextMeta);
|
|
116
|
-
if (changes)
|
|
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) {
|
|
117
134
|
steps.push({ kind: 'alter_field', model, field: name, changes });
|
|
135
|
+
}
|
|
118
136
|
}
|
|
119
137
|
// Dropped (present in prev, not in next, and not renamed away).
|
|
120
138
|
for (const name of Object.keys(prev.fields)) {
|
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() {
|
package/dist/schema/index.d.ts
CHANGED
|
@@ -21,12 +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, identityRole, extractIdentityIds,
|
|
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';
|
|
29
30
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
|
|
30
|
-
export {
|
|
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';
|
|
31
34
|
export { generateTypes } from './generate.js';
|
|
32
35
|
export { query, defineQueries, type QueryDef, type QueryRecord, type Queries, type InferQueryInput, type InferQueryResult, } from './queries.js';
|
package/dist/schema/index.js
CHANGED
|
@@ -26,17 +26,23 @@ 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, identityRole, extractIdentityIds, } from './schema.js';
|
|
38
|
+
export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
|
|
37
39
|
// Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
|
|
38
40
|
export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
|
|
39
|
-
// Schema
|
|
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)
|
|
40
46
|
export { diffSchema, classifyMigration, classifyCast, isAutoApplicable, isBlockerResolved, unresolvedBlockers, } from './diff.js';
|
|
41
47
|
// Schema → TypeScript type emission (the `generate` half; pure)
|
|
42
48
|
export { generateTypes } from './generate.js';
|