@heychunky/core 0.14.0 → 0.15.0

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 (64) hide show
  1. package/dist/admin/apps.d.ts +3 -0
  2. package/dist/admin/apps.d.ts.map +1 -1
  3. package/dist/admin/apps.js +16 -2
  4. package/dist/admin/apps.js.map +1 -1
  5. package/dist/admin/index.d.ts +2 -0
  6. package/dist/admin/index.d.ts.map +1 -1
  7. package/dist/admin/index.js +2 -0
  8. package/dist/admin/index.js.map +1 -1
  9. package/dist/admin/migrations.d.ts +19 -0
  10. package/dist/admin/migrations.d.ts.map +1 -1
  11. package/dist/admin/migrations.js +27 -0
  12. package/dist/admin/migrations.js.map +1 -1
  13. package/dist/admin/orgs.d.ts +142 -0
  14. package/dist/admin/orgs.d.ts.map +1 -0
  15. package/dist/admin/orgs.js +166 -0
  16. package/dist/admin/orgs.js.map +1 -0
  17. package/dist/admin/register.d.ts +98 -0
  18. package/dist/admin/register.d.ts.map +1 -0
  19. package/dist/admin/register.js +191 -0
  20. package/dist/admin/register.js.map +1 -0
  21. package/dist/services/db.d.ts +16 -0
  22. package/dist/services/db.d.ts.map +1 -1
  23. package/dist/services/db.js +27 -22
  24. package/dist/services/db.js.map +1 -1
  25. package/package.json +3 -2
  26. package/register/0001-baseline/01-invite-requests.sql +11 -0
  27. package/register/0001-baseline/02-invite-requests-email-key.sql +1 -0
  28. package/register/0001-baseline/03-login-codes.sql +12 -0
  29. package/register/0001-baseline/04-login-codes-email-idx.sql +1 -0
  30. package/register/0001-baseline/05-notifications.sql +11 -0
  31. package/register/0001-baseline/06-notifications-unread-idx.sql +1 -0
  32. package/register/0001-baseline/07-users.sql +13 -0
  33. package/register/0001-baseline/08-users-email-key.sql +1 -0
  34. package/register/0001-baseline/09-users-one-superadmin.sql +1 -0
  35. package/register/0001-baseline/10-users-username-unique.sql +1 -0
  36. package/register/0001-baseline/11-api-tokens.sql +13 -0
  37. package/register/0001-baseline/12-apps.sql +14 -0
  38. package/register/0001-baseline/13-apps-name-key.sql +1 -0
  39. package/register/0001-baseline/14-apps-owner-idx.sql +1 -0
  40. package/register/0001-baseline/15-contexts.sql +9 -0
  41. package/register/0001-baseline/16-credit-ledger.sql +10 -0
  42. package/register/0001-baseline/17-credit-ledger-user.sql +1 -0
  43. package/register/0001-baseline/18-runs.sql +16 -0
  44. package/register/0001-baseline/19-sessions.sql +10 -0
  45. package/register/0001-baseline/20-sessions-user-idx.sql +1 -0
  46. package/register/0001-baseline/21-app-chunks.sql +7 -0
  47. package/register/0001-baseline/22-context-members.sql +10 -0
  48. package/register/0002-orgs/01-orgs.sql +14 -0
  49. package/register/0002-orgs/02-orgs-name-key.sql +1 -0
  50. package/register/0002-orgs/03-org-members.sql +10 -0
  51. package/register/0002-orgs/04-org-members-user-idx.sql +1 -0
  52. package/register/0002-orgs/05-apps-org-id.sql +1 -0
  53. package/register/0002-orgs/06-apps-org-idx.sql +1 -0
  54. package/register/0002-orgs/07-apps-org-name-key.sql +1 -0
  55. package/register/0002-orgs/08-credit-ledger-org-id.sql +1 -0
  56. package/register/0002-orgs/09-credit-ledger-org-idx.sql +1 -0
  57. package/register/0002-orgs/10-api-tokens-org-id.sql +1 -0
  58. package/register/0002-orgs/11-api-tokens-created-by.sql +1 -0
  59. package/register/0002-orgs/12-backfill-personal-orgs.sql +15 -0
  60. package/register/0002-orgs/13-backfill-org-members.sql +12 -0
  61. package/register/0002-orgs/14-backfill-apps-org.sql +14 -0
  62. package/register/0002-orgs/15-backfill-credit-ledger-org.sql +11 -0
  63. package/register/0002-orgs/16-backfill-api-tokens-org.sql +12 -0
  64. package/register/README.md +73 -0
@@ -0,0 +1,191 @@
1
+ /**
2
+ * The register's own schema, kept the way an app's is.
3
+ *
4
+ * `.heychunky/migrations` is how an app's database changes, and the register —
5
+ * the database holding who exists and what they own — had no equivalent. Its
6
+ * tables were typed into a psql session and existed in no file anywhere, which
7
+ * is `core#7`: the factory held customer apps to a standard it did not hold
8
+ * itself to.
9
+ *
10
+ * So the register gets the same mechanism rather than a second one. The same
11
+ * ledger table, the same one-statement-per-file rule, the same "the repository
12
+ * says what exists, the database says what ran". What differs is only where the
13
+ * files come from:
14
+ *
15
+ * an app its own repository, read at a ref, because the file rides the
16
+ * branch and that is what makes "what does staging still owe"
17
+ * answerable
18
+ * the register this package, read from disk, because the register has one
19
+ * database and no ladder to climb — the schema it should have
20
+ * is the schema the installed Core ships
21
+ *
22
+ * That second property is worth saying out loud: `heychunky` pins a Core
23
+ * version, so the register schema a machine can apply is the one its CLI was
24
+ * built against. There is no way to apply an unreviewed local edit to the
25
+ * factory's live register by accident.
26
+ *
27
+ * A change is a directory, its statements numbered inside it:
28
+ *
29
+ * register/0001-baseline/07-users.sql
30
+ * register/0002-orgs/01-orgs.sql
31
+ *
32
+ * Sorted lexically, which is why both numbers pad. The directory is part of
33
+ * the ledger id, so the whole of a change is legible in `_heychunky_migrations`
34
+ * rather than twenty-two filenames that have to be mentally regrouped.
35
+ */
36
+ import { readdir, readFile } from "node:fs/promises";
37
+ import { adopt, applied, apply, isDestructive, pending } from "./migrations.js";
38
+ /**
39
+ * Where the register's migrations live.
40
+ *
41
+ * Relative to this module, which resolves the same from `src/admin/` in the
42
+ * repository and `dist/admin/` in the published package — both are one level
43
+ * under the package root. Listed in `files` so the directory ships; a Core
44
+ * that could not stand up a register would be a Core that describes one.
45
+ */
46
+ const DIR = new URL("../../register/", import.meta.url);
47
+ /** The name of the change a statement belongs to — its directory. */
48
+ export const changeOf = (id) => id.split("/")[0] ?? id;
49
+ /**
50
+ * Every statement the register's schema is made of, in order.
51
+ *
52
+ * Read rather than declared, so the SQL lives in `.sql` files somebody can
53
+ * review as SQL. A directory with no `.sql` in it is not an error — it is a
54
+ * change whose files have not been written yet, and an empty answer is the
55
+ * honest one.
56
+ */
57
+ export async function registerMigrations() {
58
+ const changes = (await readdir(DIR, { withFileTypes: true }))
59
+ .filter((e) => e.isDirectory())
60
+ .map((e) => e.name)
61
+ .sort();
62
+ const all = [];
63
+ for (const change of changes) {
64
+ const files = (await readdir(new URL(`${change}/`, DIR)))
65
+ .filter((f) => f.endsWith(".sql"))
66
+ .sort();
67
+ for (const file of files) {
68
+ const id = `${change}/${file}`;
69
+ all.push({ id, sql: (await readFile(new URL(id, DIR), "utf8")).trim() });
70
+ }
71
+ }
72
+ return all;
73
+ }
74
+ /**
75
+ * What this register has had, and what it still owes.
76
+ *
77
+ * Asks without creating: a register that has never been migrated has no ledger
78
+ * table, and `applied` reads that as "none" rather than making one. This is the
79
+ * call an unconfirmed plan makes, and a plan that wrote to the database would
80
+ * not be a plan.
81
+ */
82
+ export async function registerState(register) {
83
+ const all = await registerMigrations();
84
+ const had = await applied(register);
85
+ const owed = pending(all, had);
86
+ return { all, had, owed, destructive: owed.filter((m) => isDestructive(m.sql)) };
87
+ }
88
+ /** A migration that would alter or discard, without the separate opt-in. */
89
+ export class DestructiveMigrationError extends Error {
90
+ migrations;
91
+ constructor(migrations) {
92
+ super(`${migrations.length} statement${migrations.length === 1 ? "" : "s"} would alter or ` +
93
+ `discard what the register already holds: ${migrations.map((m) => m.id).join(", ")}. ` +
94
+ `That needs its own opt-in, not only a confirmation.`);
95
+ this.name = "DestructiveMigrationError";
96
+ this.migrations = migrations;
97
+ }
98
+ }
99
+ /**
100
+ * Bring the register up to what this Core describes.
101
+ *
102
+ * The destructive guard is the same distinction `promote` draws and for the
103
+ * same reason: confirming says "yes, run these"; accepting destruction says
104
+ * "yes, to a dropped column on the database that knows who everybody is". One
105
+ * yes covering both hides the one that cannot be undone by redeploying.
106
+ */
107
+ export async function migrateRegister(register, opts = {}, watch) {
108
+ const { owed, destructive } = await registerState(register);
109
+ if (destructive.length && !opts.acceptDestructive) {
110
+ throw new DestructiveMigrationError(destructive);
111
+ }
112
+ const ran = await apply(register, owed, watch);
113
+ return { ran, migrations: owed };
114
+ }
115
+ /**
116
+ * The relation a `create` statement makes.
117
+ *
118
+ * Only the two forms the register's schema is written in, and undefined for
119
+ * anything else — which is the point. A statement whose effect cannot be named
120
+ * cannot be checked, and `adoptChange` refuses rather than guessing.
121
+ */
122
+ export function relationOf(sql) {
123
+ const head = sql.replace(/^\s*(--[^\n]*\n|\/\*[\s\S]*?\*\/)*\s*/, "");
124
+ const table = /^create\s+table\s+(?:if\s+not\s+exists\s+)?(?:public\.)?"?([a-z_][a-z0-9_]*)"?/i
125
+ .exec(head);
126
+ if (table)
127
+ return { kind: "table", name: table[1].toLowerCase() };
128
+ const index = /^create\s+(?:unique\s+)?index\s+(?:concurrently\s+)?(?:if\s+not\s+exists\s+)?"?([a-z_][a-z0-9_]*)"?/i
129
+ .exec(head);
130
+ if (index)
131
+ return { kind: "index", name: index[1].toLowerCase() };
132
+ return undefined;
133
+ }
134
+ /** Refused because the register does not hold what it was asked to vouch for. */
135
+ export class MissingFromRegisterError extends Error {
136
+ missing;
137
+ constructor(change, missing) {
138
+ super(`${change} describes ${missing.length} thing${missing.length === 1 ? "" : "s"} this ` +
139
+ `register does not have: ${missing.map((m) => `${m.relation} (${m.id})`).join(", ")}. ` +
140
+ `Adopting would record them as applied when they are not.`);
141
+ this.name = "MissingFromRegisterError";
142
+ this.missing = missing;
143
+ }
144
+ }
145
+ /**
146
+ * Record one change as applied, having run none of it.
147
+ *
148
+ * The register predates its own ledger, so `0001-baseline` describes tables
149
+ * that have been there for months. Running it would fail on the first `create
150
+ * table`; skipping it would leave every later change looking like it depends on
151
+ * something that never happened. Recording it is the truth: those statements
152
+ * are already true of this database.
153
+ *
154
+ * **Checked rather than trusted.** The failure this command can cause is
155
+ * invisible — a false receipt, and then a schema change silently skipped
156
+ * forever after — so every statement's relation is looked up in the catalog
157
+ * first, and a change that is not entirely true of this register is refused
158
+ * with the missing pieces named. That matters most where databases have
159
+ * drifted: the factory's production register has a `runs` table and its dev one
160
+ * does not, and the honest answer there is "apply that one, adopt the rest"
161
+ * rather than a receipt covering both.
162
+ *
163
+ * Named rather than inferred. "Mark everything pending as done" would be a
164
+ * command that quietly swallows a real migration somebody meant to run — so a
165
+ * caller has to say which change it means, and mean it. A full statement id is
166
+ * accepted as well as a change name, which is how the operator resolves drift.
167
+ */
168
+ export async function adoptChange(register, change) {
169
+ const all = await registerMigrations();
170
+ const mine = all.filter((m) => changeOf(m.id) === change || m.id === change);
171
+ if (!mine.length) {
172
+ throw new Error(`No change "${change}" in this Core. There is ` +
173
+ `${[...new Set(all.map((m) => changeOf(m.id)))].join(", ")}.`);
174
+ }
175
+ const missing = [];
176
+ for (const m of mine) {
177
+ const relation = relationOf(m.sql);
178
+ if (!relation) {
179
+ throw new Error(`${m.id} does not create a table or an index, so there is no way to check it is ` +
180
+ `already true of this register. Apply it rather than adopting it.`);
181
+ }
182
+ const { rows } = await register.query(`select 1 from pg_class c join pg_namespace n on n.oid = c.relnamespace
183
+ where n.nspname = 'public' and c.relname = $1`, [relation.name]);
184
+ if (!rows.length)
185
+ missing.push({ id: m.id, relation: relation.name });
186
+ }
187
+ if (missing.length)
188
+ throw new MissingFromRegisterError(change, missing);
189
+ return adopt(register, mine);
190
+ }
191
+ //# sourceMappingURL=register.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"register.js","sourceRoot":"","sources":["../../src/admin/register.ts"],"names":[],"mappings":"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"}
@@ -7,6 +7,22 @@ export interface Column {
7
7
  type: string;
8
8
  nullable: boolean;
9
9
  }
10
+ /**
11
+ * Reject a statement that is really several.
12
+ *
13
+ * The endpoint runs one query per request. Sent two separated by a semicolon,
14
+ * it runs one of them and answers as though all was well: a `delete` followed
15
+ * by a `delete` reported "1 row affected" while the first one never happened.
16
+ * Silence is the problem — an error would have been fine. Semicolons inside a
17
+ * string literal or a dollar-quoted body are left alone, since those are one
18
+ * statement.
19
+ *
20
+ * At module scope rather than inside `db()` because it is the rule every
21
+ * migration file is written to, and a rule the suite can assert is one that
22
+ * cannot quietly stop being true. The alternative — a second copy of this
23
+ * regular expression in a test — is how the two drift.
24
+ */
25
+ export declare function refuseMultiple(sql: string): void;
10
26
  export declare function db(connectionString: string): {
11
27
  query: (sql: string, params?: unknown[]) => Promise<Rows>;
12
28
  write: (sql: string, params?: unknown[]) => Promise<Rows>;
@@ -1 +1 @@
1
- {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAgB,EAAE,CAAC,gBAAgB,EAAE,MAAM;iBAuCf,MAAM,WAAU,OAAO,EAAE,KAAQ,OAAO,CAAC,IAAI,CAAC;iBAoC9C,MAAM,WAAU,OAAO,EAAE,KAAQ,OAAO,CAAC,IAAI,CAAC;cAatD,OAAO,CAAC,MAAM,EAAE,CAAC;mBAOZ,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;EAelD"}
1
+ {"version":3,"file":"db.d.ts","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAqBA,MAAM,WAAW,IAAI;IACnB,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,MAAM;IACrB,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAahD;AAED,wBAAgB,EAAE,CAAC,gBAAgB,EAAE,MAAM;iBAuCf,MAAM,WAAU,OAAO,EAAE,KAAQ,OAAO,CAAC,IAAI,CAAC;iBAW9C,MAAM,WAAU,OAAO,EAAE,KAAQ,OAAO,CAAC,IAAI,CAAC;cAatD,OAAO,CAAC,MAAM,EAAE,CAAC;mBAOZ,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;EAelD"}
@@ -18,6 +18,33 @@
18
18
  * `fetch`.
19
19
  */
20
20
  import { errorFor } from "../http/errors.js";
21
+ /**
22
+ * Reject a statement that is really several.
23
+ *
24
+ * The endpoint runs one query per request. Sent two separated by a semicolon,
25
+ * it runs one of them and answers as though all was well: a `delete` followed
26
+ * by a `delete` reported "1 row affected" while the first one never happened.
27
+ * Silence is the problem — an error would have been fine. Semicolons inside a
28
+ * string literal or a dollar-quoted body are left alone, since those are one
29
+ * statement.
30
+ *
31
+ * At module scope rather than inside `db()` because it is the rule every
32
+ * migration file is written to, and a rule the suite can assert is one that
33
+ * cannot quietly stop being true. The alternative — a second copy of this
34
+ * regular expression in a test — is how the two drift.
35
+ */
36
+ export function refuseMultiple(sql) {
37
+ const stripped = sql
38
+ .replace(/\$\$[\s\S]*?\$\$/g, "")
39
+ .replace(/'(?:[^']|'')*'/g, "")
40
+ .replace(/"(?:[^"]|"")*"/g, "")
41
+ .replace(/--[^\n]*/g, "")
42
+ .replace(/\/\*[\s\S]*?\*\//g, "");
43
+ if (/;\s*\S/.test(stripped)) {
44
+ throw new Error("One statement at a time. This runs a single query per request, so a " +
45
+ "second one after a semicolon would be dropped without saying so.");
46
+ }
47
+ }
21
48
  export function db(connectionString) {
22
49
  let host;
23
50
  try {
@@ -59,28 +86,6 @@ export function db(connectionString) {
59
86
  const first = body.results?.[0];
60
87
  return { rows: first?.rows ?? [], rowCount: first?.rowCount ?? 0 };
61
88
  }
62
- /**
63
- * Reject a statement that is really several.
64
- *
65
- * The endpoint runs one query per request. Sent two separated by a
66
- * semicolon, it runs one of them and answers as though all was well: a
67
- * `delete` followed by a `delete` reported "1 row affected" while the first
68
- * one never happened. Silence is the problem — an error would have been
69
- * fine. Semicolons inside a string literal or a dollar-quoted body are left
70
- * alone, since those are one statement.
71
- */
72
- function refuseMultiple(sql) {
73
- const stripped = sql
74
- .replace(/\$\$[\s\S]*?\$\$/g, "")
75
- .replace(/'(?:[^']|'')*'/g, "")
76
- .replace(/"(?:[^"]|"")*"/g, "")
77
- .replace(/--[^\n]*/g, "")
78
- .replace(/\/\*[\s\S]*?\*\//g, "");
79
- if (/;\s*\S/.test(stripped)) {
80
- throw new Error("One statement at a time. This runs a single query per request, so a " +
81
- "second one after a semicolon would be dropped without saying so.");
82
- }
83
- }
84
89
  /** SQL that may change data. Gated in the registry, not here. */
85
90
  async function write(sql, params = []) {
86
91
  refuseMultiple(sql);
@@ -1 +1 @@
1
- {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@heychunky/core",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
4
4
  "description": "The engine the Chunky CLI, worker and apps run on.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,7 +15,8 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "register"
19
20
  ],
20
21
  "scripts": {
21
22
  "build": "rm -rf dist && tsc -p tsconfig.build.json",
@@ -0,0 +1,11 @@
1
+ create table invite_requests (
2
+ id uuid default gen_random_uuid() not null,
3
+ email text not null,
4
+ name text,
5
+ note text,
6
+ created_at timestamptz default now() not null,
7
+ invited_at timestamptz,
8
+ asks integer default 1 not null,
9
+ last_asked_at timestamptz default now() not null,
10
+ constraint invite_requests_pkey PRIMARY KEY (id)
11
+ );
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX invite_requests_email_key ON public.invite_requests USING btree (lower(email));
@@ -0,0 +1,12 @@
1
+ create table login_codes (
2
+ id uuid default gen_random_uuid() not null,
3
+ email text not null,
4
+ kind text not null,
5
+ code_hash text not null,
6
+ expires_at timestamptz not null,
7
+ consumed_at timestamptz,
8
+ attempts integer default 0 not null,
9
+ created_at timestamptz default now() not null,
10
+ constraint login_codes_pkey PRIMARY KEY (id),
11
+ constraint login_codes_kind_check CHECK ((kind = ANY (ARRAY['link'::text, 'pin'::text])))
12
+ );
@@ -0,0 +1 @@
1
+ CREATE INDEX login_codes_email_idx ON public.login_codes USING btree (lower(email), created_at DESC);
@@ -0,0 +1,11 @@
1
+ create table notifications (
2
+ id uuid default gen_random_uuid() not null,
3
+ kind text not null,
4
+ subject text not null,
5
+ detail jsonb,
6
+ created_at timestamptz default now() not null,
7
+ sent_at timestamptz,
8
+ send_error text,
9
+ read_at timestamptz,
10
+ constraint notifications_pkey PRIMARY KEY (id)
11
+ );
@@ -0,0 +1 @@
1
+ CREATE INDEX notifications_unread_idx ON public.notifications USING btree (created_at DESC) WHERE (read_at IS NULL);
@@ -0,0 +1,13 @@
1
+ create table users (
2
+ id uuid default gen_random_uuid() not null,
3
+ email text not null,
4
+ name text,
5
+ role text default 'developer'::text not null,
6
+ created_at timestamptz default now() not null,
7
+ app_limit integer default 3,
8
+ username text,
9
+ plan text default 'free'::text not null,
10
+ constraint users_pkey PRIMARY KEY (id),
11
+ constraint users_app_limit_check CHECK ((app_limit >= 0)),
12
+ constraint users_role_check CHECK ((role = ANY (ARRAY['superadmin'::text, 'developer'::text])))
13
+ );
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX users_email_key ON public.users USING btree (lower(email));
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX users_one_superadmin ON public.users USING btree (role) WHERE (role = 'superadmin'::text);
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX users_username_unique ON public.users USING btree (lower(username));
@@ -0,0 +1,13 @@
1
+ create table api_tokens (
2
+ id uuid default gen_random_uuid() not null,
3
+ user_id uuid not null,
4
+ name text not null,
5
+ token_hash text not null,
6
+ prefix text not null,
7
+ created_at timestamptz default now() not null,
8
+ last_used_at timestamptz,
9
+ revoked_at timestamptz,
10
+ constraint api_tokens_token_hash_key UNIQUE (token_hash),
11
+ constraint api_tokens_pkey PRIMARY KEY (id),
12
+ constraint api_tokens_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
13
+ );
@@ -0,0 +1,14 @@
1
+ create table apps (
2
+ id uuid default gen_random_uuid() not null,
3
+ name text not null,
4
+ owner_id uuid not null,
5
+ org text,
6
+ repo text,
7
+ vercel_project text,
8
+ neon_project text,
9
+ created_at timestamptz default now() not null,
10
+ provisioned jsonb,
11
+ theme text,
12
+ constraint apps_pkey PRIMARY KEY (id),
13
+ constraint apps_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE RESTRICT
14
+ );
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX apps_name_key ON public.apps USING btree (lower(name));
@@ -0,0 +1 @@
1
+ CREATE INDEX apps_owner_idx ON public.apps USING btree (owner_id);
@@ -0,0 +1,9 @@
1
+ create table contexts (
2
+ name text not null,
3
+ kind text default 'user'::text not null,
4
+ owner_id uuid not null,
5
+ created_at timestamptz default now() not null,
6
+ constraint contexts_pkey PRIMARY KEY (name),
7
+ constraint contexts_owner_id_fkey FOREIGN KEY (owner_id) REFERENCES users(id) ON DELETE CASCADE,
8
+ constraint contexts_kind_check CHECK ((kind = ANY (ARRAY['user'::text, 'org'::text])))
9
+ );
@@ -0,0 +1,10 @@
1
+ create table credit_ledger (
2
+ id uuid default gen_random_uuid() not null,
3
+ user_id uuid not null,
4
+ delta integer not null,
5
+ reason text not null,
6
+ ref text,
7
+ created_at timestamptz default now() not null,
8
+ constraint credit_ledger_pkey PRIMARY KEY (id),
9
+ constraint credit_ledger_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
10
+ );
@@ -0,0 +1 @@
1
+ CREATE INDEX credit_ledger_user ON public.credit_ledger USING btree (user_id, created_at DESC);
@@ -0,0 +1,16 @@
1
+ create table runs (
2
+ id uuid not null,
3
+ repo text not null,
4
+ app_id uuid,
5
+ operator_id uuid not null,
6
+ source text not null,
7
+ title text,
8
+ steps jsonb default '[]'::jsonb not null,
9
+ cost numeric,
10
+ outcome text default 'running'::text not null,
11
+ created_at timestamptz default now() not null,
12
+ finished_at timestamptz,
13
+ constraint runs_pkey PRIMARY KEY (id),
14
+ constraint runs_app_id_fkey FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE SET NULL,
15
+ constraint runs_operator_id_fkey FOREIGN KEY (operator_id) REFERENCES users(id)
16
+ );
@@ -0,0 +1,10 @@
1
+ create table sessions (
2
+ id uuid default gen_random_uuid() not null,
3
+ user_id uuid not null,
4
+ token_hash text not null,
5
+ expires_at timestamptz not null,
6
+ created_at timestamptz default now() not null,
7
+ constraint sessions_token_hash_key UNIQUE (token_hash),
8
+ constraint sessions_pkey PRIMARY KEY (id),
9
+ constraint sessions_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
10
+ );
@@ -0,0 +1 @@
1
+ CREATE INDEX sessions_user_idx ON public.sessions USING btree (user_id);
@@ -0,0 +1,7 @@
1
+ create table app_chunks (
2
+ app_id uuid not null,
3
+ chunk text not null,
4
+ added_at timestamptz default now() not null,
5
+ constraint app_chunks_pkey PRIMARY KEY (app_id, chunk),
6
+ constraint app_chunks_app_id_fkey FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
7
+ );
@@ -0,0 +1,10 @@
1
+ create table context_members (
2
+ context text not null,
3
+ user_id uuid not null,
4
+ role text default 'publisher'::text not null,
5
+ added_at timestamptz default now() not null,
6
+ constraint context_members_pkey PRIMARY KEY (context, user_id),
7
+ constraint context_members_context_fkey FOREIGN KEY (context) REFERENCES contexts(name) ON DELETE CASCADE,
8
+ constraint context_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
9
+ constraint context_members_role_check CHECK ((role = ANY (ARRAY['owner'::text, 'publisher'::text])))
10
+ );
@@ -0,0 +1,14 @@
1
+ create table orgs (
2
+ id uuid default gen_random_uuid() not null,
3
+ name text not null,
4
+ kind text default 'personal'::text not null,
5
+ plan text default 'free'::text not null,
6
+ github_org text,
7
+ vercel_team text,
8
+ vault_folder text,
9
+ created_at timestamptz default now() not null,
10
+ constraint orgs_pkey PRIMARY KEY (id),
11
+ constraint orgs_kind_check CHECK ((kind = ANY (ARRAY['personal'::text, 'team'::text]))),
12
+ constraint orgs_plan_check CHECK ((plan = ANY (ARRAY['free'::text, 'pro'::text, 'team'::text, 'enterprise'::text, 'super'::text]))),
13
+ constraint orgs_name_check CHECK ((name ~ '^[a-z0-9][a-z0-9-]*$'))
14
+ );
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX orgs_name_key ON public.orgs USING btree (lower(name));
@@ -0,0 +1,10 @@
1
+ create table org_members (
2
+ org_id uuid not null,
3
+ user_id uuid not null,
4
+ role text default 'publisher'::text not null,
5
+ added_at timestamptz default now() not null,
6
+ constraint org_members_pkey PRIMARY KEY (org_id, user_id),
7
+ constraint org_members_org_id_fkey FOREIGN KEY (org_id) REFERENCES orgs(id) ON DELETE CASCADE,
8
+ constraint org_members_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
9
+ constraint org_members_role_check CHECK ((role = ANY (ARRAY['admin'::text, 'publisher'::text])))
10
+ );
@@ -0,0 +1 @@
1
+ CREATE INDEX org_members_user_idx ON public.org_members USING btree (user_id);
@@ -0,0 +1 @@
1
+ alter table apps add column org_id uuid references orgs(id) on delete restrict;
@@ -0,0 +1 @@
1
+ CREATE INDEX apps_org_idx ON public.apps USING btree (org_id);
@@ -0,0 +1 @@
1
+ CREATE UNIQUE INDEX apps_org_name_key ON public.apps USING btree (org_id, lower(name));
@@ -0,0 +1 @@
1
+ alter table credit_ledger add column org_id uuid references orgs(id) on delete cascade;
@@ -0,0 +1 @@
1
+ CREATE INDEX credit_ledger_org ON public.credit_ledger USING btree (org_id, created_at DESC);
@@ -0,0 +1 @@
1
+ alter table api_tokens add column org_id uuid references orgs(id) on delete cascade;
@@ -0,0 +1 @@
1
+ alter table api_tokens add column created_by uuid references users(id) on delete set null;
@@ -0,0 +1,15 @@
1
+ -- Everyone who has claimed a username gets the personal organisation of that
2
+ -- name, carrying the plan they are on today.
3
+ --
4
+ -- The username is the scope: `@dan/auth` was already going to mean Dan, and an
5
+ -- organisation is what that scope now names. A user who has not claimed one
6
+ -- yet gets no organisation here — `claimUsername` makes it in the same breath
7
+ -- as the name, so the two can never disagree about who owns `dan`.
8
+ --
9
+ -- Guarded by `not exists` rather than `on conflict`, because the unique index
10
+ -- is on `lower(name)` and a conflict target has to name the index expression.
11
+ insert into orgs (name, kind, plan)
12
+ select u.username, 'personal', u.plan
13
+ from users u
14
+ where u.username is not null
15
+ and not exists (select 1 from orgs o where lower(o.name) = lower(u.username));
@@ -0,0 +1,12 @@
1
+ -- A personal organisation has exactly one member, and they run it.
2
+ --
3
+ -- `admin` rather than `owner`: ownership of the organisation is not a role, it
4
+ -- is what `kind = 'personal'` and the matching username already say. The two
5
+ -- roles that exist are the two that describe what somebody may do — administer
6
+ -- the organisation, or publish under it.
7
+ insert into org_members (org_id, user_id, role)
8
+ select o.id, u.id, 'admin'
9
+ from users u
10
+ join orgs o on lower(o.name) = lower(u.username) and o.kind = 'personal'
11
+ where u.username is not null
12
+ on conflict (org_id, user_id) do nothing;
@@ -0,0 +1,14 @@
1
+ -- Every app moves into its owner's personal organisation.
2
+ --
3
+ -- Joined through the username rather than through `org_members`, so exactly one
4
+ -- row can match: `orgs_name_key` is unique on `lower(name)`, and a user is a
5
+ -- member of one personal organisation by construction but the join could not
6
+ -- prove it. An `update ... from` that matches twice picks one arbitrarily and
7
+ -- says nothing, which is precisely the silent wrong backfill to avoid.
8
+ --
9
+ -- `org_id is null` makes it repeatable, and leaves alone any app already placed.
10
+ update apps a
11
+ set org_id = o.id
12
+ from users u
13
+ join orgs o on lower(o.name) = lower(u.username) and o.kind = 'personal'
14
+ where u.id = a.owner_id and a.org_id is null;
@@ -0,0 +1,11 @@
1
+ -- Credit is spent by an organisation, not by a person in it.
2
+ --
3
+ -- Empty on the factory's own register today. It is written anyway because the
4
+ -- migration has to be correct for any register it meets — a staging factory, a
5
+ -- test — and because a backfill written later, against rows that exist, is a
6
+ -- backfill written under pressure.
7
+ update credit_ledger l
8
+ set org_id = o.id
9
+ from users u
10
+ join orgs o on lower(o.name) = lower(u.username) and o.kind = 'personal'
11
+ where u.id = l.user_id and l.org_id is null;