@wtfalch/foundry 0.7.1 → 0.9.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/bootstrap/bootstrap.d.ts +12 -8
- package/dist/bootstrap/bootstrap.js +4 -12
- package/dist/bootstrap/declare.js +1 -5
- package/dist/bootstrap/versions.d.ts +8 -0
- package/dist/bootstrap/versions.js +31 -0
- package/dist/calls-sfu-deploy-support.d.ts +1 -1
- package/dist/calls-sfu-deploy-support.js +1 -1
- package/dist/calls-sfu-ssh-adapter.d.ts +1 -1
- package/dist/calls-sfu-ssh-adapter.js +1 -1
- package/dist/calls-sfu.d.ts +5 -4
- package/dist/calls-sfu.js +5 -4
- package/dist/index.d.ts +4 -5
- package/dist/index.js +6 -3
- package/dist/migrations/0024_audit_events.sql +1 -1
- package/dist/migrations/0025_audit_display.sql +1 -1
- package/dist/migrations/0026_audit_erase_email_guard.sql +1 -1
- package/dist/migrations/0027_audit_chain.sql +1 -1
- package/dist/migrations/0028_audit_rls.sql +2 -2
- package/dist/migrations/0029_hosting_entries.sql +109 -0
- package/dist/migrations-dir.d.ts +9 -0
- package/dist/migrations-dir.js +10 -0
- package/dist/org.d.ts +59 -27
- package/dist/org.js +98 -26
- package/dist/plan/calls-matrix.d.ts +2 -0
- package/dist/plan/calls-matrix.js +2 -0
- package/dist/plan/diff.d.ts +3 -1
- package/dist/plan/diff.js +100 -32
- package/dist/plan/executor.d.ts +324 -19
- package/dist/plan/executor.js +764 -34
- package/dist/plan/index.d.ts +1 -1
- package/dist/plan/index.js +1 -1
- package/dist/plan/login-host.d.ts +24 -0
- package/dist/plan/login-host.js +21 -0
- package/dist/plan/plan-apply.d.ts +2 -0
- package/dist/plan/plan-apply.js +26 -10
- package/dist/plan/platform-diff.d.ts +102 -12
- package/dist/plan/platform-diff.js +236 -15
- package/dist/plan/platform-providers.d.ts +43 -1
- package/dist/plan/platform-providers.js +245 -33
- package/dist/plan/zitadel-system-api.d.ts +64 -0
- package/dist/plan/zitadel-system-api.js +125 -0
- package/dist/plan.d.ts +43 -0
- package/dist/plan.js +16 -1
- package/dist/platform.d.ts +1 -1
- package/dist/store/audit.d.ts +13 -17
- package/dist/store/audit.js +14 -18
- package/dist/store/postgres.d.ts +14 -0
- package/dist/store/postgres.js +15 -18
- package/dist/store/schema.d.ts +20 -1
- package/dist/store/schema.js +7 -1
- package/package.json +6 -3
- package/versions.json +1 -1
- package/dist/migrate.d.ts +0 -38
- package/dist/migrate.js +0 -47
package/dist/plan.d.ts
CHANGED
|
@@ -53,4 +53,47 @@ export interface Plan {
|
|
|
53
53
|
orgId: OrgId;
|
|
54
54
|
steps: PlanStep[];
|
|
55
55
|
unmanaged: UnmanagedItem[];
|
|
56
|
+
/**
|
|
57
|
+
* #251: see {@link VersionChangeItem}. Optional so a `Plan` built before
|
|
58
|
+
* this field existed, or a test fixture that predates it, still satisfies
|
|
59
|
+
* this type with none.
|
|
60
|
+
*/
|
|
61
|
+
versionChanges?: VersionChangeItem[];
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The action kind a {@link VersionChangeItem} carries -- a distinct, typed
|
|
65
|
+
* value so Archon can key off it to render and later gate these lines
|
|
66
|
+
* (wtfalch/archon#133), and so it is never mistaken for a `StepAction.kind`
|
|
67
|
+
* (`plan/diff.ts`) any `StepExecutor` dispatches on.
|
|
68
|
+
*
|
|
69
|
+
* #251 round 3: an earlier attempt planned these as ordinary `PlanStep`s in
|
|
70
|
+
* `Plan.steps`. That made a plan holding only version-change lines for
|
|
71
|
+
* zitadel/synapse/mas (whose own step never re-applies a running app's
|
|
72
|
+
* image tag -- see {@link VersionChangeItem}) never read as empty, which is
|
|
73
|
+
* the condition `bootstrap/drive.ts` loops on, and `apply` tried to run a
|
|
74
|
+
* step for those services that does nothing. Version-change lines are
|
|
75
|
+
* carried on their own `Plan.versionChanges` field instead, never on
|
|
76
|
+
* `steps`, so they are display-only everywhere a `Plan` is read.
|
|
77
|
+
*/
|
|
78
|
+
export declare const VERSION_CHANGE_KIND = "version-change";
|
|
79
|
+
/**
|
|
80
|
+
* One informational line reporting a service whose version has drifted --
|
|
81
|
+
* either from what the next apply would actually put live (mail, archon,
|
|
82
|
+
* ai, files, integrations: each already redeploys when its own recorded
|
|
83
|
+
* applied ref differs from the declared one), or, for a service whose own
|
|
84
|
+
* step never patches a running app's image tag once created (zitadel,
|
|
85
|
+
* synapse, mas), from what is actually live. #251: the data to show this
|
|
86
|
+
* already existed per-service with no line anywhere saying a foundry
|
|
87
|
+
* release bumping a pinned version, or an operator changing a declared
|
|
88
|
+
* `gitRef`, had changed what the next apply would do -- or, for the second
|
|
89
|
+
* group, that it would do nothing at all.
|
|
90
|
+
*
|
|
91
|
+
* Never a {@link PlanStep}: it creates or changes nothing live, and is never
|
|
92
|
+
* passed to a `StepExecutor`.
|
|
93
|
+
*/
|
|
94
|
+
export interface VersionChangeItem {
|
|
95
|
+
kind: typeof VERSION_CHANGE_KIND;
|
|
96
|
+
/** The service this line is about, e.g. 'mail', 'archon', 'zitadel'. */
|
|
97
|
+
service: string;
|
|
98
|
+
description: string;
|
|
56
99
|
}
|
package/dist/plan.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* The action kind a {@link VersionChangeItem} carries -- a distinct, typed
|
|
3
|
+
* value so Archon can key off it to render and later gate these lines
|
|
4
|
+
* (wtfalch/archon#133), and so it is never mistaken for a `StepAction.kind`
|
|
5
|
+
* (`plan/diff.ts`) any `StepExecutor` dispatches on.
|
|
6
|
+
*
|
|
7
|
+
* #251 round 3: an earlier attempt planned these as ordinary `PlanStep`s in
|
|
8
|
+
* `Plan.steps`. That made a plan holding only version-change lines for
|
|
9
|
+
* zitadel/synapse/mas (whose own step never re-applies a running app's
|
|
10
|
+
* image tag -- see {@link VersionChangeItem}) never read as empty, which is
|
|
11
|
+
* the condition `bootstrap/drive.ts` loops on, and `apply` tried to run a
|
|
12
|
+
* step for those services that does nothing. Version-change lines are
|
|
13
|
+
* carried on their own `Plan.versionChanges` field instead, never on
|
|
14
|
+
* `steps`, so they are display-only everywhere a `Plan` is read.
|
|
15
|
+
*/
|
|
16
|
+
export const VERSION_CHANGE_KIND = 'version-change';
|
package/dist/platform.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { OrgId } from './org.js';
|
|
|
4
4
|
* {@link DeclaredPlatform}. `enabled` turns the service's own provisioning
|
|
5
5
|
* on; `hostname` is where it is served (default `<service>.<apex>`,
|
|
6
6
|
* {@link defaultPlatformHostname}); `gitRef` is the release it runs, the
|
|
7
|
-
* same "declared, not discovered" shape `
|
|
7
|
+
* same "declared, not discovered" shape `HostingEntry.branch` uses for
|
|
8
8
|
* an org's own app (`org.ts`).
|
|
9
9
|
*/
|
|
10
10
|
export interface PlatformService {
|
package/dist/store/audit.d.ts
CHANGED
|
@@ -21,23 +21,19 @@ import type { FoundryStoreDb } from './postgres.js';
|
|
|
21
21
|
* backfill can still be written later, where discarding the old rows
|
|
22
22
|
* outright could not be undone.
|
|
23
23
|
*
|
|
24
|
-
* `checkRuntimeRole` defaults to `
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* `
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* database level regardless (0001's triggers, 0004's hashing), only the
|
|
38
|
-
* extra fail-closed check on the *connection* is off. Options, not a second
|
|
39
|
-
* positional argument (`CLAUDE.md` rule 7): a caller -- today only a test
|
|
40
|
-
* fixture -- can pass `true` once foundry's own database gets that split.
|
|
24
|
+
* `checkRuntimeRole` defaults to `true`, `createLedger`'s own default. The
|
|
25
|
+
* connection must be a restricted role: not a superuser, no BYPASSRLS, and no
|
|
26
|
+
* UPDATE, DELETE or TRUNCATE on `audit_events`. A call through the owner
|
|
27
|
+
* connection throws `UnsafeRuntimeRoleError`. The check reads attributes of
|
|
28
|
+
* the connected role, not its name.
|
|
29
|
+
*
|
|
30
|
+
* The host provides that role. After it migrates with the owner URL, it calls
|
|
31
|
+
* `ensureRuntimeRole` from `@wtfalch/db/runtime-role` with
|
|
32
|
+
* `appendOnly: ['audit_events']` and the four audit functions in `grants`
|
|
33
|
+
* (README, "Foundry's own Postgres"), then connects the server as the runtime
|
|
34
|
+
* role. A test fixture without that split (PGlite's one user is a superuser)
|
|
35
|
+
* passes `checkRuntimeRole: false`. Options, not a second positional argument
|
|
36
|
+
* (`CLAUDE.md` rule 7).
|
|
41
37
|
*/
|
|
42
38
|
export declare function createPostgresAuditLog(db: FoundryStoreDb, options?: {
|
|
43
39
|
checkRuntimeRole?: boolean;
|
package/dist/store/audit.js
CHANGED
|
@@ -61,29 +61,25 @@ function boundPlan(plan) {
|
|
|
61
61
|
* backfill can still be written later, where discarding the old rows
|
|
62
62
|
* outright could not be undone.
|
|
63
63
|
*
|
|
64
|
-
* `checkRuntimeRole` defaults to `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* `
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
* database level regardless (0001's triggers, 0004's hashing), only the
|
|
78
|
-
* extra fail-closed check on the *connection* is off. Options, not a second
|
|
79
|
-
* positional argument (`CLAUDE.md` rule 7): a caller -- today only a test
|
|
80
|
-
* fixture -- can pass `true` once foundry's own database gets that split.
|
|
64
|
+
* `checkRuntimeRole` defaults to `true`, `createLedger`'s own default. The
|
|
65
|
+
* connection must be a restricted role: not a superuser, no BYPASSRLS, and no
|
|
66
|
+
* UPDATE, DELETE or TRUNCATE on `audit_events`. A call through the owner
|
|
67
|
+
* connection throws `UnsafeRuntimeRoleError`. The check reads attributes of
|
|
68
|
+
* the connected role, not its name.
|
|
69
|
+
*
|
|
70
|
+
* The host provides that role. After it migrates with the owner URL, it calls
|
|
71
|
+
* `ensureRuntimeRole` from `@wtfalch/db/runtime-role` with
|
|
72
|
+
* `appendOnly: ['audit_events']` and the four audit functions in `grants`
|
|
73
|
+
* (README, "Foundry's own Postgres"), then connects the server as the runtime
|
|
74
|
+
* role. A test fixture without that split (PGlite's one user is a superuser)
|
|
75
|
+
* passes `checkRuntimeRole: false`. Options, not a second positional argument
|
|
76
|
+
* (`CLAUDE.md` rule 7).
|
|
81
77
|
*/
|
|
82
78
|
export function createPostgresAuditLog(db, options = {}) {
|
|
83
79
|
const ledger = createLedger({
|
|
84
80
|
vocabulary: FOUNDRY_LEDGER_VOCABULARY,
|
|
85
81
|
hashChain: true,
|
|
86
|
-
checkRuntimeRole: options.checkRuntimeRole
|
|
82
|
+
checkRuntimeRole: options.checkRuntimeRole,
|
|
87
83
|
});
|
|
88
84
|
const targetFor = (orgId, target) => ({
|
|
89
85
|
type: target === 'platform' ? 'platform' : 'org',
|
package/dist/store/postgres.d.ts
CHANGED
|
@@ -7,6 +7,20 @@ import type { DeclaredOrgStore, PlatformStore, RequestedChangeStore } from '../s
|
|
|
7
7
|
* connection itself.
|
|
8
8
|
*/
|
|
9
9
|
export type FoundryStoreDb = PgDatabase<PgQueryResultHKT, any, any>;
|
|
10
|
+
/**
|
|
11
|
+
* The Postgres unique-violation constraint name, however the driver hands the
|
|
12
|
+
* error back -- directly (PGlite in this package's own tests) or wrapped in
|
|
13
|
+
* drizzle-orm's own `DrizzleQueryError`, whose `cause` carries the raw driver
|
|
14
|
+
* error (postgres-js in a host) -- or `undefined` when this is not a unique
|
|
15
|
+
* violation. Checked by Postgres error code, never by message text, since
|
|
16
|
+
* message text is not a contract. Same code-check pattern as
|
|
17
|
+
* `../runner/runner.ts`'s own `isUniqueViolation` -- kept as a local copy
|
|
18
|
+
* here rather than shared, since neither module depends on the other; this
|
|
19
|
+
* one additionally reads the constraint name (`constraint` under node-postgres and PGlite, `constraint_name` under postgres-js) so `create`/`update` can tell a slug
|
|
20
|
+
* collision from a domain collision (#131) rather than reporting both the
|
|
21
|
+
* same way.
|
|
22
|
+
*/
|
|
23
|
+
export declare function uniqueViolationConstraint(error: unknown): string | undefined;
|
|
10
24
|
/**
|
|
11
25
|
* Thrown by `create` and `update` when the store refuses a write: an id
|
|
12
26
|
* that already exists, an id that does not, a slug that fails
|
package/dist/store/postgres.js
CHANGED
|
@@ -19,17 +19,22 @@ const NON_NEGATIVE_INTEGER = /^(0|[1-9]\d*)$/;
|
|
|
19
19
|
* message text is not a contract. Same code-check pattern as
|
|
20
20
|
* `../runner/runner.ts`'s own `isUniqueViolation` -- kept as a local copy
|
|
21
21
|
* here rather than shared, since neither module depends on the other; this
|
|
22
|
-
* one additionally reads `constraint` so `create`/`update` can tell a slug
|
|
22
|
+
* one additionally reads the constraint name (`constraint` under node-postgres and PGlite, `constraint_name` under postgres-js) so `create`/`update` can tell a slug
|
|
23
23
|
* collision from a domain collision (#131) rather than reporting both the
|
|
24
24
|
* same way.
|
|
25
25
|
*/
|
|
26
|
-
function uniqueViolationConstraint(error) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
26
|
+
export function uniqueViolationConstraint(error) {
|
|
27
|
+
// Walk `cause`: drizzle-orm wraps the driver error, and how deep differs by version. Bounded so a cyclic `cause` ends.
|
|
28
|
+
let current = error;
|
|
29
|
+
for (let depth = 0; depth < 5 && current !== null && typeof current === 'object'; depth += 1) {
|
|
30
|
+
const e = current;
|
|
31
|
+
if (e.code === UNIQUE_VIOLATION) {
|
|
32
|
+
// node-postgres spells it `constraint`; postgres-js spells it `constraint_name`.
|
|
33
|
+
const name = e.constraint ?? e.constraint_name;
|
|
34
|
+
return typeof name === 'string' ? name : undefined;
|
|
35
|
+
}
|
|
36
|
+
current = e.cause;
|
|
37
|
+
}
|
|
33
38
|
return undefined;
|
|
34
39
|
}
|
|
35
40
|
/**
|
|
@@ -74,13 +79,9 @@ function rowToOrg(row) {
|
|
|
74
79
|
hosting: {
|
|
75
80
|
enabled: row.hostingEnabled,
|
|
76
81
|
usesDomain: row.hostingUsesDomain,
|
|
77
|
-
database: row.hostingDatabase,
|
|
78
82
|
tunnel: row.hostingTunnel,
|
|
79
|
-
gitBranch: row.hostingGitBranch,
|
|
80
|
-
buildPack: row.hostingBuildPack,
|
|
81
83
|
issuerApps: row.hostingIssuerApps,
|
|
82
|
-
|
|
83
|
-
appEnv: row.hostingAppEnv,
|
|
84
|
+
entries: row.hostingEntries,
|
|
84
85
|
},
|
|
85
86
|
sso: { enabled: row.ssoEnabled },
|
|
86
87
|
guests: { enabled: row.guestsEnabled },
|
|
@@ -117,13 +118,9 @@ function orgToRow(org) {
|
|
|
117
118
|
siteUsesDomain: org.site.usesDomain,
|
|
118
119
|
hostingEnabled: org.hosting.enabled,
|
|
119
120
|
hostingUsesDomain: org.hosting.usesDomain,
|
|
120
|
-
hostingDatabase: org.hosting.database,
|
|
121
121
|
hostingTunnel: org.hosting.tunnel,
|
|
122
|
-
hostingGitBranch: org.hosting.gitBranch,
|
|
123
|
-
hostingBuildPack: org.hosting.buildPack,
|
|
124
122
|
hostingIssuerApps: org.hosting.issuerApps,
|
|
125
|
-
|
|
126
|
-
hostingAppEnv: org.hosting.appEnv ?? {},
|
|
123
|
+
hostingEntries: org.hosting.entries,
|
|
127
124
|
ssoEnabled: org.sso.enabled,
|
|
128
125
|
guestsEnabled: org.guests.enabled,
|
|
129
126
|
soloEnabled: org.solo.enabled,
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AppEnvSource, IssuerApp } from '../org.js';
|
|
1
|
+
import type { AppEnvSource, HostingEntry, IssuerApp } from '../org.js';
|
|
2
2
|
import type { Plan } from '../plan.js';
|
|
3
3
|
import type { Checkpoint } from '../state.js';
|
|
4
4
|
/**
|
|
@@ -463,6 +463,25 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
463
463
|
}, {}, {
|
|
464
464
|
$type: Record<string, AppEnvSource>;
|
|
465
465
|
}>;
|
|
466
|
+
hostingEntries: import("drizzle-orm/pg-core").PgColumn<{
|
|
467
|
+
name: "hosting_entries";
|
|
468
|
+
tableName: "foundry_declared_orgs";
|
|
469
|
+
dataType: "json";
|
|
470
|
+
columnType: "PgJsonb";
|
|
471
|
+
data: HostingEntry[];
|
|
472
|
+
driverParam: unknown;
|
|
473
|
+
notNull: true;
|
|
474
|
+
hasDefault: true;
|
|
475
|
+
isPrimaryKey: false;
|
|
476
|
+
isAutoincrement: false;
|
|
477
|
+
hasRuntimeDefault: false;
|
|
478
|
+
enumValues: undefined;
|
|
479
|
+
baseColumn: never;
|
|
480
|
+
identity: undefined;
|
|
481
|
+
generated: undefined;
|
|
482
|
+
}, {}, {
|
|
483
|
+
$type: HostingEntry[];
|
|
484
|
+
}>;
|
|
466
485
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
467
486
|
name: "sso_enabled";
|
|
468
487
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -43,11 +43,17 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
43
43
|
hostingIssuerApps: jsonb('hosting_issuer_apps').notNull().default([]).$type(),
|
|
44
44
|
hostingAccessEmails: jsonb('hosting_access_emails').notNull().default([]).$type(),
|
|
45
45
|
// HostingConfig.appEnv (org.ts, #243). Maps the app's own env var names to
|
|
46
|
-
// values the plan already mints; empty by default.
|
|
46
|
+
// values the plan already mints; empty by default. Read only by migration
|
|
47
|
+
// 0029, which copies it into `hostingEntries`.
|
|
47
48
|
hostingAppEnv: jsonb('hosting_app_env')
|
|
48
49
|
.notNull()
|
|
49
50
|
.default({})
|
|
50
51
|
.$type(),
|
|
52
|
+
// HostingConfig.entries (org.ts): one entry per app. Replaces the flat
|
|
53
|
+
// hosting_database, hosting_git_branch, hosting_build_pack,
|
|
54
|
+
// hosting_access_emails and hosting_app_env columns, which stay for an older
|
|
55
|
+
// reader and are no longer written.
|
|
56
|
+
hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
|
|
51
57
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
52
58
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
53
59
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"types": "./dist/index.d.ts",
|
|
16
16
|
"default": "./dist/index.js"
|
|
17
17
|
},
|
|
18
|
+
"./migrations-dir": {
|
|
19
|
+
"types": "./dist/migrations-dir.d.ts",
|
|
20
|
+
"default": "./dist/migrations-dir.js"
|
|
21
|
+
},
|
|
18
22
|
"./migrations/*.sql": "./dist/migrations/*.sql",
|
|
19
23
|
"./package.json": "./package.json"
|
|
20
24
|
},
|
|
@@ -30,7 +34,6 @@
|
|
|
30
34
|
"typecheck": "tsc --noEmit",
|
|
31
35
|
"test": "vitest run",
|
|
32
36
|
"db:provision": "node scripts/foundry-db-provision.mjs",
|
|
33
|
-
"db:migrate": "node scripts/foundry-db-migrate.mjs",
|
|
34
37
|
"calls-sfu:deploy": "node scripts/calls-sfu-deploy.mjs"
|
|
35
38
|
},
|
|
36
39
|
"peerDependencies": {
|
|
@@ -47,8 +50,8 @@
|
|
|
47
50
|
"devDependencies": {
|
|
48
51
|
"@electric-sql/pglite": "^0.5.8",
|
|
49
52
|
"@types/node": "^22",
|
|
53
|
+
"@wtfalch/db": "0.5.2",
|
|
50
54
|
"drizzle-orm": "0.45.2",
|
|
51
|
-
"postgres": "3.4.9",
|
|
52
55
|
"typescript": "^5.9.0",
|
|
53
56
|
"vitest": "^4.1.6"
|
|
54
57
|
}
|
package/versions.json
CHANGED
package/dist/migrate.d.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/** This package's own migrations -- `../migrations` from `src/`, `./migrations` once built to `dist/` (`package.json`'s `build` script copies the `.sql` files there alongside `migrate.js`). */
|
|
2
|
-
export declare const MIGRATIONS_DIR: string;
|
|
3
|
-
/**
|
|
4
|
-
* The one capability `migrate` needs from a driver: run SQL text (one
|
|
5
|
-
* statement or several) and run a callback inside a transaction. Deliberately
|
|
6
|
-
* not a drizzle `PgDatabase` or a concrete driver type -- drizzle's own
|
|
7
|
-
* `.execute()` routes a multi-statement string through a single prepared
|
|
8
|
-
* query on PGlite, which rejects it, so a migration file's raw text has to
|
|
9
|
-
* reach the driver's own multi-statement path (`postgres`'s `.unsafe()`,
|
|
10
|
-
* PGlite's `.exec()`) instead. Same "narrow port, no concrete client type"
|
|
11
|
-
* shape `plan/executor.ts`'s `CoolifyPort` uses, for the same reason: a real
|
|
12
|
-
* driver satisfies this structurally, with a few lines of adapter at the
|
|
13
|
-
* call site (see `scripts/foundry-db-migrate.mjs` and `migrate.test.ts`).
|
|
14
|
-
*/
|
|
15
|
-
export interface MigrateSql {
|
|
16
|
-
/** Run SQL text and discard any result -- the tracking table's own DDL, and each migration file's contents. */
|
|
17
|
-
exec(text: string): Promise<void>;
|
|
18
|
-
/**
|
|
19
|
-
* Run one literal SELECT and return its rows. Only ever called here with
|
|
20
|
-
* fixed text this module builds itself (the tracking table's name, and a
|
|
21
|
-
* migration filename already checked against `/^\d{4}_.+\.sql$/`), so
|
|
22
|
-
* there is no external input to inject.
|
|
23
|
-
*/
|
|
24
|
-
query<T = Record<string, unknown>>(text: string): Promise<T[]>;
|
|
25
|
-
/** Run `fn` inside one transaction, handing it a `MigrateSql` scoped to it; rolled back if `fn` throws. */
|
|
26
|
-
transaction<T>(fn: (tx: MigrateSql) => Promise<T>): Promise<T>;
|
|
27
|
-
}
|
|
28
|
-
export interface MigrationResult {
|
|
29
|
-
/** Names of the migration files applied this run, in filename order -- empty when the database was already current. */
|
|
30
|
-
applied: string[];
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Applies every migration file in `dir` (default: this package's own
|
|
34
|
-
* `../migrations`) not yet recorded in `foundry_schema_migrations`, in
|
|
35
|
-
* filename order, one transaction per file. Safe to call repeatedly: a file
|
|
36
|
-
* already recorded is never re-read, let alone re-run.
|
|
37
|
-
*/
|
|
38
|
-
export declare function migrate(sql: MigrateSql, dir?: string): Promise<MigrationResult>;
|
package/dist/migrate.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { readFileSync, readdirSync } from 'node:fs';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
// #R2 (map gap: foundry's own Postgres, two commands): a tracked, idempotent
|
|
5
|
-
// migration runner. `./test/db.ts` and `./runner/test/db.ts` already read
|
|
6
|
-
// and sort this same directory to build one concatenated blob for a fresh
|
|
7
|
-
// PGlite -- fine for a test fixture that always starts empty, wrong for a
|
|
8
|
-
// real host that must apply only what a `foundry_schema_migrations` table
|
|
9
|
-
// does not yet list. `migrationFiles` below is that same discovery, reused
|
|
10
|
-
// rather than re-implemented, kept per-file instead of concatenated.
|
|
11
|
-
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
-
/** This package's own migrations -- `../migrations` from `src/`, `./migrations` once built to `dist/` (`package.json`'s `build` script copies the `.sql` files there alongside `migrate.js`). */
|
|
13
|
-
export const MIGRATIONS_DIR = join(here, 'migrations');
|
|
14
|
-
const TRACKING_TABLE = 'foundry_schema_migrations';
|
|
15
|
-
function migrationFiles(dir) {
|
|
16
|
-
return readdirSync(dir)
|
|
17
|
-
.filter((f) => /^\d{4}_.+\.sql$/.test(f))
|
|
18
|
-
.sort()
|
|
19
|
-
.map((name) => ({ name, path: join(dir, name) }));
|
|
20
|
-
}
|
|
21
|
-
/**
|
|
22
|
-
* Applies every migration file in `dir` (default: this package's own
|
|
23
|
-
* `../migrations`) not yet recorded in `foundry_schema_migrations`, in
|
|
24
|
-
* filename order, one transaction per file. Safe to call repeatedly: a file
|
|
25
|
-
* already recorded is never re-read, let alone re-run.
|
|
26
|
-
*/
|
|
27
|
-
export async function migrate(sql, dir = MIGRATIONS_DIR) {
|
|
28
|
-
await sql.exec(`create table if not exists ${TRACKING_TABLE} (name text primary key, applied_at timestamptz not null default now())`);
|
|
29
|
-
const appliedRows = await sql.query(`select name from ${TRACKING_TABLE}`);
|
|
30
|
-
const applied = new Set(appliedRows.map((row) => row.name));
|
|
31
|
-
const pending = migrationFiles(dir).filter((file) => !applied.has(file.name));
|
|
32
|
-
const done = [];
|
|
33
|
-
for (const file of pending) {
|
|
34
|
-
const text = readFileSync(file.path, 'utf8');
|
|
35
|
-
await sql.transaction(async (tx) => {
|
|
36
|
-
await tx.exec(text);
|
|
37
|
-
// `file.name` is drawn from `migrationFiles`' own regex-filtered
|
|
38
|
-
// `readdirSync`, never from outside this module -- see `MigrateSql`'s
|
|
39
|
-
// doc comment. Quotes are still escaped, so an oddly-named future
|
|
40
|
-
// migration file cannot break the statement.
|
|
41
|
-
const escaped = file.name.replace(/'/g, "''");
|
|
42
|
-
await tx.exec(`insert into ${TRACKING_TABLE} (name) values ('${escaped}')`);
|
|
43
|
-
});
|
|
44
|
-
done.push(file.name);
|
|
45
|
-
}
|
|
46
|
-
return { applied: done };
|
|
47
|
-
}
|