@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.
- package/dist/admin/apps.d.ts +3 -0
- package/dist/admin/apps.d.ts.map +1 -1
- package/dist/admin/apps.js +16 -2
- package/dist/admin/apps.js.map +1 -1
- package/dist/admin/index.d.ts +2 -0
- package/dist/admin/index.d.ts.map +1 -1
- package/dist/admin/index.js +2 -0
- package/dist/admin/index.js.map +1 -1
- package/dist/admin/migrations.d.ts +19 -0
- package/dist/admin/migrations.d.ts.map +1 -1
- package/dist/admin/migrations.js +27 -0
- package/dist/admin/migrations.js.map +1 -1
- package/dist/admin/orgs.d.ts +142 -0
- package/dist/admin/orgs.d.ts.map +1 -0
- package/dist/admin/orgs.js +166 -0
- package/dist/admin/orgs.js.map +1 -0
- package/dist/admin/register.d.ts +98 -0
- package/dist/admin/register.d.ts.map +1 -0
- package/dist/admin/register.js +191 -0
- package/dist/admin/register.js.map +1 -0
- package/dist/services/db.d.ts +16 -0
- package/dist/services/db.d.ts.map +1 -1
- package/dist/services/db.js +27 -22
- package/dist/services/db.js.map +1 -1
- package/package.json +3 -2
- package/register/0001-baseline/01-invite-requests.sql +11 -0
- package/register/0001-baseline/02-invite-requests-email-key.sql +1 -0
- package/register/0001-baseline/03-login-codes.sql +12 -0
- package/register/0001-baseline/04-login-codes-email-idx.sql +1 -0
- package/register/0001-baseline/05-notifications.sql +11 -0
- package/register/0001-baseline/06-notifications-unread-idx.sql +1 -0
- package/register/0001-baseline/07-users.sql +13 -0
- package/register/0001-baseline/08-users-email-key.sql +1 -0
- package/register/0001-baseline/09-users-one-superadmin.sql +1 -0
- package/register/0001-baseline/10-users-username-unique.sql +1 -0
- package/register/0001-baseline/11-api-tokens.sql +13 -0
- package/register/0001-baseline/12-apps.sql +14 -0
- package/register/0001-baseline/13-apps-name-key.sql +1 -0
- package/register/0001-baseline/14-apps-owner-idx.sql +1 -0
- package/register/0001-baseline/15-contexts.sql +9 -0
- package/register/0001-baseline/16-credit-ledger.sql +10 -0
- package/register/0001-baseline/17-credit-ledger-user.sql +1 -0
- package/register/0001-baseline/18-runs.sql +16 -0
- package/register/0001-baseline/19-sessions.sql +10 -0
- package/register/0001-baseline/20-sessions-user-idx.sql +1 -0
- package/register/0001-baseline/21-app-chunks.sql +7 -0
- package/register/0001-baseline/22-context-members.sql +10 -0
- package/register/0002-orgs/01-orgs.sql +14 -0
- package/register/0002-orgs/02-orgs-name-key.sql +1 -0
- package/register/0002-orgs/03-org-members.sql +10 -0
- package/register/0002-orgs/04-org-members-user-idx.sql +1 -0
- package/register/0002-orgs/05-apps-org-id.sql +1 -0
- package/register/0002-orgs/06-apps-org-idx.sql +1 -0
- package/register/0002-orgs/07-apps-org-name-key.sql +1 -0
- package/register/0002-orgs/08-credit-ledger-org-id.sql +1 -0
- package/register/0002-orgs/09-credit-ledger-org-idx.sql +1 -0
- package/register/0002-orgs/10-api-tokens-org-id.sql +1 -0
- package/register/0002-orgs/11-api-tokens-created-by.sql +1 -0
- package/register/0002-orgs/12-backfill-personal-orgs.sql +15 -0
- package/register/0002-orgs/13-backfill-org-members.sql +12 -0
- package/register/0002-orgs/14-backfill-apps-org.sql +14 -0
- package/register/0002-orgs/15-backfill-credit-ledger-org.sql +11 -0
- package/register/0002-orgs/16-backfill-api-tokens-org.sql +12 -0
- 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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"}
|
package/dist/services/db.d.ts
CHANGED
|
@@ -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;
|
|
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"}
|
package/dist/services/db.js
CHANGED
|
@@ -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);
|
package/dist/services/db.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/services/db.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAC;AAa7C,MAAM,UAAU,EAAE,CAAC,gBAAwB;IACzC,IAAI,IAAY,CAAC;IACjB,IAAI,CAAC;QACH,IAAI,GAAG,IAAI,GAAG,CAAC,gBAAgB,CAAC,CAAC,QAAQ,CAAC;IAC5C,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,IAAI,KAAK,CAAC,2EAA2E,CAAC,CAAC;IAC/F,CAAC;IACD,MAAM,QAAQ,GAAG,WAAW,IAAI,MAAM,CAAC;IAEvC,KAAK,UAAU,IAAI,CACjB,IAAa,EACb,UAAkC,EAAE;QAEpC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,QAAQ,EAAE;YAChC,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,kBAAkB;gBAClC,wBAAwB,EAAE,gBAAgB;gBAC1C,GAAG,OAAO;aACX;YACD,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC;SAC3B,CAAC,CAAC;QACH,MAAM,MAAM,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAyB,CAAC;QAC1D,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,sEAAsE;YACtE,uEAAuE;YACvE,gEAAgE;YAChE,MAAM,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC;QACjF,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAED;;;;;;OAMG;IACH,KAAK,UAAU,KAAK,CAAC,GAAW,EAAE,SAAoB,EAAE;QACtD,cAAc,CAAC,GAAG,CAAC,CAAC;QACpB,MAAM,IAAI,GAAG,CAAC,MAAM,IAAI,CACtB,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,CAAC,EAAE,EACrC,EAAE,sBAAsB,EAAE,MAAM,EAAE,CACnC,CAA4E,CAAC;QAC9E,MAAM,KAAK,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QAChC,OAAO,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,IAAI,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,IAAI,CAAC,EAAE,CAAC;IACrE,CAAC;IAED
|
|
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.
|
|
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;
|