@wtfalch/foundry 0.16.16 → 0.17.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 +22 -1
- package/dist/bootstrap/bootstrap.d.ts +6 -1
- package/dist/bootstrap/bootstrap.js +26 -3
- package/dist/bootstrap/declare.d.ts +11 -1
- package/dist/bootstrap/declare.js +12 -2
- package/dist/index.d.ts +9 -4
- package/dist/index.js +7 -0
- package/dist/migrations/0038_calls_version_pin.sql +8 -0
- package/dist/migrations/0039_destroy_record.sql +7 -0
- package/dist/migrations/0040_company_included_uuid.sql +72 -0
- package/dist/migrations/0041_company_included_uuid_keys.sql +132 -0
- package/dist/migrations/0042_mutation_leases.sql +12 -0
- package/dist/org.d.ts +54 -0
- package/dist/org.js +43 -1
- package/dist/plan/adopt.d.ts +76 -0
- package/dist/plan/adopt.js +184 -0
- package/dist/plan/agora.d.ts +21 -0
- package/dist/plan/agora.js +37 -0
- package/dist/plan/broker-entries.d.ts +9 -1
- package/dist/plan/broker-entries.js +13 -5
- package/dist/plan/chat-matrix.d.ts +1 -1
- package/dist/plan/chat-matrix.js +11 -3
- package/dist/plan/core-files.d.ts +25 -0
- package/dist/plan/core-files.js +25 -0
- package/dist/plan/diff.d.ts +11 -3
- package/dist/plan/diff.js +27 -62
- package/dist/plan/entry-ai-key.d.ts +3 -3
- package/dist/plan/entry-files-key.d.ts +3 -3
- package/dist/plan/executor.d.ts +57 -23
- package/dist/plan/executor.js +134 -50
- package/dist/plan/index.d.ts +4 -0
- package/dist/plan/index.js +2 -0
- package/dist/plan/live.d.ts +5 -0
- package/dist/plan/login-host.d.ts +1 -1
- package/dist/plan/login-host.js +6 -5
- package/dist/plan/plan-apply.d.ts +139 -6
- package/dist/plan/plan-apply.js +546 -76
- package/dist/plan/platform-diff.d.ts +14 -0
- package/dist/plan/platform-diff.js +16 -1
- package/dist/plan/platform-org.d.ts +16 -0
- package/dist/plan/platform-org.js +17 -1
- package/dist/plan/platform-providers.d.ts +1 -1
- package/dist/plan/platform-providers.js +73 -11
- package/dist/plan/providers-readers.d.ts +2 -0
- package/dist/plan/providers-readers.js +4 -0
- package/dist/plan/rollout.d.ts +52 -0
- package/dist/plan/rollout.js +37 -0
- package/dist/plan/step-system.d.ts +22 -0
- package/dist/plan/step-system.js +37 -0
- package/dist/plan.d.ts +3 -0
- package/dist/state.d.ts +13 -0
- package/dist/store/audit.js +15 -1
- package/dist/store/postgres.d.ts +1 -1
- package/dist/store/postgres.js +127 -14
- package/dist/store/schema.d.ts +134 -5
- package/dist/store/schema.js +12 -1
- package/dist/store.d.ts +47 -4
- package/dist/store.js +9 -1
- package/package.json +1 -1
package/dist/store/postgres.js
CHANGED
|
@@ -1,11 +1,31 @@
|
|
|
1
1
|
import { and, asc, desc, eq, sql } from 'drizzle-orm';
|
|
2
|
-
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateDomain, validateHosting, validateMail, validateOwners, validateSite, } from '../org.js';
|
|
2
|
+
import { APP_ENTRY_KEY, DEFAULT_MAIL_APPS, DEFAULT_NO_REPLY_LOCAL_PART, validateBuyerIdentity, validateCalls, validateDomain, validateHosting, validateMail, validateOwners, validateSite, } from '../org.js';
|
|
3
3
|
import { lockedHostnames } from '../plan/locked-hostnames.js';
|
|
4
4
|
import { validatePlatform } from '../platform.js';
|
|
5
5
|
import { RequestedChangeError, classifyChange } from '../requested-changes.js';
|
|
6
6
|
import { validateSlug } from '../slug.js';
|
|
7
|
-
import {
|
|
7
|
+
import { StaleLeaseError, } from '../store.js';
|
|
8
|
+
import { declaredOrgs, foundryAppliedState, foundryAppliedVersions, foundryDeclaredPlatform, foundryMutationLeases, foundryRequestedChanges, } from './schema.js';
|
|
8
9
|
const UNIQUE_VIOLATION = '23505';
|
|
10
|
+
/**
|
|
11
|
+
* #441: runs `write`, and with a `fence` only if its token is still the
|
|
12
|
+
* scope's current one. The lease row is locked for the write, so a takeover
|
|
13
|
+
* cannot slip between the check and the write.
|
|
14
|
+
*/
|
|
15
|
+
async function writeFenced(db, fence, write) {
|
|
16
|
+
if (!fence)
|
|
17
|
+
return write(db);
|
|
18
|
+
await db.transaction(async (tx) => {
|
|
19
|
+
const [row] = await tx
|
|
20
|
+
.select({ token: foundryMutationLeases.token })
|
|
21
|
+
.from(foundryMutationLeases)
|
|
22
|
+
.where(eq(foundryMutationLeases.scope, fence.scope))
|
|
23
|
+
.for('update');
|
|
24
|
+
if (!row || row.token !== fence.token)
|
|
25
|
+
throw new StaleLeaseError(fence.scope);
|
|
26
|
+
await write(tx);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
9
29
|
/** `migrations/0004_slug_unique.sql` / `migrations/0015_domain_unique.sql`'s index names, read back off a caught unique-violation to tell which one fired. */
|
|
10
30
|
const SLUG_UNIQUE_INDEX = 'foundry_declared_orgs_slug_idx';
|
|
11
31
|
const DOMAIN_UNIQUE_INDEX = 'foundry_declared_orgs_domain_idx';
|
|
@@ -75,7 +95,21 @@ function rowToOrg(row) {
|
|
|
75
95
|
roleMailboxes: row.roleMailboxes,
|
|
76
96
|
},
|
|
77
97
|
chat: { enabled: row.chatEnabled, usesDomain: row.chatUsesDomain },
|
|
78
|
-
calls: {
|
|
98
|
+
calls: {
|
|
99
|
+
enabled: row.callsEnabled,
|
|
100
|
+
usesDomain: row.callsUsesDomain,
|
|
101
|
+
// Omitted, not `null`, while neither column is set: every existing
|
|
102
|
+
// row (and every test fixture that builds a `CallsConfig` literal
|
|
103
|
+
// before this field existed) round-trips unchanged.
|
|
104
|
+
...(row.callsVersionPinSynapse || row.callsVersionPinMas
|
|
105
|
+
? {
|
|
106
|
+
versionPin: {
|
|
107
|
+
...(row.callsVersionPinSynapse ? { synapse: row.callsVersionPinSynapse } : {}),
|
|
108
|
+
...(row.callsVersionPinMas ? { mas: row.callsVersionPinMas } : {}),
|
|
109
|
+
},
|
|
110
|
+
}
|
|
111
|
+
: {}),
|
|
112
|
+
},
|
|
79
113
|
issuer: { usesDomain: row.issuerUsesDomain },
|
|
80
114
|
site: { enabled: row.siteEnabled, usesDomain: row.siteUsesDomain },
|
|
81
115
|
hosting: {
|
|
@@ -119,6 +153,8 @@ function orgToRow(org) {
|
|
|
119
153
|
chatUsesDomain: org.chat.usesDomain,
|
|
120
154
|
callsEnabled: org.calls.enabled,
|
|
121
155
|
callsUsesDomain: org.calls.usesDomain,
|
|
156
|
+
callsVersionPinSynapse: org.calls.versionPin?.synapse ?? null,
|
|
157
|
+
callsVersionPinMas: org.calls.versionPin?.mas ?? null,
|
|
122
158
|
issuerUsesDomain: org.issuer.usesDomain,
|
|
123
159
|
siteEnabled: org.site.enabled,
|
|
124
160
|
siteUsesDomain: org.site.usesDomain,
|
|
@@ -268,11 +304,25 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
268
304
|
// `mail.ownerMailboxAddress` just because mail is on -- `validateMail`'s
|
|
269
305
|
// own doc comment says why. Also covers #34's `mailApps` check, folded
|
|
270
306
|
// into the same `validateMail`.
|
|
271
|
-
|
|
272
|
-
|
|
307
|
+
//
|
|
308
|
+
// #422: `allowExistingTier2` grandfathers an org already stored as a
|
|
309
|
+
// domain plus `mail.mode: 'received'` -- `update` passes it true only
|
|
310
|
+
// when that exact combination was already the row's state before this
|
|
311
|
+
// patch, so an unrelated edit to an already-tier-2 org still goes
|
|
312
|
+
// through. `create` never passes it: a brand new org cannot already be
|
|
313
|
+
// grandfathered.
|
|
314
|
+
function requireValidMail(org, opts) {
|
|
315
|
+
const reason = validateMail(org, opts);
|
|
273
316
|
if (reason)
|
|
274
317
|
throw new DeclaredOrgError(`mail invalid: ${reason} (#162)`);
|
|
275
318
|
}
|
|
319
|
+
// #259: same place `requireValidMail` checks mail, on both `create` and
|
|
320
|
+
// the merged result of `update`.
|
|
321
|
+
function requireValidCalls(org) {
|
|
322
|
+
const reason = validateCalls(org);
|
|
323
|
+
if (reason)
|
|
324
|
+
throw new DeclaredOrgError(`calls invalid: ${reason} (#259)`);
|
|
325
|
+
}
|
|
276
326
|
// `as`: `update` is overloaded in the interface (#351), which one object-literal method cannot express.
|
|
277
327
|
return {
|
|
278
328
|
async create(org) {
|
|
@@ -284,6 +334,7 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
284
334
|
requireValidSite(org);
|
|
285
335
|
requireValidOwner(org);
|
|
286
336
|
requireValidMail(org);
|
|
337
|
+
requireValidCalls(org);
|
|
287
338
|
try {
|
|
288
339
|
await db.insert(declaredOrgs).values(orgToRow(org));
|
|
289
340
|
}
|
|
@@ -339,6 +390,23 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
339
390
|
}
|
|
340
391
|
return null;
|
|
341
392
|
},
|
|
393
|
+
async findOtherOrgRecordingCoolifyId(id, exceptOrgId) {
|
|
394
|
+
const rows = await db
|
|
395
|
+
.select({ orgId: foundryAppliedState.orgId, serviceIds: foundryAppliedState.serviceIds })
|
|
396
|
+
.from(foundryAppliedState);
|
|
397
|
+
for (const row of rows) {
|
|
398
|
+
if (row.orgId === exceptOrgId)
|
|
399
|
+
continue;
|
|
400
|
+
for (const [key, value] of Object.entries(row.serviceIds)) {
|
|
401
|
+
const coolify = key === 'coolify_project' ||
|
|
402
|
+
key.startsWith('coolify:app:') ||
|
|
403
|
+
key.startsWith('coolify:database:');
|
|
404
|
+
if (coolify && value === id)
|
|
405
|
+
return row.orgId;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return null;
|
|
409
|
+
},
|
|
342
410
|
async update(id, patch, opts) {
|
|
343
411
|
const current = rowToOrg(await requireRow(id));
|
|
344
412
|
// #351: already behind, so refuse without validating a write that will not happen.
|
|
@@ -366,7 +434,11 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
366
434
|
requireValidHosting(next);
|
|
367
435
|
requireValidSite(next);
|
|
368
436
|
requireValidOwner(next);
|
|
369
|
-
|
|
437
|
+
// #422: grandfather an org already stored as domain + `received` --
|
|
438
|
+
// refuse only when this patch is what newly puts it in that state.
|
|
439
|
+
const wasTier2 = current.domain !== null && current.mail.mode === 'received';
|
|
440
|
+
requireValidMail(next, { allowExistingTier2: wasTier2 });
|
|
441
|
+
requireValidCalls(next);
|
|
370
442
|
try {
|
|
371
443
|
// #351: with `expectedVersion` the version check is part of this one
|
|
372
444
|
// statement, so an edit that lands after the read above makes it match
|
|
@@ -420,10 +492,44 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
420
492
|
declaredOrgVersion: row.lastAppliedDeclaredOrgVersion,
|
|
421
493
|
}
|
|
422
494
|
: null,
|
|
495
|
+
...(row.destroy ? { destroy: row.destroy } : {}),
|
|
423
496
|
};
|
|
424
497
|
return state;
|
|
425
498
|
},
|
|
426
|
-
async
|
|
499
|
+
async acquireMutationLease(scope, holder, ttlMs) {
|
|
500
|
+
const expiresAt = sql `now() + ${ttlMs} * interval '1 millisecond'`;
|
|
501
|
+
const [row] = await db
|
|
502
|
+
.insert(foundryMutationLeases)
|
|
503
|
+
.values({ scope, token: 1, holder, expiresAt })
|
|
504
|
+
.onConflictDoUpdate({
|
|
505
|
+
target: foundryMutationLeases.scope,
|
|
506
|
+
set: { token: sql `${foundryMutationLeases.token} + 1`, holder, expiresAt },
|
|
507
|
+
setWhere: sql `${foundryMutationLeases.expiresAt} <= now()`,
|
|
508
|
+
})
|
|
509
|
+
.returning({ token: foundryMutationLeases.token });
|
|
510
|
+
if (row)
|
|
511
|
+
return { scope, token: row.token };
|
|
512
|
+
const [held] = await db
|
|
513
|
+
.select({ expiresAt: foundryMutationLeases.expiresAt })
|
|
514
|
+
.from(foundryMutationLeases)
|
|
515
|
+
.where(eq(foundryMutationLeases.scope, scope));
|
|
516
|
+
return { heldUntil: held?.expiresAt ?? new Date() };
|
|
517
|
+
},
|
|
518
|
+
async renewMutationLease(lease, ttlMs) {
|
|
519
|
+
const rows = await db
|
|
520
|
+
.update(foundryMutationLeases)
|
|
521
|
+
.set({ expiresAt: sql `now() + ${ttlMs} * interval '1 millisecond'` })
|
|
522
|
+
.where(and(eq(foundryMutationLeases.scope, lease.scope), eq(foundryMutationLeases.token, lease.token)))
|
|
523
|
+
.returning({ token: foundryMutationLeases.token });
|
|
524
|
+
return rows.length > 0;
|
|
525
|
+
},
|
|
526
|
+
async releaseMutationLease(lease) {
|
|
527
|
+
await db
|
|
528
|
+
.update(foundryMutationLeases)
|
|
529
|
+
.set({ expiresAt: sql `now()` })
|
|
530
|
+
.where(and(eq(foundryMutationLeases.scope, lease.scope), eq(foundryMutationLeases.token, lease.token)));
|
|
531
|
+
},
|
|
532
|
+
async putAppliedState(state, fence) {
|
|
427
533
|
const values = {
|
|
428
534
|
orgId: state.orgId,
|
|
429
535
|
serviceIds: state.serviceIds,
|
|
@@ -431,21 +537,25 @@ export function createPostgresDeclaredOrgStore(db) {
|
|
|
431
537
|
failedFields: state.failedFields,
|
|
432
538
|
lastAppliedAt: state.lastApplied ? new Date(state.lastApplied.at) : null,
|
|
433
539
|
lastAppliedDeclaredOrgVersion: state.lastApplied?.declaredOrgVersion ?? null,
|
|
540
|
+
destroy: state.destroy ?? null,
|
|
434
541
|
};
|
|
435
|
-
await db
|
|
542
|
+
await writeFenced(db, fence, (tx) => tx
|
|
436
543
|
.insert(foundryAppliedState)
|
|
437
544
|
.values(values)
|
|
438
|
-
.onConflictDoUpdate({ target: foundryAppliedState.orgId, set: values })
|
|
545
|
+
.onConflictDoUpdate({ target: foundryAppliedState.orgId, set: values })
|
|
546
|
+
.then(() => undefined));
|
|
439
547
|
},
|
|
440
|
-
async delete(id) {
|
|
548
|
+
async delete(id, fence) {
|
|
441
549
|
// Applied state first: `0002_applied_state.sql`'s
|
|
442
550
|
// `foundry_applied_state_org_fkey` references the declared org with no
|
|
443
551
|
// cascade, so deleting the org first would fail while its applied
|
|
444
552
|
// state row still points at it. Both deletes are no-ops (idempotent)
|
|
445
553
|
// when the row is already gone, matching this method's own doc
|
|
446
554
|
// comment on `../store.ts`.
|
|
447
|
-
await db
|
|
448
|
-
|
|
555
|
+
await writeFenced(db, fence, async (tx) => {
|
|
556
|
+
await tx.delete(foundryAppliedState).where(eq(foundryAppliedState.orgId, id));
|
|
557
|
+
await tx.delete(declaredOrgs).where(eq(declaredOrgs.id, id));
|
|
558
|
+
});
|
|
449
559
|
},
|
|
450
560
|
};
|
|
451
561
|
}
|
|
@@ -549,8 +659,11 @@ export function createPostgresPlatformStore(db) {
|
|
|
549
659
|
.limit(1);
|
|
550
660
|
return row?.serviceIds ?? {};
|
|
551
661
|
},
|
|
552
|
-
async putServiceIds(serviceIds) {
|
|
553
|
-
await db
|
|
662
|
+
async putServiceIds(serviceIds, fence) {
|
|
663
|
+
await writeFenced(db, fence, (tx) => tx
|
|
664
|
+
.update(foundryDeclaredPlatform)
|
|
665
|
+
.set({ serviceIds })
|
|
666
|
+
.then(() => undefined));
|
|
554
667
|
},
|
|
555
668
|
};
|
|
556
669
|
}
|
package/dist/store/schema.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { AppEnvSource, HostingEntry, IssuerApp, OwnerIdentity } from '../org.js';
|
|
2
2
|
import type { Plan } from '../plan.js';
|
|
3
|
-
import type { Checkpoint } from '../state.js';
|
|
3
|
+
import type { Checkpoint, DestroyRecord } from '../state.js';
|
|
4
4
|
/**
|
|
5
5
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
6
6
|
* the SQL carries the CHECKs drizzle-kit does not generate here (this
|
|
@@ -270,6 +270,40 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
|
|
|
270
270
|
identity: undefined;
|
|
271
271
|
generated: undefined;
|
|
272
272
|
}, {}, {}>;
|
|
273
|
+
callsVersionPinSynapse: import("drizzle-orm/pg-core").PgColumn<{
|
|
274
|
+
name: "calls_version_pin_synapse";
|
|
275
|
+
tableName: "foundry_declared_orgs";
|
|
276
|
+
dataType: "string";
|
|
277
|
+
columnType: "PgText";
|
|
278
|
+
data: string;
|
|
279
|
+
driverParam: string;
|
|
280
|
+
notNull: false;
|
|
281
|
+
hasDefault: false;
|
|
282
|
+
isPrimaryKey: false;
|
|
283
|
+
isAutoincrement: false;
|
|
284
|
+
hasRuntimeDefault: false;
|
|
285
|
+
enumValues: [string, ...string[]];
|
|
286
|
+
baseColumn: never;
|
|
287
|
+
identity: undefined;
|
|
288
|
+
generated: undefined;
|
|
289
|
+
}, {}, {}>;
|
|
290
|
+
callsVersionPinMas: import("drizzle-orm/pg-core").PgColumn<{
|
|
291
|
+
name: "calls_version_pin_mas";
|
|
292
|
+
tableName: "foundry_declared_orgs";
|
|
293
|
+
dataType: "string";
|
|
294
|
+
columnType: "PgText";
|
|
295
|
+
data: string;
|
|
296
|
+
driverParam: string;
|
|
297
|
+
notNull: false;
|
|
298
|
+
hasDefault: false;
|
|
299
|
+
isPrimaryKey: false;
|
|
300
|
+
isAutoincrement: false;
|
|
301
|
+
hasRuntimeDefault: false;
|
|
302
|
+
enumValues: [string, ...string[]];
|
|
303
|
+
baseColumn: never;
|
|
304
|
+
identity: undefined;
|
|
305
|
+
generated: undefined;
|
|
306
|
+
}, {}, {}>;
|
|
273
307
|
issuerUsesDomain: import("drizzle-orm/pg-core").PgColumn<{
|
|
274
308
|
name: "issuer_uses_domain";
|
|
275
309
|
tableName: "foundry_declared_orgs";
|
|
@@ -945,6 +979,101 @@ export declare const foundryAppliedState: import("drizzle-orm/pg-core").PgTableW
|
|
|
945
979
|
identity: undefined;
|
|
946
980
|
generated: undefined;
|
|
947
981
|
}, {}, {}>;
|
|
982
|
+
destroy: import("drizzle-orm/pg-core").PgColumn<{
|
|
983
|
+
name: "destroy";
|
|
984
|
+
tableName: "foundry_applied_state";
|
|
985
|
+
dataType: "json";
|
|
986
|
+
columnType: "PgJsonb";
|
|
987
|
+
data: DestroyRecord;
|
|
988
|
+
driverParam: unknown;
|
|
989
|
+
notNull: false;
|
|
990
|
+
hasDefault: false;
|
|
991
|
+
isPrimaryKey: false;
|
|
992
|
+
isAutoincrement: false;
|
|
993
|
+
hasRuntimeDefault: false;
|
|
994
|
+
enumValues: undefined;
|
|
995
|
+
baseColumn: never;
|
|
996
|
+
identity: undefined;
|
|
997
|
+
generated: undefined;
|
|
998
|
+
}, {}, {
|
|
999
|
+
$type: DestroyRecord;
|
|
1000
|
+
}>;
|
|
1001
|
+
};
|
|
1002
|
+
dialect: "pg";
|
|
1003
|
+
}>;
|
|
1004
|
+
/** Mirrors `migrations/0042_mutation_leases.sql`: one row per lock scope; `token` only ever rises (#441). */
|
|
1005
|
+
export declare const foundryMutationLeases: import("drizzle-orm/pg-core").PgTableWithColumns<{
|
|
1006
|
+
name: "foundry_mutation_leases";
|
|
1007
|
+
schema: undefined;
|
|
1008
|
+
columns: {
|
|
1009
|
+
scope: import("drizzle-orm/pg-core").PgColumn<{
|
|
1010
|
+
name: "scope";
|
|
1011
|
+
tableName: "foundry_mutation_leases";
|
|
1012
|
+
dataType: "string";
|
|
1013
|
+
columnType: "PgText";
|
|
1014
|
+
data: string;
|
|
1015
|
+
driverParam: string;
|
|
1016
|
+
notNull: true;
|
|
1017
|
+
hasDefault: false;
|
|
1018
|
+
isPrimaryKey: true;
|
|
1019
|
+
isAutoincrement: false;
|
|
1020
|
+
hasRuntimeDefault: false;
|
|
1021
|
+
enumValues: [string, ...string[]];
|
|
1022
|
+
baseColumn: never;
|
|
1023
|
+
identity: undefined;
|
|
1024
|
+
generated: undefined;
|
|
1025
|
+
}, {}, {}>;
|
|
1026
|
+
token: import("drizzle-orm/pg-core").PgColumn<{
|
|
1027
|
+
name: "token";
|
|
1028
|
+
tableName: "foundry_mutation_leases";
|
|
1029
|
+
dataType: "number";
|
|
1030
|
+
columnType: "PgBigInt53";
|
|
1031
|
+
data: number;
|
|
1032
|
+
driverParam: string | number;
|
|
1033
|
+
notNull: true;
|
|
1034
|
+
hasDefault: false;
|
|
1035
|
+
isPrimaryKey: false;
|
|
1036
|
+
isAutoincrement: false;
|
|
1037
|
+
hasRuntimeDefault: false;
|
|
1038
|
+
enumValues: undefined;
|
|
1039
|
+
baseColumn: never;
|
|
1040
|
+
identity: undefined;
|
|
1041
|
+
generated: undefined;
|
|
1042
|
+
}, {}, {}>;
|
|
1043
|
+
holder: import("drizzle-orm/pg-core").PgColumn<{
|
|
1044
|
+
name: "holder";
|
|
1045
|
+
tableName: "foundry_mutation_leases";
|
|
1046
|
+
dataType: "string";
|
|
1047
|
+
columnType: "PgText";
|
|
1048
|
+
data: string;
|
|
1049
|
+
driverParam: string;
|
|
1050
|
+
notNull: true;
|
|
1051
|
+
hasDefault: false;
|
|
1052
|
+
isPrimaryKey: false;
|
|
1053
|
+
isAutoincrement: false;
|
|
1054
|
+
hasRuntimeDefault: false;
|
|
1055
|
+
enumValues: [string, ...string[]];
|
|
1056
|
+
baseColumn: never;
|
|
1057
|
+
identity: undefined;
|
|
1058
|
+
generated: undefined;
|
|
1059
|
+
}, {}, {}>;
|
|
1060
|
+
expiresAt: import("drizzle-orm/pg-core").PgColumn<{
|
|
1061
|
+
name: "expires_at";
|
|
1062
|
+
tableName: "foundry_mutation_leases";
|
|
1063
|
+
dataType: "date";
|
|
1064
|
+
columnType: "PgTimestamp";
|
|
1065
|
+
data: Date;
|
|
1066
|
+
driverParam: string;
|
|
1067
|
+
notNull: true;
|
|
1068
|
+
hasDefault: false;
|
|
1069
|
+
isPrimaryKey: false;
|
|
1070
|
+
isAutoincrement: false;
|
|
1071
|
+
hasRuntimeDefault: false;
|
|
1072
|
+
enumValues: undefined;
|
|
1073
|
+
baseColumn: never;
|
|
1074
|
+
identity: undefined;
|
|
1075
|
+
generated: undefined;
|
|
1076
|
+
}, {}, {}>;
|
|
948
1077
|
};
|
|
949
1078
|
dialect: "pg";
|
|
950
1079
|
}>;
|
|
@@ -1107,7 +1236,7 @@ export declare const foundryAuditEvents: import("drizzle-orm/pg-core").PgTableWi
|
|
|
1107
1236
|
tableName: "foundry_audit_events";
|
|
1108
1237
|
dataType: "string";
|
|
1109
1238
|
columnType: "PgText";
|
|
1110
|
-
data: "plan" | "
|
|
1239
|
+
data: "plan" | "destroy" | "apply";
|
|
1111
1240
|
driverParam: string;
|
|
1112
1241
|
notNull: true;
|
|
1113
1242
|
hasDefault: false;
|
|
@@ -1119,7 +1248,7 @@ export declare const foundryAuditEvents: import("drizzle-orm/pg-core").PgTableWi
|
|
|
1119
1248
|
identity: undefined;
|
|
1120
1249
|
generated: undefined;
|
|
1121
1250
|
}, {}, {
|
|
1122
|
-
$type: "plan" | "
|
|
1251
|
+
$type: "plan" | "destroy" | "apply";
|
|
1123
1252
|
}>;
|
|
1124
1253
|
actorId: import("drizzle-orm/pg-core").PgColumn<{
|
|
1125
1254
|
name: "actor_id";
|
|
@@ -1177,7 +1306,7 @@ export declare const foundryAuditEvents: import("drizzle-orm/pg-core").PgTableWi
|
|
|
1177
1306
|
tableName: "foundry_audit_events";
|
|
1178
1307
|
dataType: "string";
|
|
1179
1308
|
columnType: "PgText";
|
|
1180
|
-
data: "applied" | "
|
|
1309
|
+
data: "applied" | "failed" | "refused" | "destroyed";
|
|
1181
1310
|
driverParam: string;
|
|
1182
1311
|
notNull: false;
|
|
1183
1312
|
hasDefault: false;
|
|
@@ -1189,7 +1318,7 @@ export declare const foundryAuditEvents: import("drizzle-orm/pg-core").PgTableWi
|
|
|
1189
1318
|
identity: undefined;
|
|
1190
1319
|
generated: undefined;
|
|
1191
1320
|
}, {}, {
|
|
1192
|
-
$type: "applied" | "
|
|
1321
|
+
$type: "applied" | "failed" | "refused" | "destroyed";
|
|
1193
1322
|
}>;
|
|
1194
1323
|
reason: import("drizzle-orm/pg-core").PgColumn<{
|
|
1195
1324
|
name: "reason";
|
package/dist/store/schema.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { bigserial, boolean, integer, jsonb, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
|
|
1
|
+
import { bigint, bigserial, boolean, integer, jsonb, pgTable, text, timestamp, } from 'drizzle-orm/pg-core';
|
|
2
2
|
/**
|
|
3
3
|
* Mirrors `migrations/0001_declared_org.sql`, which is the source of truth:
|
|
4
4
|
* the SQL carries the CHECKs drizzle-kit does not generate here (this
|
|
@@ -31,6 +31,9 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
|
|
|
31
31
|
chatUsesDomain: boolean('chat_uses_domain').notNull().default(false),
|
|
32
32
|
callsEnabled: boolean('calls_enabled').notNull().default(false),
|
|
33
33
|
callsUsesDomain: boolean('calls_uses_domain').notNull().default(false),
|
|
34
|
+
// #259: null holds no pin -- the stack follows versions.json, same as today.
|
|
35
|
+
callsVersionPinSynapse: text('calls_version_pin_synapse'),
|
|
36
|
+
callsVersionPinMas: text('calls_version_pin_mas'),
|
|
34
37
|
issuerUsesDomain: boolean('issuer_uses_domain').notNull().default(false),
|
|
35
38
|
// SiteConfig (org.ts, T10). A brand new org has no cms site.
|
|
36
39
|
siteEnabled: boolean('site_enabled').notNull().default(false),
|
|
@@ -101,6 +104,14 @@ export const foundryAppliedState = pgTable('foundry_applied_state', {
|
|
|
101
104
|
failedFields: jsonb('failed_fields').notNull().default([]).$type(),
|
|
102
105
|
lastAppliedAt: timestamp('last_applied_at', { withTimezone: true }),
|
|
103
106
|
lastAppliedDeclaredOrgVersion: text('last_applied_declared_org_version'),
|
|
107
|
+
destroy: jsonb('destroy').$type(),
|
|
108
|
+
});
|
|
109
|
+
/** Mirrors `migrations/0042_mutation_leases.sql`: one row per lock scope; `token` only ever rises (#441). */
|
|
110
|
+
export const foundryMutationLeases = pgTable('foundry_mutation_leases', {
|
|
111
|
+
scope: text('scope').primaryKey(),
|
|
112
|
+
token: bigint('token', { mode: 'number' }).notNull(),
|
|
113
|
+
holder: text('holder').notNull(),
|
|
114
|
+
expiresAt: timestamp('expires_at', { withTimezone: true }).notNull(),
|
|
104
115
|
});
|
|
105
116
|
/**
|
|
106
117
|
* Mirrors `migrations/0036_applied_versions.sql`, the source of truth: an
|
package/dist/store.d.ts
CHANGED
|
@@ -13,6 +13,24 @@ export interface StaleUpdate {
|
|
|
13
13
|
/** The org as it stands now. */
|
|
14
14
|
current: DeclaredOrg;
|
|
15
15
|
}
|
|
16
|
+
/**
|
|
17
|
+
* A held mutation lease (#441). `token` rises on every acquire of a scope, so
|
|
18
|
+
* a worker whose lease expired and was taken over holds a token the store no
|
|
19
|
+
* longer accepts: its fenced writes are refused.
|
|
20
|
+
*/
|
|
21
|
+
export interface MutationLease {
|
|
22
|
+
scope: string;
|
|
23
|
+
token: number;
|
|
24
|
+
}
|
|
25
|
+
/** What `acquireMutationLease` returns while another holder has the scope: when that lease runs out. */
|
|
26
|
+
export interface HeldLease {
|
|
27
|
+
heldUntil: Date;
|
|
28
|
+
}
|
|
29
|
+
/** A fenced write (`putAppliedState`, `putServiceIds`) came from a lease that is no longer the current one (#441). Nothing was written. */
|
|
30
|
+
export declare class StaleLeaseError extends Error {
|
|
31
|
+
readonly scope: string;
|
|
32
|
+
constructor(scope: string);
|
|
33
|
+
}
|
|
16
34
|
/**
|
|
17
35
|
* The persistence seam https://github.com/wtfalch/foundry/issues/18
|
|
18
36
|
* implements: create, read and edit a {@link DeclaredOrg}, and read and
|
|
@@ -44,6 +62,12 @@ export interface DeclaredOrgStore {
|
|
|
44
62
|
* record instead.
|
|
45
63
|
*/
|
|
46
64
|
findOrgIdForZone(zoneId: string): Promise<OrgId | null>;
|
|
65
|
+
/**
|
|
66
|
+
* Archon#271: an org other than `exceptOrgId` whose applied state records
|
|
67
|
+
* `id` as a Coolify app, database or project, or `null`. Adopt refuses a
|
|
68
|
+
* resource another org already holds.
|
|
69
|
+
*/
|
|
70
|
+
findOtherOrgRecordingCoolifyId(id: string, exceptOrgId: OrgId): Promise<OrgId | null>;
|
|
47
71
|
/**
|
|
48
72
|
* Combinations foundry knows are impossible are rejected with a reason
|
|
49
73
|
* (#18 "Done when"), so a caller must be ready for this to throw rather
|
|
@@ -58,7 +82,26 @@ export interface DeclaredOrgStore {
|
|
|
58
82
|
*/
|
|
59
83
|
update(id: OrgId, patch: Partial<DeclaredOrg>, opts: UpdateOptions): Promise<DeclaredOrg | StaleUpdate>;
|
|
60
84
|
getAppliedState(id: OrgId): Promise<AppliedState | null>;
|
|
61
|
-
|
|
85
|
+
/**
|
|
86
|
+
* With `fence` (#441), compare-and-set: the write happens only while
|
|
87
|
+
* `fence.token` is still the scope's current lease token, otherwise it
|
|
88
|
+
* throws {@link StaleLeaseError} and writes nothing.
|
|
89
|
+
*/
|
|
90
|
+
putAppliedState(state: AppliedState, fence?: MutationLease): Promise<void>;
|
|
91
|
+
/**
|
|
92
|
+
* #441: takes the exclusive mutation lease for `scope` (`company:<orgId>`
|
|
93
|
+
* or `platform`), or returns {@link HeldLease} while another holder's lease
|
|
94
|
+
* has not expired. Every successful acquire issues a higher token than the last.
|
|
95
|
+
*/
|
|
96
|
+
acquireMutationLease(scope: string, holder: string, ttlMs: number): Promise<MutationLease | HeldLease>;
|
|
97
|
+
/**
|
|
98
|
+
* Extends the lease to `ttlMs` from now, only while `lease.token` is still
|
|
99
|
+
* the scope's current token. Returns false once it is not: the holder lost
|
|
100
|
+
* the scope and must stop before its next provider step.
|
|
101
|
+
*/
|
|
102
|
+
renewMutationLease(lease: MutationLease, ttlMs: number): Promise<boolean>;
|
|
103
|
+
/** Frees the lease at once. A no-op if the lease was already taken over. */
|
|
104
|
+
releaseMutationLease(lease: MutationLease): Promise<void>;
|
|
62
105
|
/**
|
|
63
106
|
* Removes the declared org and its applied state together. Called only by
|
|
64
107
|
* `FoundryPlanApply.destroy` (`plan/plan-apply.ts`), once every resource it
|
|
@@ -68,7 +111,7 @@ export interface DeclaredOrgStore {
|
|
|
68
111
|
* not an error, so a retried destroy that reaches this step twice never
|
|
69
112
|
* throws.
|
|
70
113
|
*/
|
|
71
|
-
delete(id: OrgId): Promise<void>;
|
|
114
|
+
delete(id: OrgId, fence?: MutationLease): Promise<void>;
|
|
72
115
|
}
|
|
73
116
|
/**
|
|
74
117
|
* The persistence seam for #166's {@link DeclaredPlatform}, mirroring
|
|
@@ -87,8 +130,8 @@ export interface PlatformStore {
|
|
|
87
130
|
* before the first apply. `put` never touches it.
|
|
88
131
|
*/
|
|
89
132
|
getServiceIds(): Promise<Record<string, string>>;
|
|
90
|
-
/** Replaces the recorded ids. A no-op until the platform is declared. */
|
|
91
|
-
putServiceIds(serviceIds: Record<string, string
|
|
133
|
+
/** Replaces the recorded ids. A no-op until the platform is declared. With `fence` (#441), compare-and-set as {@link DeclaredOrgStore.putAppliedState}. */
|
|
134
|
+
putServiceIds(serviceIds: Record<string, string>, fence?: MutationLease): Promise<void>;
|
|
92
135
|
}
|
|
93
136
|
/**
|
|
94
137
|
* One recorded apply, appended to the history #253 keeps beside
|
package/dist/store.js
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
|
|
1
|
+
/** A fenced write (`putAppliedState`, `putServiceIds`) came from a lease that is no longer the current one (#441). Nothing was written. */
|
|
2
|
+
export class StaleLeaseError extends Error {
|
|
3
|
+
scope;
|
|
4
|
+
constructor(scope) {
|
|
5
|
+
super(`mutation lease for ${scope} was lost to a newer holder; nothing written`);
|
|
6
|
+
this.scope = scope;
|
|
7
|
+
this.name = 'StaleLeaseError';
|
|
8
|
+
}
|
|
9
|
+
}
|