@heychunky/core 0.13.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 +50 -0
- package/dist/admin/apps.d.ts.map +1 -1
- package/dist/admin/apps.js +69 -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/provision.d.ts +10 -0
- package/dist/admin/provision.d.ts.map +1 -1
- package/dist/admin/provision.js +1 -1
- package/dist/admin/provision.js.map +1 -1
- 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/admin/run.d.ts.map +1 -1
- package/dist/admin/run.js +14 -1
- package/dist/admin/run.js.map +1 -1
- package/dist/admin/services/github.d.ts.map +1 -1
- package/dist/admin/services/github.js +15 -1
- package/dist/admin/services/github.js.map +1 -1
- package/dist/admin/services/vercel.d.ts +2 -0
- package/dist/admin/services/vercel.d.ts.map +1 -1
- package/dist/admin/services/vercel.js.map +1 -1
- package/dist/admin/users.d.ts +34 -0
- package/dist/admin/users.d.ts.map +1 -1
- package/dist/admin/users.js +45 -3
- package/dist/admin/users.js.map +1 -1
- package/dist/registry.d.ts +40 -0
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +27 -0
- package/dist/registry.js.map +1 -1
- 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,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;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- A token names its own organisation, which is what answers "whose data is
|
|
2
|
+
-- this" for a request that arrives with no session behind it.
|
|
3
|
+
--
|
|
4
|
+
-- `created_by` keeps the person, because a token is minted by somebody and a
|
|
5
|
+
-- revoked key is only findable if the register remembers who made it. The
|
|
6
|
+
-- organisation is the tenancy; the user is the provenance — the same split
|
|
7
|
+
-- `apps` makes between `org_id` and `owner_id`.
|
|
8
|
+
update api_tokens t
|
|
9
|
+
set org_id = o.id, created_by = coalesce(t.created_by, t.user_id)
|
|
10
|
+
from users u
|
|
11
|
+
join orgs o on lower(o.name) = lower(u.username) and o.kind = 'personal'
|
|
12
|
+
where u.id = t.user_id and t.org_id is null;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# The register's own schema
|
|
2
|
+
|
|
3
|
+
`.heychunky/migrations` is how an *app's* database changes: files ride the
|
|
4
|
+
branch, and `_heychunky_migrations` records what each database has applied. The
|
|
5
|
+
register had no equivalent — it is not an app and has no repository of its own,
|
|
6
|
+
so its schema lived only in the database and in whoever remembered typing it.
|
|
7
|
+
|
|
8
|
+
It has one now. This directory is that repository, and the mechanism is the
|
|
9
|
+
same one an app gets rather than a second one.
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
register/
|
|
13
|
+
0001-baseline/ the register as it stood on 2026-09-08, 22 statements
|
|
14
|
+
0002-orgs/ organisations, the tenancy boundary — 16 statements
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**A change is a directory, its statements numbered inside it.** One statement
|
|
18
|
+
per file, because the database client sends one query per request and answers a
|
|
19
|
+
string holding two as though all was well. The ledger id is the path —
|
|
20
|
+
`0002-orgs/05-apps-org-id.sql` — so what a database has applied reads as whole
|
|
21
|
+
changes rather than as filenames somebody has to regroup mentally.
|
|
22
|
+
|
|
23
|
+
Both numbers pad, because everything sorts lexically.
|
|
24
|
+
|
|
25
|
+
## Applying it
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
heychunky register what it has, and what it owes
|
|
29
|
+
heychunky register migrate --confirm apply what it owes
|
|
30
|
+
heychunky register verify what must be true of the rows
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
All four take `--app <app> --env <environment>` to reach a register other than
|
|
34
|
+
the factory's own. There is more than one: heychunky-app runs in six
|
|
35
|
+
environments and each has a database of this shape, which is how the schema
|
|
36
|
+
reaches the one the tests and the dev deployment use.
|
|
37
|
+
|
|
38
|
+
The files are read from the installed Core rather than from a repository at a
|
|
39
|
+
ref, which is the one place this differs from an app's migrations. An app's
|
|
40
|
+
files ride the branch, because "what does staging still owe" has to be
|
|
41
|
+
answerable from the branch alone. The register has one database and no ladder,
|
|
42
|
+
so the schema it can be brought to is the schema its CLI was built against —
|
|
43
|
+
and no unreviewed local edit can reach it by accident.
|
|
44
|
+
|
|
45
|
+
## Adopting, for a database that predates the ledger
|
|
46
|
+
|
|
47
|
+
The baseline is a record before it is a script. The live register already held
|
|
48
|
+
every statement in it, so running it would fail on the first `create table`:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
heychunky register adopt 0001-baseline --confirm
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
That records the statements as applied having run none of them. It is **checked
|
|
55
|
+
rather than trusted** — every statement's table or index is looked up in the
|
|
56
|
+
catalog first, and a change that is not entirely true of the register is refused
|
|
57
|
+
with the missing pieces named. The failure it guards is invisible otherwise: a
|
|
58
|
+
false receipt, and then a schema change silently skipped forever after.
|
|
59
|
+
|
|
60
|
+
It found real drift the first time it ran. The factory's dev register had no
|
|
61
|
+
`runs` table where production did, so the honest sequence there was to apply
|
|
62
|
+
that one statement and adopt the rest. A single statement id is accepted as well
|
|
63
|
+
as a change name, which is how that is done.
|
|
64
|
+
|
|
65
|
+
## What is deliberately not enforced
|
|
66
|
+
|
|
67
|
+
**Deleting a user leaves their personal organisation behind.** `org_members`
|
|
68
|
+
cascades; `orgs` does not, because an organisation outlives the people in it —
|
|
69
|
+
and no foreign key can say "except the personal one". There is no delete-a-user
|
|
70
|
+
path in the product today, so the invariant is left to `register verify`, which
|
|
71
|
+
counts a personal organisation whose name is nobody's username. When deleting a
|
|
72
|
+
user becomes a real operation it deletes the organisation too, and that check is
|
|
73
|
+
what will catch anyone who forgets.
|