@trylight/app 0.1.5 → 0.1.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/bin/light-db.mjs +1 -3
- package/bin/tsx.mjs +9 -0
- package/dist/db/baseline.js +106 -10
- package/package.json +1 -1
package/bin/light-db.mjs
CHANGED
package/bin/tsx.mjs
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The app's schema files are TypeScript; tsx lets the CLI import them. Both
|
|
2
|
+
// hooks: in an app whose package.json has no "type": "module" (Next.js), Node
|
|
3
|
+
// loads db/schema as CommonJS, and its extensionless imports resolve only
|
|
4
|
+
// through tsx's require hook.
|
|
5
|
+
import { register as registerCjs } from "tsx/cjs/api";
|
|
6
|
+
import { register } from "tsx/esm/api";
|
|
7
|
+
|
|
8
|
+
registerCjs();
|
|
9
|
+
register();
|
package/dist/db/baseline.js
CHANGED
|
@@ -64,13 +64,18 @@ export async function baseline(root, databaseUrl, schemaModule = "@trylight/app/
|
|
|
64
64
|
throw new BaselineRefused("db/ already exists. Baseline writes db/schema and db/migrations from the database, once; move db/ aside first.");
|
|
65
65
|
}
|
|
66
66
|
const pulled = pull(databaseUrl);
|
|
67
|
-
const { specialOpclasses,
|
|
68
|
-
if (
|
|
69
|
-
throw new BaselineRefused(`The database role baseline reads with can't see the
|
|
67
|
+
const { specialOpclasses, hiddenConstraints, keys } = await catalogFacts(databaseUrl);
|
|
68
|
+
if (hiddenConstraints.length > 0) {
|
|
69
|
+
throw new BaselineRefused(`The database role baseline reads with can't see the constraints of ${hiddenConstraints.join(", ")}: Postgres shows them only to a role with REFERENCES (or more) on the table, ` +
|
|
70
70
|
"which the platform grants an app's reader (scripts/grant-reader-references.ts). Nothing was written.");
|
|
71
71
|
}
|
|
72
|
-
const
|
|
73
|
-
const
|
|
72
|
+
const { snapshot, added } = addKeys(pulled.snapshot, keys);
|
|
73
|
+
const live = pulledTables.parse(snapshot.tables);
|
|
74
|
+
const declarations = pulled.source
|
|
75
|
+
.split(/\n(?=export const )/)
|
|
76
|
+
.slice(1)
|
|
77
|
+
.map(readDeclaration)
|
|
78
|
+
.map((declaration) => writeKeys(declaration, added));
|
|
74
79
|
const tables = declarations.filter((d) => d.kind === "pgTable" && !PLATFORM_TABLES.includes(d.dbName));
|
|
75
80
|
const enums = declarations.filter((d) => d.kind === "pgEnum");
|
|
76
81
|
const skipped = declarations.filter((d) => d.kind === "pgTable" && PLATFORM_TABLES.includes(d.dbName)).map((d) => d.dbName);
|
|
@@ -104,7 +109,7 @@ export async function baseline(root, databaseUrl, schemaModule = "@trylight/app/
|
|
|
104
109
|
const full = snapshotSchema.parse(JSON.parse(JSON.stringify(generateDrizzleJson(schema, EMPTY_SNAPSHOT.id))));
|
|
105
110
|
const recorded = asTheyAre(full, live);
|
|
106
111
|
// What entity() says must be exactly what the database has.
|
|
107
|
-
const drift = await driftFrom(
|
|
112
|
+
const drift = await driftFrom(snapshot, recorded);
|
|
108
113
|
if (drift.length > 0) {
|
|
109
114
|
throw new BaselineRefused(`db/schema can't say these tables exactly as the database has them; nothing was written. A sync would run:\n${drift.join("\n")}\n` +
|
|
110
115
|
"Change them in the old app's own migrations (the CLI's `app migrate` applies them) so they fit, then run baseline again.");
|
|
@@ -132,7 +137,11 @@ export async function baseline(root, databaseUrl, schemaModule = "@trylight/app/
|
|
|
132
137
|
throw error;
|
|
133
138
|
}
|
|
134
139
|
}
|
|
135
|
-
/**
|
|
140
|
+
/**
|
|
141
|
+
* What pull can't tell: indexes with a non-default operator class, tables
|
|
142
|
+
* whose constraints this role can't see, and every table's primary key and
|
|
143
|
+
* unique constraints (see addKeys).
|
|
144
|
+
*/
|
|
136
145
|
async function catalogFacts(databaseUrl) {
|
|
137
146
|
const sql = postgres(databaseUrl, { max: 1, onnotice: () => undefined });
|
|
138
147
|
try {
|
|
@@ -149,7 +158,19 @@ async function catalogFacts(databaseUrl) {
|
|
|
149
158
|
select c.relname as table from pg_class c join pg_namespace n on n.oid = c.relnamespace
|
|
150
159
|
where n.nspname = 'public' and c.relkind in ('r', 'p') and not has_table_privilege(c.oid, 'REFERENCES')
|
|
151
160
|
order by 1`;
|
|
152
|
-
|
|
161
|
+
// pg_constraint and pg_attribute show every table's constraints to any role, owner or not.
|
|
162
|
+
const keys = await sql `
|
|
163
|
+
select t.relname as table, c.conname as name, c.contype::text as kind,
|
|
164
|
+
array_agg(a.attname::text order by k.position) as columns
|
|
165
|
+
from pg_constraint c
|
|
166
|
+
join pg_class t on t.oid = c.conrelid
|
|
167
|
+
join pg_namespace n on n.oid = t.relnamespace
|
|
168
|
+
cross join lateral unnest(c.conkey) with ordinality as k(attnum, position)
|
|
169
|
+
join pg_attribute a on a.attrelid = c.conrelid and a.attnum = k.attnum
|
|
170
|
+
where n.nspname = 'public' and c.contype in ('p', 'u')
|
|
171
|
+
group by t.relname, c.conname, c.contype
|
|
172
|
+
order by 1, 2`;
|
|
173
|
+
return { specialOpclasses, hiddenConstraints: hidden.map((row) => row.table), keys };
|
|
153
174
|
}
|
|
154
175
|
finally {
|
|
155
176
|
await sql.end();
|
|
@@ -185,6 +206,80 @@ export function pull(databaseUrl) {
|
|
|
185
206
|
rmSync(dir, { recursive: true, force: true });
|
|
186
207
|
}
|
|
187
208
|
}
|
|
209
|
+
/**
|
|
210
|
+
* pull reads keys from information_schema.constraint_column_usage, which Postgres fills only for tables
|
|
211
|
+
* the role owns, so as an app's reader it finds none. Adds each key its snapshot lacks, as pull records
|
|
212
|
+
* it for an owner; returns those keys for writeKeys.
|
|
213
|
+
*/
|
|
214
|
+
function addKeys(snapshot, keys) {
|
|
215
|
+
const tables = structuredClone(snapshot.tables);
|
|
216
|
+
const added = [];
|
|
217
|
+
for (const key of keys) {
|
|
218
|
+
const table = tables[`public.${key.table}`];
|
|
219
|
+
if (table === undefined)
|
|
220
|
+
throw new Error(`drizzle-kit pull didn't read ${key.table}, which has key ${key.name}.`);
|
|
221
|
+
const column = keyColumn(key);
|
|
222
|
+
if (key.kind === "u") {
|
|
223
|
+
if (key.name in table.uniqueConstraints)
|
|
224
|
+
continue;
|
|
225
|
+
table.uniqueConstraints[key.name] = { name: key.name, columns: key.columns, nullsNotDistinct: false };
|
|
226
|
+
}
|
|
227
|
+
else if (column === null) {
|
|
228
|
+
if (key.name in table.compositePrimaryKeys)
|
|
229
|
+
continue;
|
|
230
|
+
table.compositePrimaryKeys[key.name] = { name: key.name, columns: key.columns };
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
const pulled = pulledColumn.parse(table.columns[column]);
|
|
234
|
+
if (pulled.primaryKey)
|
|
235
|
+
continue;
|
|
236
|
+
table.columns[column] = { ...pulled, primaryKey: true };
|
|
237
|
+
}
|
|
238
|
+
added.push(key);
|
|
239
|
+
}
|
|
240
|
+
return { snapshot: { ...snapshot, tables }, added };
|
|
241
|
+
}
|
|
242
|
+
/** A primary key's one column, which pull puts on the column itself; null for a composite key. */
|
|
243
|
+
function keyColumn(key) {
|
|
244
|
+
const [column, ...rest] = key.columns;
|
|
245
|
+
return key.kind === "p" && column !== undefined && rest.length === 0 ? column : null;
|
|
246
|
+
}
|
|
247
|
+
/** A pulled table with the keys addKeys added to its snapshot, written as pull writes them. */
|
|
248
|
+
function writeKeys(declaration, added) {
|
|
249
|
+
const keys = added.filter((key) => key.table === declaration.dbName);
|
|
250
|
+
if (declaration.kind !== "pgTable" || keys.length === 0)
|
|
251
|
+
return declaration;
|
|
252
|
+
const lines = declaration.text.split("\n");
|
|
253
|
+
const entries = [];
|
|
254
|
+
for (const key of keys) {
|
|
255
|
+
const column = keyColumn(key);
|
|
256
|
+
const columns = key.columns.map((name) => `table.${name}`).join(", ");
|
|
257
|
+
if (key.kind === "u") {
|
|
258
|
+
entries.push(`\tunique("${key.name}").on(${columns}),`);
|
|
259
|
+
}
|
|
260
|
+
else if (column === null) {
|
|
261
|
+
entries.push(`\tprimaryKey({ columns: [${columns}], name: "${key.name}"}),`);
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
const at = lines.findIndex((line) => line.startsWith(`\t${column}: `) && line.endsWith(","));
|
|
265
|
+
const line = lines[at];
|
|
266
|
+
if (line === undefined)
|
|
267
|
+
throw new Error(`drizzle-kit pull wrote ${declaration.dbName}.${column} in a shape baseline can't read:\n${declaration.text}`);
|
|
268
|
+
lines[at] = `${line.slice(0, -1)}.primaryKey(),`;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
let text = lines.join("\n");
|
|
272
|
+
if (entries.length > 0) {
|
|
273
|
+
const config = entries.join("\n");
|
|
274
|
+
if (text.endsWith("\n]);"))
|
|
275
|
+
text = `${text.slice(0, -"\n]);".length)}\n${config}\n]);`;
|
|
276
|
+
else if (text.endsWith("\n});"))
|
|
277
|
+
text = `${text.slice(0, -");".length)}, (table) => [\n${config}\n]);`;
|
|
278
|
+
else
|
|
279
|
+
throw new Error(`drizzle-kit pull wrote ${declaration.dbName} in a shape baseline can't read:\n${declaration.text}`);
|
|
280
|
+
}
|
|
281
|
+
return { ...declaration, text };
|
|
282
|
+
}
|
|
188
283
|
function readDeclaration(text) {
|
|
189
284
|
const match = DECLARATION.exec(text);
|
|
190
285
|
if (match === null)
|
|
@@ -243,7 +338,8 @@ function importedNames(source) {
|
|
|
243
338
|
throw new Error(`drizzle-kit pull wrote no pg-core import baseline can read:\n${source}`);
|
|
244
339
|
return z.string().parse(match[1]).split(",").map((name) => name.trim());
|
|
245
340
|
}
|
|
246
|
-
|
|
341
|
+
// Not after a dot: a column's `.primaryKey()` imports nothing.
|
|
342
|
+
const uses = (text, name, after) => new RegExp(`(?<![.\\w])${name}${after}`).test(text);
|
|
247
343
|
/**
|
|
248
344
|
* One pulled `pgTable` as a db/schema file: an `entity()` (which declares its
|
|
249
345
|
* standard columns itself) when it fits one, else the table as pulled.
|
|
@@ -264,7 +360,7 @@ function tableFile(table, live, asEntity, context) {
|
|
|
264
360
|
if (!body.includes(ACCESS))
|
|
265
361
|
throw new Error(`drizzle-kit pull wrote ${table.dbName} in a shape baseline can't read:\n${table.text}`);
|
|
266
362
|
}
|
|
267
|
-
const pgCore = [...context.pgCore, "customType"].filter((name) => uses(body, name, "[<(]"));
|
|
363
|
+
const pgCore = [...new Set([...context.pgCore, "customType", "primaryKey", "unique"])].filter((name) => uses(body, name, "[<(]"));
|
|
268
364
|
const referenced = new Set(Object.values(live.foreignKeys).map((key) => key.tableTo));
|
|
269
365
|
const others = context.tables.filter((other) => other !== table && referenced.has(other.dbName));
|
|
270
366
|
const types = new Set(Object.values(live.columns).map((column) => column.type));
|
package/package.json
CHANGED