@wtfalch/authz-store 0.5.0 → 0.6.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/README.md +94 -14
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/migrations-dir.d.ts +12 -0
- package/dist/migrations-dir.js +13 -0
- package/dist/policy-entry.d.ts +2 -2
- package/dist/policy-entry.js +2 -2
- package/dist/tenants.js +10 -6
- package/migrations/0006_erase_person.sql +13 -21
- package/package.json +8 -6
- package/dist/migrate.d.ts +0 -9
- package/dist/migrate.js +0 -52
package/README.md
CHANGED
|
@@ -128,7 +128,8 @@ same helper otf already used, in place of the `(error as {code?}).code ===
|
|
|
128
128
|
'23505'` duck-typing Boule's and archon's copies both wrote — declared as a
|
|
129
129
|
peer dependency, matching `drizzle-orm`'s role (a host-supplied instance),
|
|
130
130
|
pinned exact at `0.4.0` (the estate's `@wtfalch/*` pin rule), plus a matching
|
|
131
|
-
exact `devDependency` for this package's own build and test.
|
|
131
|
+
exact `devDependency` for this package's own build and test. From 0.6.0 the
|
|
132
|
+
peer is the range `>=0.5.2 <0.6.0` and the `devDependency` is exact `0.5.2`.
|
|
132
133
|
|
|
133
134
|
Wave 7 (authz#83, authz#95 decision 2, authz#98) moves `credentials.ts`
|
|
134
135
|
(`mintCredential`, `revokeCredential`, `credentialsFor`) and `bootstrap.ts`
|
|
@@ -375,7 +376,7 @@ are: each genuinely needs the secret/keys column it names (an export row's
|
|
|
375
376
|
0.4.0 also renames the SQL function `migrations/0006_erase_person.sql`
|
|
376
377
|
creates, from `erase_person` to `authz_erase_person`. The old name collided
|
|
377
378
|
with each host's own erasure function of the same name and signature, which
|
|
378
|
-
also scrubs a host's `profiles`;
|
|
379
|
+
also scrubs a host's `profiles`; the migration's `CREATE OR REPLACE FUNCTION`
|
|
379
380
|
replaced a host's function outright, silently dropping that scrub. 0.3.0
|
|
380
381
|
published an hour before this fix and no persistent database had applied
|
|
381
382
|
0006 yet, so this is a same-file rename, not a new numbered migration.
|
|
@@ -392,10 +393,13 @@ have `expires_at`. Chat reads this to act on expiry; see
|
|
|
392
393
|
https://github.com/wtfalch/authz/issues/42.
|
|
393
394
|
|
|
394
395
|
Tests run against a real Postgres — PGlite, compiled to wasm and started in the
|
|
395
|
-
test process — so no container, service or network is needed.
|
|
396
|
-
the
|
|
397
|
-
|
|
398
|
-
nullability or an index name.
|
|
396
|
+
test process — so no container, service or network is needed. Every test applies
|
|
397
|
+
the shipped SQL through `@wtfalch/db`'s `runMigrationSources`, and
|
|
398
|
+
`src/migrate.test.ts` fails when that SQL and the drizzle definitions disagree on
|
|
399
|
+
a column, a type, nullability or an index name. Set `TEST_DATABASE_URL` to run the
|
|
400
|
+
same suite on a real Postgres, each test file in its own uniquely named schema,
|
|
401
|
+
dropped afterwards; roles, grants and the nested-transaction savepoint path are
|
|
402
|
+
tested only there.
|
|
399
403
|
|
|
400
404
|
## 0.4.0
|
|
401
405
|
|
|
@@ -422,8 +426,8 @@ constraint as-is).
|
|
|
422
426
|
`StoreBinding`, `PolicyData` and `PolicyRoleTemplate`, and nothing else, for a
|
|
423
427
|
client component that only needs the policy catalogue for permission labels.
|
|
424
428
|
The main entry (`.`) still exports everything it does today — this is
|
|
425
|
-
additive — but it also drags in `
|
|
426
|
-
|
|
429
|
+
additive — but it also drags in `node:crypto` and `drizzle-orm`, which break
|
|
430
|
+
a production client build; `./policy`'s import
|
|
427
431
|
graph is proved free of any `node:` module, `drizzle-orm` or a database handle
|
|
428
432
|
by `scripts/tests/package-contract.test.mjs`. 0.4.0 also exports the five
|
|
429
433
|
membership row helpers from `src/membership-rows.ts` off the main entry —
|
|
@@ -436,11 +440,87 @@ passed, exactly as before.
|
|
|
436
440
|
|
|
437
441
|
## Migrations
|
|
438
442
|
|
|
439
|
-
The package ships its own SQL in `migrations
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
443
|
+
The package ships its own SQL in `migrations/` and applies none of it: the host
|
|
444
|
+
does, with `runMigrationSources` from `@wtfalch/db/migrate` (0.5.2 or later), before its
|
|
445
|
+
own migrations, so every table the package owns exists before a host migration
|
|
446
|
+
references it. The directory comes from the subpath
|
|
447
|
+
`@wtfalch/authz-store/migrations-dir`, which imports only `node:url`, so a
|
|
448
|
+
migration image or a bundler never loads the main entry for it:
|
|
449
|
+
|
|
450
|
+
```ts
|
|
451
|
+
import { runMigrationSources } from '@wtfalch/db/migrate';
|
|
452
|
+
import { migrationsDir } from '@wtfalch/authz-store/migrations-dir';
|
|
453
|
+
|
|
454
|
+
const ownerUrl = process.env.DATABASE_URL_OWNER;
|
|
455
|
+
if (!ownerUrl) throw new Error('DATABASE_URL_OWNER is required');
|
|
456
|
+
|
|
457
|
+
await runMigrationSources({
|
|
458
|
+
url: ownerUrl,
|
|
459
|
+
schema: 'myservice',
|
|
460
|
+
sources: [
|
|
461
|
+
{ name: 'authz-store', dir: migrationsDir },
|
|
462
|
+
{ name: 'app', dir: 'drizzle' },
|
|
463
|
+
],
|
|
464
|
+
});
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
Each file runs once, in one transaction, and is recorded with its checksum in
|
|
468
|
+
`_migrations_sources` in the host's schema. Never edit a shipped file; a change
|
|
469
|
+
is a new numbered one. The SQL names no schema: the host's `schema` decides
|
|
470
|
+
where the tables land. The runtime connection puts that schema on its
|
|
471
|
+
`search_path`, and `ensureRuntimeRole({ schemas: ['myservice'] })` covers it.
|
|
472
|
+
`authz_erase_person` is a `SECURITY DEFINER` function that pins the
|
|
473
|
+
`search_path` of the migration run; the runtime role needs EXECUTE on it, which
|
|
474
|
+
the host grants (see below). Migrate with `@wtfalch/db` 0.5.2 or later: 0.5.0
|
|
475
|
+
and 0.5.1 let a temp table shadow the store's tables inside that function.
|
|
476
|
+
|
|
477
|
+
### Moving from 0.5
|
|
478
|
+
|
|
479
|
+
Before, `migrateStore(db)` applied the SQL on the host's one database and
|
|
480
|
+
recorded it in `authz_store_migrations`, and the host ran a grant for
|
|
481
|
+
`authz_erase_person` (or named its runtime role `<database>_rt`):
|
|
482
|
+
|
|
483
|
+
```ts
|
|
484
|
+
import { migrateStore } from '@wtfalch/authz-store';
|
|
485
|
+
|
|
486
|
+
await migrateStore(db);
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
After, with the owner credential, a schema, the store's source first, and the
|
|
490
|
+
grant on `ensureRuntimeRole`:
|
|
491
|
+
|
|
492
|
+
```ts
|
|
493
|
+
import { runMigrationSources } from '@wtfalch/db/migrate';
|
|
494
|
+
import { ensureRuntimeRole } from '@wtfalch/db/runtime-role';
|
|
495
|
+
import { migrationsDir } from '@wtfalch/authz-store/migrations-dir';
|
|
496
|
+
|
|
497
|
+
const ownerUrl = process.env.DATABASE_URL_OWNER;
|
|
498
|
+
const runtimeUrl = process.env.DATABASE_URL;
|
|
499
|
+
if (!ownerUrl || !runtimeUrl) throw new Error('DATABASE_URL_OWNER and DATABASE_URL are required');
|
|
500
|
+
|
|
501
|
+
await runMigrationSources({
|
|
502
|
+
url: ownerUrl,
|
|
503
|
+
schema: 'myservice',
|
|
504
|
+
sources: [
|
|
505
|
+
{ name: 'authz-store', dir: migrationsDir },
|
|
506
|
+
{ name: 'app', dir: 'drizzle' },
|
|
507
|
+
],
|
|
508
|
+
});
|
|
509
|
+
await ensureRuntimeRole({
|
|
510
|
+
ownerUrl,
|
|
511
|
+
runtimeUrl,
|
|
512
|
+
schemas: ['myservice'],
|
|
513
|
+
grants: ['myservice.authz_erase_person(text,text,text)'],
|
|
514
|
+
});
|
|
515
|
+
// Runtime connection: createDatabase({ url, searchPath: ['myservice', 'public'] })
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`migrateStore` and its `authz_store_migrations` table are gone; a database that
|
|
519
|
+
used them keeps a stale table that nothing reads and nothing adopts. 0.6.0
|
|
520
|
+
assumes databases are recreated empty. Pass the native Drizzle handle,
|
|
521
|
+
`withDrizzle(...).orm` or `tx.orm`, to every function, with no cast. Install
|
|
522
|
+
`@wtfalch/db` `>=0.5.2 <0.6.0` yourself: it is a peer dependency (the runtime
|
|
523
|
+
code imports `sqlState` from it).
|
|
444
524
|
|
|
445
525
|
`0001_baseline.sql` is every store table as manage's live schema had it on
|
|
446
526
|
2026-09-22. Three things stay with each app, because each lists something the
|
|
@@ -453,7 +533,7 @@ app owns:
|
|
|
453
533
|
The baseline is for a new database. An existing host must verify its schema
|
|
454
534
|
and migration history before adopting it. Its reconciliation brings the
|
|
455
535
|
schema to the baseline before recording `0001_baseline.sql` in
|
|
456
|
-
`
|
|
536
|
+
`_migrations_sources`; it must preserve runtime-role privileges and the
|
|
457
537
|
host-specific constraints above. The historical map records OTF's missing
|
|
458
538
|
`authz_roles.updated_by` and `'activation'` assignment source; verify these
|
|
459
539
|
against the host being migrated rather than assuming that snapshot is current.
|
package/dist/index.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export { policyActivationApprovals, policyActivations, policyAssignments, policy
|
|
|
4
4
|
export { accessTeamMembers, accessTeams, type AppRolePermission, } from './resource-schema.js';
|
|
5
5
|
export { attachProposals, authzEvents, breakGlassSessions, credentials, invitations, memberships, tenants, type AttachProposal, type AuthzEvent, type BreakGlassSession, type Credential, type Invitation, type Membership, type NewAttachProposal, type NewAuthzEvent, type NewBreakGlassSession, type NewCredential, type NewInvitation, type NewMembership, type NewTenant, type Tenant, } from './schema.js';
|
|
6
6
|
export { ownerCoverage, ownerSeatCovered, principalIsReachable, type OwnerCoverage, type OwnerCoverageOptions, type PolicyBinding, } from './owners.js';
|
|
7
|
-
export { migrateStore } from './migrate.js';
|
|
8
7
|
export { guestGrants, policyGrants, type GrantStatus, type GuestGrant } from './grants.js';
|
|
9
8
|
export { permissionOf, type StoreBinding, type HostResourceResolver, type ResourceTarget, } from './binding.js';
|
|
10
9
|
export { isCustomRoleKey } from './role-keys.js';
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,6 @@ export { policyActivationApprovals, policyActivations, policyAssignments, policy
|
|
|
4
4
|
export { accessTeamMembers, accessTeams, } from './resource-schema.js';
|
|
5
5
|
export { attachProposals, authzEvents, breakGlassSessions, credentials, invitations, memberships, tenants, } from './schema.js';
|
|
6
6
|
export { ownerCoverage, ownerSeatCovered, principalIsReachable, } from './owners.js';
|
|
7
|
-
export { migrateStore } from './migrate.js';
|
|
8
7
|
export { guestGrants, policyGrants } from './grants.js';
|
|
9
8
|
export { permissionOf, } from './binding.js';
|
|
10
9
|
export { isCustomRoleKey } from './role-keys.js';
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
3
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
4
|
+
* `{ name: 'authz-store', dir: migrationsDir }`.
|
|
5
|
+
*
|
|
6
|
+
* Its own subpath (`@wtfalch/authz-store/migrations-dir`) and no other
|
|
7
|
+
* imports: a migration image imports this without Drizzle and authz, and a
|
|
8
|
+
* bundler never meets this file's `import.meta.url` through the main entry.
|
|
9
|
+
* `migrations/` sits next to `dist/` in the package and next to `src/` in
|
|
10
|
+
* this repository, so one relative path is right in both.
|
|
11
|
+
*/
|
|
12
|
+
export declare const migrationsDir: string;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { fileURLToPath } from 'node:url';
|
|
2
|
+
/**
|
|
3
|
+
* Absolute path of the directory holding this package's numbered `.sql`
|
|
4
|
+
* migrations. The host passes it to `runMigrationSources` as
|
|
5
|
+
* `{ name: 'authz-store', dir: migrationsDir }`.
|
|
6
|
+
*
|
|
7
|
+
* Its own subpath (`@wtfalch/authz-store/migrations-dir`) and no other
|
|
8
|
+
* imports: a migration image imports this without Drizzle and authz, and a
|
|
9
|
+
* bundler never meets this file's `import.meta.url` through the main entry.
|
|
10
|
+
* `migrations/` sits next to `dist/` in the package and next to `src/` in
|
|
11
|
+
* this repository, so one relative path is right in both.
|
|
12
|
+
*/
|
|
13
|
+
export const migrationsDir = fileURLToPath(new URL('../migrations/', import.meta.url));
|
package/dist/policy-entry.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@wtfalch/authz-store/policy` — a client-safe subpath for `definePolicy` and the types its
|
|
3
|
-
* signature needs. The main entry (`.`)
|
|
4
|
-
* `
|
|
3
|
+
* signature needs. The main entry (`.`) imports `node:crypto` and
|
|
4
|
+
* `drizzle-orm`, so a client component that only reads the policy catalogue for permission
|
|
5
5
|
* labels cannot import from `.` without pulling Node into a production browser build
|
|
6
6
|
* (https://github.com/wtfalch/boule/pull/162). Nothing this module imports, directly or
|
|
7
7
|
* transitively, may import a `node:` module, `drizzle-orm`, or a database handle — proved by
|
package/dist/policy-entry.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `@wtfalch/authz-store/policy` — a client-safe subpath for `definePolicy` and the types its
|
|
3
|
-
* signature needs. The main entry (`.`)
|
|
4
|
-
* `
|
|
3
|
+
* signature needs. The main entry (`.`) imports `node:crypto` and
|
|
4
|
+
* `drizzle-orm`, so a client component that only reads the policy catalogue for permission
|
|
5
5
|
* labels cannot import from `.` without pulling Node into a production browser build
|
|
6
6
|
* (https://github.com/wtfalch/boule/pull/162). Nothing this module imports, directly or
|
|
7
7
|
* transitively, may import a `node:` module, `drizzle-orm`, or a database handle — proved by
|
package/dist/tenants.js
CHANGED
|
@@ -111,7 +111,9 @@ export async function createTenant(principal, db, policy, config, profiles, opti
|
|
|
111
111
|
return refused('slug_taken', 'That address is already taken.');
|
|
112
112
|
let tenantRow;
|
|
113
113
|
try {
|
|
114
|
-
|
|
114
|
+
// A savepoint: a unique violation aborts a Postgres transaction, and the rest of this
|
|
115
|
+
// function runs in it. Rolling back to the savepoint lets the catch below return normally.
|
|
116
|
+
[tenantRow] = await tx.transaction((sp) => sp
|
|
115
117
|
.insert(tenants)
|
|
116
118
|
.values({
|
|
117
119
|
kind: 'customer',
|
|
@@ -121,7 +123,7 @@ export async function createTenant(principal, db, policy, config, profiles, opti
|
|
|
121
123
|
selfDenied: [],
|
|
122
124
|
createdBy: principal.id,
|
|
123
125
|
})
|
|
124
|
-
.returning({ id: tenants.id });
|
|
126
|
+
.returning({ id: tenants.id }));
|
|
125
127
|
}
|
|
126
128
|
catch (error) {
|
|
127
129
|
if (isUniqueViolation(error))
|
|
@@ -197,10 +199,11 @@ export async function renameTenant(access, db, scopeColumn, options) {
|
|
|
197
199
|
return refused('slug_taken', 'That address is already taken.');
|
|
198
200
|
}
|
|
199
201
|
try {
|
|
200
|
-
|
|
202
|
+
// A savepoint, for the same reason as in `createTenant`.
|
|
203
|
+
await tx.transaction((sp) => sp
|
|
201
204
|
.update(tenants)
|
|
202
205
|
.set({ name: nextName, slug: nextSlug, updatedAt: new Date() })
|
|
203
|
-
.where(eq(tenants.id, tenantRow.id));
|
|
206
|
+
.where(eq(tenants.id, tenantRow.id)));
|
|
204
207
|
}
|
|
205
208
|
catch (error) {
|
|
206
209
|
if (isUniqueViolation(error))
|
|
@@ -611,7 +614,8 @@ export async function createTenantAsOperator(operatorAccess, db, scopeColumn, po
|
|
|
611
614
|
}
|
|
612
615
|
let tenantRow;
|
|
613
616
|
try {
|
|
614
|
-
|
|
617
|
+
// A savepoint, for the same reason as in `createTenant`.
|
|
618
|
+
[tenantRow] = await tx.transaction((sp) => sp
|
|
615
619
|
.insert(tenants)
|
|
616
620
|
.values({
|
|
617
621
|
kind: 'customer',
|
|
@@ -621,7 +625,7 @@ export async function createTenantAsOperator(operatorAccess, db, scopeColumn, po
|
|
|
621
625
|
selfDenied: [],
|
|
622
626
|
createdBy: currentAccess.actor.id,
|
|
623
627
|
})
|
|
624
|
-
.returning({ id: tenants.id });
|
|
628
|
+
.returning({ id: tenants.id }));
|
|
625
629
|
}
|
|
626
630
|
catch (error) {
|
|
627
631
|
if (isUniqueViolation(error))
|
|
@@ -12,12 +12,17 @@
|
|
|
12
12
|
--
|
|
13
13
|
-- 0.4.0 note: this function was originally named `erase_person`, the same
|
|
14
14
|
-- name and signature as each host's own erasure function (the one that also
|
|
15
|
-
-- scrubs `profiles`). `
|
|
15
|
+
-- scrubs `profiles`). `CREATE OR REPLACE FUNCTION` replaces a same-named function outright,
|
|
16
16
|
-- so applying this migration silently dropped a host's own `profiles` scrub.
|
|
17
17
|
-- Renamed here to `authz_erase_person` before any persistent database ever
|
|
18
18
|
-- applied it: 0.3.0 published an hour before this fix, and no host had run
|
|
19
19
|
-- this migration against a real database yet, so this is a same-file rename,
|
|
20
20
|
-- not a new numbered migration.
|
|
21
|
+
--
|
|
22
|
+
-- 0.6.0 note: edited in place, again before any persistent database applied
|
|
23
|
+
-- it (databases were recreated empty when migrations moved to the host's
|
|
24
|
+
-- runner, in the host's own schema). The function now pins the `search_path`
|
|
25
|
+
-- of the session that creates it, and the runtime-role grant moved to the host.
|
|
21
26
|
|
|
22
27
|
-- ----------------------------------------------- authz_erase_person(...)
|
|
23
28
|
-- The single sanctioned write to `authz_events` (D9, M3).
|
|
@@ -45,15 +50,17 @@
|
|
|
45
50
|
-- a list to keep in step forever; what a row meant is in `action`,
|
|
46
51
|
-- `target_*` and `reason`, none of which this touches.
|
|
47
52
|
--
|
|
48
|
-
-- `search_path` is pinned
|
|
49
|
-
--
|
|
50
|
-
--
|
|
51
|
-
--
|
|
53
|
+
-- `search_path` is pinned to the one the host's migration run set, which is
|
|
54
|
+
-- the host's schema first, so the unqualified names below resolve there. A
|
|
55
|
+
-- SECURITY DEFINER function that resolves an unqualified name through the
|
|
56
|
+
-- caller's `search_path` runs whatever they put in front of it, with the
|
|
57
|
+
-- owner's privileges, which is the standard way this feature becomes a
|
|
58
|
+
-- privilege escalation.
|
|
52
59
|
CREATE OR REPLACE FUNCTION "authz_erase_person"("subject" text, "pseudonym" text, "subject_email" text)
|
|
53
60
|
RETURNS integer
|
|
54
61
|
LANGUAGE plpgsql
|
|
55
62
|
SECURITY DEFINER
|
|
56
|
-
SET search_path
|
|
63
|
+
SET search_path FROM CURRENT
|
|
57
64
|
AS $$
|
|
58
65
|
DECLARE
|
|
59
66
|
touched integer;
|
|
@@ -126,18 +133,3 @@ $$;
|
|
|
126
133
|
-- new function to PUBLIC by default.
|
|
127
134
|
REVOKE ALL ON FUNCTION "authz_erase_person"(text, text, text) FROM PUBLIC;
|
|
128
135
|
--> statement-breakpoint
|
|
129
|
-
|
|
130
|
-
-- The store has no runtime-role convention of its own, unlike Boule's fixed
|
|
131
|
-
-- `<database>_rt`, granted unconditionally. A host whose runtime role
|
|
132
|
-
-- happens to be named that way gets EXECUTE for free; a host with no such
|
|
133
|
-
-- role gets nothing granted here and grants EXECUTE itself, rather than
|
|
134
|
-
-- this migration failing outright for naming a role that does not exist.
|
|
135
|
-
DO $$
|
|
136
|
-
DECLARE
|
|
137
|
-
rt text := current_database() || '_rt';
|
|
138
|
-
BEGIN
|
|
139
|
-
IF EXISTS (SELECT 1 FROM pg_roles WHERE rolname = rt) THEN
|
|
140
|
-
EXECUTE format('GRANT EXECUTE ON FUNCTION "authz_erase_person"(text, text, text) TO %I', rt);
|
|
141
|
-
END IF;
|
|
142
|
-
END
|
|
143
|
-
$$;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/authz-store",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Persistence and lifecycle for @wtfalch/authz: the storage a host would otherwise write itself.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -17,11 +17,15 @@
|
|
|
17
17
|
"types": "./dist/policy-entry.d.ts",
|
|
18
18
|
"default": "./dist/policy-entry.js"
|
|
19
19
|
},
|
|
20
|
+
"./migrations-dir": {
|
|
21
|
+
"types": "./dist/migrations-dir.d.ts",
|
|
22
|
+
"default": "./dist/migrations-dir.js"
|
|
23
|
+
},
|
|
20
24
|
"./package.json": "./package.json"
|
|
21
25
|
},
|
|
22
26
|
"peerDependencies": {
|
|
23
27
|
"@wtfalch/authz": "^0.17.0",
|
|
24
|
-
"@wtfalch/db": "0.
|
|
28
|
+
"@wtfalch/db": ">=0.5.2 <0.6.0",
|
|
25
29
|
"drizzle-orm": ">=0.39.3 <1.0.0",
|
|
26
30
|
"zod": "^4.1.13"
|
|
27
31
|
},
|
|
@@ -35,12 +39,10 @@
|
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@electric-sql/pglite": "^0.5.8",
|
|
37
41
|
"@types/node": "^22",
|
|
38
|
-
"@wtfalch/db": "0.
|
|
42
|
+
"@wtfalch/db": "0.5.2",
|
|
39
43
|
"@wtfalch/keys": "0.4.2",
|
|
40
|
-
"drizzle-
|
|
41
|
-
"drizzle-orm": "^0.39.0",
|
|
44
|
+
"drizzle-orm": "0.39.3",
|
|
42
45
|
"fast-check": "^4.9.0",
|
|
43
|
-
"postgres": "^3.4.5",
|
|
44
46
|
"typescript": "^5.9.0",
|
|
45
47
|
"vitest": "^4.1.6",
|
|
46
48
|
"zod": "^4.5.4"
|
package/dist/migrate.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { PgDatabase, PgQueryResultHKT } from 'drizzle-orm/pg-core';
|
|
2
|
-
type HostDb = PgDatabase<PgQueryResultHKT, any, any>;
|
|
3
|
-
/**
|
|
4
|
-
* Applies the package's migrations that `authz_store_migrations` has no row for, in file-name
|
|
5
|
-
* order, one transaction per file. Returns the names it applied. The host runs this before its own
|
|
6
|
-
* migrations, so everything the package owns exists before a host migration references it.
|
|
7
|
-
*/
|
|
8
|
-
export declare function migrateStore(db: HostDb): Promise<string[]>;
|
|
9
|
-
export {};
|
package/dist/migrate.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import { readFile, readdir } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { sql } from 'drizzle-orm';
|
|
5
|
-
/**
|
|
6
|
-
* The shipped SQL, next to `dist/` in the package and next to `src/` in this repository. Computed
|
|
7
|
-
* lazily inside `migrateStore` rather than as a module-level `new URL('../migrations/',
|
|
8
|
-
* import.meta.url)`: Turbopack resolves that literal statically when an app imports the package
|
|
9
|
-
* barrel, and fails with "Can't resolve '../migrations/'" because the directory holds no importable
|
|
10
|
-
* module.
|
|
11
|
-
*/
|
|
12
|
-
function migrationsDir() {
|
|
13
|
-
return join(dirname(fileURLToPath(import.meta.url)), '..', 'migrations');
|
|
14
|
-
}
|
|
15
|
-
/** Files split on this line, the marker drizzle-kit writes, so any driver can run them one statement at a time. */
|
|
16
|
-
const BREAKPOINT = '--> statement-breakpoint';
|
|
17
|
-
/** One key for every host, so two containers starting at once apply each file once between them. */
|
|
18
|
-
const LOCK = 7_310_422_001;
|
|
19
|
-
/**
|
|
20
|
-
* Applies the package's migrations that `authz_store_migrations` has no row for, in file-name
|
|
21
|
-
* order, one transaction per file. Returns the names it applied. The host runs this before its own
|
|
22
|
-
* migrations, so everything the package owns exists before a host migration references it.
|
|
23
|
-
*/
|
|
24
|
-
export async function migrateStore(db) {
|
|
25
|
-
const dir = migrationsDir();
|
|
26
|
-
const names = (await readdir(dir)).filter((n) => n.endsWith('.sql')).sort();
|
|
27
|
-
const applied = [];
|
|
28
|
-
for (const name of names) {
|
|
29
|
-
const text = await readFile(join(dir, name), 'utf8');
|
|
30
|
-
const ran = await db.transaction(async (tx) => {
|
|
31
|
-
await tx.execute(sql `select pg_advisory_xact_lock(${LOCK})`);
|
|
32
|
-
// Created under the lock: two runners creating it at once collide in pg_type.
|
|
33
|
-
await tx.execute(sql `create table if not exists authz_store_migrations (name text primary key, applied_at timestamptz not null default now())`);
|
|
34
|
-
const done = await tx.execute(sql `select 1 from authz_store_migrations where name = ${name}`);
|
|
35
|
-
if (rowsOf(done).length > 0)
|
|
36
|
-
return false;
|
|
37
|
-
for (const statement of text.split(BREAKPOINT)) {
|
|
38
|
-
if (statement.trim())
|
|
39
|
-
await tx.execute(sql.raw(statement));
|
|
40
|
-
}
|
|
41
|
-
await tx.execute(sql `insert into authz_store_migrations (name) values (${name})`);
|
|
42
|
-
return true;
|
|
43
|
-
});
|
|
44
|
-
if (ran)
|
|
45
|
-
applied.push(name);
|
|
46
|
-
}
|
|
47
|
-
return applied;
|
|
48
|
-
}
|
|
49
|
-
/** postgres-js returns the rows as the result; node-postgres and PGlite wrap them in `rows`. */
|
|
50
|
-
function rowsOf(result) {
|
|
51
|
-
return Array.isArray(result) ? result : (result.rows ?? []);
|
|
52
|
-
}
|