@wtfalch/foundry 0.11.0 → 0.13.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.
@@ -79,6 +79,23 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
79
79
  identity: undefined;
80
80
  generated: undefined;
81
81
  }, {}, {}>;
82
+ version: import("drizzle-orm/pg-core").PgColumn<{
83
+ name: "version";
84
+ tableName: "foundry_declared_orgs";
85
+ dataType: "number";
86
+ columnType: "PgInteger";
87
+ data: number;
88
+ driverParam: string | number;
89
+ notNull: true;
90
+ hasDefault: true;
91
+ isPrimaryKey: false;
92
+ isAutoincrement: false;
93
+ hasRuntimeDefault: false;
94
+ enumValues: undefined;
95
+ baseColumn: never;
96
+ identity: undefined;
97
+ generated: undefined;
98
+ }, {}, {}>;
82
99
  mailMode: import("drizzle-orm/pg-core").PgColumn<{
83
100
  name: "mail_mode";
84
101
  tableName: "foundry_declared_orgs";
@@ -482,33 +499,6 @@ export declare const declaredOrgs: import("drizzle-orm/pg-core").PgTableWithColu
482
499
  }, {}, {
483
500
  $type: HostingEntry[];
484
501
  }>;
485
- hostingAdopt: import("drizzle-orm/pg-core").PgColumn<{
486
- name: "hosting_adopt";
487
- tableName: "foundry_declared_orgs";
488
- dataType: "json";
489
- columnType: "PgJsonb";
490
- data: {
491
- database?: string;
492
- tunnel?: string;
493
- connectorApp?: string;
494
- };
495
- driverParam: unknown;
496
- notNull: false;
497
- hasDefault: false;
498
- isPrimaryKey: false;
499
- isAutoincrement: false;
500
- hasRuntimeDefault: false;
501
- enumValues: undefined;
502
- baseColumn: never;
503
- identity: undefined;
504
- generated: undefined;
505
- }, {}, {
506
- $type: {
507
- database?: string;
508
- tunnel?: string;
509
- connectorApp?: string;
510
- };
511
- }>;
512
502
  ssoEnabled: import("drizzle-orm/pg-core").PgColumn<{
513
503
  name: "sso_enabled";
514
504
  tableName: "foundry_declared_orgs";
@@ -1836,140 +1826,6 @@ export declare const foundryRequestedChanges: import("drizzle-orm/pg-core").PgTa
1836
1826
  };
1837
1827
  dialect: "pg";
1838
1828
  }>;
1839
- /**
1840
- * Mirrors `migrations/0031_adoptions.sql`, which is the source of truth (the
1841
- * CHECKs live there). One row per live resource foundry took over (#311).
1842
- * `settings` holds non-secret settings only; secrets go to the vault under
1843
- * `vaultRef`.
1844
- */
1845
- export declare const foundryAdoptions: import("drizzle-orm/pg-core").PgTableWithColumns<{
1846
- name: "foundry_adoptions";
1847
- schema: undefined;
1848
- columns: {
1849
- orgId: import("drizzle-orm/pg-core").PgColumn<{
1850
- name: "org_id";
1851
- tableName: "foundry_adoptions";
1852
- dataType: "string";
1853
- columnType: "PgText";
1854
- data: string;
1855
- driverParam: string;
1856
- notNull: true;
1857
- hasDefault: false;
1858
- isPrimaryKey: false;
1859
- isAutoincrement: false;
1860
- hasRuntimeDefault: false;
1861
- enumValues: [string, ...string[]];
1862
- baseColumn: never;
1863
- identity: undefined;
1864
- generated: undefined;
1865
- }, {}, {}>;
1866
- resourceKey: import("drizzle-orm/pg-core").PgColumn<{
1867
- name: "resource_key";
1868
- tableName: "foundry_adoptions";
1869
- dataType: "string";
1870
- columnType: "PgText";
1871
- data: string;
1872
- driverParam: string;
1873
- notNull: true;
1874
- hasDefault: false;
1875
- isPrimaryKey: false;
1876
- isAutoincrement: false;
1877
- hasRuntimeDefault: false;
1878
- enumValues: [string, ...string[]];
1879
- baseColumn: never;
1880
- identity: undefined;
1881
- generated: undefined;
1882
- }, {}, {}>;
1883
- kind: import("drizzle-orm/pg-core").PgColumn<{
1884
- name: "kind";
1885
- tableName: "foundry_adoptions";
1886
- dataType: "string";
1887
- columnType: "PgText";
1888
- data: string;
1889
- driverParam: string;
1890
- notNull: true;
1891
- hasDefault: false;
1892
- isPrimaryKey: false;
1893
- isAutoincrement: false;
1894
- hasRuntimeDefault: false;
1895
- enumValues: [string, ...string[]];
1896
- baseColumn: never;
1897
- identity: undefined;
1898
- generated: undefined;
1899
- }, {}, {}>;
1900
- takenAt: import("drizzle-orm/pg-core").PgColumn<{
1901
- name: "taken_at";
1902
- tableName: "foundry_adoptions";
1903
- dataType: "date";
1904
- columnType: "PgTimestamp";
1905
- data: Date;
1906
- driverParam: string;
1907
- notNull: true;
1908
- hasDefault: false;
1909
- isPrimaryKey: false;
1910
- isAutoincrement: false;
1911
- hasRuntimeDefault: false;
1912
- enumValues: undefined;
1913
- baseColumn: never;
1914
- identity: undefined;
1915
- generated: undefined;
1916
- }, {}, {}>;
1917
- settings: import("drizzle-orm/pg-core").PgColumn<{
1918
- name: "settings";
1919
- tableName: "foundry_adoptions";
1920
- dataType: "json";
1921
- columnType: "PgJsonb";
1922
- data: Record<string, unknown>;
1923
- driverParam: unknown;
1924
- notNull: true;
1925
- hasDefault: false;
1926
- isPrimaryKey: false;
1927
- isAutoincrement: false;
1928
- hasRuntimeDefault: false;
1929
- enumValues: undefined;
1930
- baseColumn: never;
1931
- identity: undefined;
1932
- generated: undefined;
1933
- }, {}, {
1934
- $type: Record<string, unknown>;
1935
- }>;
1936
- vaultRef: import("drizzle-orm/pg-core").PgColumn<{
1937
- name: "vault_ref";
1938
- tableName: "foundry_adoptions";
1939
- dataType: "string";
1940
- columnType: "PgText";
1941
- data: string;
1942
- driverParam: string;
1943
- notNull: false;
1944
- hasDefault: false;
1945
- isPrimaryKey: false;
1946
- isAutoincrement: false;
1947
- hasRuntimeDefault: false;
1948
- enumValues: [string, ...string[]];
1949
- baseColumn: never;
1950
- identity: undefined;
1951
- generated: undefined;
1952
- }, {}, {}>;
1953
- state: import("drizzle-orm/pg-core").PgColumn<{
1954
- name: "state";
1955
- tableName: "foundry_adoptions";
1956
- dataType: "string";
1957
- columnType: "PgText";
1958
- data: string;
1959
- driverParam: string;
1960
- notNull: true;
1961
- hasDefault: true;
1962
- isPrimaryKey: false;
1963
- isAutoincrement: false;
1964
- hasRuntimeDefault: false;
1965
- enumValues: [string, ...string[]];
1966
- baseColumn: never;
1967
- identity: undefined;
1968
- generated: undefined;
1969
- }, {}, {}>;
1970
- };
1971
- dialect: "pg";
1972
- }>;
1973
1829
  export type DeclaredOrgRow = typeof declaredOrgs.$inferSelect;
1974
1830
  export type NewDeclaredOrgRow = typeof declaredOrgs.$inferInsert;
1975
1831
  export type AppliedStateRow = typeof foundryAppliedState.$inferSelect;
@@ -1979,4 +1835,3 @@ export type DeclaredPlatformRow = typeof foundryDeclaredPlatform.$inferSelect;
1979
1835
  export type NewDeclaredPlatformRow = typeof foundryDeclaredPlatform.$inferInsert;
1980
1836
  export type RequestedChangeRow = typeof foundryRequestedChanges.$inferSelect;
1981
1837
  export type NewRequestedChangeRow = typeof foundryRequestedChanges.$inferInsert;
1982
- export type AdoptionRow = typeof foundryAdoptions.$inferSelect;
@@ -1,4 +1,4 @@
1
- import { bigserial, boolean, jsonb, pgTable, primaryKey, text, timestamp, } from 'drizzle-orm/pg-core';
1
+ import { 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
@@ -10,6 +10,8 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
10
10
  slug: text('slug').notNull(),
11
11
  displayName: text('display_name').notNull(),
12
12
  domain: text('domain'),
13
+ // Bumped by 1 on every write (migrations/0033, #351): the compare-and-set token for `update`'s `expectedVersion`.
14
+ version: integer('version').notNull().default(1),
13
15
  mailMode: text('mail_mode').notNull().default('off'),
14
16
  mailUsesDomain: boolean('mail_uses_domain').notNull().default(false),
15
17
  // MailConfig (org.ts, #162). null until an operator declares one -- the
@@ -54,9 +56,6 @@ export const declaredOrgs = pgTable('foundry_declared_orgs', {
54
56
  // hosting_access_emails and hosting_app_env columns, which stay for an older
55
57
  // reader and are no longer written.
56
58
  hostingEntries: jsonb('hosting_entries').notNull().default([]).$type(),
57
- // HostingConfig.adopt (org.ts): the live resources the org takes over by
58
- // uuid. null when it adopts none. An entry's own `adopt` lives in `hostingEntries`.
59
- hostingAdopt: jsonb('hosting_adopt').$type(),
60
59
  ssoEnabled: boolean('sso_enabled').notNull().default(false),
61
60
  guestsEnabled: boolean('guests_enabled').notNull().default(false),
62
61
  soloEnabled: boolean('solo_enabled').notNull().default(false),
@@ -175,18 +174,3 @@ export const foundryRequestedChanges = pgTable('foundry_requested_changes', {
175
174
  createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
176
175
  state: text('state').notNull().default('needs_approval'),
177
176
  });
178
- /**
179
- * Mirrors `migrations/0031_adoptions.sql`, which is the source of truth (the
180
- * CHECKs live there). One row per live resource foundry took over (#311).
181
- * `settings` holds non-secret settings only; secrets go to the vault under
182
- * `vaultRef`.
183
- */
184
- export const foundryAdoptions = pgTable('foundry_adoptions', {
185
- orgId: text('org_id').notNull(),
186
- resourceKey: text('resource_key').notNull(),
187
- kind: text('kind').notNull(),
188
- takenAt: timestamp('taken_at', { withTimezone: true }).notNull(),
189
- settings: jsonb('settings').notNull().$type(),
190
- vaultRef: text('vault_ref'),
191
- state: text('state').notNull().default('adopted'),
192
- }, (table) => [primaryKey({ columns: [table.orgId, table.resourceKey] })]);
package/dist/store.d.ts CHANGED
@@ -2,6 +2,17 @@ import type { DeclaredOrg, OrgId } from './org.js';
2
2
  import type { DeclaredPlatform } from './platform.js';
3
3
  import type { RequestedChange, RequestedChangeValue } from './requested-changes.js';
4
4
  import type { AppliedState } from './state.js';
5
+ /** `DeclaredOrgStore.update`'s options (#351). */
6
+ export interface UpdateOptions {
7
+ /** The {@link DeclaredOrg.version} the caller's patch was computed from. */
8
+ expectedVersion: number;
9
+ }
10
+ /** `update` refused a write because the org's version no longer matches `expectedVersion` (#351). */
11
+ export interface StaleUpdate {
12
+ stale: true;
13
+ /** The org as it stands now. */
14
+ current: DeclaredOrg;
15
+ }
5
16
  /**
6
17
  * The persistence seam https://github.com/wtfalch/foundry/issues/18
7
18
  * implements: create, read and edit a {@link DeclaredOrg}, and read and
@@ -39,6 +50,13 @@ export interface DeclaredOrgStore {
39
50
  * than silently drop a field.
40
51
  */
41
52
  update(id: OrgId, patch: Partial<DeclaredOrg>): Promise<DeclaredOrg>;
53
+ /**
54
+ * With `expectedVersion` (#351), the write happens only if the org is still
55
+ * at that {@link DeclaredOrg.version}. Otherwise nothing is written and the
56
+ * result is `{ stale: true, current }`, the org as it now stands. Never
57
+ * throws for a version mismatch.
58
+ */
59
+ update(id: OrgId, patch: Partial<DeclaredOrg>, opts: UpdateOptions): Promise<DeclaredOrg | StaleUpdate>;
42
60
  getAppliedState(id: OrgId): Promise<AppliedState | null>;
43
61
  putAppliedState(state: AppliedState): Promise<void>;
44
62
  /**
@@ -96,40 +114,3 @@ export interface RequestedChangeStore {
96
114
  /** Every request an org has made, oldest first. */
97
115
  listForOrg(orgId: OrgId): Promise<RequestedChange[]>;
98
116
  }
99
- export type AdoptionKind = 'app' | 'database' | 'tunnel' | 'connector' | 'access';
100
- export type AdoptionState = 'adopted' | 'confirmed' | 'restored' | 'released';
101
- /**
102
- * What foundry recorded before it first changed a live resource it took over
103
- * (#311). `settings` holds non-secret settings only; secrets live in the
104
- * `AdoptionVault` (`plan/adoption-vault.ts`) under `vaultRef`.
105
- */
106
- export interface Adoption {
107
- orgId: OrgId;
108
- /** The scope key of the resource, for example `coolify:app:<orgId>:<entryKey>`. */
109
- resourceKey: string;
110
- kind: AdoptionKind;
111
- /** ISO time the snapshot was taken. */
112
- takenAt: string;
113
- settings: Record<string, unknown>;
114
- /** The vault ref of the secret part of the snapshot, or `null` when it has none. */
115
- vaultRef: string | null;
116
- state: AdoptionState;
117
- }
118
- /** The persistence seam for #311's {@link Adoption} rows, beside {@link DeclaredOrgStore}. */
119
- export interface AdoptionStore {
120
- /**
121
- * Inserts the row, or replaces it in any state; the row is stored as
122
- * `adopted`. A new snapshot after a restore or a release replaces the old one.
123
- */
124
- put(adoption: Omit<Adoption, 'state'>): Promise<void>;
125
- get(orgId: OrgId, resourceKey: string): Promise<Adoption | null>;
126
- /** Every row of an org, ordered by resource key. */
127
- list(orgId: OrgId): Promise<Adoption[]>;
128
- /** Moves a row to `state`. A no-op for a row that does not exist. */
129
- setState(orgId: OrgId, resourceKey: string, state: AdoptionState): Promise<void>;
130
- /**
131
- * Replaces a row's `settings` and nothing else: its state, `takenAt` and
132
- * `vaultRef` stay (#312). A no-op for a row that does not exist.
133
- */
134
- setSettings(orgId: OrgId, resourceKey: string, settings: Record<string, unknown>): Promise<void>;
135
- }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.11.0",
3
+ "version": "0.13.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",
@@ -1,112 +0,0 @@
1
- import type { DeclaredOrg, HostingEntry } from '../org.js';
2
- import type { Adoption } from '../store.js';
3
- /**
4
- * Adoption (#311): foundry takes over a hand-made Coolify app or database
5
- * where it is. Nothing in this file holds an env value: an env var is named,
6
- * never quoted, in a detail line, an error or a blocked item (rule 6).
7
- */
8
- interface AdoptedBase {
9
- /** The `foundry_adoptions.resource_key`, and the `coolify:adopt` step's key. */
10
- resourceKey: string;
11
- uuid: string;
12
- }
13
- /**
14
- * One app (with the entry that adopts it) or database an org declares by uuid,
15
- * or (#312) its Cloudflare tunnel (`uuid` is the tunnel id) or the Coolify app
16
- * that runs `cloudflared` for it.
17
- */
18
- export type AdoptedResource = (AdoptedBase & {
19
- kind: 'database';
20
- }) | (AdoptedBase & {
21
- kind: 'app';
22
- entry: HostingEntry;
23
- }) | (AdoptedBase & {
24
- kind: 'tunnel';
25
- }) | (AdoptedBase & {
26
- kind: 'connector';
27
- });
28
- /** The `foundry_adoptions.resource_key` of the org's adopted tunnel: also its `serviceIds` key (#312). */
29
- export declare const adoptedTunnelKey: (org: DeclaredOrg) => string;
30
- /** The `foundry_adoptions.resource_key` of the org's adopted connector app (#312). */
31
- export declare const adoptedConnectorKey: (org: DeclaredOrg) => string;
32
- /** Every resource the org adopts, the database first, then the apps, the tunnel and its connector. */
33
- export declare function adoptedResources(org: DeclaredOrg): AdoptedResource[];
34
- /**
35
- * Every uuid the org declares as adopted, whether or not hosting is enabled
36
- * now: a uuid in this set is never stopped or deleted by foundry (#311).
37
- */
38
- export declare function declaredAdoptUuids(org: DeclaredOrg): Set<string>;
39
- /**
40
- * A snapshot is ACTIVE when its row is `adopted` or `confirmed` and the uuid
41
- * it recorded is the uuid the org declares now (#311). Only then may foundry
42
- * write to the live resource.
43
- */
44
- export declare function isActiveSnapshot(row: Adoption | null | undefined, uuid: string): boolean;
45
- /**
46
- * A row in `adopted`, `confirmed` or `restored` protects its key: foundry never
47
- * manages, stops or deletes that resource as its own. Only `released` lets go (#311).
48
- */
49
- export declare function protectsKey(row: Adoption | null | undefined): boolean;
50
- /** Where the secret part of a snapshot lives in the `AdoptionVault`. */
51
- export declare function adoptionVaultRef(org: DeclaredOrg, resource: AdoptedResource): string;
52
- export type LiveApp = Record<string, unknown>;
53
- export declare function snapshotAppSettings(live: LiveApp): Record<string, unknown>;
54
- export declare const bareName: (name: string) => string;
55
- export declare const liveNames: (live: LiveApp) => string[];
56
- /**
57
- * What an adopted entry changes on its live app: only a setting it declares
58
- * that differs from live. The entry cannot tell a declared `branch`,
59
- * `buildPack` or `autoDeploy` from its default, so those stay as they are; a
60
- * `null` folder or port and an empty name list declare nothing. Never
61
- * `custom_network_aliases`.
62
- */
63
- export declare function adoptedAppPatch(entry: HostingEntry, names: readonly string[], live: LiveApp): Record<string, unknown>;
64
- /** One line per setting the patch changes, old value beside new. */
65
- export declare function adoptedPatchDetails(patch: Record<string, unknown>, live: LiveApp): string[];
66
- /**
67
- * The PATCH body that puts an app's snapshot settings back. A field is sent
68
- * only when the entry's declaration would have changed it from the snapshot
69
- * (`adoptedAppPatch`), so a setting foundry never touched, or a person edited
70
- * later, stays. With no entry, a field is sent when `live` differs from the
71
- * snapshot. Coolify reads `fqdn` but takes `domains`. A field the snapshot had
72
- * as null is cleared the way the plain app path clears it: a folder back to
73
- * `/`, domains to the empty list. A port is never cleared, as on that path.
74
- */
75
- export declare function restoreAppPatch(entry: HostingEntry | undefined, names: readonly string[], settings: Record<string, unknown>, live: LiveApp): Record<string, unknown>;
76
- /** One route of a tunnel's ingress list. Other fields (`originRequest`, `path`) ride along untouched. */
77
- export interface IngressRoute {
78
- hostname?: string;
79
- service: string;
80
- [field: string]: unknown;
81
- }
82
- /** A hostname as compared: lower case, no trailing dot (DNS names ignore both). */
83
- export declare const normalizeHostname: (hostname: string) => string;
84
- /**
85
- * What the merge leaves alone: every live route that is not for a declared
86
- * name and not foundry's own route for a name it dropped. Whatever its shape:
87
- * a hostname, only a `path`, or neither.
88
- */
89
- export declare function foreignRoutes(live: readonly IngressRoute[], declared: readonly string[], service: string, written: ReadonlySet<string>): IngressRoute[];
90
- /** The declared names whose live routes the merge replaces: there are some, and they are not already foundry's one route. */
91
- export declare function replacedNames(live: readonly IngressRoute[], declared: readonly string[], service: string): string[];
92
- /** The names foundry removes from `live`: written, no longer declared, still its own route. */
93
- export declare const removedNames: (live: readonly IngressRoute[], declared: readonly string[], service: string, written: ReadonlySet<string>) => string[];
94
- /**
95
- * The ingress list an adopted tunnel gets (#312): the declared exact names,
96
- * then every foreign live route in its live order and unchanged, with the
97
- * snapshot's routes for a name foundry dropped put back among them, then the
98
- * declared wildcards, then the live catch-all as it is. Only a live list that
99
- * does not end in a catch-all gets `http_status:404`.
100
- */
101
- export declare function mergeIngress(live: readonly IngressRoute[], declared: readonly string[], service: string, written: ReadonlySet<string>, snapshot: readonly IngressRoute[]): IngressRoute[];
102
- /**
103
- * The ingress list restore writes (#312): the live list without foundry's own
104
- * route for a name it wrote (`written` accumulates for the life of the
105
- * snapshot), plus the snapshot's routes for that name. A name with no live
106
- * route that is exactly foundry's is left alone, as is a route a person added
107
- * or edited, and the live catch-all. The `http_status:404` foundry appended is
108
- * removed only while it is still last and the snapshot had no catch-all; an
109
- * empty snapshot keeps the live catch-all, or gets one.
110
- */
111
- export declare function restoreIngress(live: readonly IngressRoute[], snapshot: readonly IngressRoute[], service: string, written: ReadonlySet<string>): IngressRoute[];
112
- export {};