@snolla/data 0.0.0-stage → 1.0.7
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/dist/actor.d.ts +28 -0
- package/dist/actor.js +63 -0
- package/dist/audit.d.ts +36 -0
- package/dist/audit.js +198 -0
- package/dist/dataProvider.d.ts +41 -0
- package/dist/dataProvider.js +148 -0
- package/dist/dataSource.d.ts +4 -0
- package/dist/dataSource.js +65 -0
- package/dist/dbProvider.d.ts +1261 -0
- package/dist/dbProvider.js +1898 -0
- package/dist/freeze.d.ts +29 -0
- package/dist/freeze.js +69 -0
- package/dist/guard.d.ts +53 -0
- package/dist/guard.js +397 -0
- package/dist/insert.d.ts +9 -0
- package/dist/insert.js +44 -0
- package/dist/migrations/siteid.d.mts +738 -0
- package/dist/migrations/siteid.mjs +512 -0
- package/dist/publish.d.ts +1 -0
- package/dist/publish.js +1 -0
- package/dist/schema/dsl.d.ts +64 -0
- package/dist/schema/dsl.js +28 -0
- package/dist/schema/generate.d.mts +14 -0
- package/dist/schema/generate.mjs +282 -0
- package/dist/schema/generated/ddl.d.ts +7 -0
- package/dist/schema/generated/ddl.js +11 -0
- package/dist/schema/generated/kysely-types.d.ts +1988 -0
- package/dist/schema/generated/kysely-types.js +5 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +7 -0
- package/dist/schema/provision.d.mts +11 -0
- package/dist/schema/provision.mjs +111 -0
- package/dist/schema/tables.d.ts +2 -0
- package/dist/schema/tables.js +3301 -0
- package/dist/scripts/canon-audit.d.mts +25 -0
- package/dist/scripts/canon-audit.mjs +213 -0
- package/dist/scripts/canon-smoke.d.mts +1 -0
- package/dist/scripts/canon-smoke.mjs +44 -0
- package/dist/scripts/dialect-ddl-stand.d.mts +1 -0
- package/dist/scripts/dialect-ddl-stand.mjs +136 -0
- package/dist/scripts/owner-spec-check.d.mts +1 -0
- package/dist/scripts/owner-spec-check.mjs +166 -0
- package/dist/scripts/owners/owner-resolve-spec.json +540 -0
- package/dist/scripts/owners-audit.d.mts +60 -0
- package/dist/scripts/owners-audit.mjs +443 -0
- package/dist/scripts/owners-fixture.d.mts +20 -0
- package/dist/scripts/owners-fixture.mjs +192 -0
- package/dist/scripts/schema-lengths-audit.d.mts +22 -0
- package/dist/scripts/schema-lengths-audit.mjs +146 -0
- package/dist/scripts/siteid-migrate.d.mts +7 -0
- package/dist/scripts/siteid-migrate.mjs +260 -0
- package/dist/scripts/tenant-invariant-audit.d.mts +1 -0
- package/dist/scripts/tenant-invariant-audit.mjs +34 -0
- package/dist/singleMode.d.ts +5 -0
- package/dist/singleMode.js +23 -0
- package/dist/sqliteDialect.d.ts +6 -0
- package/dist/sqliteDialect.js +92 -0
- package/dist/templateDefs.d.ts +38 -0
- package/dist/templateDefs.js +163 -0
- package/dist/test/fixtures/broken-driver/index.d.ts +3 -0
- package/dist/test/fixtures/broken-driver/index.js +5 -0
- package/dist/test/support/dialect-stand.d.ts +37 -0
- package/dist/test/support/dialect-stand.js +162 -0
- package/dist/test/support/render-parity.d.ts +44 -0
- package/dist/test/support/render-parity.js +262 -0
- package/package.json +50 -5
- package/README.md +0 -3
|
@@ -0,0 +1,512 @@
|
|
|
1
|
+
// SiteId migration planner (task:3872, plan:48 Q5.3, requirements:64 п.1).
|
|
2
|
+
//
|
|
3
|
+
// The production copy is the LEGACY schema: the tables that always carried
|
|
4
|
+
// `SiteId` still do, the other canon site tables do not, `Sites` has no
|
|
5
|
+
// `Kind`/`Writable`. A
|
|
6
|
+
// site cannot be exported from that source — the transfer's guard scopes every
|
|
7
|
+
// read by `SiteId`. This module turns the canon plus a live introspection of the
|
|
8
|
+
// source into a deterministic migration plan, in the order a cutover needs:
|
|
9
|
+
//
|
|
10
|
+
// 1. add the missing `SiteId` columns (nullable);
|
|
11
|
+
// 2. resolve the tenant key in ONE parents-first schedule: each table's parent edges as a CHAIN
|
|
12
|
+
// ordered by class (canonical hard FK, the source's own FK, the naming convention) where a
|
|
13
|
+
// link only fills what the previous one left NULL, interleaved with the polymorphic roots
|
|
14
|
+
// (Gallery / Modules / numbering / watermarks, each root planted once every table its
|
|
15
|
+
// `resolvers` read carries a site); nothing is parked on a sentinel (task:4092);
|
|
16
|
+
// 3. enforce `NOT NULL` + index on every added column;
|
|
17
|
+
// 4. add `Sites.Writable`, then NOT NULL + index — a table still holding rows with no site is
|
|
18
|
+
// refused by name, not parked;
|
|
19
|
+
//
|
|
20
|
+
// The plan is pure data — no database — so it is testable and reviewable; the
|
|
21
|
+
// schema is the authority (a table that only exists in the legacy source is never
|
|
22
|
+
// planned), and anything the FK chain cannot reach is reported, not guessed away.
|
|
23
|
+
import { createRequire } from 'node:module';
|
|
24
|
+
import { canonicalSchema, isSiteScoped } from "../schema/index.js";
|
|
25
|
+
const require = createRequire(import.meta.url);
|
|
26
|
+
const ownerSpec = require('../scripts/owners/owner-resolve-spec.json');
|
|
27
|
+
// Polymorphic roots: a site table whose site cannot be read off a single hard FK
|
|
28
|
+
// (a polymorphic owner, a seed, or a union of template tables). The resolver contract lives in
|
|
29
|
+
// `owner-resolve-spec.json`; anything else unresolved is a bug.
|
|
30
|
+
// `Folders` belongs here for the same reason as `Gallery`: its owner is the legacy
|
|
31
|
+
// `IAssetsOwner` (13 implementors, id-probe) — the canon has no FK for it, so the planner used
|
|
32
|
+
// to hand every folder to the sweep (task:3976).
|
|
33
|
+
export const POLY_ROOTS = ['Folders', 'Gallery', 'Modules', '_Modules', 'NumberingCounters', 'GalleryImageSizeWatermark', 'GalleryImagesTemplates'];
|
|
34
|
+
/**
|
|
35
|
+
* The roots that declare `siteFrom: 'none'`: a dictionary with no owner column that nobody
|
|
36
|
+
* populates, so it has no site of its own and every row it holds would be the sweep's. The claim
|
|
37
|
+
* is only true while the table is empty — `declaredNoneWithRows` is where the plan checks it
|
|
38
|
+
* against the source instead of trusting a snapshot of the spec (task:3998).
|
|
39
|
+
*/
|
|
40
|
+
export const declaredNoneTables = (spec = ownerSpec) => Object.entries(spec.polyRoots ?? {})
|
|
41
|
+
.filter(([, root]) => root?.siteFrom === 'none')
|
|
42
|
+
.map(([name]) => name);
|
|
43
|
+
/**
|
|
44
|
+
* The order oracle (task:3998): every link must read a parent that carries a site EARLIER in the
|
|
45
|
+
* schedule.
|
|
46
|
+
*
|
|
47
|
+
* Readiness inside the planner is a membership question ("is the parent in `resolved`"), and a
|
|
48
|
+
* set cannot tell earlier from later. The rendered plan is a sequence of UPDATEs, so a link whose
|
|
49
|
+
* parent is planted after it joins against a still-NULL key and the row is quarantined for no
|
|
50
|
+
* reason — the defect of task:3891, one level down. Positions, not sets, are what the renderer
|
|
51
|
+
* obeys; this is the check that says so.
|
|
52
|
+
*/
|
|
53
|
+
export const misorderedLinks = (schedule) => {
|
|
54
|
+
const position = new Map();
|
|
55
|
+
schedule.forEach((op, index) => {
|
|
56
|
+
if (!position.has(op.table))
|
|
57
|
+
position.set(op.table, index);
|
|
58
|
+
});
|
|
59
|
+
const found = [];
|
|
60
|
+
schedule.forEach((op, index) => {
|
|
61
|
+
for (const link of op.links ?? []) {
|
|
62
|
+
const parentAt = position.get(link.parent);
|
|
63
|
+
// no position: the parent carried SiteId before the schedule started, or it is not a
|
|
64
|
+
// site table at all — neither is an order this run can get wrong
|
|
65
|
+
if (parentAt !== undefined && parentAt > index)
|
|
66
|
+
found.push({ table: op.table, parent: link.parent });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return found;
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* Parents-first order over the hard FKs between the site tables — the mirror of the transfer's
|
|
73
|
+
* insert order, and the order a purge has to REVERSE: a child dies before its parent, or the
|
|
74
|
+
* foreign key refuses the delete.
|
|
75
|
+
*/
|
|
76
|
+
const siteFkOrder = (tables) => {
|
|
77
|
+
const byName = new Map(tables.map((table) => [table.name, table]));
|
|
78
|
+
const parentsOf = (name) => (byName.get(name)?.fks ?? [])
|
|
79
|
+
.filter((fk) => !fk.soft && fk.ref !== name && byName.has(fk.ref))
|
|
80
|
+
.map((fk) => fk.ref);
|
|
81
|
+
const order = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
while (order.length < tables.length) {
|
|
84
|
+
const ready = tables.filter((table) => !seen.has(table.name) && parentsOf(table.name).every((parent) => seen.has(parent)));
|
|
85
|
+
// a hard cycle cannot be ordered; take the canon order for what is left rather than spin
|
|
86
|
+
const batch = ready.length ? ready : tables.filter((table) => !seen.has(table.name));
|
|
87
|
+
for (const table of batch) {
|
|
88
|
+
seen.add(table.name);
|
|
89
|
+
order.push(table);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return order;
|
|
93
|
+
};
|
|
94
|
+
/** The canon as the production copy looked before the migration. */
|
|
95
|
+
export const legacyFromCanon = (canon = canonicalSchema) => {
|
|
96
|
+
const tables = {};
|
|
97
|
+
for (const table of canon.tables) {
|
|
98
|
+
const columns = Object.keys(table.columns).filter((name) => {
|
|
99
|
+
if (table.siteIdAdded && name === 'SiteId')
|
|
100
|
+
return false;
|
|
101
|
+
if (table.name === 'Sites' && name === 'Writable')
|
|
102
|
+
return false;
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
tables[table.name] = {
|
|
106
|
+
columns,
|
|
107
|
+
pk: Object.entries(table.columns).find(([, column]) => column.pk === true)?.[0] ?? null,
|
|
108
|
+
// the legacy schema has no soft references — those are a canon construct
|
|
109
|
+
fks: (table.fks ?? [])
|
|
110
|
+
.filter((fk) => !fk.soft)
|
|
111
|
+
.map((fk) => ({ columns: fk.columns, ref: fk.ref, refColumns: fk.refColumns })),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return { dialect: 'mssql', tables };
|
|
115
|
+
};
|
|
116
|
+
/** Introspection: `{ dialect, tables: { Name: { columns, pk, fks } } }` — from a live source or the synthetic one. */ export const planSiteIdMigration = ({ canon = canonicalSchema, introspection, spec = ownerSpec } = {}) => {
|
|
117
|
+
if (!introspection?.tables)
|
|
118
|
+
throw new Error('plan: a source introspection is required');
|
|
119
|
+
const source = introspection.tables;
|
|
120
|
+
const site = canon.tables.filter((t) => isSiteScoped(t) && source[t.name]);
|
|
121
|
+
const siteNames = new Set(site.map((t) => t.name));
|
|
122
|
+
const hasSiteId = (name) => (source[name]?.columns ?? []).includes('SiteId');
|
|
123
|
+
const addColumns = site
|
|
124
|
+
.filter((t) => !hasSiteId(t.name))
|
|
125
|
+
.map((t) => ({ op: 'addColumn', table: t.name, column: 'SiteId', columnType: 'uuid' }));
|
|
126
|
+
// ---- edges: canon hard FKs + live FKs + convention (`XxxId` == a table PK) --
|
|
127
|
+
// An edge is one way to read a site off a parent, and a table may have several. The CLASS is
|
|
128
|
+
// the whole point: a canonical hard FK is the declared owner, a live FK is the source's own
|
|
129
|
+
// constraint, the naming convention is the last resort. The chain is ordered by it
|
|
130
|
+
// (task:3894 — the planner used to keep the FIRST arc the graph happened to walk and drop the
|
|
131
|
+
// rest: `ProductTypes` resolved through an empty `CustomCategoryTypes` while its canonical
|
|
132
|
+
// `Stores` edge was never emitted, and 24/24 rows went to the sentinel with the correct
|
|
133
|
+
// answer one edge away).
|
|
134
|
+
const CANON_FK = 0;
|
|
135
|
+
const LIVE_FK = 1;
|
|
136
|
+
const CONVENTION = 2;
|
|
137
|
+
const edgeByKey = new Map();
|
|
138
|
+
let declaration = 0;
|
|
139
|
+
const addEdge = (rawRank, parent, table, childKey, parentKey) => {
|
|
140
|
+
// a half-edge is nothing to join on, and a key that points at ITSELF (`SiteId` -> `Sites.SiteId`)
|
|
141
|
+
// plants no site — it is already one
|
|
142
|
+
if (!parent || !table || !childKey || !parentKey)
|
|
143
|
+
return;
|
|
144
|
+
// a table's own hierarchy IS a path (task:4092): a `ContentPageParts` row inherits the site
|
|
145
|
+
// of its parent PART, so the self-edge runs LAST — after the table has been planted from
|
|
146
|
+
// its real parents — and only fills what is still NULL
|
|
147
|
+
const rank = parent === table ? CONVENTION + 1 : rawRank;
|
|
148
|
+
const key = `${parent}|${table}|${childKey}|${parentKey}`;
|
|
149
|
+
// the same join seen from two classes is one join — the better class wins, or the plan
|
|
150
|
+
// would render the same UPDATE twice
|
|
151
|
+
if (edgeByKey.has(key))
|
|
152
|
+
return;
|
|
153
|
+
// canonical edges keep the order the canon declares them in — for one child that order is
|
|
154
|
+
// the ranking within the class (the owner column is declared before the weaker link)
|
|
155
|
+
edgeByKey.set(key, { rank, order: rank === CANON_FK ? declaration++ : Number.MAX_SAFE_INTEGER, parent, table, childKey, parentKey });
|
|
156
|
+
};
|
|
157
|
+
for (const table of canon.tables) {
|
|
158
|
+
for (const fk of table.fks ?? [])
|
|
159
|
+
if (!fk.soft)
|
|
160
|
+
addEdge(CANON_FK, fk.ref, table.name, fk.columns[0], fk.refColumns[0]);
|
|
161
|
+
}
|
|
162
|
+
for (const [name, info] of Object.entries(source)) {
|
|
163
|
+
for (const fk of info.fks ?? [])
|
|
164
|
+
addEdge(LIVE_FK, fk.ref, name, fk.columns[0], fk.refColumns[0]);
|
|
165
|
+
}
|
|
166
|
+
const pkOf = new Map(Object.entries(source).map(([name, info]) => [name, info.pk]));
|
|
167
|
+
for (const [name, info] of Object.entries(source)) {
|
|
168
|
+
for (const column of info.columns) {
|
|
169
|
+
if (column === 'SiteId')
|
|
170
|
+
continue;
|
|
171
|
+
const stem = column.replace(/_?Id$/, '');
|
|
172
|
+
if (!stem || stem === column)
|
|
173
|
+
continue;
|
|
174
|
+
// a convention edge is only safe when exactly ONE canon table has that
|
|
175
|
+
// primary key — `TemplateId` points at half a dozen tables otherwise
|
|
176
|
+
const candidates = [...pkOf].filter(([candidate, pk]) => pk === column && candidate !== name && siteNames.has(candidate));
|
|
177
|
+
if (candidates.length === 1)
|
|
178
|
+
addEdge(CONVENTION, candidates[0][0], name, column, column);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const compareEdges = (a, b) => a.rank - b.rank
|
|
182
|
+
|| a.order - b.order
|
|
183
|
+
|| a.parent.localeCompare(b.parent)
|
|
184
|
+
|| a.childKey.localeCompare(b.childKey);
|
|
185
|
+
const linksInto = new Map();
|
|
186
|
+
for (const edge of [...edgeByKey.values()].sort(compareEdges)) {
|
|
187
|
+
if (!linksInto.has(edge.table))
|
|
188
|
+
linksInto.set(edge.table, []);
|
|
189
|
+
linksInto.get(edge.table).push(edge);
|
|
190
|
+
}
|
|
191
|
+
// ---- the schedule: chains parents-first, interleaved with the polymorphic roots -----
|
|
192
|
+
// `schedule` is the ORDER, and it is the only one: a table may join a parent only after that
|
|
193
|
+
// parent carries a site. Rendering the joins as one block and the roots as another (the shape
|
|
194
|
+
// this replaced) backfilled GalleryImage/GalleryImageSize/Watermark/ModuleProperties from a
|
|
195
|
+
// parent that was still NULL, and the sweep then parked whole families in quarantine
|
|
196
|
+
// (task:3891). The same discipline now covers a table's SECOND parent: a fallback that runs
|
|
197
|
+
// before its primary reads a NULL, and the row is quarantined for no reason (task:3894).
|
|
198
|
+
//
|
|
199
|
+
// `plantable` OVER-approximates on purpose: it is reachability with the order ignored, so a
|
|
200
|
+
// pure FK cycle whose members only reach each other is plantable and still unresolvable — it
|
|
201
|
+
// is reported in `unresolved` (as it must be: nothing in the cycle carries a site) rather
|
|
202
|
+
// than stepped with an empty chain.
|
|
203
|
+
// "The column exists" and "the rows carry a site" are two different facts (task:4092):
|
|
204
|
+
// `FeedPages`, `ContentPageParts` and `_Templates` carry `SiteId` in the legacy source and were
|
|
205
|
+
// therefore given no step at all — whatever stayed NULL went to the sentinel. A table needs a
|
|
206
|
+
// step whenever some parent can hand it a site. "No parent" means: no FK to a
|
|
207
|
+
// site table, no live FK, no convention edge, no poly spec link — the DSL treats `SiteId` as the
|
|
208
|
+
// tenant key itself, so the Blog/Catalog/Store families declare nothing to inherit from. With the
|
|
209
|
+
// key already in the source they are self-evident (their rows were scoped before this migration);
|
|
210
|
+
// without it they are `unresolved` and the run refuses instead of inventing a site (task:4092).
|
|
211
|
+
const parentsOf = (name) => (linksInto.get(name) ?? []).filter((edge) => edge.parent !== 'Sites' && siteNames.has(edge.parent));
|
|
212
|
+
const siteOrder = site.map((t) => t.name);
|
|
213
|
+
const selfEvident = new Set(siteOrder.filter((name) => hasSiteId(name) && !parentsOf(name).length && !POLY_ROOTS.includes(name)));
|
|
214
|
+
const resolved = new Set(['Sites', ...selfEvident]);
|
|
215
|
+
const ordered = site.map((t) => t.name);
|
|
216
|
+
const declaredNone = (name) => spec.polyRoots?.[name]?.siteFrom === 'none';
|
|
217
|
+
// A root is READY only when every table its resolvers read carries a site:
|
|
218
|
+
// `GalleryImageSizeWatermark` hangs off `GalleryImageSize`, which is itself a child of the
|
|
219
|
+
// `Gallery` root. Planting it in the same round as `Gallery` would read a NULL from a parent
|
|
220
|
+
// planted later — the defect of task:3891, one level deeper (task:3876).
|
|
221
|
+
const polyReady = (name, carries) => {
|
|
222
|
+
const links = polyLinks(name, spec);
|
|
223
|
+
if (!links.length)
|
|
224
|
+
return declaredNone(name);
|
|
225
|
+
return links.every((link) => carries.has(link.table));
|
|
226
|
+
};
|
|
227
|
+
// Which added tables can EVER carry a site — pure reachability over the edge graph, no order.
|
|
228
|
+
// An edge from a table that never carries one (`Administrators`) is not a path, and neither is
|
|
229
|
+
// an arc into a table with no path of its own: `resolved` means "the plan carries a path",
|
|
230
|
+
// never "the graph found an arc" (task:3894).
|
|
231
|
+
const plantable = new Set(resolved);
|
|
232
|
+
for (;;) {
|
|
233
|
+
let changed = false;
|
|
234
|
+
for (const name of ordered) {
|
|
235
|
+
if (plantable.has(name) || selfEvident.has(name))
|
|
236
|
+
continue;
|
|
237
|
+
const reachable = POLY_ROOTS.includes(name)
|
|
238
|
+
? polyReady(name, plantable)
|
|
239
|
+
: (linksInto.get(name) ?? []).some((edge) => plantable.has(edge.parent));
|
|
240
|
+
if (reachable) {
|
|
241
|
+
plantable.add(name);
|
|
242
|
+
changed = true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!changed)
|
|
246
|
+
break;
|
|
247
|
+
}
|
|
248
|
+
const schedule = [];
|
|
249
|
+
const pending = new Set(ordered.filter((name) => !selfEvident.has(name)));
|
|
250
|
+
// A chain step links only the parents that ALREADY carry a site: every other parent is either
|
|
251
|
+
// still to come (then the table waits for it) or has no path at all (then it is no link).
|
|
252
|
+
const linkKey = (link) => `${link.parent}|${link.childKey}|${link.parentKey}`;
|
|
253
|
+
const linksOf = (name) => (linksInto.get(name) ?? [])
|
|
254
|
+
.filter((edge) => edge.parent === name || resolved.has(edge.parent))
|
|
255
|
+
.map((edge) => ({ parent: edge.parent, childKey: edge.childKey, parentKey: edge.parentKey }));
|
|
256
|
+
// Which links a table has already been given: a cycle break emits a partial chain, and the
|
|
257
|
+
// rest is a catch-up step later, never a link emitted twice.
|
|
258
|
+
const linked = new Map();
|
|
259
|
+
const step = (name) => {
|
|
260
|
+
pending.delete(name);
|
|
261
|
+
resolved.add(name);
|
|
262
|
+
const links = linksOf(name);
|
|
263
|
+
const seen = linked.get(name) ?? new Set();
|
|
264
|
+
linked.set(name, seen);
|
|
265
|
+
for (const link of links)
|
|
266
|
+
seen.add(linkKey(link));
|
|
267
|
+
schedule.push({ op: 'backfillJoin', table: name, links });
|
|
268
|
+
};
|
|
269
|
+
for (;;) {
|
|
270
|
+
let progress = false;
|
|
271
|
+
// A table waits for EVERY parent its chain will read that can carry a site.
|
|
272
|
+
for (;;) {
|
|
273
|
+
const ready = ordered.filter((name) => pending.has(name) && !POLY_ROOTS.includes(name) && plantable.has(name)
|
|
274
|
+
&& (linksInto.get(name) ?? []).filter((edge) => plantable.has(edge.parent) && edge.parent !== name).every((edge) => resolved.has(edge.parent)));
|
|
275
|
+
if (!ready.length)
|
|
276
|
+
break;
|
|
277
|
+
for (const name of ready) {
|
|
278
|
+
step(name);
|
|
279
|
+
progress = true;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
for (const name of ordered.filter((n) => pending.has(n) && POLY_ROOTS.includes(n) && polyReady(n, resolved))) {
|
|
283
|
+
const links = polyLinks(name, spec);
|
|
284
|
+
// A root may declare that it has no site of its own (`siteFrom: 'none'`) — a
|
|
285
|
+
// dictionary with no owner column that nobody populates. Then there is nothing to
|
|
286
|
+
// resolve, and the sweep is the whole answer. A root that is neither linked nor
|
|
287
|
+
// declared cannot be resolved at all: it is reported so the run refuses (the CLI
|
|
288
|
+
// exits on a non-empty `unresolved`) instead of quietly handing a whole family to
|
|
289
|
+
// the quarantine site.
|
|
290
|
+
pending.delete(name);
|
|
291
|
+
resolved.add(name);
|
|
292
|
+
schedule.push({ op: 'backfillPoly', table: name, links, declaredNone: declaredNone(name) });
|
|
293
|
+
// A root is not ONLY its owner probe: `Folders` also hangs off `*_ThemeId` (canon FK),
|
|
294
|
+
// and a theme folder carries no owner at all. Leaving the chained half out dropped 768
|
|
295
|
+
// theme folders — and the 10 827 files beneath them — into the purge (task:3976).
|
|
296
|
+
const chained = linksOf(name).map((link) => ({ parent: link.parent, childKey: link.childKey, parentKey: link.parentKey }));
|
|
297
|
+
if (chained.length) {
|
|
298
|
+
const seen = linked.get(name) ?? new Set();
|
|
299
|
+
linked.set(name, seen);
|
|
300
|
+
for (const link of chained)
|
|
301
|
+
seen.add(linkKey(link));
|
|
302
|
+
schedule.push({ op: 'backfillJoin', table: name, links: chained });
|
|
303
|
+
}
|
|
304
|
+
progress = true;
|
|
305
|
+
}
|
|
306
|
+
if (progress)
|
|
307
|
+
continue;
|
|
308
|
+
// Work is left and nothing is ready: the remaining tables wait on each other (a mutual FK
|
|
309
|
+
// pair — `FeedParams`/`FeedPageParams`). One deterministic step (canon order) breaks the
|
|
310
|
+
// cycle, so an honest plan always exists and the run terminates. A POLYMORPHIC root is
|
|
311
|
+
// never a candidate: its site comes from its own contract, and guessing one off a passing
|
|
312
|
+
// FK is exactly the silent guess task:3891 removed. A pair with no root at all has no
|
|
313
|
+
// site to inherit (that is not a cycle to break) and lands in `unresolved`.
|
|
314
|
+
const stuck = ordered.find((name) => pending.has(name) && plantable.has(name) && !POLY_ROOTS.includes(name)
|
|
315
|
+
&& (linksInto.get(name) ?? []).some((edge) => resolved.has(edge.parent)));
|
|
316
|
+
if (stuck === undefined)
|
|
317
|
+
break;
|
|
318
|
+
step(stuck);
|
|
319
|
+
}
|
|
320
|
+
// A table stepped by the cycle break linked only what carried a site at that moment, so its
|
|
321
|
+
// remaining parents — resolved by now, every table that can carry a site is — are emitted as
|
|
322
|
+
// ONE catch-up step at the end. A fallback link that never runs leaves rows in quarantine the
|
|
323
|
+
// plan itself could have resolved (task:3894). A table that is never stepped at all has no
|
|
324
|
+
// resolved parent (or it would be plantable), so nothing is emitted for it here.
|
|
325
|
+
for (const name of ordered) {
|
|
326
|
+
// self-evident tables were given no step on purpose: they have no parent to inherit from
|
|
327
|
+
if (pending.has(name) || selfEvident.has(name) || POLY_ROOTS.includes(name))
|
|
328
|
+
continue;
|
|
329
|
+
const seen = linked.get(name) ?? new Set();
|
|
330
|
+
const missing = linksOf(name).filter((link) => !seen.has(linkKey(link)));
|
|
331
|
+
if (missing.length)
|
|
332
|
+
schedule.push({ op: 'backfillJoin', table: name, links: missing });
|
|
333
|
+
}
|
|
334
|
+
const unresolved = [...pending].sort();
|
|
335
|
+
// The plan's two named views of the schedule. They are derived, never accumulated
|
|
336
|
+
// separately: a second list is a second order, and the second order was wrong.
|
|
337
|
+
const backfills = schedule.filter((op) => op.op === 'backfillJoin');
|
|
338
|
+
const poly = schedule.filter((op) => op.op === 'backfillPoly');
|
|
339
|
+
// There is NO sweep and no sentinel (task:4092). A row that ends the run without a site is a
|
|
340
|
+
// missing edge in the resolver graph, not data to be filed away: parking it made the loss
|
|
341
|
+
// invisible (169 175 rows on a green run) and hid the real defect — two tables that already
|
|
342
|
+
// carried `SiteId` were never backfilled at all. The renderer refuses instead, naming the
|
|
343
|
+
// table, and `renderPurgeMssql` deletes only what an operator explicitly condemns.
|
|
344
|
+
const purge = [...siteFkOrder(site)].reverse().map((table) => ({ op: 'purgeNull', table: table.name, pk: pkOf.get(table.name) ?? null }));
|
|
345
|
+
// NOT NULL and the index are the target shape for EVERY site table, not only the ones this run
|
|
346
|
+
// gave a column to: a key the catalog still reports nullable is the same defect as a missing one
|
|
347
|
+
// (task:4092). The canon says NOT NULL everywhere, so the migration owes the database that shape.
|
|
348
|
+
const siteNamesSorted = [...siteNames].sort();
|
|
349
|
+
const keyIsNullable = (name) => !hasSiteId(name) || source[name]?.nullable?.SiteId === true;
|
|
350
|
+
// An index on the key blocks the ALTER (SQL Server 4922), so it comes off first and goes back
|
|
351
|
+
// afterwards — with the definition the catalog gave, not one the canon guessed: `UK1_Pages` is a
|
|
352
|
+
// business rule (unique per site and path) and dropping it for good would be silent damage.
|
|
353
|
+
const indexesOnKey = (name) => (source[name]?.indexes ?? [])
|
|
354
|
+
.filter((ix) => ix.columns.some((column) => column.name === 'SiteId') || (ix.include ?? []).includes('SiteId'))
|
|
355
|
+
.map((ix) => ({
|
|
356
|
+
name: ix.name,
|
|
357
|
+
unique: Boolean(ix.unique),
|
|
358
|
+
constraint: Boolean(ix.constraint),
|
|
359
|
+
columns: ix.columns,
|
|
360
|
+
include: ix.include ?? [],
|
|
361
|
+
filter: ix.filter ?? null,
|
|
362
|
+
}));
|
|
363
|
+
const notNull = siteNamesSorted
|
|
364
|
+
.filter(keyIsNullable)
|
|
365
|
+
.map((table) => ({ op: 'setNotNull', table, column: 'SiteId', rebuild: indexesOnKey(table) }));
|
|
366
|
+
const indexes = siteNamesSorted.map((table) => ({ op: 'createIndex', table, column: 'SiteId', name: `IX_${table}_SiteId` }));
|
|
367
|
+
// A declared-none root is a promise that the table is empty. Check it against the source the
|
|
368
|
+
// plan was built from (the CLI measures those tables with `introspect({ countRows })`), because
|
|
369
|
+
// the promise was otherwise only compared with a snapshot in `owner-spec-check` — and a row in
|
|
370
|
+
// a dictionary believed empty goes to the quarantine with nothing in the run to notice
|
|
371
|
+
// (task:3998). The plan still carries the step; the CLI is where the run is refused.
|
|
372
|
+
const rowCounts = introspection.rowCounts ?? {};
|
|
373
|
+
const declaredNoneWithRows = declaredNoneTables(spec)
|
|
374
|
+
.filter((name) => source[name])
|
|
375
|
+
.map((name) => ({ table: name, rows: Number(rowCounts[name] ?? 0) }))
|
|
376
|
+
.filter((entry) => entry.rows > 0);
|
|
377
|
+
const misordered = misorderedLinks(schedule);
|
|
378
|
+
return {
|
|
379
|
+
addColumns,
|
|
380
|
+
// the order a run must follow: joins and poly roots interleaved parents-first
|
|
381
|
+
schedule,
|
|
382
|
+
selfEvident: [...selfEvident].sort(),
|
|
383
|
+
backfills,
|
|
384
|
+
poly,
|
|
385
|
+
purge,
|
|
386
|
+
notNull,
|
|
387
|
+
indexes,
|
|
388
|
+
registry: {
|
|
389
|
+
addColumns: [
|
|
390
|
+
{ op: 'addColumn', table: 'Sites', column: 'Writable', columnType: 'bool' },
|
|
391
|
+
],
|
|
392
|
+
default: { op: 'backfillRegistry', table: 'Sites', writable: true },
|
|
393
|
+
},
|
|
394
|
+
unresolved,
|
|
395
|
+
declaredNoneWithRows,
|
|
396
|
+
misordered,
|
|
397
|
+
summary: {
|
|
398
|
+
added: addColumns.length,
|
|
399
|
+
chained: backfills.length,
|
|
400
|
+
links: backfills.reduce((total, op) => total + op.links.length, 0),
|
|
401
|
+
polymorphic: poly.length,
|
|
402
|
+
purge: purge.length,
|
|
403
|
+
notNull: notNull.length,
|
|
404
|
+
unresolved: unresolved.length,
|
|
405
|
+
declaredNoneWithRows: declaredNoneWithRows.length,
|
|
406
|
+
misordered: misordered.length,
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* The `(owner column -> table, pk)` links a polymorphic root resolves through.
|
|
412
|
+
*
|
|
413
|
+
* The spec is the contract (task:3876): a root declares its `resolvers` explicitly — a parent
|
|
414
|
+
* chain for watermarks, the soft owner references for counters, the polymorphic owner list for
|
|
415
|
+
* galleries and modules. There is NO fallback: a root the spec does not describe resolves through
|
|
416
|
+
* nothing, lands in `unresolved`, and the CLI refuses to run the plan — a spec edit that forgets a
|
|
417
|
+
* root must fail loudly, not silently probe whatever `observedOwners` happened to list first
|
|
418
|
+
* (task:3895: `Gallery` and `_Modules` leaned on exactly that implicit order).
|
|
419
|
+
*/
|
|
420
|
+
export const polyLinks = (name, spec) => (spec.polyRoots?.[name]?.resolvers ?? []).filter((link) => link?.column && link?.table && link?.pk);
|
|
421
|
+
/** MSSQL rendering of the plan — the source dialect of a cutover. */
|
|
422
|
+
const dropIndexSql = (table, index) => (index.constraint
|
|
423
|
+
? `ALTER TABLE [dbo].[${table}] DROP CONSTRAINT [${index.name}];`
|
|
424
|
+
: `DROP INDEX [${index.name}] ON [dbo].[${table}];`);
|
|
425
|
+
const createIndexSql = (table, index) => {
|
|
426
|
+
const columns = index.columns.map((column) => `[${column.name}]${column.descending ? ' DESC' : ''}`).join(', ');
|
|
427
|
+
const include = index.include?.length ? ` INCLUDE (${index.include.map((name) => `[${name}]`).join(', ')})` : '';
|
|
428
|
+
const filter = index.filter ? ` WHERE ${index.filter}` : '';
|
|
429
|
+
return index.constraint
|
|
430
|
+
? `ALTER TABLE [dbo].[${table}] ADD CONSTRAINT [${index.name}] UNIQUE (${columns});`
|
|
431
|
+
: `CREATE ${index.unique ? 'UNIQUE ' : ''}INDEX [${index.name}] ON [dbo].[${table}] (${columns})${include}${filter};`;
|
|
432
|
+
};
|
|
433
|
+
export const renderMssql = (plan) => {
|
|
434
|
+
const lines = ['-- snolla: SiteId migration (generated from the canon, task:3872). Run on a COPY.', ''];
|
|
435
|
+
const mssqlType = (type) => ({ uuid: 'UNIQUEIDENTIFIER', string: 'NVARCHAR(20)', bool: 'BIT' })[type] ?? 'NVARCHAR(255)';
|
|
436
|
+
// the plan states the tenant key's type once; step 1 and step 5 must not be able to disagree
|
|
437
|
+
// about it (task:3898: step 5 hardcoded UNIQUEIDENTIFIER while step 1 read the spec)
|
|
438
|
+
const keyTypes = new Map(plan.addColumns.map((op) => [op.table, op.columnType]));
|
|
439
|
+
const [writableColumn] = plan.registry.addColumns.map((op) => op.column);
|
|
440
|
+
const literal = (value) => (typeof value === 'boolean' ? (value ? 1 : 0) : `'${value}'`);
|
|
441
|
+
// every step below is guarded: a rehearsal is re-run after an interruption, and the purge is a
|
|
442
|
+
// decision taken after the apply — neither may need the whole plan to be a no-op (task:3996)
|
|
443
|
+
lines.push('-- step 1: add the tenant key (nullable first)');
|
|
444
|
+
for (const op of plan.addColumns) {
|
|
445
|
+
lines.push(`IF COL_LENGTH('dbo.${op.table}','SiteId') IS NULL ALTER TABLE [dbo].[${op.table}] ADD [SiteId] ${mssqlType(op.columnType)} NULL;`);
|
|
446
|
+
}
|
|
447
|
+
lines.push('', '-- step 2: resolve the tenant key parents-first — a polymorphic root plants its\n-- children, so the root is resolved BEFORE the joins that read it (task:3891)');
|
|
448
|
+
for (const op of plan.schedule) {
|
|
449
|
+
if (op.op === 'backfillJoin') {
|
|
450
|
+
if ((op.links ?? []).length > 1) {
|
|
451
|
+
lines.push(`-- [dbo].[${op.table}]: ${op.links.length} candidate parents, the declared owner first;\n-- each fallback only fills what the previous link left NULL (task:3894)`);
|
|
452
|
+
}
|
|
453
|
+
for (const link of op.links ?? []) {
|
|
454
|
+
// `SiteId -> Sites.SiteId` is the key reading itself: it carries the table in the
|
|
455
|
+
// readiness graph, and it writes nothing
|
|
456
|
+
if (link.childKey === 'SiteId' && link.parentKey === 'SiteId')
|
|
457
|
+
continue;
|
|
458
|
+
lines.push(`UPDATE c SET c.[SiteId] = p.[SiteId] FROM [dbo].[${op.table}] c JOIN [dbo].[${link.parent}] p ON p.[${link.parentKey}] = c.[${link.childKey}] WHERE c.[SiteId] IS NULL;`);
|
|
459
|
+
}
|
|
460
|
+
continue;
|
|
461
|
+
}
|
|
462
|
+
const links = (op.links ?? []).filter((link) => link.table && link.pk && link.column);
|
|
463
|
+
if (!links.length) {
|
|
464
|
+
lines.push(op.declaredNone
|
|
465
|
+
? `-- DECLARED NONE: [dbo].[${op.table}] carries no site of its own (owner-resolve-spec siteFrom: none) — any row it holds is the sweep's.`
|
|
466
|
+
: `-- MANUAL: [dbo].[${op.table}] — no single owner column in the spec; resolve from the legacy link (owner-resolve-spec.json).`);
|
|
467
|
+
}
|
|
468
|
+
for (const link of links) {
|
|
469
|
+
lines.push(`UPDATE c SET c.[SiteId] = o.[SiteId] FROM [dbo].[${op.table}] c JOIN [dbo].[${link.table}] o ON o.[${link.pk}] = c.[${link.column}] WHERE c.[SiteId] IS NULL;`);
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
lines.push('', '-- step 3: the registry flag — every real site is writable (nothing is parked, task:4092)');
|
|
473
|
+
// the registry DDL is rendered from `plan.registry`: the plan states it, the renderer prints it
|
|
474
|
+
// (task:3898 — the same DDL written in two places drifts apart silently)
|
|
475
|
+
for (const op of plan.registry.addColumns) {
|
|
476
|
+
lines.push(`IF COL_LENGTH('dbo.${op.table}','${op.column}') IS NULL ALTER TABLE [dbo].[${op.table}] ADD [${op.column}] ${mssqlType(op.columnType)} NULL;`);
|
|
477
|
+
}
|
|
478
|
+
lines.push(`UPDATE [dbo].[${plan.registry.default.table}] SET [${writableColumn}] = ${literal(plan.registry.default.writable)};`);
|
|
479
|
+
lines.push('', '-- step 4: enforce NOT NULL + index — a table still holding rows with no site THROWS and', '-- names itself. A dangling row is a missing edge in the resolver graph (task:4092); parking', '-- it on a sentinel site is what made the loss invisible while the run reported green.');
|
|
480
|
+
for (const op of plan.notNull) {
|
|
481
|
+
lines.push(`IF EXISTS (SELECT 1 FROM [dbo].[${op.table}] WHERE [SiteId] IS NULL) THROW 50000, '${op.table}: rows with no site — fix the edge (owner-resolve-spec / canon FK), do not park them', 1;`);
|
|
482
|
+
// an index that sits on the key blocks the ALTER (4922): off, tighten, back on — the definition
|
|
483
|
+
// is the catalog's, so a legacy unique constraint survives the trip
|
|
484
|
+
for (const index of op.rebuild ?? [])
|
|
485
|
+
lines.push(dropIndexSql(op.table, index));
|
|
486
|
+
lines.push(`ALTER TABLE [dbo].[${op.table}] ALTER COLUMN [SiteId] ${mssqlType(keyTypes.get(op.table) ?? 'uuid')} NOT NULL;`);
|
|
487
|
+
for (const index of op.rebuild ?? [])
|
|
488
|
+
lines.push(createIndexSql(op.table, index));
|
|
489
|
+
}
|
|
490
|
+
for (const op of plan.indexes) {
|
|
491
|
+
lines.push(`IF NOT EXISTS (SELECT 1 FROM sys.indexes WHERE [name] = '${op.name}' AND [object_id] = OBJECT_ID('dbo.${op.table}')) CREATE INDEX [${op.name}] ON [dbo].[${op.table}] ([SiteId]);`);
|
|
492
|
+
}
|
|
493
|
+
return lines.join('\n') + '\n';
|
|
494
|
+
};
|
|
495
|
+
/**
|
|
496
|
+
* The explicit deletion pass (task:3976, reshaped by task:4092) — the destructive half, rendered
|
|
497
|
+
* separately on purpose.
|
|
498
|
+
*
|
|
499
|
+
* It deletes exactly the rows the plan could not place, and nothing else: no sentinel site, no
|
|
500
|
+
* invented tenant. Children first — the order is the reverse of the parents-first one, because a
|
|
501
|
+
* hard FK refuses the other order. An operator runs this only after reading the refusal the
|
|
502
|
+
* cutover SQL raised: every run of it is a decision that these rows are dead, not misplaced.
|
|
503
|
+
*/
|
|
504
|
+
export const renderPurgeMssql = (plan) => {
|
|
505
|
+
const lines = ['-- snolla: delete the rows with no site (task:4092). Children first — a hard FK refuses the other order.', ''];
|
|
506
|
+
for (const op of plan.purge ?? []) {
|
|
507
|
+
if (!op.pk)
|
|
508
|
+
continue;
|
|
509
|
+
lines.push(`DELETE FROM [dbo].[${op.table}] WHERE [SiteId] IS NULL;`);
|
|
510
|
+
}
|
|
511
|
+
return lines.join('\n') + '\n';
|
|
512
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/publish.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
export declare const CANON_TYPES: readonly ["uuid", "string", "text", "int", "bigint", "bool", "decimal", "datetime", "date", "binary", "json", "enum"];
|
|
2
|
+
export type CanonType = (typeof CANON_TYPES)[number];
|
|
3
|
+
/**
|
|
4
|
+
* How a row gets into a database during a move (task:3936, design wiki:3874 §2).
|
|
5
|
+
*
|
|
6
|
+
* One schema serves every mode — a row is not in "another database" because of its
|
|
7
|
+
* scope. The labels say how a transfer brings it along:
|
|
8
|
+
* * `site` — carried by the row's own `SiteId`, with the tenant's ordinary tables;
|
|
9
|
+
* * `control` / `registry` / `dictionary` — carries no `SiteId`; provisioned by the
|
|
10
|
+
* rows that reference it (or by the cutover), so a bundle does not carry it whole.
|
|
11
|
+
*/
|
|
12
|
+
export type TableScope = 'site' | 'control' | 'registry' | 'dictionary';
|
|
13
|
+
export interface ColumnSpec {
|
|
14
|
+
type: CanonType;
|
|
15
|
+
/** for `string`: max characters (dialect-neutral, characters not bytes). */
|
|
16
|
+
length?: number;
|
|
17
|
+
/** for `decimal`: total digits. */
|
|
18
|
+
precision?: number;
|
|
19
|
+
/** for `decimal`: digits after the point. */
|
|
20
|
+
scale?: number;
|
|
21
|
+
/** default false — NOT NULL unless stated otherwise. */
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
pk?: boolean;
|
|
24
|
+
/** canonical default expression in canon form (dialect-neutral), if any. */
|
|
25
|
+
default?: string;
|
|
26
|
+
}
|
|
27
|
+
export interface IndexSpec {
|
|
28
|
+
columns: string[];
|
|
29
|
+
unique?: boolean;
|
|
30
|
+
}
|
|
31
|
+
export interface FkSpec {
|
|
32
|
+
columns: string[];
|
|
33
|
+
ref: string;
|
|
34
|
+
refColumns: string[];
|
|
35
|
+
/**
|
|
36
|
+
* Soft reference (plan:48 R1-Q3): the column keeps pointing at a
|
|
37
|
+
* control-plane row but the FK is not enforced in the site database —
|
|
38
|
+
* audit links to `Administrators` and the like.
|
|
39
|
+
*/
|
|
40
|
+
soft?: boolean;
|
|
41
|
+
}
|
|
42
|
+
export interface TableSpec {
|
|
43
|
+
name: string;
|
|
44
|
+
scope: TableScope;
|
|
45
|
+
/** true for the 81 tables that gain `SiteId` during the migration. */
|
|
46
|
+
siteIdAdded?: boolean;
|
|
47
|
+
columns: Record<string, ColumnSpec>;
|
|
48
|
+
indexes?: IndexSpec[];
|
|
49
|
+
fks?: FkSpec[];
|
|
50
|
+
}
|
|
51
|
+
export interface SiteSeed {
|
|
52
|
+
SiteId: string;
|
|
53
|
+
Alias: string;
|
|
54
|
+
Title: string;
|
|
55
|
+
Writable: boolean;
|
|
56
|
+
Live: boolean;
|
|
57
|
+
Production: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface CanonSchema {
|
|
60
|
+
/** revision of the canon; a bump is a migration (plan:49 Q5). */
|
|
61
|
+
version: number;
|
|
62
|
+
tables: TableSpec[];
|
|
63
|
+
}
|
|
64
|
+
export declare const isSiteScoped: (t: TableSpec) => boolean;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Canonical schema DSL (task:3812, R2 decisions — plan:49 Q2/Q3/Q4).
|
|
2
|
+
//
|
|
3
|
+
// THE canon is a typed, dialect-neutral description of the product schema.
|
|
4
|
+
// Generators derive from it (task:3820): Kysely `Database` types, per-dialect
|
|
5
|
+
// DDL, the temporary Prisma bridge. Nothing else may describe the schema —
|
|
6
|
+
// hand-written duplicates are what this file exists to remove.
|
|
7
|
+
//
|
|
8
|
+
// Conventions:
|
|
9
|
+
// * identifiers are PascalCase, exactly as in the legacy DB (plan:49 Q3) —
|
|
10
|
+
// no renaming tax in ETL or in diffing against the live database;
|
|
11
|
+
// * column types come from the 12-type vocabulary below (plan:49 Q4); a type
|
|
12
|
+
// outside it is a canon bug, not a convenience;
|
|
13
|
+
// * `nullable: false` is the default: a nullable column says so explicitly.
|
|
14
|
+
export const CANON_TYPES = [
|
|
15
|
+
'uuid',
|
|
16
|
+
'string',
|
|
17
|
+
'text',
|
|
18
|
+
'int',
|
|
19
|
+
'bigint',
|
|
20
|
+
'bool',
|
|
21
|
+
'decimal',
|
|
22
|
+
'datetime',
|
|
23
|
+
'date',
|
|
24
|
+
'binary',
|
|
25
|
+
'json',
|
|
26
|
+
'enum',
|
|
27
|
+
];
|
|
28
|
+
export const isSiteScoped = (t) => t.scope === 'site';
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export const DIALECTS: string[];
|
|
2
|
+
export function mapType(column: any, dialect: any): any;
|
|
3
|
+
/** Postgres refuses identifiers longer than 63 characters; it is the strictest dialect. */
|
|
4
|
+
export const MAX_IDENTIFIER_LENGTH: 63;
|
|
5
|
+
export function indexName(base: any, index?: number): any;
|
|
6
|
+
export function constraintName(table: any, fk: any, index: any): any;
|
|
7
|
+
export function renderDdlSql(dialect: any): string;
|
|
8
|
+
export function renderDdlModule(): string;
|
|
9
|
+
export function renderKyselyTypes(): string;
|
|
10
|
+
export const ARTIFACTS: {
|
|
11
|
+
'ddl.ts': () => string;
|
|
12
|
+
'kysely-types.ts': () => string;
|
|
13
|
+
};
|
|
14
|
+
export function writeAll(): Promise<void>;
|