Skip to content

Commit 2f09b13

Browse files
authored
Add one-off subject backfill from connection rows (#1491)
1 parent 4a582ad commit 2f09b13

2 files changed

Lines changed: 68 additions & 0 deletions

File tree

apps/cloud/package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030
"typecheck:slow": "tsc --noEmit",
3131
"db:backfill-org-slugs:prod": "op run --env-file=.env.production -- bun run scripts/backfill-org-slugs.ts",
3232
"db:backfill-org-slugs:dev": "op run --env-file=.env.op -- bun run scripts/backfill-org-slugs.ts",
33+
"db:backfill-subjects:prod": "op run --env-file=.env.production -- bun run scripts/backfill-subjects.ts",
34+
"db:backfill-subjects:dev": "op run --env-file=.env.op -- bun run scripts/backfill-subjects.ts",
3335
"routes:gen": "bun scripts/gen-routes.ts"
3436
},
3537
"dependencies": {
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/* oxlint-disable executor/no-try-catch-or-throw -- boundary: out-of-band migration script over a raw postgres connection */
2+
// ---------------------------------------------------------------------------
3+
// One-off data backfill: mint `subject` rows for principals that predate the
4+
// subject table (added by migration 0012, written only on sightings from then
5+
// on).
6+
//
7+
// bun run db:backfill-subjects:prod # op run --env-file=.env.production
8+
// bun run db:backfill-subjects:dev # against the local PGlite dev db
9+
//
10+
// Source of truth is the `connection` table: before 0012, "which users exist
11+
// under this tenant" was answerable only through connection rows, so a distinct
12+
// (tenant, subject) there — excluding the org sentinel '' — is exactly the set
13+
// of users the admin console under-reports until their next request.
14+
//
15+
// created_at ← the tenant+subject's oldest connection, the closest honest
16+
// answer to "when did this user first appear".
17+
// last_seen_at ← NULL. The column means "seen on a request", no request has
18+
// been recorded, and the console renders NULL as "Never" —
19+
// which is true since the sighting code deployed.
20+
//
21+
// Idempotent — ON CONFLICT on the (tenant, external_id) unique index leaves
22+
// rows the runtime has since minted (or a prior run inserted) untouched, so
23+
// re-running is safe and never rewinds a live row's fields.
24+
// Pass --dry-run to print the plan without writing.
25+
// ---------------------------------------------------------------------------
26+
27+
import postgres from "postgres";
28+
import { createId } from "@executor-js/fumadb/cuid";
29+
30+
const dryRun = process.argv.includes("--dry-run");
31+
32+
const connectionString = process.env.DATABASE_URL;
33+
if (!connectionString) {
34+
console.error("DATABASE_URL is not set");
35+
process.exit(1);
36+
}
37+
38+
const sql = postgres(connectionString, { max: 1, prepare: false, ssl: "require" });
39+
40+
const rows = await sql<{ tenant: string; subject: string; first_connection_at: Date }[]>`
41+
SELECT tenant, subject, MIN(created_at) AS first_connection_at
42+
FROM connection
43+
WHERE subject <> ''
44+
GROUP BY tenant, subject
45+
ORDER BY tenant, subject
46+
`;
47+
console.log(`${rows.length} (tenant, subject) pair(s) with connections`);
48+
49+
let inserted = 0;
50+
for (const row of rows) {
51+
console.log(
52+
`${row.tenant} ${row.subject} first connection ${row.first_connection_at.toISOString()}`,
53+
);
54+
if (dryRun) continue;
55+
const result = await sql`
56+
INSERT INTO subject (row_id, tenant, external_id, created_at, last_seen_at, status)
57+
VALUES (${createId()}, ${row.tenant}, ${row.subject}, ${row.first_connection_at}, NULL, NULL)
58+
ON CONFLICT (tenant, external_id) DO NOTHING
59+
`;
60+
inserted += result.count;
61+
}
62+
63+
console.log(
64+
dryRun ? `dry run — would insert up to ${rows.length} row(s)` : `inserted ${inserted} row(s)`,
65+
);
66+
await sql.end();

0 commit comments

Comments
 (0)