@wtfalch/foundry 0.1.0 → 0.2.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/apply.d.ts +14 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +3 -2
- package/dist/migrations/0008_hosting.sql +24 -0
- package/dist/migrations/0009_hosting_git_branch_build_pack.sql +14 -0
- package/dist/migrations/0010_owner.sql +13 -0
- package/dist/migrations/0011_test_org.sql +12 -0
- package/dist/org.d.ts +107 -0
- package/dist/org.js +65 -0
- package/dist/plan/diff.d.ts +20 -5
- package/dist/plan/diff.js +402 -9
- package/dist/plan/executor.d.ts +474 -0
- package/dist/plan/executor.js +507 -0
- package/dist/plan/index.d.ts +6 -4
- package/dist/plan/index.js +3 -2
- package/dist/plan/live.d.ts +8 -4
- package/dist/plan/plan-apply.d.ts +114 -4
- package/dist/plan/plan-apply.js +251 -14
- package/dist/plan/providers-readers.d.ts +48 -13
- package/dist/plan/providers-readers.js +93 -16
- package/dist/state.d.ts +26 -5
- package/dist/store/postgres.d.ts +3 -1
- package/dist/store/postgres.js +65 -2
- package/dist/store/schema.d.ts +190 -0
- package/dist/store/schema.js +18 -0
- package/dist/store.d.ts +10 -0
- package/package.json +9 -11
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* A {@link ScopeResolver} backed by `AppliedState.serviceIds`
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* org today, through the same `DeclaredOrgStore` seam #18 built.
|
|
2
|
+
* A {@link ScopeResolver} backed by `AppliedState.serviceIds` (`../state.ts`)
|
|
3
|
+
* — the one place foundry records a provider's own id for an org today,
|
|
4
|
+
* through the same `DeclaredOrgStore` seam #18 built.
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* `
|
|
12
|
-
*
|
|
6
|
+
* `runSteps` (`plan-apply.ts`) now writes one `serviceIds` entry per
|
|
7
|
+
* resource it creates, keyed `system:kind:key` ({@link storeRecordedIds}
|
|
8
|
+
* below reads those back), and #20's `FoundryStepExecutor` (`executor.ts`)
|
|
9
|
+
* now also calls `StepExecutionContext.recordScopeId` for `coolify_project`,
|
|
10
|
+
* `zitadel_org` and `zitadel_project`, so a resolver built with one of those
|
|
11
|
+
* three `serviceKey`s returns the real id once that step has run. A
|
|
12
|
+
* Cloudflare zone is the one scope id nothing writes yet: `executor.ts`'s
|
|
13
|
+
* `cloudflareZone` records its result only under `cloudflare:zone:<domain>`
|
|
14
|
+
* (a resource id, read back by {@link storeRecordedIds}), not under a plain
|
|
15
|
+
* `cloudflare_zone` scope key — a resolver built with that `serviceKey`
|
|
16
|
+
* still returns `null` for every org. That is the correct "nothing live yet"
|
|
17
|
+
* answer: a reader passed `null` returns `[]`, which `computePlanDetail`
|
|
18
|
+
* (`diff.ts`) already reads as "no live resource".
|
|
13
19
|
*/
|
|
14
20
|
export function storeScopeResolver(store, serviceKey) {
|
|
15
21
|
return async (orgId) => {
|
|
@@ -20,6 +26,56 @@ export function storeScopeResolver(store, serviceKey) {
|
|
|
20
26
|
return applied?.serviceIds[serviceKey] || null;
|
|
21
27
|
};
|
|
22
28
|
}
|
|
29
|
+
export function storeRecordedIds(store, system) {
|
|
30
|
+
return async (orgId) => {
|
|
31
|
+
const applied = await store.getAppliedState(orgId);
|
|
32
|
+
const recorded = {};
|
|
33
|
+
if (!applied)
|
|
34
|
+
return recorded;
|
|
35
|
+
for (const [resourceKey, providerId] of Object.entries(applied.serviceIds)) {
|
|
36
|
+
const [keySystem, kind, ...rest] = resourceKey.split(':');
|
|
37
|
+
if (keySystem !== system || kind === undefined || rest.length === 0)
|
|
38
|
+
continue;
|
|
39
|
+
recorded[providerId] = `${kind}:${rest.join(':')}`;
|
|
40
|
+
}
|
|
41
|
+
return recorded;
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Maps each live resource whose own id is one `recorded` names onto the
|
|
46
|
+
* foundry `kind`/`key` it was recorded under, keeping `config` exactly as
|
|
47
|
+
* the provider gave it (design doc, "Design for (1)": "the reader then maps
|
|
48
|
+
* each provider resource whose `key` equals a recorded provider id onto the
|
|
49
|
+
* recorded foundry `kind` and `key`"). A resource nothing recorded passes
|
|
50
|
+
* through unchanged, so `computePlanDetail` (`diff.ts`) still reports it
|
|
51
|
+
* unmanaged (#10) rather than this boundary guessing at a match.
|
|
52
|
+
*
|
|
53
|
+
* `dropUnrecordedKinds` names kinds that are dropped outright instead of
|
|
54
|
+
* passed through when unrecorded. Cloudflare's `tunnel` is the one case: a
|
|
55
|
+
* tunnel is account-wide, so an unrecorded one belongs to some other org,
|
|
56
|
+
* not this one, and every org's plan would otherwise report the same
|
|
57
|
+
* foreign tunnel as unmanaged.
|
|
58
|
+
*/
|
|
59
|
+
function applyRecordedIds(resources, recorded, dropUnrecordedKinds = new Set()) {
|
|
60
|
+
const mapped = [];
|
|
61
|
+
for (const resource of resources) {
|
|
62
|
+
const foundryKey = recorded[resource.key];
|
|
63
|
+
if (!foundryKey) {
|
|
64
|
+
if (!dropUnrecordedKinds.has(resource.kind))
|
|
65
|
+
mapped.push(resource);
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const separator = foundryKey.indexOf(':');
|
|
69
|
+
mapped.push({
|
|
70
|
+
...resource,
|
|
71
|
+
kind: foundryKey.slice(0, separator),
|
|
72
|
+
key: foundryKey.slice(separator + 1),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return mapped;
|
|
76
|
+
}
|
|
77
|
+
/** Cloudflare tunnels this org has not recorded are dropped — see {@link applyRecordedIds}. */
|
|
78
|
+
const CLOUDFLARE_DROP_UNRECORDED = new Set(['tunnel']);
|
|
23
79
|
/** `@wtfalch/providers`' own `LiveResource` (`resource.ts`) has no `system` field — foundry's adds it at this boundary. */
|
|
24
80
|
function toFoundryResources(system, resources) {
|
|
25
81
|
return resources.map((r) => ({ system, kind: r.kind, key: r.key, config: r.config }));
|
|
@@ -32,15 +88,21 @@ function toFoundryResources(system, resources) {
|
|
|
32
88
|
export class ProvidersCoolifyReader {
|
|
33
89
|
client;
|
|
34
90
|
resolveScope;
|
|
35
|
-
|
|
91
|
+
resolveRecordedIds;
|
|
92
|
+
constructor(client, resolveScope, resolveRecordedIds) {
|
|
36
93
|
this.client = client;
|
|
37
94
|
this.resolveScope = resolveScope;
|
|
95
|
+
this.resolveRecordedIds = resolveRecordedIds;
|
|
38
96
|
}
|
|
39
97
|
async listResources(orgId) {
|
|
40
98
|
const projectUuid = await this.resolveScope(orgId);
|
|
41
99
|
if (!projectUuid)
|
|
42
100
|
return [];
|
|
43
|
-
|
|
101
|
+
const [resources, recorded] = await Promise.all([
|
|
102
|
+
this.client.listResources(projectUuid),
|
|
103
|
+
this.resolveRecordedIds(orgId),
|
|
104
|
+
]);
|
|
105
|
+
return applyRecordedIds(toFoundryResources('coolify', resources), recorded);
|
|
44
106
|
}
|
|
45
107
|
}
|
|
46
108
|
/**
|
|
@@ -51,15 +113,24 @@ export class ProvidersCoolifyReader {
|
|
|
51
113
|
export class ProvidersCloudflareReader {
|
|
52
114
|
client;
|
|
53
115
|
resolveScope;
|
|
54
|
-
|
|
116
|
+
resolveRecordedIds;
|
|
117
|
+
constructor(client, resolveScope, resolveRecordedIds) {
|
|
55
118
|
this.client = client;
|
|
56
119
|
this.resolveScope = resolveScope;
|
|
120
|
+
this.resolveRecordedIds = resolveRecordedIds;
|
|
57
121
|
}
|
|
58
122
|
async listResources(orgId) {
|
|
59
123
|
const zoneId = await this.resolveScope(orgId);
|
|
60
124
|
if (!zoneId)
|
|
61
125
|
return [];
|
|
62
|
-
|
|
126
|
+
const [resources, recorded] = await Promise.all([
|
|
127
|
+
this.client.listResources(zoneId),
|
|
128
|
+
this.resolveRecordedIds(orgId),
|
|
129
|
+
]);
|
|
130
|
+
// A zone needs no mapping: its key is already the zone name, which is
|
|
131
|
+
// foundry's own zone key (`diff.ts`'s `planZone`), so it never matches a
|
|
132
|
+
// recorded provider id and always passes through unchanged.
|
|
133
|
+
return applyRecordedIds(toFoundryResources('cloudflare', resources), recorded, CLOUDFLARE_DROP_UNRECORDED);
|
|
63
134
|
}
|
|
64
135
|
}
|
|
65
136
|
/**
|
|
@@ -70,14 +141,20 @@ export class ProvidersCloudflareReader {
|
|
|
70
141
|
export class ProvidersZitadelReader {
|
|
71
142
|
client;
|
|
72
143
|
resolveScope;
|
|
73
|
-
|
|
144
|
+
resolveRecordedIds;
|
|
145
|
+
constructor(client, resolveScope, resolveRecordedIds) {
|
|
74
146
|
this.client = client;
|
|
75
147
|
this.resolveScope = resolveScope;
|
|
148
|
+
this.resolveRecordedIds = resolveRecordedIds;
|
|
76
149
|
}
|
|
77
150
|
async listResources(orgId) {
|
|
78
151
|
const zitadelOrgId = await this.resolveScope(orgId);
|
|
79
152
|
if (!zitadelOrgId)
|
|
80
153
|
return [];
|
|
81
|
-
|
|
154
|
+
const [resources, recorded] = await Promise.all([
|
|
155
|
+
this.client.listResources(zitadelOrgId),
|
|
156
|
+
this.resolveRecordedIds(orgId),
|
|
157
|
+
]);
|
|
158
|
+
return applyRecordedIds(toFoundryResources('zitadel', resources), recorded);
|
|
82
159
|
}
|
|
83
160
|
}
|
package/dist/state.d.ts
CHANGED
|
@@ -34,17 +34,38 @@ export interface LastApplied {
|
|
|
34
34
|
export interface AppliedState {
|
|
35
35
|
orgId: OrgId;
|
|
36
36
|
/**
|
|
37
|
-
* One id per
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
37
|
+
* One id per resource `plan/plan-apply.ts`'s `runSteps` has made true,
|
|
38
|
+
* keyed `system:kind:key` — the same resource key `plan/diff.ts` plans
|
|
39
|
+
* against (e.g. `coolify:app:<orgId>`, `zitadel:app:<name>`), so two
|
|
40
|
+
* resources that share a `kind` (an org's several declared ZITADEL apps,
|
|
41
|
+
* #71) never overwrite each other's id the way a bare `kind` key did.
|
|
42
|
+
* `plan/providers-readers.ts`'s `storeRecordedIds` reads this map back to
|
|
43
|
+
* match a live provider resource to the foundry resource it was recorded
|
|
44
|
+
* for, instead of reporting it unmanaged (design:
|
|
45
|
+
* `.claude/campaign/2026-09-24-keys-and-memberships.md`, "Design for
|
|
46
|
+
* (1)").
|
|
47
|
+
*
|
|
48
|
+
* A provider *scope* id — `coolify_project`, `zitadel_org`,
|
|
49
|
+
* `zitadel_project`, a Cloudflare zone id (`plan/providers-readers.ts`'s
|
|
50
|
+
* `ScopeResolver`, https://github.com/wtfalch/foundry/issues/14's resolved
|
|
51
|
+
* research #4) — is a different id space and keeps its own plain key; a
|
|
52
|
+
* scope key has no `:` in it, so it can never collide with the
|
|
53
|
+
* `system:kind:key` form. `plan/executor.ts`'s `FoundryStepExecutor` is
|
|
54
|
+
* what writes these today, through `plan-apply.ts`'s
|
|
55
|
+
* `StepExecutionContext.recordScopeId` — backed by the very `serviceIds`
|
|
56
|
+
* map `runSteps` builds here and persists in one write at the end of a
|
|
57
|
+
* run, not a separate store write of its own (`StepExecutionContext`'s doc
|
|
58
|
+
* comment explains why: a second writer would race `runSteps`' own final
|
|
59
|
+
* one).
|
|
41
60
|
*/
|
|
42
61
|
serviceIds: Record<string, string>;
|
|
43
62
|
/** Checkpoints from the current or most recent apply. */
|
|
44
63
|
checkpoints: Checkpoint[];
|
|
45
64
|
/**
|
|
46
65
|
* The name of every {@link DeclaredOrg} field the most recent apply could
|
|
47
|
-
* not make true
|
|
66
|
+
* not make true: because nothing implements it yet, or because its step
|
|
67
|
+
* had not completed when the apply failed. `apply` recomputes it every
|
|
68
|
+
* time (#25), in `plan/plan-apply.ts`'s `failedFields`.
|
|
48
69
|
*
|
|
49
70
|
* Decision (#18): this field did not exist on the type this ticket found.
|
|
50
71
|
* #7 ("Shape") and #18's "Done when" both require `apply` to "fail loudly
|
package/dist/store/postgres.d.ts
CHANGED
|
@@ -34,7 +34,9 @@ export declare class DeclaredOrgError extends Error {
|
|
|
34
34
|
* on both `create` and `update`. #27 adds a third, narrower rule: storage and
|
|
35
35
|
* AI spend limits are bigint-safe decimal strings of non-negative integers
|
|
36
36
|
* (or `null` for uncapped), checked the same way on both `create` and the
|
|
37
|
-
* merged result of `update`.
|
|
37
|
+
* merged result of `update`. #67 adds a fourth (`org.ts`'s
|
|
38
|
+
* `validateBuyerIdentity`) and #71 a fifth (`org.ts`'s `validateHosting`),
|
|
39
|
+
* checked the same way. The broader "combinations foundry knows are
|
|
38
40
|
* impossible are rejected with a reason" line needs a ruleset (domain
|
|
39
41
|
* requirements per service, `adoptedApp.form`, mail-level transitions…)
|
|
40
42
|
* that later tickets, not #18, #27 or #28, decide; `update` still merges and
|
package/dist/store/postgres.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { eq, sql } from 'drizzle-orm';
|
|
2
|
-
import { validateBuyerIdentity } from '../org.js';
|
|
2
|
+
import { validateBuyerIdentity, validateHosting, validateOwner } from '../org.js';
|
|
3
3
|
import { validateSlug } from '../slug.js';
|
|
4
4
|
import { declaredOrgs, foundryAppliedState, } from './schema.js';
|
|
5
5
|
const UNIQUE_VIOLATION = '23505';
|
|
@@ -50,6 +50,15 @@ function rowToOrg(row) {
|
|
|
50
50
|
calls: { usesDomain: row.callsUsesDomain },
|
|
51
51
|
issuer: { usesDomain: row.issuerUsesDomain },
|
|
52
52
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
53
|
+
hosting: {
|
|
54
|
+
enabled: row.hostingEnabled,
|
|
55
|
+
usesDomain: row.hostingUsesDomain,
|
|
56
|
+
database: row.hostingDatabase,
|
|
57
|
+
tunnel: row.hostingTunnel,
|
|
58
|
+
gitBranch: row.hostingGitBranch,
|
|
59
|
+
buildPack: row.hostingBuildPack,
|
|
60
|
+
issuerApps: row.hostingIssuerApps,
|
|
61
|
+
},
|
|
53
62
|
sso: { enabled: row.ssoEnabled },
|
|
54
63
|
guests: { enabled: row.guestsEnabled },
|
|
55
64
|
solo: { enabled: row.soloEnabled },
|
|
@@ -60,6 +69,8 @@ function rowToOrg(row) {
|
|
|
60
69
|
adoptedApp: row.adoptedAppForm
|
|
61
70
|
? { form: row.adoptedAppForm }
|
|
62
71
|
: null,
|
|
72
|
+
owner: rowToOwner(row),
|
|
73
|
+
test: row.isTest,
|
|
63
74
|
};
|
|
64
75
|
}
|
|
65
76
|
function orgToRow(org) {
|
|
@@ -75,6 +86,13 @@ function orgToRow(org) {
|
|
|
75
86
|
issuerUsesDomain: org.issuer.usesDomain,
|
|
76
87
|
siteEnabled: org.site.enabled,
|
|
77
88
|
siteUsesDomain: org.site.usesDomain,
|
|
89
|
+
hostingEnabled: org.hosting.enabled,
|
|
90
|
+
hostingUsesDomain: org.hosting.usesDomain,
|
|
91
|
+
hostingDatabase: org.hosting.database,
|
|
92
|
+
hostingTunnel: org.hosting.tunnel,
|
|
93
|
+
hostingGitBranch: org.hosting.gitBranch,
|
|
94
|
+
hostingBuildPack: org.hosting.buildPack,
|
|
95
|
+
hostingIssuerApps: org.hosting.issuerApps,
|
|
78
96
|
ssoEnabled: org.sso.enabled,
|
|
79
97
|
guestsEnabled: org.guests.enabled,
|
|
80
98
|
soloEnabled: org.solo.enabled,
|
|
@@ -90,6 +108,10 @@ function orgToRow(org) {
|
|
|
90
108
|
vatRegistered: org.buyer.vatRegistered,
|
|
91
109
|
bespokeAppRepo: org.bespokeAppRepo,
|
|
92
110
|
adoptedAppForm: org.adoptedApp?.form ?? null,
|
|
111
|
+
ownerEmail: org.owner?.email ?? null,
|
|
112
|
+
ownerGivenName: org.owner?.givenName ?? null,
|
|
113
|
+
ownerFamilyName: org.owner?.familyName ?? null,
|
|
114
|
+
isTest: org.test,
|
|
93
115
|
};
|
|
94
116
|
}
|
|
95
117
|
/**
|
|
@@ -117,6 +139,17 @@ function rowToBuyer(row) {
|
|
|
117
139
|
vatRegistered: row.vatRegistered,
|
|
118
140
|
};
|
|
119
141
|
}
|
|
142
|
+
/**
|
|
143
|
+
* Reassembles `DeclaredOrg.owner` from the row's three separate nullable
|
|
144
|
+
* columns -- present only when all three are, same "all parts or none"
|
|
145
|
+
* pattern {@link rowToBuyer} uses for the buyer's address.
|
|
146
|
+
*/
|
|
147
|
+
function rowToOwner(row) {
|
|
148
|
+
const { ownerEmail, ownerGivenName, ownerFamilyName } = row;
|
|
149
|
+
if (ownerEmail === null || ownerGivenName === null || ownerFamilyName === null)
|
|
150
|
+
return null;
|
|
151
|
+
return { email: ownerEmail, givenName: ownerGivenName, familyName: ownerFamilyName };
|
|
152
|
+
}
|
|
120
153
|
/**
|
|
121
154
|
* `DeclaredOrgStore` (`../store.ts`) on Postgres, for #18: create, read and
|
|
122
155
|
* edit a {@link DeclaredOrg}, and read and write the {@link AppliedState}
|
|
@@ -129,7 +162,9 @@ function rowToBuyer(row) {
|
|
|
129
162
|
* on both `create` and `update`. #27 adds a third, narrower rule: storage and
|
|
130
163
|
* AI spend limits are bigint-safe decimal strings of non-negative integers
|
|
131
164
|
* (or `null` for uncapped), checked the same way on both `create` and the
|
|
132
|
-
* merged result of `update`.
|
|
165
|
+
* merged result of `update`. #67 adds a fourth (`org.ts`'s
|
|
166
|
+
* `validateBuyerIdentity`) and #71 a fifth (`org.ts`'s `validateHosting`),
|
|
167
|
+
* checked the same way. The broader "combinations foundry knows are
|
|
133
168
|
* impossible are rejected with a reason" line needs a ruleset (domain
|
|
134
169
|
* requirements per service, `adoptedApp.form`, mail-level transitions…)
|
|
135
170
|
* that later tickets, not #18, #27 or #28, decide; `update` still merges and
|
|
@@ -169,11 +204,27 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
169
204
|
if (reason)
|
|
170
205
|
throw new DeclaredOrgError(`buyer identity invalid: ${reason} (#67)`);
|
|
171
206
|
}
|
|
207
|
+
// #71: same place `requireValidBuyer` checks the buyer identity (#67), on
|
|
208
|
+
// both `create` and the merged result of `update`.
|
|
209
|
+
function requireValidHosting(org) {
|
|
210
|
+
const reason = validateHosting(org.hosting);
|
|
211
|
+
if (reason)
|
|
212
|
+
throw new DeclaredOrgError(`hosting invalid: ${reason} (#71)`);
|
|
213
|
+
}
|
|
214
|
+
// Same place `requireValidHosting` checks hosting, on both `create` and
|
|
215
|
+
// the merged result of `update`.
|
|
216
|
+
function requireValidOwner(org) {
|
|
217
|
+
const reason = validateOwner(org.owner);
|
|
218
|
+
if (reason)
|
|
219
|
+
throw new DeclaredOrgError(`owner invalid: ${reason}`);
|
|
220
|
+
}
|
|
172
221
|
return {
|
|
173
222
|
async create(org) {
|
|
174
223
|
requireValidSlug(org.slug);
|
|
175
224
|
requireValidLimits(org);
|
|
176
225
|
requireValidBuyer(org);
|
|
226
|
+
requireValidHosting(org);
|
|
227
|
+
requireValidOwner(org);
|
|
177
228
|
try {
|
|
178
229
|
await db.insert(declaredOrgs).values(orgToRow(org));
|
|
179
230
|
}
|
|
@@ -212,6 +263,8 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
212
263
|
const next = { ...current, ...patch, id: current.id };
|
|
213
264
|
requireValidLimits(next);
|
|
214
265
|
requireValidBuyer(next);
|
|
266
|
+
requireValidHosting(next);
|
|
267
|
+
requireValidOwner(next);
|
|
215
268
|
try {
|
|
216
269
|
await db
|
|
217
270
|
.update(declaredOrgs)
|
|
@@ -263,5 +316,15 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
263
316
|
.values(values)
|
|
264
317
|
.onConflictDoUpdate({ target: foundryAppliedState.orgId, set: values });
|
|
265
318
|
},
|
|
319
|
+
async delete(id) {
|
|
320
|
+
// Applied state first: `0002_applied_state.sql`'s
|
|
321
|
+
// `foundry_applied_state_org_fkey` references the declared org with no
|
|
322
|
+
// cascade, so deleting the org first would fail while its applied
|
|
323
|
+
// state row still points at it. Both deletes are no-ops (idempotent)
|
|
324
|
+
// when the row is already gone, matching this method's own doc
|
|
325
|
+
// comment on `../store.ts`.
|
|
326
|
+
await db.delete(foundryAppliedState).where(eq(foundryAppliedState.orgId, id));
|
|
327
|
+
await db.delete(declaredOrgs).where(eq(declaredOrgs.id, id));
|
|
328
|
+
},
|
|
266
329
|
};
|
|
267
330
|
}
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IssuerApp } from '../org.js';
|
|
1
2
|
import type { Checkpoint } from '../state.js';
|
|
2
3
|
/**
|
|
3
4
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
@@ -196,6 +197,127 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
196
197
|
identity: undefined;
|
|
197
198
|
generated: undefined;
|
|
198
199
|
}, {}, {}>;
|
|
200
|
+
hostingEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
201
|
+
name: "hosting_enabled";
|
|
202
|
+
tableName: "foundry_declared_orgs";
|
|
203
|
+
dataType: "boolean";
|
|
204
|
+
columnType: "PgBoolean";
|
|
205
|
+
data: boolean;
|
|
206
|
+
driverParam: boolean;
|
|
207
|
+
notNull: true;
|
|
208
|
+
hasDefault: true;
|
|
209
|
+
isPrimaryKey: false;
|
|
210
|
+
isAutoincrement: false;
|
|
211
|
+
hasRuntimeDefault: false;
|
|
212
|
+
enumValues: undefined;
|
|
213
|
+
baseColumn: never;
|
|
214
|
+
identity: undefined;
|
|
215
|
+
generated: undefined;
|
|
216
|
+
}, {}, {}>;
|
|
217
|
+
hostingUsesDomain: import("drizzle-orm/pg-core").PgColumn<{
|
|
218
|
+
name: "hosting_uses_domain";
|
|
219
|
+
tableName: "foundry_declared_orgs";
|
|
220
|
+
dataType: "boolean";
|
|
221
|
+
columnType: "PgBoolean";
|
|
222
|
+
data: boolean;
|
|
223
|
+
driverParam: boolean;
|
|
224
|
+
notNull: true;
|
|
225
|
+
hasDefault: true;
|
|
226
|
+
isPrimaryKey: false;
|
|
227
|
+
isAutoincrement: false;
|
|
228
|
+
hasRuntimeDefault: false;
|
|
229
|
+
enumValues: undefined;
|
|
230
|
+
baseColumn: never;
|
|
231
|
+
identity: undefined;
|
|
232
|
+
generated: undefined;
|
|
233
|
+
}, {}, {}>;
|
|
234
|
+
hostingDatabase: import("drizzle-orm/pg-core").PgColumn<{
|
|
235
|
+
name: "hosting_database";
|
|
236
|
+
tableName: "foundry_declared_orgs";
|
|
237
|
+
dataType: "boolean";
|
|
238
|
+
columnType: "PgBoolean";
|
|
239
|
+
data: boolean;
|
|
240
|
+
driverParam: boolean;
|
|
241
|
+
notNull: true;
|
|
242
|
+
hasDefault: true;
|
|
243
|
+
isPrimaryKey: false;
|
|
244
|
+
isAutoincrement: false;
|
|
245
|
+
hasRuntimeDefault: false;
|
|
246
|
+
enumValues: undefined;
|
|
247
|
+
baseColumn: never;
|
|
248
|
+
identity: undefined;
|
|
249
|
+
generated: undefined;
|
|
250
|
+
}, {}, {}>;
|
|
251
|
+
hostingTunnel: import("drizzle-orm/pg-core").PgColumn<{
|
|
252
|
+
name: "hosting_tunnel";
|
|
253
|
+
tableName: "foundry_declared_orgs";
|
|
254
|
+
dataType: "boolean";
|
|
255
|
+
columnType: "PgBoolean";
|
|
256
|
+
data: boolean;
|
|
257
|
+
driverParam: boolean;
|
|
258
|
+
notNull: true;
|
|
259
|
+
hasDefault: true;
|
|
260
|
+
isPrimaryKey: false;
|
|
261
|
+
isAutoincrement: false;
|
|
262
|
+
hasRuntimeDefault: false;
|
|
263
|
+
enumValues: undefined;
|
|
264
|
+
baseColumn: never;
|
|
265
|
+
identity: undefined;
|
|
266
|
+
generated: undefined;
|
|
267
|
+
}, {}, {}>;
|
|
268
|
+
hostingGitBranch: import("drizzle-orm/pg-core").PgColumn<{
|
|
269
|
+
name: "hosting_git_branch";
|
|
270
|
+
tableName: "foundry_declared_orgs";
|
|
271
|
+
dataType: "string";
|
|
272
|
+
columnType: "PgText";
|
|
273
|
+
data: string;
|
|
274
|
+
driverParam: string;
|
|
275
|
+
notNull: true;
|
|
276
|
+
hasDefault: true;
|
|
277
|
+
isPrimaryKey: false;
|
|
278
|
+
isAutoincrement: false;
|
|
279
|
+
hasRuntimeDefault: false;
|
|
280
|
+
enumValues: [string, ...string[]];
|
|
281
|
+
baseColumn: never;
|
|
282
|
+
identity: undefined;
|
|
283
|
+
generated: undefined;
|
|
284
|
+
}, {}, {}>;
|
|
285
|
+
hostingBuildPack: import("drizzle-orm/pg-core").PgColumn<{
|
|
286
|
+
name: "hosting_build_pack";
|
|
287
|
+
tableName: "foundry_declared_orgs";
|
|
288
|
+
dataType: "string";
|
|
289
|
+
columnType: "PgText";
|
|
290
|
+
data: string;
|
|
291
|
+
driverParam: string;
|
|
292
|
+
notNull: true;
|
|
293
|
+
hasDefault: true;
|
|
294
|
+
isPrimaryKey: false;
|
|
295
|
+
isAutoincrement: false;
|
|
296
|
+
hasRuntimeDefault: false;
|
|
297
|
+
enumValues: [string, ...string[]];
|
|
298
|
+
baseColumn: never;
|
|
299
|
+
identity: undefined;
|
|
300
|
+
generated: undefined;
|
|
301
|
+
}, {}, {}>;
|
|
302
|
+
hostingIssuerApps: import("drizzle-orm/pg-core").PgColumn<{
|
|
303
|
+
name: "hosting_issuer_apps";
|
|
304
|
+
tableName: "foundry_declared_orgs";
|
|
305
|
+
dataType: "json";
|
|
306
|
+
columnType: "PgJsonb";
|
|
307
|
+
data: IssuerApp[];
|
|
308
|
+
driverParam: unknown;
|
|
309
|
+
notNull: true;
|
|
310
|
+
hasDefault: true;
|
|
311
|
+
isPrimaryKey: false;
|
|
312
|
+
isAutoincrement: false;
|
|
313
|
+
hasRuntimeDefault: false;
|
|
314
|
+
enumValues: undefined;
|
|
315
|
+
baseColumn: never;
|
|
316
|
+
identity: undefined;
|
|
317
|
+
generated: undefined;
|
|
318
|
+
}, {}, {
|
|
319
|
+
$type: IssuerApp[];
|
|
320
|
+
}>;
|
|
199
321
|
ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
|
|
200
322
|
name: "sso_enabled";
|
|
201
323
|
tableName: "foundry_declared_orgs";
|
|
@@ -451,6 +573,74 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
451
573
|
identity: undefined;
|
|
452
574
|
generated: undefined;
|
|
453
575
|
}, {}, {}>;
|
|
576
|
+
ownerEmail: import("drizzle-orm/pg-core").PgColumn<{
|
|
577
|
+
name: "owner_email";
|
|
578
|
+
tableName: "foundry_declared_orgs";
|
|
579
|
+
dataType: "string";
|
|
580
|
+
columnType: "PgText";
|
|
581
|
+
data: string;
|
|
582
|
+
driverParam: string;
|
|
583
|
+
notNull: false;
|
|
584
|
+
hasDefault: false;
|
|
585
|
+
isPrimaryKey: false;
|
|
586
|
+
isAutoincrement: false;
|
|
587
|
+
hasRuntimeDefault: false;
|
|
588
|
+
enumValues: [string, ...string[]];
|
|
589
|
+
baseColumn: never;
|
|
590
|
+
identity: undefined;
|
|
591
|
+
generated: undefined;
|
|
592
|
+
}, {}, {}>;
|
|
593
|
+
ownerGivenName: import("drizzle-orm/pg-core").PgColumn<{
|
|
594
|
+
name: "owner_given_name";
|
|
595
|
+
tableName: "foundry_declared_orgs";
|
|
596
|
+
dataType: "string";
|
|
597
|
+
columnType: "PgText";
|
|
598
|
+
data: string;
|
|
599
|
+
driverParam: string;
|
|
600
|
+
notNull: false;
|
|
601
|
+
hasDefault: false;
|
|
602
|
+
isPrimaryKey: false;
|
|
603
|
+
isAutoincrement: false;
|
|
604
|
+
hasRuntimeDefault: false;
|
|
605
|
+
enumValues: [string, ...string[]];
|
|
606
|
+
baseColumn: never;
|
|
607
|
+
identity: undefined;
|
|
608
|
+
generated: undefined;
|
|
609
|
+
}, {}, {}>;
|
|
610
|
+
ownerFamilyName: import("drizzle-orm/pg-core").PgColumn<{
|
|
611
|
+
name: "owner_family_name";
|
|
612
|
+
tableName: "foundry_declared_orgs";
|
|
613
|
+
dataType: "string";
|
|
614
|
+
columnType: "PgText";
|
|
615
|
+
data: string;
|
|
616
|
+
driverParam: string;
|
|
617
|
+
notNull: false;
|
|
618
|
+
hasDefault: false;
|
|
619
|
+
isPrimaryKey: false;
|
|
620
|
+
isAutoincrement: false;
|
|
621
|
+
hasRuntimeDefault: false;
|
|
622
|
+
enumValues: [string, ...string[]];
|
|
623
|
+
baseColumn: never;
|
|
624
|
+
identity: undefined;
|
|
625
|
+
generated: undefined;
|
|
626
|
+
}, {}, {}>;
|
|
627
|
+
isTest: import("drizzle-orm/pg-core").PgColumn<{
|
|
628
|
+
name: "is_test";
|
|
629
|
+
tableName: "foundry_declared_orgs";
|
|
630
|
+
dataType: "boolean";
|
|
631
|
+
columnType: "PgBoolean";
|
|
632
|
+
data: boolean;
|
|
633
|
+
driverParam: boolean;
|
|
634
|
+
notNull: true;
|
|
635
|
+
hasDefault: true;
|
|
636
|
+
isPrimaryKey: false;
|
|
637
|
+
isAutoincrement: false;
|
|
638
|
+
hasRuntimeDefault: false;
|
|
639
|
+
enumValues: undefined;
|
|
640
|
+
baseColumn: never;
|
|
641
|
+
identity: undefined;
|
|
642
|
+
generated: undefined;
|
|
643
|
+
}, {}, {}>;
|
|
454
644
|
createdAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
455
645
|
name: "created_at";
|
|
456
646
|
tableName: "foundry_declared_orgs";
|
package/dist/store/schema.js
CHANGED
|
@@ -18,6 +18,14 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
18
18
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|
|
19
19
|
siteEnabled: boolean('site_enabled').notNull().default(false),
|
|
20
20
|
siteUsesDomain: boolean('site_uses_domain').notNull().default(false),
|
|
21
|
+
// HostingConfig (org.ts, #71). A brand new org has no hosted app.
|
|
22
|
+
hostingEnabled: boolean('hosting_enabled').notNull().default(false),
|
|
23
|
+
hostingUsesDomain: boolean('hosting_uses_domain').notNull().default(false),
|
|
24
|
+
hostingDatabase: boolean('hosting_database').notNull().default(false),
|
|
25
|
+
hostingTunnel: boolean('hosting_tunnel').notNull().default(false),
|
|
26
|
+
hostingGitBranch: text('hosting_git_branch').notNull().default('main'),
|
|
27
|
+
hostingBuildPack: text('hosting_build_pack').notNull().default('nixpacks'),
|
|
28
|
+
hostingIssuerApps: jsonb('hosting_issuer_apps').notNull().default([]).$type(),
|
|
21
29
|
ssoEnabled: boolean('sso_enabled').notNull().default(false),
|
|
22
30
|
guestsEnabled: boolean('guests_enabled').notNull().default(false),
|
|
23
31
|
soloEnabled: boolean('solo_enabled').notNull().default(false),
|
|
@@ -40,6 +48,16 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
40
48
|
vatRegistered: boolean('vat_registered'),
|
|
41
49
|
bespokeAppRepo: text('bespoke_app_repo'),
|
|
42
50
|
adoptedAppForm: text('adopted_app_form'),
|
|
51
|
+
// OwnerIdentity (org.ts): the org's owner, invited by ZITADEL email. All
|
|
52
|
+
// three nullable -- a brand new org has none of them, reassembled as one
|
|
53
|
+
// object only once all three are non-null (same shape as BuyerIdentity's
|
|
54
|
+
// own contact fields above).
|
|
55
|
+
ownerEmail: text('owner_email'),
|
|
56
|
+
ownerGivenName: text('owner_given_name'),
|
|
57
|
+
ownerFamilyName: text('owner_family_name'),
|
|
58
|
+
// `test: boolean` (org.ts): only a test org can be destroyed. A brand new
|
|
59
|
+
// org defaults to real (false).
|
|
60
|
+
isTest: boolean('is_test').notNull().default(false),
|
|
43
61
|
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
|
44
62
|
updatedAt: timestamp('updated_at', { withTimezone: true }).notNull().defaultNow(),
|
|
45
63
|
});
|
package/dist/store.d.ts
CHANGED
|
@@ -20,4 +20,14 @@ export interface DeclaredOrgStore {
|
|
|
20
20
|
update(id: OrgId, patch: Partial<DeclaredOrg>): Promise<DeclaredOrg>;
|
|
21
21
|
getAppliedState(id: OrgId): Promise<AppliedState | null>;
|
|
22
22
|
putAppliedState(state: AppliedState): Promise<void>;
|
|
23
|
+
/**
|
|
24
|
+
* Removes the declared org and its applied state together. Called only by
|
|
25
|
+
* `FoundryPlanApply.destroy` (`plan/plan-apply.ts`), once every resource it
|
|
26
|
+
* recorded for this org has already been deleted from Coolify, Cloudflare
|
|
27
|
+
* and ZITADEL -- this is the last step, not the first. Idempotent: deleting
|
|
28
|
+
* an id that does not exist (or no longer has applied state) is a no-op,
|
|
29
|
+
* not an error, so a retried destroy that reaches this step twice never
|
|
30
|
+
* throws.
|
|
31
|
+
*/
|
|
32
|
+
delete(id: OrgId): Promise<void>;
|
|
23
33
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wtfalch/foundry",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.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",
|
|
@@ -9,9 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"license": "MIT",
|
|
11
11
|
"type": "module",
|
|
12
|
-
"files": [
|
|
13
|
-
"dist"
|
|
14
|
-
],
|
|
12
|
+
"files": ["dist"],
|
|
15
13
|
"exports": {
|
|
16
14
|
".": {
|
|
17
15
|
"types": "./dist/index.d.ts",
|
|
@@ -27,11 +25,16 @@
|
|
|
27
25
|
"engines": {
|
|
28
26
|
"node": ">=22.0.0"
|
|
29
27
|
},
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/migrations && find src/migrations -maxdepth 1 -name '*.sql' -exec cp {} dist/migrations/ \\;",
|
|
30
|
+
"typecheck": "tsc --noEmit",
|
|
31
|
+
"test": "vitest run"
|
|
32
|
+
},
|
|
30
33
|
"peerDependencies": {
|
|
31
34
|
"drizzle-orm": ">=0.45.2"
|
|
32
35
|
},
|
|
33
36
|
"dependencies": {
|
|
34
|
-
"@wtfalch/providers": "0.
|
|
37
|
+
"@wtfalch/providers": "0.5.0"
|
|
35
38
|
},
|
|
36
39
|
"devDependencies": {
|
|
37
40
|
"@electric-sql/pglite": "^0.5.8",
|
|
@@ -40,10 +43,5 @@
|
|
|
40
43
|
"postgres": "3.4.9",
|
|
41
44
|
"typescript": "^5.9.0",
|
|
42
45
|
"vitest": "^4.1.6"
|
|
43
|
-
},
|
|
44
|
-
"scripts": {
|
|
45
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && mkdir -p dist/migrations && find src/migrations -maxdepth 1 -name '*.sql' -exec cp {} dist/migrations/ \\;",
|
|
46
|
-
"typecheck": "tsc --noEmit",
|
|
47
|
-
"test": "vitest run"
|
|
48
46
|
}
|
|
49
|
-
}
|
|
47
|
+
}
|