|
| 1 | +// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license. |
| 2 | + |
| 3 | +/** |
| 4 | + * [ADR-0131 D3/D5] The ONE definition of the deployment's Default |
| 5 | + * Organization: the organization with `slug = 'default'`, else the only |
| 6 | + * organization there is. Several organizations, none of them `default`, name |
| 7 | + * none; so does a deployment that holds no organization. |
| 8 | + * |
| 9 | + * Under `single` it is the owner every organization-less write derives to |
| 10 | + * (D9) and the owner of the `admin_full_access` grant that anchors platform |
| 11 | + * standing (D5), so every reader must name the same row: a reader that named |
| 12 | + * another one would have the boot create, the promotion write, the resolver |
| 13 | + * read and the last-admin guard protect four different anchors. |
| 14 | + * |
| 15 | + * What differs between the readers is HOW they read, never what they pick: |
| 16 | + * the resolver fails loud on an unreachable store, the bootstrap reads an |
| 17 | + * unreadable store as nobody, the last-admin guard simulates a pending write |
| 18 | + * over the rows it reads, and the ownership ceremony reads raw SQL with no |
| 19 | + * engine. So the rule takes the read as a parameter |
| 20 | + * ({@link DefaultOrganizationRead}) and each reader keeps its own read policy. |
| 21 | + * ⛔ Never restate the rule beside a read: hand the read to |
| 22 | + * {@link findDefaultOrganization}. |
| 23 | + */ |
| 24 | + |
| 25 | +/** [ADR-0131 D3] The slug the boot invariant creates the Default Organization with. */ |
| 26 | +export const DEFAULT_ORGANIZATION_SLUG = 'default'; |
| 27 | + |
| 28 | +/** |
| 29 | + * One read of `sys_organization` the definition asks for: rows matching |
| 30 | + * `where`, at most `limit` of them. It asks two: `{ slug: 'default' }` with |
| 31 | + * limit 1, then `{}` with limit 2 (enough to tell "the only one" from |
| 32 | + * "several"). A reader that simulates a pending write answers with the rows as |
| 33 | + * that write would leave them. |
| 34 | + */ |
| 35 | +export type DefaultOrganizationRead<R> = ( |
| 36 | + where: Readonly<Record<string, unknown>>, |
| 37 | + limit: number, |
| 38 | +) => Promise<readonly R[]>; |
| 39 | + |
| 40 | +/** What {@link findDefaultOrganization} answers. */ |
| 41 | +export interface DefaultOrganizationAnswer<R> { |
| 42 | + /** The Default Organization's id, or `undefined` when none can be named. */ |
| 43 | + id: string | undefined; |
| 44 | + /** The row it was named from, as the read returned it. */ |
| 45 | + row: R | undefined; |
| 46 | + /** |
| 47 | + * `true` when none could be named because there are several organizations |
| 48 | + * and none is `default` — the shape the boot invariant reports as ambiguous |
| 49 | + * rather than creating one beside them. |
| 50 | + */ |
| 51 | + several: boolean; |
| 52 | +} |
| 53 | + |
| 54 | +/** A row's id as the definition reads it: a non-empty string, or nothing. */ |
| 55 | +function idOf(row: unknown): string | undefined { |
| 56 | + const id = (row as { id?: unknown } | null | undefined)?.id; |
| 57 | + return id === undefined || id === null || String(id) === '' ? undefined : String(id); |
| 58 | +} |
| 59 | + |
| 60 | +/** |
| 61 | + * [ADR-0131 D3/D5] Name the Default Organization through `read`. A read that |
| 62 | + * throws propagates: the caller's read policy decides what an unreadable store |
| 63 | + * means. |
| 64 | + */ |
| 65 | +export async function findDefaultOrganization<R>(read: DefaultOrganizationRead<R>): Promise<DefaultOrganizationAnswer<R>> { |
| 66 | + const bySlug = await read({ slug: DEFAULT_ORGANIZATION_SLUG }, 1); |
| 67 | + const named = idOf(bySlug[0]); |
| 68 | + if (named !== undefined) return { id: named, row: bySlug[0], several: false }; |
| 69 | + const all = await read({}, 2); |
| 70 | + const sole = all.length === 1 ? idOf(all[0]) : undefined; |
| 71 | + if (sole !== undefined) return { id: sole, row: all[0], several: false }; |
| 72 | + return { id: undefined, row: undefined, several: all.length > 1 }; |
| 73 | +} |
0 commit comments