@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.
Files changed (67) hide show
  1. package/dist/actor.d.ts +28 -0
  2. package/dist/actor.js +63 -0
  3. package/dist/audit.d.ts +36 -0
  4. package/dist/audit.js +198 -0
  5. package/dist/dataProvider.d.ts +41 -0
  6. package/dist/dataProvider.js +148 -0
  7. package/dist/dataSource.d.ts +4 -0
  8. package/dist/dataSource.js +65 -0
  9. package/dist/dbProvider.d.ts +1261 -0
  10. package/dist/dbProvider.js +1898 -0
  11. package/dist/freeze.d.ts +29 -0
  12. package/dist/freeze.js +69 -0
  13. package/dist/guard.d.ts +53 -0
  14. package/dist/guard.js +397 -0
  15. package/dist/insert.d.ts +9 -0
  16. package/dist/insert.js +44 -0
  17. package/dist/migrations/siteid.d.mts +738 -0
  18. package/dist/migrations/siteid.mjs +512 -0
  19. package/dist/publish.d.ts +1 -0
  20. package/dist/publish.js +1 -0
  21. package/dist/schema/dsl.d.ts +64 -0
  22. package/dist/schema/dsl.js +28 -0
  23. package/dist/schema/generate.d.mts +14 -0
  24. package/dist/schema/generate.mjs +282 -0
  25. package/dist/schema/generated/ddl.d.ts +7 -0
  26. package/dist/schema/generated/ddl.js +11 -0
  27. package/dist/schema/generated/kysely-types.d.ts +1988 -0
  28. package/dist/schema/generated/kysely-types.js +5 -0
  29. package/dist/schema/index.d.ts +2 -0
  30. package/dist/schema/index.js +7 -0
  31. package/dist/schema/provision.d.mts +11 -0
  32. package/dist/schema/provision.mjs +111 -0
  33. package/dist/schema/tables.d.ts +2 -0
  34. package/dist/schema/tables.js +3301 -0
  35. package/dist/scripts/canon-audit.d.mts +25 -0
  36. package/dist/scripts/canon-audit.mjs +213 -0
  37. package/dist/scripts/canon-smoke.d.mts +1 -0
  38. package/dist/scripts/canon-smoke.mjs +44 -0
  39. package/dist/scripts/dialect-ddl-stand.d.mts +1 -0
  40. package/dist/scripts/dialect-ddl-stand.mjs +136 -0
  41. package/dist/scripts/owner-spec-check.d.mts +1 -0
  42. package/dist/scripts/owner-spec-check.mjs +166 -0
  43. package/dist/scripts/owners/owner-resolve-spec.json +540 -0
  44. package/dist/scripts/owners-audit.d.mts +60 -0
  45. package/dist/scripts/owners-audit.mjs +443 -0
  46. package/dist/scripts/owners-fixture.d.mts +20 -0
  47. package/dist/scripts/owners-fixture.mjs +192 -0
  48. package/dist/scripts/schema-lengths-audit.d.mts +22 -0
  49. package/dist/scripts/schema-lengths-audit.mjs +146 -0
  50. package/dist/scripts/siteid-migrate.d.mts +7 -0
  51. package/dist/scripts/siteid-migrate.mjs +260 -0
  52. package/dist/scripts/tenant-invariant-audit.d.mts +1 -0
  53. package/dist/scripts/tenant-invariant-audit.mjs +34 -0
  54. package/dist/singleMode.d.ts +5 -0
  55. package/dist/singleMode.js +23 -0
  56. package/dist/sqliteDialect.d.ts +6 -0
  57. package/dist/sqliteDialect.js +92 -0
  58. package/dist/templateDefs.d.ts +38 -0
  59. package/dist/templateDefs.js +163 -0
  60. package/dist/test/fixtures/broken-driver/index.d.ts +3 -0
  61. package/dist/test/fixtures/broken-driver/index.js +5 -0
  62. package/dist/test/support/dialect-stand.d.ts +37 -0
  63. package/dist/test/support/dialect-stand.js +162 -0
  64. package/dist/test/support/render-parity.d.ts +44 -0
  65. package/dist/test/support/render-parity.js +262 -0
  66. package/package.json +50 -5
  67. package/README.md +0 -3
@@ -0,0 +1,282 @@
1
+ // Canon generators (task:3820; decisions: plan:49 Q2/Q4/Q5).
2
+ //
3
+ // Input: the canon (schema/tables.ts) — the single description of the schema.
4
+ // Output (committed, reviewed, and diffed by the test as a staleness guard):
5
+ // * schema/generated/kysely-types.ts — the Kysely `Database` types;
6
+ // * schema/generated/ddl.ts — per-dialect DDL.
7
+ //
8
+ // Run: node packages/data/schema/generate.mjs (writes the artifacts)
9
+ // node packages/data/schema/generate.mjs --check (exit 1 when stale)
10
+ //
11
+ // Nothing here may embed schema knowledge of its own: the only knowledge is the
12
+ // type mapping table below, and it is pinned by the test against plan:49 Q4.
13
+ import { writeFile, mkdir, readFile } from 'node:fs/promises';
14
+ import path from 'node:path';
15
+ import { fileURLToPath, pathToFileURL } from 'node:url';
16
+ import { canonicalSchema, CANON_TYPES, isSiteScoped } from "./index.js";
17
+ export const DIALECTS = ['postgres', 'mariadb', 'sqlite', 'mssql'];
18
+ // SQL Server is the fourth dialect since the operator made it a cell target (task:3994):
19
+ // bracket quoting and its own type map, nothing else in the canon changes.
20
+ const QUOTE = { postgres: '"', mariadb: '`', sqlite: '"', mssql: ['[', ']'] };
21
+ /** canon type -> dialect type (plan:49 Q4). */
22
+ export const mapType = (column, dialect) => {
23
+ const table = {
24
+ uuid: { postgres: 'uuid', mariadb: 'char(36)', sqlite: 'text', mssql: 'uniqueidentifier' },
25
+ string: {
26
+ postgres: `varchar(${column.length})`,
27
+ mariadb: `varchar(${column.length})`,
28
+ sqlite: 'text',
29
+ mssql: `nvarchar(${column.length})`,
30
+ },
31
+ // SQL Server has no unbounded `text`: `nvarchar(max)` is its equivalent, and the
32
+ // old `text`/`ntext` types are deprecated (task:3994).
33
+ text: { postgres: 'text', mariadb: 'mediumtext', sqlite: 'text', mssql: 'nvarchar(max)' },
34
+ int: { postgres: 'integer', mariadb: 'int', sqlite: 'integer', mssql: 'int' },
35
+ bigint: { postgres: 'bigint', mariadb: 'bigint', sqlite: 'integer', mssql: 'bigint' },
36
+ bool: { postgres: 'boolean', mariadb: 'tinyint(1)', sqlite: 'integer', mssql: 'bit' },
37
+ decimal: {
38
+ postgres: `numeric(${column.precision},${column.scale})`,
39
+ mariadb: `decimal(${column.precision},${column.scale})`,
40
+ mssql: `decimal(${column.precision},${column.scale})`,
41
+ // SQLite keeps exact decimals as text: there is no decimal type, and
42
+ // float would lose money. Arithmetic happens in the application.
43
+ sqlite: 'text',
44
+ },
45
+ datetime: { postgres: 'timestamp', mariadb: 'datetime(3)', sqlite: 'text', mssql: 'datetime2(3)' },
46
+ date: { postgres: 'date', mariadb: 'date', sqlite: 'text', mssql: 'date' },
47
+ binary: { postgres: 'bytea', mariadb: 'longblob', sqlite: 'blob', mssql: 'varbinary(max)' },
48
+ // SQL Server has no `json` type; the canon's JSON rides as text (parsed by the codec).
49
+ json: { postgres: 'jsonb', mariadb: 'json', sqlite: 'text', mssql: 'nvarchar(max)' },
50
+ enum: { postgres: 'varchar(255)', mariadb: 'varchar(255)', sqlite: 'text', mssql: 'nvarchar(255)' },
51
+ }[column.type];
52
+ if (!table)
53
+ throw new Error(`canon type without a dialect mapping: ${column.type}`);
54
+ return table[dialect];
55
+ };
56
+ /** canon type -> TypeScript type. Decimals are strings: floats lose money. */
57
+ const tsType = (column) => {
58
+ const base = {
59
+ uuid: 'string',
60
+ string: 'string',
61
+ text: 'string',
62
+ int: 'number',
63
+ bigint: 'string | number | bigint',
64
+ bool: 'boolean',
65
+ decimal: 'string',
66
+ datetime: 'string',
67
+ date: 'string',
68
+ binary: 'Uint8Array',
69
+ json: 'unknown',
70
+ enum: 'string',
71
+ }[column.type];
72
+ if (!base)
73
+ throw new Error(`canon type without a TS mapping: ${column.type}`);
74
+ return column.nullable === true ? `${base} | null` : base;
75
+ };
76
+ const q = (dialect, name) => Array.isArray(QUOTE[dialect])
77
+ ? `${QUOTE[dialect][0]}${name}${QUOTE[dialect][1]}`
78
+ : `${QUOTE[dialect]}${name}${QUOTE[dialect]}`;
79
+ const booleanLiteral = (dialect, value) => (dialect === 'postgres' ? String(value) : value ? '1' : '0');
80
+ /** The dialects whose DDL can carry a foreign key only as a separate `alter table`. */
81
+ const CONSTRAINT_AFTER_TABLES = new Set(['postgres', 'mariadb', 'mssql']);
82
+ /** Canonical default token -> dialect expression (plan:49 Q4: differences live here). */
83
+ const canonDefault = (dialect, token) => ({
84
+ now: { postgres: 'now()', mariadb: 'current_timestamp(3)', sqlite: "(datetime('now'))", mssql: 'sysdatetime()' },
85
+ true: { postgres: 'true', mariadb: '1', sqlite: '1', mssql: '1' },
86
+ false: { postgres: 'false', mariadb: '0', sqlite: '0', mssql: '0' },
87
+ }[token] ?? { postgres: token, mariadb: token, sqlite: token, mssql: token })[dialect];
88
+ // ---------------------------------------------------------------------------
89
+ // DDL
90
+ // ---------------------------------------------------------------------------
91
+ const softFkComment = (dialect, table, fk) => `-- soft fk: ${table}.${fk.columns.join(', ')} -> ${fk.ref}.${fk.refColumns.join(', ')} ` +
92
+ `(control-plane link, not enforced in a site database)`;
93
+ const createTable = (dialect, table) => {
94
+ const lines = [];
95
+ lines.push(`create table ${q(dialect, table.name)} (`);
96
+ const parts = [];
97
+ for (const [name, column] of Object.entries(table.columns)) {
98
+ let def = ` ${q(dialect, name)} ${mapType(column, dialect)}`;
99
+ def += column.nullable === true ? ' null' : ' not null';
100
+ if (column.default !== undefined)
101
+ def += ` default ${canonDefault(dialect, column.default)}`;
102
+ // SQLite cannot add a constraint after the fact, so hard FKs are inline.
103
+ const inline = (table.fks ?? []).filter((fk) => !fk.soft && fk.columns.length === 1 && fk.columns[0] === name);
104
+ for (const fk of dialect === 'sqlite' ? inline : []) {
105
+ def += ` references ${q(dialect, fk.ref)} (${fk.refColumns.map((c) => q(dialect, c)).join(', ')})`;
106
+ }
107
+ parts.push(def);
108
+ }
109
+ const pk = Object.entries(table.columns).filter(([, c]) => c.pk).map(([n]) => q(dialect, n));
110
+ if (pk.length)
111
+ parts.push(` primary key (${pk.join(', ')})`);
112
+ lines.push(parts.join(',\n'));
113
+ lines.push(');');
114
+ return lines.join('\n');
115
+ };
116
+ const createIndexes = (dialect, table) => (table.indexes ?? [])
117
+ .map((ix) => `create ${ix.unique ? 'unique ' : ''}index ${q(dialect, indexName(`${table.name}_${ix.columns.join('_')}_idx`))} ` +
118
+ `on ${q(dialect, table.name)} (${ix.columns.map((c) => q(dialect, c)).join(', ')});`)
119
+ .join('\n');
120
+ const createForeignKeys = (dialect, table) => {
121
+ if (!CONSTRAINT_AFTER_TABLES.has(dialect))
122
+ return '';
123
+ return (table.fks ?? [])
124
+ .filter((fk) => !fk.soft)
125
+ .map((fk, i) => {
126
+ const name = constraintName(table.name, fk, i);
127
+ return (`alter table ${q(dialect, table.name)} add constraint ${q(dialect, name)} ` +
128
+ `foreign key (${fk.columns.map((c) => q(dialect, c)).join(', ')}) ` +
129
+ `references ${q(dialect, fk.ref)} (${fk.refColumns.map((c) => q(dialect, c)).join(', ')});`);
130
+ })
131
+ .join('\n');
132
+ };
133
+ /** Postgres refuses identifiers longer than 63 characters; it is the strictest dialect. */
134
+ export const MAX_IDENTIFIER_LENGTH = 63;
135
+ // FNV-1a (32 bit) in base36: a short fingerprint of the full name. Truncation
136
+ // without it collides silently — two long names that differ only after the cut
137
+ // become one name — while the fingerprint keeps names deterministic, so the
138
+ // generated DDL stays diff-stable.
139
+ const fingerprint = (value) => {
140
+ let hash = 0x811c9dc5;
141
+ for (let i = 0; i < value.length; i += 1) {
142
+ hash ^= value.charCodeAt(i);
143
+ hash = Math.imul(hash, 0x01000193) >>> 0;
144
+ }
145
+ return hash.toString(36);
146
+ };
147
+ /**
148
+ * One naming rule for every generated constraint and index. `index`
149
+ * disambiguates repeated shapes on the same table (the second FK over the same
150
+ * pair) and keeps the historical suffix; a name over the limit is cut and
151
+ * fingerprinted instead of silently shortened.
152
+ */
153
+ export const indexName = (base, index = 0) => {
154
+ const candidate = index === 0 ? base : `${base}_${index + 1}`;
155
+ if (candidate.length <= MAX_IDENTIFIER_LENGTH)
156
+ return candidate;
157
+ const suffix = `_${fingerprint(candidate)}`;
158
+ return `${candidate.slice(0, MAX_IDENTIFIER_LENGTH - suffix.length)}${suffix}`;
159
+ };
160
+ export const constraintName = (table, fk, index) => indexName(`FK_${table}_${fk.ref}_${fk.columns.join('_')}`, index);
161
+ export const renderDdlSql = (dialect) => {
162
+ if (!DIALECTS.includes(dialect))
163
+ throw new Error(`unknown dialect: ${dialect}`);
164
+ const parts = [
165
+ `-- GENERATED from the canonical schema — do not edit; run schema/generate.mjs`,
166
+ `-- dialect: ${dialect} · canon version: ${canonicalSchema.version} · tables: ${canonicalSchema.tables.length}`,
167
+ ];
168
+ if (dialect === 'sqlite') {
169
+ parts.push('-- SQLite carries foreign keys inline; run the script with foreign_keys=off,');
170
+ parts.push('-- then enable the pragma once every table exists.');
171
+ }
172
+ for (const table of canonicalSchema.tables) {
173
+ parts.push('', createTable(dialect, table));
174
+ const indexes = createIndexes(dialect, table);
175
+ if (indexes)
176
+ parts.push(indexes);
177
+ }
178
+ // Constraints come after every table exists: a canon table may reference a
179
+ // table that sorts later, and MariaDB/Postgres refuse forward references.
180
+ for (const table of canonicalSchema.tables) {
181
+ const fks = createForeignKeys(dialect, table);
182
+ if (fks)
183
+ parts.push('', fks);
184
+ for (const fk of (table.fks ?? []).filter((f) => f.soft))
185
+ parts.push(softFkComment(dialect, table, fk));
186
+ }
187
+ parts.push('');
188
+ return parts.join('\n');
189
+ };
190
+ export const renderDdlModule = () => {
191
+ const body = DIALECTS.map((dialect) => `export const ${dialect}: string = ${JSON.stringify(renderDdlSql(dialect))};`).join('\n\n');
192
+ return [
193
+ '// GENERATED from the canonical schema — do not edit; run schema/generate.mjs',
194
+ '//',
195
+ '// Per-dialect DDL for the whole canon. The migration runner picks the',
196
+ '// dialect it is connected to (plan:49 Q5); the canon version below is the',
197
+ '// migration version.',
198
+ '',
199
+ `export const CANON_VERSION = ${canonicalSchema.version};`,
200
+ '',
201
+ // The dialect list comes from DIALECTS: a hand-written union here drifted
202
+ // once (mssql rendered, but not in the type) and tsc refused the module.
203
+ `export type Dialect = ${DIALECTS.map((d) => `'${d}'`).join(' | ')};`,
204
+ '',
205
+ // The record comes after the bodies: `{ postgres, ... }` reads the bindings,
206
+ // so a record above them is a block-scoped use before declaration.
207
+ body,
208
+ '',
209
+ `export const DDL: Record<Dialect, string> = { ${DIALECTS.join(', ')} };`,
210
+ '',
211
+ ].join('\n');
212
+ };
213
+ // ---------------------------------------------------------------------------
214
+ // Kysely types
215
+ // ---------------------------------------------------------------------------
216
+ export const renderKyselyTypes = () => {
217
+ const out = [
218
+ '// GENERATED from the canonical schema — do not edit; run schema/generate.mjs',
219
+ '//',
220
+ '// Row types for Kysely plus the `Database` interface. Decimals and datetimes',
221
+ '// are strings (exactness first, plan:49 Q4); optional columns admit null.',
222
+ '',
223
+ ];
224
+ for (const table of canonicalSchema.tables) {
225
+ out.push(`export interface ${table.name}Row {`);
226
+ for (const [name, column] of Object.entries(table.columns)) {
227
+ out.push(` ${name}: ${tsType(column)};`);
228
+ }
229
+ out.push('}', '');
230
+ }
231
+ out.push('export interface Database {');
232
+ for (const table of canonicalSchema.tables)
233
+ out.push(` ${table.name}: ${table.name}Row;`);
234
+ out.push('}', '');
235
+ return out.join('\n');
236
+ };
237
+ export const ARTIFACTS = {
238
+ 'ddl.ts': renderDdlModule,
239
+ 'kysely-types.ts': renderKyselyTypes,
240
+ };
241
+ // ---------------------------------------------------------------------------
242
+ // CLI
243
+ // ---------------------------------------------------------------------------
244
+ const here = path.dirname(fileURLToPath(import.meta.url));
245
+ const outDir = path.join(here, 'generated');
246
+ export const writeAll = async () => {
247
+ await mkdir(outDir, { recursive: true });
248
+ for (const [name, render] of Object.entries(ARTIFACTS)) {
249
+ await writeFile(path.join(outDir, name), render(), 'utf8');
250
+ }
251
+ };
252
+ const same = (current, wanted) => (current ?? '').replace(/\r\n/g, '\n') === wanted.replace(/\r\n/g, '\n');
253
+ const check = async () => {
254
+ const stale = [];
255
+ const targets = Object.entries(ARTIFACTS).map(([name, render]) => [path.join(outDir, name), render, name]);
256
+ for (const [target, render, name] of targets) {
257
+ let current = null;
258
+ try {
259
+ current = await readFile(target, 'utf8');
260
+ }
261
+ catch {
262
+ /* missing counts as stale */
263
+ }
264
+ if (!same(current, render()))
265
+ stale.push(name);
266
+ }
267
+ return stale;
268
+ };
269
+ if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
270
+ if (process.argv.includes('--check')) {
271
+ const stale = await check();
272
+ if (stale.length) {
273
+ console.error(`stale generated artifacts: ${stale.join(', ')} — run schema/generate.mjs`);
274
+ process.exit(1);
275
+ }
276
+ console.log('generated artifacts are up to date');
277
+ }
278
+ else {
279
+ await writeAll();
280
+ console.log(`wrote ${Object.keys(ARTIFACTS).length} artifacts`);
281
+ }
282
+ }
@@ -0,0 +1,7 @@
1
+ export declare const CANON_VERSION = 1;
2
+ export type Dialect = 'postgres' | 'mariadb' | 'sqlite' | 'mssql';
3
+ export declare const postgres: string;
4
+ export declare const mariadb: string;
5
+ export declare const sqlite: string;
6
+ export declare const mssql: string;
7
+ export declare const DDL: Record<Dialect, string>;