@nylorun/runtime 0.10.0-beta → 0.12.0-beta
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/CHANGELOG.md +86 -0
- package/README.md +53 -51
- package/dist/adapters/execution/restate.d.ts +130 -0
- package/dist/adapters/execution/restate.js +442 -0
- package/dist/adapters/sandbox/virtual.js +1 -1
- package/dist/adapters/streams/s2.d.ts +18 -0
- package/dist/adapters/streams/s2.js +361 -0
- package/dist/core/executors.d.ts +4 -1
- package/dist/core/executors.js +20 -1
- package/dist/core/flow-host.d.ts +93 -132
- package/dist/core/flow-host.js +282 -547
- package/dist/core/sandbox-routes.d.ts +13 -4
- package/dist/core/sandbox-routes.js +14 -12
- package/dist/execution/memory.d.ts +54 -0
- package/dist/execution/memory.js +217 -0
- package/dist/execution/types.d.ts +125 -0
- package/dist/execution/types.js +52 -0
- package/dist/host/config-for.d.ts +2 -1
- package/dist/host/config-for.js +3 -21
- package/dist/host/config.d.ts +0 -8
- package/dist/host/create-host.d.ts +36 -1
- package/dist/host/create-host.js +51 -24
- package/dist/host/execution.d.ts +61 -0
- package/dist/host/execution.js +69 -0
- package/dist/host/http.d.ts +4 -0
- package/dist/host/http.js +6 -0
- package/dist/host/index.d.ts +3 -1
- package/dist/host/index.js +2 -0
- package/dist/host/main.js +106 -47
- package/dist/host/stack-config.d.ts +72 -0
- package/dist/host/stack-config.js +193 -0
- package/dist/index.d.ts +1 -1
- package/dist/infra/database.d.ts +16 -0
- package/dist/infra/database.js +31 -0
- package/dist/infra/execution.d.ts +39 -0
- package/dist/infra/execution.js +68 -0
- package/dist/infra/index.d.ts +42 -0
- package/dist/infra/index.js +56 -0
- package/dist/infra/readiness.d.ts +33 -0
- package/dist/infra/readiness.js +53 -0
- package/dist/infra/streams.d.ts +17 -0
- package/dist/infra/streams.js +33 -0
- package/dist/infra/workers.d.ts +39 -0
- package/dist/infra/workers.js +31 -0
- package/dist/model/pi-model.d.ts +2 -2
- package/dist/model/pi-model.js +5 -5
- package/dist/sandbox/manager.d.ts +26 -8
- package/dist/sandbox/manager.js +95 -47
- package/dist/sandbox/select.d.ts +1 -1
- package/dist/sandbox/select.js +4 -5
- package/dist/sandbox/types.d.ts +2 -2
- package/dist/store/canonical.d.ts +5 -0
- package/dist/store/canonical.js +17 -0
- package/dist/store/cursor.d.ts +9 -0
- package/dist/store/cursor.js +18 -0
- package/dist/store/memory.d.ts +57 -0
- package/dist/store/memory.js +645 -0
- package/dist/store/ownership.d.ts +19 -0
- package/dist/store/ownership.js +33 -0
- package/dist/store/postgres/connect.d.ts +25 -0
- package/dist/store/postgres/connect.js +25 -0
- package/dist/store/postgres/locking.d.ts +16 -0
- package/dist/store/postgres/locking.js +22 -0
- package/dist/store/postgres/migrations/001_initial.d.ts +27 -0
- package/dist/store/postgres/migrations/001_initial.js +196 -0
- package/dist/store/postgres/migrations/002_session_owner.d.ts +6 -0
- package/dist/store/postgres/migrations/002_session_owner.js +13 -0
- package/dist/store/postgres/migrations/index.d.ts +49 -0
- package/dist/store/postgres/migrations/index.js +76 -0
- package/dist/store/postgres/names.d.ts +11 -0
- package/dist/store/postgres/names.js +42 -0
- package/dist/store/postgres/store.d.ts +11 -0
- package/dist/store/postgres/store.js +797 -0
- package/dist/store/postgres/tenants.d.ts +90 -0
- package/dist/store/postgres/tenants.js +214 -0
- package/dist/store/types.d.ts +442 -0
- package/dist/store/types.js +10 -0
- package/dist/streams/basin.d.ts +23 -0
- package/dist/streams/basin.js +51 -0
- package/dist/streams/memory.d.ts +25 -0
- package/dist/streams/memory.js +124 -0
- package/dist/streams/relay.d.ts +37 -0
- package/dist/streams/relay.js +254 -0
- package/dist/streams/types.d.ts +115 -0
- package/dist/streams/types.js +86 -0
- package/dist/tenant/actions.d.ts +153 -0
- package/dist/tenant/actions.js +191 -0
- package/dist/tenant/advance.d.ts +15 -0
- package/dist/tenant/advance.js +439 -0
- package/dist/tenant/auth.d.ts +32 -0
- package/dist/tenant/auth.js +169 -0
- package/dist/tenant/commands.d.ts +3 -0
- package/dist/tenant/commands.js +393 -0
- package/dist/tenant/context.d.ts +147 -0
- package/dist/tenant/context.js +48 -0
- package/dist/tenant/effects.d.ts +20 -0
- package/dist/tenant/effects.js +564 -0
- package/dist/tenant/envelope.d.ts +0 -3
- package/dist/tenant/envelope.js +2 -34
- package/dist/tenant/ephemeral.d.ts +15 -4
- package/dist/tenant/ephemeral.js +106 -38
- package/dist/tenant/http.d.ts +38 -0
- package/dist/tenant/http.js +53 -0
- package/dist/tenant/live.d.ts +92 -0
- package/dist/tenant/live.js +290 -0
- package/dist/tenant/model-setting.d.ts +16 -0
- package/dist/tenant/model-setting.js +12 -0
- package/dist/tenant/module.d.ts +29 -8
- package/dist/tenant/module.js +165 -158
- package/dist/tenant/paths.d.ts +12 -8
- package/dist/tenant/paths.js +38 -12
- package/dist/tenant/principals.d.ts +12 -13
- package/dist/tenant/principals.js +50 -38
- package/dist/tenant/quarantine-error.d.ts +1 -1
- package/dist/tenant/quarantine-error.js +2 -8
- package/dist/tenant/quarantine.d.ts +0 -8
- package/dist/tenant/quarantine.js +8 -30
- package/dist/tenant/reset.d.ts +7 -6
- package/dist/tenant/reset.js +7 -51
- package/dist/tenant/routes-tenant.d.ts +12 -0
- package/dist/tenant/routes-tenant.js +137 -0
- package/dist/tenant/routes.d.ts +9 -0
- package/dist/tenant/routes.js +151 -0
- package/dist/tenant/runtime.d.ts +59 -58
- package/dist/tenant/runtime.js +243 -2201
- package/dist/tenant/scheduler.d.ts +25 -0
- package/dist/tenant/scheduler.js +84 -0
- package/dist/tenant/session.d.ts +60 -0
- package/dist/tenant/session.js +94 -0
- package/dist/tenant/sessions.d.ts +40 -0
- package/dist/tenant/sessions.js +163 -0
- package/dist/tenant/status.d.ts +13 -4
- package/dist/tenant/status.js +76 -41
- package/dist/tenant/store-memory.d.ts +9 -12
- package/dist/tenant/store-memory.js +79 -68
- package/dist/tenant/store-pg.d.ts +15 -0
- package/dist/tenant/store-pg.js +140 -0
- package/dist/tenant/streams.d.ts +126 -0
- package/dist/tenant/streams.js +419 -0
- package/dist/tenant/sweep.d.ts +15 -0
- package/dist/tenant/sweep.js +122 -0
- package/dist/tenant/transcript.d.ts +104 -0
- package/dist/tenant/transcript.js +58 -0
- package/dist/tenant/types.d.ts +101 -15
- package/dist/tenant/types.js +23 -1
- package/dist/tenant/worker.d.ts +84 -0
- package/dist/tenant/worker.js +189 -0
- package/dist/vault/service.d.ts +44 -31
- package/dist/vault/service.js +454 -360
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +15 -14
- package/dist/adapters/sandbox/microsandbox.d.ts +0 -2
- package/dist/adapters/sandbox/microsandbox.js +0 -238
- package/dist/core/store.d.ts +0 -32
- package/dist/core/store.js +0 -143
- package/dist/launcher/commands.d.ts +0 -24
- package/dist/launcher/commands.js +0 -260
- package/dist/launcher/ensure-config.d.ts +0 -16
- package/dist/launcher/ensure-config.js +0 -79
- package/dist/launcher/errors.d.ts +0 -12
- package/dist/launcher/errors.js +0 -22
- package/dist/launcher/host-config.d.ts +0 -17
- package/dist/launcher/host-config.js +0 -64
- package/dist/launcher/host-state.d.ts +0 -17
- package/dist/launcher/host-state.js +0 -61
- package/dist/launcher/lifecycle.d.ts +0 -66
- package/dist/launcher/lifecycle.js +0 -407
- package/dist/launcher/locks.d.ts +0 -26
- package/dist/launcher/locks.js +0 -117
- package/dist/launcher/logs.d.ts +0 -18
- package/dist/launcher/logs.js +0 -117
- package/dist/launcher/main.d.ts +0 -2
- package/dist/launcher/main.js +0 -66
- package/dist/launcher/output.d.ts +0 -11
- package/dist/launcher/output.js +0 -68
- package/dist/launcher/paths.d.ts +0 -20
- package/dist/launcher/paths.js +0 -36
- package/dist/launcher/probe.d.ts +0 -23
- package/dist/launcher/probe.js +0 -72
- package/dist/launcher/protocol.d.ts +0 -62
- package/dist/launcher/protocol.js +0 -1
- package/dist/launcher/schema-guard.d.ts +0 -6
- package/dist/launcher/schema-guard.js +0 -36
- package/dist/launcher/spawn.d.ts +0 -22
- package/dist/launcher/spawn.js +0 -85
- package/dist/launcher/status.d.ts +0 -6
- package/dist/launcher/status.js +0 -65
- package/dist/tenant/migration.d.ts +0 -16
- package/dist/tenant/migration.js +0 -116
- package/dist/tenant/schema.d.ts +0 -23
- package/dist/tenant/schema.js +0 -151
- package/dist/tenant/store-fs.d.ts +0 -16
- package/dist/tenant/store-fs.js +0 -205
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Tenant catalog on one Postgres database (architecture §8.2).
|
|
3
|
+
*
|
|
4
|
+
* A Tenant exists when its schema `"tenant_<id>"` exists (see `names.ts`); the
|
|
5
|
+
* Host keeps no other registry. The envelope is the one row of the schema's
|
|
6
|
+
* `tenant` table, and its `schemaVersion` is the schema's migrated version.
|
|
7
|
+
*
|
|
8
|
+
* - `createTenant` runs in one transaction: create the schema, migrate it,
|
|
9
|
+
* write the envelope and the bootstrap principals. A crash leaves nothing.
|
|
10
|
+
* Creating an existing Tenant with the same bootstrap principals returns
|
|
11
|
+
* `exists`; with different ones it throws `TenantConflictError`.
|
|
12
|
+
* - `openTenant` migrates an older schema forward and returns its store. A
|
|
13
|
+
* schema newer than this Runtime, a failed migration or an unreadable
|
|
14
|
+
* envelope quarantines that Tenant only. A lost connection or an unavailable
|
|
15
|
+
* server says nothing about the Tenant, so it is thrown, never a quarantine.
|
|
16
|
+
* - `deleteTenant` drops the schema with everything in it.
|
|
17
|
+
*
|
|
18
|
+
* Create, migrate and delete of one schema are serialized by a
|
|
19
|
+
* transaction-level advisory lock on the schema name.
|
|
20
|
+
*/
|
|
21
|
+
import type { Sql } from "postgres";
|
|
22
|
+
import { type TenantEnvelope } from "@nylorun/core/contracts";
|
|
23
|
+
import type { BootstrapPrincipal, Quarantine } from "../../tenant/types.js";
|
|
24
|
+
import type { SessionStore } from "../types.js";
|
|
25
|
+
import { type Migration } from "./migrations/index.js";
|
|
26
|
+
export interface PostgresTenantCatalogOptions {
|
|
27
|
+
/** The shared pool. The catalog and its stores never end it. */
|
|
28
|
+
sql: Sql;
|
|
29
|
+
/** Clock for principal `createdAt`. Defaults to `() => new Date()`. */
|
|
30
|
+
now?: () => Date;
|
|
31
|
+
/** Passed to every opened store. */
|
|
32
|
+
onError?: (error: unknown) => void;
|
|
33
|
+
/** Tests only: the migrations this Runtime knows. */
|
|
34
|
+
migrations?: readonly Migration[];
|
|
35
|
+
}
|
|
36
|
+
export interface TenantListing {
|
|
37
|
+
id: string;
|
|
38
|
+
/** Null when the envelope cannot be read; `quarantine` says why. */
|
|
39
|
+
envelope: TenantEnvelope | null;
|
|
40
|
+
quarantine?: Quarantine;
|
|
41
|
+
}
|
|
42
|
+
export type CreateTenantResult = {
|
|
43
|
+
status: "created";
|
|
44
|
+
envelope: TenantEnvelope;
|
|
45
|
+
} | {
|
|
46
|
+
status: "exists";
|
|
47
|
+
envelope: TenantEnvelope;
|
|
48
|
+
};
|
|
49
|
+
export type OpenTenantResult = {
|
|
50
|
+
status: "ok";
|
|
51
|
+
store: SessionStore;
|
|
52
|
+
envelope: TenantEnvelope;
|
|
53
|
+
migrated: {
|
|
54
|
+
from: number;
|
|
55
|
+
to: number;
|
|
56
|
+
};
|
|
57
|
+
} | {
|
|
58
|
+
status: "quarantined";
|
|
59
|
+
reason: Quarantine;
|
|
60
|
+
} | {
|
|
61
|
+
status: "not-found";
|
|
62
|
+
};
|
|
63
|
+
export interface PostgresTenantCatalog {
|
|
64
|
+
/** Every `tenant_*` schema that encodes a Tenant id, ordered by id. */
|
|
65
|
+
listTenants(): Promise<TenantListing[]>;
|
|
66
|
+
/** The ids of `listTenants`, without reading any envelope. */
|
|
67
|
+
listTenantIds(): Promise<string[]>;
|
|
68
|
+
/** Whether the Tenant's schema exists. */
|
|
69
|
+
tenantExists(id: string): Promise<boolean>;
|
|
70
|
+
/** Throws a `QuarantineError` (`envelope-invalid`) when unreadable. */
|
|
71
|
+
readEnvelope(id: string): Promise<TenantEnvelope>;
|
|
72
|
+
createTenant(input: {
|
|
73
|
+
envelope: TenantEnvelope;
|
|
74
|
+
principals: BootstrapPrincipal;
|
|
75
|
+
}): Promise<CreateTenantResult>;
|
|
76
|
+
/** Whether the stored bootstrap (and Studio) principals equal `bootstrap`. */
|
|
77
|
+
bootstrapMatches(id: string, bootstrap: BootstrapPrincipal): Promise<boolean>;
|
|
78
|
+
openTenant(id: string, options?: {
|
|
79
|
+
now?: () => Date;
|
|
80
|
+
onError?: (error: unknown) => void;
|
|
81
|
+
}): Promise<OpenTenantResult>;
|
|
82
|
+
/** Migrates an existing Tenant schema. Throws `schema-too-new` or the migration error. */
|
|
83
|
+
migrateTenant(id: string): Promise<{
|
|
84
|
+
from: number;
|
|
85
|
+
to: number;
|
|
86
|
+
}>;
|
|
87
|
+
/** Drops the schema and everything in it. Returns false when it did not exist. */
|
|
88
|
+
deleteTenant(id: string): Promise<boolean>;
|
|
89
|
+
}
|
|
90
|
+
export declare function createPostgresTenantCatalog(options: PostgresTenantCatalogOptions): PostgresTenantCatalog;
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { isTenantId } from "@nylorun/core/compatibility";
|
|
2
|
+
import { TenantEnvelopeSchema, } from "@nylorun/core/contracts";
|
|
3
|
+
import { STUDIO_PRINCIPAL_ID } from "../../tenant/principals.js";
|
|
4
|
+
import { TenantConflictError, asQuarantine, quarantine, } from "../../tenant/quarantine.js";
|
|
5
|
+
import { MIGRATIONS, lockSchema, migrateSchemaInTx, readSchemaVersion, schemaExists, } from "./migrations/index.js";
|
|
6
|
+
import { TENANT_SCHEMA_PREFIX, tenantIdFromSchema, tenantSchemaName } from "./names.js";
|
|
7
|
+
import { createPostgresSessionStore } from "./store.js";
|
|
8
|
+
/**
|
|
9
|
+
* Whether Postgres rejected a statement (a SQLSTATE), rather than the connection or the
|
|
10
|
+
* server failing (classes 08, 53, 57, and the driver's own connection errors). Only the
|
|
11
|
+
* first says something about the Tenant's schema.
|
|
12
|
+
*/
|
|
13
|
+
function isStatementError(error) {
|
|
14
|
+
const code = error?.code;
|
|
15
|
+
return (typeof code === "string" &&
|
|
16
|
+
/^[0-9A-Z]{5}$/.test(code) &&
|
|
17
|
+
!/^(08|53|57)/.test(code));
|
|
18
|
+
}
|
|
19
|
+
export function createPostgresTenantCatalog(options) {
|
|
20
|
+
const { sql } = options;
|
|
21
|
+
const now = options.now ?? (() => new Date());
|
|
22
|
+
const migrations = options.migrations ?? MIGRATIONS;
|
|
23
|
+
const latest = migrations.length;
|
|
24
|
+
async function readEnvelopeWith(q, id) {
|
|
25
|
+
const schema = tenantSchemaName(id);
|
|
26
|
+
const invalid = (message) => quarantine("envelope-invalid", message, { tenantId: id });
|
|
27
|
+
let rows;
|
|
28
|
+
try {
|
|
29
|
+
rows = await q `
|
|
30
|
+
SELECT id, name, created_at, updated_at, schema_version
|
|
31
|
+
FROM ${q(`${schema}.tenant`)}`;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
// A missing schema or `tenant` table is the Tenant's problem; anything else
|
|
35
|
+
// (a lost connection) is not, and is thrown as it is.
|
|
36
|
+
const code = error.code;
|
|
37
|
+
if (code !== "42P01" && code !== "3F000")
|
|
38
|
+
throw error;
|
|
39
|
+
throw invalid(`Tenant envelope could not be read: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
if (rows.length !== 1)
|
|
42
|
+
throw invalid("Tenant envelope is missing");
|
|
43
|
+
const row = rows[0];
|
|
44
|
+
const parsed = TenantEnvelopeSchema.safeParse({
|
|
45
|
+
id: row.id,
|
|
46
|
+
name: row.name,
|
|
47
|
+
createdAt: row.created_at,
|
|
48
|
+
updatedAt: row.updated_at,
|
|
49
|
+
schemaVersion: row.schema_version,
|
|
50
|
+
});
|
|
51
|
+
if (!parsed.success)
|
|
52
|
+
throw invalid("Tenant envelope is not valid");
|
|
53
|
+
if (parsed.data.id !== id)
|
|
54
|
+
throw invalid(`Tenant envelope id ${parsed.data.id} does not match schema ${schema}`);
|
|
55
|
+
return parsed.data;
|
|
56
|
+
}
|
|
57
|
+
async function bootstrapMatchesWith(q, id, bootstrap) {
|
|
58
|
+
const schema = tenantSchemaName(id);
|
|
59
|
+
const rows = await q `
|
|
60
|
+
SELECT id, token_hash, idempotency_key FROM ${q(`${schema}.principals`)}
|
|
61
|
+
WHERE role = 'application'`;
|
|
62
|
+
const app = rows.find((row) => row.id === bootstrap.principalId);
|
|
63
|
+
if (!app ||
|
|
64
|
+
app.token_hash !== bootstrap.credentialHash ||
|
|
65
|
+
app.idempotency_key !== bootstrap.idempotencyKey)
|
|
66
|
+
return false;
|
|
67
|
+
const studio = rows.find((row) => row.id === STUDIO_PRINCIPAL_ID);
|
|
68
|
+
if (studio?.token_hash !== bootstrap.studioCredentialHash)
|
|
69
|
+
return false;
|
|
70
|
+
return (bootstrap.derivedPrincipals ?? []).every((principal) => rows.find((row) => row.id === principal.id)?.token_hash ===
|
|
71
|
+
principal.credentialHash);
|
|
72
|
+
}
|
|
73
|
+
const catalog = {
|
|
74
|
+
async listTenantIds() {
|
|
75
|
+
const schemas = await sql `
|
|
76
|
+
SELECT nspname FROM pg_namespace
|
|
77
|
+
WHERE starts_with(nspname, ${TENANT_SCHEMA_PREFIX})
|
|
78
|
+
ORDER BY nspname COLLATE "C"`;
|
|
79
|
+
return schemas.flatMap(({ nspname }) => tenantIdFromSchema(nspname) ?? []);
|
|
80
|
+
},
|
|
81
|
+
async tenantExists(id) {
|
|
82
|
+
return isTenantId(id) && schemaExists(sql, tenantSchemaName(id));
|
|
83
|
+
},
|
|
84
|
+
async listTenants() {
|
|
85
|
+
const listings = [];
|
|
86
|
+
for (const id of await catalog.listTenantIds()) {
|
|
87
|
+
try {
|
|
88
|
+
listings.push({ id, envelope: await readEnvelopeWith(sql, id) });
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
listings.push({
|
|
92
|
+
id,
|
|
93
|
+
envelope: null,
|
|
94
|
+
quarantine: asQuarantine(error) ??
|
|
95
|
+
quarantine("open-failed", "Tenant schema could not be read", {
|
|
96
|
+
tenantId: id,
|
|
97
|
+
}).toQuarantine(),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return listings;
|
|
102
|
+
},
|
|
103
|
+
readEnvelope(id) {
|
|
104
|
+
return readEnvelopeWith(sql, id);
|
|
105
|
+
},
|
|
106
|
+
async createTenant({ envelope, principals }) {
|
|
107
|
+
const input = TenantEnvelopeSchema.parse(envelope);
|
|
108
|
+
const schema = tenantSchemaName(input.id);
|
|
109
|
+
const outcome = await sql.begin(async (tx) => {
|
|
110
|
+
await lockSchema(tx, schema);
|
|
111
|
+
if (await schemaExists(tx, schema))
|
|
112
|
+
return { created: false };
|
|
113
|
+
await migrateSchemaInTx(tx, schema, migrations);
|
|
114
|
+
const stored = { ...input, schemaVersion: latest };
|
|
115
|
+
await tx `
|
|
116
|
+
INSERT INTO ${tx(`${schema}.tenant`)} (id, name, created_at, updated_at, schema_version)
|
|
117
|
+
VALUES (${stored.id}, ${stored.name}, ${stored.createdAt}, ${stored.updatedAt}, ${stored.schemaVersion})`;
|
|
118
|
+
const createdAt = now().toISOString();
|
|
119
|
+
await tx `
|
|
120
|
+
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
121
|
+
VALUES (${principals.principalId}, 'application', ${principals.credentialHash},
|
|
122
|
+
${principals.idempotencyKey}, ${createdAt})`;
|
|
123
|
+
if (principals.studioCredentialHash)
|
|
124
|
+
await tx `
|
|
125
|
+
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
126
|
+
VALUES (${STUDIO_PRINCIPAL_ID}, 'application', ${principals.studioCredentialHash},
|
|
127
|
+
NULL, ${createdAt})`;
|
|
128
|
+
for (const derived of principals.derivedPrincipals ?? [])
|
|
129
|
+
await tx `
|
|
130
|
+
INSERT INTO ${tx(`${schema}.principals`)} (id, role, token_hash, idempotency_key, created_at)
|
|
131
|
+
VALUES (${derived.id}, 'application', ${derived.credentialHash}, NULL, ${createdAt})`;
|
|
132
|
+
return { created: true, envelope: stored };
|
|
133
|
+
});
|
|
134
|
+
if (outcome.created)
|
|
135
|
+
return { status: "created", envelope: outcome.envelope };
|
|
136
|
+
if (!(await bootstrapMatchesWith(sql, input.id, principals)))
|
|
137
|
+
throw new TenantConflictError();
|
|
138
|
+
return { status: "exists", envelope: await readEnvelopeWith(sql, input.id) };
|
|
139
|
+
},
|
|
140
|
+
async bootstrapMatches(id, bootstrap) {
|
|
141
|
+
if (!(await schemaExists(sql, tenantSchemaName(id))))
|
|
142
|
+
return false;
|
|
143
|
+
return bootstrapMatchesWith(sql, id, bootstrap);
|
|
144
|
+
},
|
|
145
|
+
async openTenant(id, storeOptions = {}) {
|
|
146
|
+
if (!isTenantId(id))
|
|
147
|
+
return { status: "not-found" };
|
|
148
|
+
const schema = tenantSchemaName(id);
|
|
149
|
+
const version = await readSchemaVersion(sql, schema);
|
|
150
|
+
if (version === undefined)
|
|
151
|
+
return { status: "not-found" };
|
|
152
|
+
if (version > latest)
|
|
153
|
+
return {
|
|
154
|
+
status: "quarantined",
|
|
155
|
+
reason: quarantine("schema-too-new", `Tenant schema version ${version} is newer than this Runtime's ${latest}`, { tenantId: id }).toQuarantine(),
|
|
156
|
+
};
|
|
157
|
+
let migrated = { from: version, to: version };
|
|
158
|
+
if (version < latest) {
|
|
159
|
+
try {
|
|
160
|
+
migrated = await catalog.migrateTenant(id);
|
|
161
|
+
}
|
|
162
|
+
catch (error) {
|
|
163
|
+
if (!asQuarantine(error) && !isStatementError(error))
|
|
164
|
+
throw error;
|
|
165
|
+
return {
|
|
166
|
+
status: "quarantined",
|
|
167
|
+
reason: asQuarantine(error) ??
|
|
168
|
+
quarantine("migration-failed", `Tenant schema migration from version ${version} failed: ${error.message}`, { tenantId: id }).toQuarantine(),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
let envelope;
|
|
173
|
+
try {
|
|
174
|
+
envelope = await readEnvelopeWith(sql, id);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
const reason = asQuarantine(error);
|
|
178
|
+
if (!reason)
|
|
179
|
+
throw error;
|
|
180
|
+
return { status: "quarantined", reason };
|
|
181
|
+
}
|
|
182
|
+
const store = createPostgresSessionStore({
|
|
183
|
+
sql,
|
|
184
|
+
tenantId: id,
|
|
185
|
+
schema,
|
|
186
|
+
schemaVersion: latest,
|
|
187
|
+
now: storeOptions.now,
|
|
188
|
+
onError: storeOptions.onError ?? options.onError,
|
|
189
|
+
});
|
|
190
|
+
return { status: "ok", store, envelope, migrated };
|
|
191
|
+
},
|
|
192
|
+
async migrateTenant(id) {
|
|
193
|
+
const schema = tenantSchemaName(id);
|
|
194
|
+
const result = await sql.begin(async (tx) => {
|
|
195
|
+
await lockSchema(tx, schema);
|
|
196
|
+
if (!(await schemaExists(tx, schema)))
|
|
197
|
+
throw new Error(`Tenant ${id} does not exist`);
|
|
198
|
+
return { result: await migrateSchemaInTx(tx, schema, migrations) };
|
|
199
|
+
});
|
|
200
|
+
return result.result;
|
|
201
|
+
},
|
|
202
|
+
async deleteTenant(id) {
|
|
203
|
+
const schema = tenantSchemaName(id);
|
|
204
|
+
return sql.begin(async (tx) => {
|
|
205
|
+
await lockSchema(tx, schema);
|
|
206
|
+
if (!(await schemaExists(tx, schema)))
|
|
207
|
+
return false;
|
|
208
|
+
await tx `DROP SCHEMA ${tx(schema)} CASCADE`;
|
|
209
|
+
return true;
|
|
210
|
+
});
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
return catalog;
|
|
214
|
+
}
|