@trylight/app 0.1.5 → 0.1.6

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 (2) hide show
  1. package/dist/db/baseline.js +106 -10
  2. package/package.json +9 -11
@@ -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, hiddenKeys } = await catalogFacts(databaseUrl);
68
- if (hiddenKeys.length > 0) {
69
- throw new BaselineRefused(`The database role baseline reads with can't see the keys of ${hiddenKeys.join(", ")}: Postgres shows them only to a role with REFERENCES (or more) on the table, ` +
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 live = pulledTables.parse(pulled.snapshot.tables);
73
- const declarations = pulled.source.split(/\n(?=export const )/).slice(1).map(readDeclaration);
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(pulled.snapshot, recorded);
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
- /** What pull can't tell: indexes with a non-default operator class, and tables whose keys this role can't see. */
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
- return { specialOpclasses, hiddenKeys: hidden.map((row) => row.table) };
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
- const uses = (text, name, after) => new RegExp(`\\b${name}${after}`).test(text);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trylight/app",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "The Light SDK every v2 org app depends on: entities (Drizzle tables with a typed client), sign-in, contracts and in-app chat.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -12,7 +12,6 @@
12
12
  "url": "https://github.com/shubh9/mii.git",
13
13
  "directory": "packages/light-app"
14
14
  },
15
- "packageManager": "pnpm@11.0.9",
16
15
  "files": [
17
16
  "dist",
18
17
  "bin"
@@ -42,14 +41,6 @@
42
41
  "default": "./dist/vite.js"
43
42
  }
44
43
  },
45
- "scripts": {
46
- "build": "rm -rf dist && tsc -p tsconfig.build.json",
47
- "typecheck": "tsc --noEmit",
48
- "lint": "eslint src test --max-warnings 0",
49
- "test": "node --test --import tsx \"test/**/*.test.ts\"",
50
- "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build",
51
- "prepack": "pnpm run build"
52
- },
53
44
  "peerDependencies": {
54
45
  "@tanstack/react-query": ">=5.69",
55
46
  "drizzle-orm": ">=0.45",
@@ -82,5 +73,12 @@
82
73
  "typescript-eslint": "^8.70.1",
83
74
  "vite": "^8.2.0",
84
75
  "zod": "^4.6.5"
76
+ },
77
+ "scripts": {
78
+ "build": "rm -rf dist && tsc -p tsconfig.build.json",
79
+ "typecheck": "tsc --noEmit",
80
+ "lint": "eslint src test --max-warnings 0",
81
+ "test": "node --test --import tsx \"test/**/*.test.ts\"",
82
+ "check": "pnpm run typecheck && pnpm run lint && pnpm run test && pnpm run build"
85
83
  }
86
- }
84
+ }