@stratum-hq/lib 1.2.1 → 1.3.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/crypto.d.ts.map +1 -1
- package/dist/crypto.js +7 -1
- package/dist/crypto.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/migrate-schemas.d.ts.map +1 -1
- package/dist/migrate-schemas.js +19 -8
- package/dist/migrate-schemas.js.map +1 -1
- package/dist/migrations/022_roles_name_per_tenant.sql +10 -0
- package/dist/migrations/024_propagate_ancestry_ltree.sql +42 -0
- package/dist/migrations/025_redact_sensitive_config_audit.sql +19 -0
- package/dist/migrations/026_redact_webhook_secret_audit.sql +28 -0
- package/dist/migrations/027_tenant_status_pending.sql +8 -0
- package/dist/services/abac-service.d.ts.map +1 -1
- package/dist/services/abac-service.js +49 -37
- package/dist/services/abac-service.js.map +1 -1
- package/dist/services/api-key-service.d.ts.map +1 -1
- package/dist/services/api-key-service.js +55 -33
- package/dist/services/api-key-service.js.map +1 -1
- package/dist/services/config-service.d.ts.map +1 -1
- package/dist/services/config-service.js +14 -6
- package/dist/services/config-service.js.map +1 -1
- package/dist/services/event-service.d.ts +12 -0
- package/dist/services/event-service.d.ts.map +1 -1
- package/dist/services/event-service.js +240 -79
- package/dist/services/event-service.js.map +1 -1
- package/dist/services/permission-service.d.ts.map +1 -1
- package/dist/services/permission-service.js +30 -13
- package/dist/services/permission-service.js.map +1 -1
- package/dist/services/retention-service.d.ts +7 -1
- package/dist/services/retention-service.d.ts.map +1 -1
- package/dist/services/retention-service.js +47 -7
- package/dist/services/retention-service.js.map +1 -1
- package/dist/services/role-service.d.ts +7 -0
- package/dist/services/role-service.d.ts.map +1 -1
- package/dist/services/role-service.js +25 -11
- package/dist/services/role-service.js.map +1 -1
- package/dist/services/tenant-service.d.ts +12 -3
- package/dist/services/tenant-service.d.ts.map +1 -1
- package/dist/services/tenant-service.js +119 -52
- package/dist/services/tenant-service.js.map +1 -1
- package/dist/services/webhook-service.d.ts +5 -0
- package/dist/services/webhook-service.d.ts.map +1 -1
- package/dist/services/webhook-service.js +14 -0
- package/dist/services/webhook-service.js.map +1 -1
- package/dist/stratum.d.ts +25 -12
- package/dist/stratum.d.ts.map +1 -1
- package/dist/stratum.js +105 -41
- package/dist/stratum.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,6 +4,7 @@ exports.purgeExpiredData = purgeExpiredData;
|
|
|
4
4
|
exports.purgeTenant = purgeTenant;
|
|
5
5
|
exports.exportTenantData = exportTenantData;
|
|
6
6
|
const core_1 = require("@stratum-hq/core");
|
|
7
|
+
const db_adapters_1 = require("@stratum-hq/db-adapters");
|
|
7
8
|
const pool_helpers_js_1 = require("../pool-helpers.js");
|
|
8
9
|
const DEFAULT_RETENTION_DAYS = 90;
|
|
9
10
|
/**
|
|
@@ -28,18 +29,54 @@ async function purgeExpiredData(pool, retentionDays = DEFAULT_RETENTION_DAYS) {
|
|
|
28
29
|
return { deleted_count: totalDeleted };
|
|
29
30
|
});
|
|
30
31
|
}
|
|
32
|
+
// Guard: reject if tenant has children (FK RESTRICT would crash otherwise)
|
|
33
|
+
async function assertNoChildren(client, tenantId) {
|
|
34
|
+
const childCheck = await client.query(`SELECT COUNT(*)::int AS count FROM tenants WHERE parent_id = $1`, [tenantId]);
|
|
35
|
+
const childCount = childCheck.rows[0].count;
|
|
36
|
+
if (childCount > 0) {
|
|
37
|
+
throw new core_1.StratumError(core_1.ErrorCode.TENANT_HAS_CHILDREN, `Cannot purge tenant ${tenantId}: has ${childCount} child tenant(s). Purge children first.`, 409, { tenant_id: tenantId, child_count: childCount });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Whether purge drops this tenant's schema or database. Only a tenant that was
|
|
42
|
+
* activated owns storage: a pending tenant's provisioning never completed, and
|
|
43
|
+
* may have failed because something else already held the name. The storage
|
|
44
|
+
* name is derived by the same db-adapters helpers provisioning uses.
|
|
45
|
+
*/
|
|
46
|
+
function ownsStorage(tenant, strategy) {
|
|
47
|
+
return tenant !== undefined && tenant.status !== "pending" && tenant.isolation_strategy === strategy;
|
|
48
|
+
}
|
|
31
49
|
/**
|
|
32
50
|
* GDPR Article 17 — Right to Erasure.
|
|
33
|
-
* Hard-delete ALL data belonging to a specific tenant, in correct FK order
|
|
51
|
+
* Hard-delete ALL data belonging to a specific tenant, in correct FK order,
|
|
52
|
+
* including its own schema (SCHEMA_PER_TENANT) or database (DB_PER_TENANT).
|
|
53
|
+
*
|
|
54
|
+
* The schema is dropped in the same transaction as the rows, so both go or
|
|
55
|
+
* neither does. DROP DATABASE cannot run in a transaction, so the database is
|
|
56
|
+
* dropped first: if that fails nothing has changed, and if removing the rows
|
|
57
|
+
* then fails, purging again completes (the drop is IF EXISTS).
|
|
34
58
|
*/
|
|
35
59
|
async function purgeTenant(pool, tenantId) {
|
|
36
|
-
await (0, pool_helpers_js_1.
|
|
37
|
-
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
60
|
+
const before = await (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
61
|
+
await assertNoChildren(client, tenantId);
|
|
62
|
+
const res = await client.query(`SELECT slug, isolation_strategy, status FROM tenants WHERE id = $1`, [tenantId]);
|
|
63
|
+
return res.rows[0];
|
|
64
|
+
});
|
|
65
|
+
if (ownsStorage(before, "DB_PER_TENANT")) {
|
|
66
|
+
const client = await pool.connect();
|
|
67
|
+
try {
|
|
68
|
+
await (0, db_adapters_1.dropDatabase)(client, before.slug);
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
client.release();
|
|
42
72
|
}
|
|
73
|
+
}
|
|
74
|
+
await (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
|
|
75
|
+
// Lock the row first so a concurrent create under this tenant (which holds
|
|
76
|
+
// it FOR SHARE) finishes and is counted, rather than failing the delete.
|
|
77
|
+
const res = await client.query(`SELECT slug, isolation_strategy, status FROM tenants WHERE id = $1 FOR UPDATE`, [tenantId]);
|
|
78
|
+
const tenant = res.rows[0];
|
|
79
|
+
await assertNoChildren(client, tenantId);
|
|
43
80
|
// Delete in FK-safe order (children before parents)
|
|
44
81
|
await client.query(`DELETE FROM config_entries WHERE tenant_id = $1`, [tenantId]);
|
|
45
82
|
await client.query(`DELETE FROM permission_policies WHERE tenant_id = $1`, [tenantId]);
|
|
@@ -56,6 +93,9 @@ async function purgeTenant(pool, tenantId) {
|
|
|
56
93
|
await client.query(`DELETE FROM audit_logs WHERE tenant_id = $1 OR (resource_type = 'tenant' AND resource_id = $1::text)`, [tenantId]);
|
|
57
94
|
// Finally, the tenant itself
|
|
58
95
|
await client.query(`DELETE FROM tenants WHERE id = $1`, [tenantId]);
|
|
96
|
+
if (ownsStorage(tenant, "SCHEMA_PER_TENANT")) {
|
|
97
|
+
await (0, db_adapters_1.dropSchema)(client, tenant.slug);
|
|
98
|
+
}
|
|
59
99
|
});
|
|
60
100
|
}
|
|
61
101
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"retention-service.js","sourceRoot":"","sources":["../../src/services/retention-service.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"retention-service.js","sourceRoot":"","sources":["../../src/services/retention-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -37,6 +37,11 @@ export declare function removeRoleFromKey(pool: pg.Pool, keyId: string): Promise
|
|
|
37
37
|
* asks. Assigning a role therefore governs authorization everywhere.
|
|
38
38
|
*/
|
|
39
39
|
export declare function resolveEffectiveScopes(pool: pg.Pool, keyId: string): Promise<string[]>;
|
|
40
|
+
/**
|
|
41
|
+
* {@link resolveEffectiveScopes} on a client the caller already holds, so a
|
|
42
|
+
* caller inside withClient does not need a second pool connection.
|
|
43
|
+
*/
|
|
44
|
+
export declare function resolveEffectiveScopesOnClient(client: pg.PoolClient, keyId: string): Promise<string[]>;
|
|
40
45
|
/**
|
|
41
46
|
* Resolve effective scopes for an API key. Thin alias over
|
|
42
47
|
* {@link resolveEffectiveScopes}, kept as the public facade name.
|
|
@@ -59,6 +64,8 @@ export declare function resolveKeyScopes(pool: pg.Pool, keyId: string): Promise<
|
|
|
59
64
|
* authorizing the assignment.
|
|
60
65
|
*/
|
|
61
66
|
export declare function assignRole(pool: pg.Pool, principalType: string, principalId: string, roleId: string, tenantId?: string): Promise<boolean>;
|
|
67
|
+
/** The role currently assigned to a principal, or null when it has none. */
|
|
68
|
+
export declare function getPrincipalRoleId(pool: pg.Pool, principalType: string, principalId: string): Promise<string | null>;
|
|
62
69
|
/** Remove a principal's role assignment. Returns true if one was removed. */
|
|
63
70
|
export declare function removeRole(pool: pg.Pool, principalType: string, principalId: string): Promise<boolean>;
|
|
64
71
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-service.d.ts","sourceRoot":"","sources":["../../src/services/role-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAGpB,MAAM,WAAW,IAAI;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,IAAI,CAAC;IACjB,UAAU,EAAE,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC3B;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAUrF;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAQ7E;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC,CAcjF;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC,CAoCxG;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ5E;AAED,wBAAsB,eAAe,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQpG;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAQtF;AAED;;;;;;;;;GASG;AACH,wBAAsB,sBAAsB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"role-service.d.ts","sourceRoot":"","sources":["../../src/services/role-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,8 +8,10 @@ exports.deleteRole = deleteRole;
|
|
|
8
8
|
exports.assignRoleToKey = assignRoleToKey;
|
|
9
9
|
exports.removeRoleFromKey = removeRoleFromKey;
|
|
10
10
|
exports.resolveEffectiveScopes = resolveEffectiveScopes;
|
|
11
|
+
exports.resolveEffectiveScopesOnClient = resolveEffectiveScopesOnClient;
|
|
11
12
|
exports.resolveKeyScopes = resolveKeyScopes;
|
|
12
13
|
exports.assignRole = assignRole;
|
|
14
|
+
exports.getPrincipalRoleId = getPrincipalRoleId;
|
|
13
15
|
exports.removeRole = removeRole;
|
|
14
16
|
exports.resolvePrincipalScopes = resolvePrincipalScopes;
|
|
15
17
|
const pool_helpers_js_1 = require("../pool-helpers.js");
|
|
@@ -94,17 +96,22 @@ async function removeRoleFromKey(pool, keyId) {
|
|
|
94
96
|
* asks. Assigning a role therefore governs authorization everywhere.
|
|
95
97
|
*/
|
|
96
98
|
async function resolveEffectiveScopes(pool, keyId) {
|
|
97
|
-
return (0, pool_helpers_js_1.withClient)(pool,
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
99
|
+
return (0, pool_helpers_js_1.withClient)(pool, (client) => resolveEffectiveScopesOnClient(client, keyId));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* {@link resolveEffectiveScopes} on a client the caller already holds, so a
|
|
103
|
+
* caller inside withClient does not need a second pool connection.
|
|
104
|
+
*/
|
|
105
|
+
async function resolveEffectiveScopesOnClient(client, keyId) {
|
|
106
|
+
const res = await client.query(`SELECT ak.scopes, r.scopes as role_scopes
|
|
107
|
+
FROM api_keys ak
|
|
108
|
+
LEFT JOIN roles r ON r.id = ak.role_id
|
|
109
|
+
WHERE ak.id = $1 AND ak.revoked_at IS NULL`, [keyId]);
|
|
110
|
+
if (res.rows.length === 0)
|
|
111
|
+
return ["read"];
|
|
112
|
+
const row = res.rows[0];
|
|
113
|
+
// Role scopes override key scopes when a role is assigned.
|
|
114
|
+
return row.role_scopes ?? row.scopes ?? ["read"];
|
|
108
115
|
}
|
|
109
116
|
/**
|
|
110
117
|
* Resolve effective scopes for an API key. Thin alias over
|
|
@@ -144,6 +151,13 @@ async function assignRole(pool, principalType, principalId, roleId, tenantId) {
|
|
|
144
151
|
return res.rows.length > 0;
|
|
145
152
|
});
|
|
146
153
|
}
|
|
154
|
+
/** The role currently assigned to a principal, or null when it has none. */
|
|
155
|
+
async function getPrincipalRoleId(pool, principalType, principalId) {
|
|
156
|
+
return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
157
|
+
const res = await client.query(`SELECT role_id FROM principal_roles WHERE principal_type = $1 AND principal_id = $2`, [principalType, principalId]);
|
|
158
|
+
return res.rows[0]?.role_id ?? null;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
147
161
|
/** Remove a principal's role assignment. Returns true if one was removed. */
|
|
148
162
|
async function removeRole(pool, principalType, principalId) {
|
|
149
163
|
return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"role-service.js","sourceRoot":"","sources":["../../src/services/role-service.ts"],"names":[],"mappings":";;AA0BA,gCAUC;AAED,0BAQC;AAED,8BAcC;AAED,gCAoCC;AAED,gCAQC;AAED,0CAQC;AAED,8CAQC;AAYD,
|
|
1
|
+
{"version":3,"file":"role-service.js","sourceRoot":"","sources":["../../src/services/role-service.ts"],"names":[],"mappings":";;AA0BA,gCAUC;AAED,0BAQC;AAED,8BAcC;AAED,gCAoCC;AAED,gCAQC;AAED,0CAQC;AAED,8CAQC;AAYD,wDAEC;AAMD,wEAYC;AAMD,4CAEC;AAkBD,gCAuBC;AAGD,gDAYC;AAGD,gCAcC;AAaD,wDAkBC;AAjRD,wDAAiE;AAyB1D,KAAK,UAAU,UAAU,CAAC,IAAa,EAAE,KAAsB;IACpE,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B;;mBAEa,EACb,CAAC,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,WAAW,IAAI,IAAI,EAAE,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,SAAS,IAAI,IAAI,CAAC,CAC/E,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,OAAO,CAAC,IAAa,EAAE,EAAU;IACrD,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,mCAAmC,EACnC,CAAC,EAAE,CAAC,CACL,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,SAAS,CAAC,IAAa,EAAE,QAAiB;IAC9D,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,IAAI,QAAQ,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,iFAAiF,EACjF,CAAC,QAAQ,CAAC,CACX,CAAC;YACF,OAAO,GAAG,CAAC,IAAI,CAAC;QAClB,CAAC;QACD,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,uCAAuC,CACxC,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC;IAClB,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,UAAU,CAAC,IAAa,EAAE,EAAU,EAAE,KAAsB;IAChF,OAAO,IAAA,iCAAe,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QAC5C,MAAM,QAAQ,GAAG,MAAM,MAAM,CAAC,KAAK,CACjC,mCAAmC,EACnC,CAAC,EAAE,CAAC,CACL,CAAC;QACF,IAAI,QAAQ,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAE5C,MAAM,IAAI,GAAa,EAAE,CAAC;QAC1B,MAAM,MAAM,GAAc,EAAE,CAAC;QAC7B,IAAI,GAAG,GAAG,CAAC,CAAC;QAEZ,IAAI,KAAK,CAAC,IAAI,KAAK,SAAS,EAAE,CAAC;YAC7B,IAAI,CAAC,IAAI,CAAC,WAAW,GAAG,EAAE,EAAE,CAAC,CAAC;YAC9B,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC1B,CAAC;QACD,IAAI,KAAK,CAAC,WAAW,KAAK,SAAS,EAAE,CAAC;YACpC,IAAI,CAAC,IAAI,CAAC,kBAAkB,GAAG,EAAE,EAAE,CAAC,CAAC;YACrC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,KAAK,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YAC/B,IAAI,CAAC,IAAI,CAAC,aAAa,GAAG,EAAE,EAAE,CAAC,CAAC;YAChC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAC5B,CAAC;QAED,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAE/C,IAAI,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC;QAChC,MAAM,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QAEhB,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,oBAAoB,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,GAAG,cAAc,EACpE,MAAM,CACP,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACrB,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,UAAU,CAAC,IAAa,EAAE,EAAU;IACxD,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,8CAA8C,EAC9C,CAAC,EAAE,CAAC,CACL,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,eAAe,CAAC,IAAa,EAAE,KAAa,EAAE,MAAc;IAChF,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,oFAAoF,EACpF,CAAC,MAAM,EAAE,KAAK,CAAC,CAChB,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAEM,KAAK,UAAU,iBAAiB,CAAC,IAAa,EAAE,KAAa;IAClE,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,sFAAsF,EACtF,CAAC,KAAK,CAAC,CACR,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;GASG;AACI,KAAK,UAAU,sBAAsB,CAAC,IAAa,EAAE,KAAa;IACvE,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,8BAA8B,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AACrF,CAAC;AAED;;;GAGG;AACI,KAAK,UAAU,8BAA8B,CAAC,MAAqB,EAAE,KAAa;IACvF,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B;;;gDAG4C,EAC5C,CAAC,KAAK,CAAC,CACR,CAAC;IACF,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IACxB,2DAA2D;IAC3D,OAAO,GAAG,CAAC,WAAW,IAAI,GAAG,CAAC,MAAM,IAAI,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED;;;GAGG;AACH,SAAgB,gBAAgB,CAAC,IAAa,EAAE,KAAa;IAC3D,OAAO,sBAAsB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;;;;;;;;;;GAeG;AACI,KAAK,UAAU,UAAU,CAC9B,IAAa,EACb,aAAqB,EACrB,WAAmB,EACnB,MAAc,EACd,QAAiB;IAEjB,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B;;;;;;;;;yBASmB,EACnB,CAAC,aAAa,EAAE,WAAW,EAAE,MAAM,EAAE,QAAQ,IAAI,IAAI,CAAC,CACvD,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAED,4EAA4E;AACrE,KAAK,UAAU,kBAAkB,CACtC,IAAa,EACb,aAAqB,EACrB,WAAmB;IAEnB,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B,qFAAqF,EACrF,CAAC,aAAa,EAAE,WAAW,CAAC,CAC7B,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,OAAO,IAAI,IAAI,CAAC;IACtC,CAAC,CAAC,CAAC;AACL,CAAC;AAED,6EAA6E;AACtE,KAAK,UAAU,UAAU,CAC9B,IAAa,EACb,aAAqB,EACrB,WAAmB;IAEnB,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B;;yBAEmB,EACnB,CAAC,aAAa,EAAE,WAAW,CAAC,CAC7B,CAAC;QACF,OAAO,GAAG,CAAC,IAAI,CAAC,MAAM,GAAG,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;;;;GAUG;AACI,KAAK,UAAU,sBAAsB,CAC1C,IAAa,EACb,aAAqB,EACrB,WAAmB,EACnB,QAAiB;IAEjB,OAAO,IAAA,4BAAU,EAAC,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;QACvC,MAAM,GAAG,GAAG,MAAM,MAAM,CAAC,KAAK,CAC5B;;;;2EAIqE,EACrE,CAAC,aAAa,EAAE,WAAW,EAAE,QAAQ,IAAI,IAAI,CAAC,CAC/C,CAAC;QACF,IAAI,GAAG,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,EAAE,CAAC;QACrC,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,IAAI,EAAE,CAAC;IAClC,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import pg from "pg";
|
|
2
|
-
import { type TenantNode, type CreateTenantInput, type UpdateTenantInput, type PaginationInput, type PaginatedResult } from "@stratum-hq/core";
|
|
2
|
+
import { type TenantNode, type CreateTenantInput, type UpdateTenantInput, type PaginationInput, type PaginatedResult, type TenantStatus } from "@stratum-hq/core";
|
|
3
3
|
export declare function createTenant(pool: pg.Pool, input: CreateTenantInput): Promise<TenantNode>;
|
|
4
4
|
export declare function getTenant(pool: pg.Pool, id: string, includeArchived?: boolean): Promise<TenantNode>;
|
|
5
5
|
/**
|
|
@@ -10,11 +10,15 @@ export declare function getTenant(pool: pg.Pool, id: string, includeArchived?: b
|
|
|
10
10
|
*
|
|
11
11
|
* Mirrors getTenant's not-found and state contract: throws TenantNotFoundError
|
|
12
12
|
* when no row matches, and — unless `includeArchived` is set — TenantArchivedError
|
|
13
|
-
* / TenantSuspendedError for a non-active row. `includeArchived` is the "give me
|
|
13
|
+
* / TenantSuspendedError / TenantPendingError for a non-active row. `includeArchived` is the "give me
|
|
14
14
|
* the row whatever its state" escape hatch and bypasses both non-active states.
|
|
15
15
|
*/
|
|
16
16
|
export declare function getTenantBySlug(pool: pg.Pool, slug: string, includeArchived?: boolean): Promise<TenantNode>;
|
|
17
|
-
|
|
17
|
+
/**
|
|
18
|
+
* List tenants in one status, `active` by default. Pending, suspended and
|
|
19
|
+
* archived tenants appear only when that status is asked for.
|
|
20
|
+
*/
|
|
21
|
+
export declare function listTenants(pool: pg.Pool, pagination: PaginationInput, status?: TenantStatus): Promise<PaginatedResult<TenantNode>>;
|
|
18
22
|
export declare function updateTenant(pool: pg.Pool, id: string, patch: UpdateTenantInput): Promise<TenantNode>;
|
|
19
23
|
/**
|
|
20
24
|
* Suspend an active tenant: reversible block on access. Rejects if the tenant
|
|
@@ -32,6 +36,11 @@ export declare function archiveTenant(pool: pg.Pool, id: string): Promise<Tenant
|
|
|
32
36
|
* its parent is not active. Resuming is top-down.
|
|
33
37
|
*/
|
|
34
38
|
export declare function resumeTenant(pool: pg.Pool, id: string): Promise<TenantNode>;
|
|
39
|
+
/**
|
|
40
|
+
* Activate a pending tenant once its schema or database has been provisioned.
|
|
41
|
+
* Rejects if the tenant is not pending, or if its parent is not active.
|
|
42
|
+
*/
|
|
43
|
+
export declare function activateTenant(pool: pg.Pool, id: string): Promise<TenantNode>;
|
|
35
44
|
/**
|
|
36
45
|
* Soft-delete a tenant.
|
|
37
46
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tenant-service.d.ts","sourceRoot":"","sources":["../../src/services/tenant-service.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,IAAI,CAAC;AAEpB,OAAO,EACL,KAAK,UAAU,EACf,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACtB,KAAK,eAAe,EACpB,KAAK,eAAe,
|
|
1
|
+
{"version":3,"file":"tenant-service.d.ts","sourceRoot":"","sources":["../../src/services/tenant-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -8,6 +8,7 @@ exports.updateTenant = updateTenant;
|
|
|
8
8
|
exports.suspendTenant = suspendTenant;
|
|
9
9
|
exports.archiveTenant = archiveTenant;
|
|
10
10
|
exports.resumeTenant = resumeTenant;
|
|
11
|
+
exports.activateTenant = activateTenant;
|
|
11
12
|
exports.deleteTenant = deleteTenant;
|
|
12
13
|
exports.moveTenant = moveTenant;
|
|
13
14
|
exports.batchCreateTenants = batchCreateTenants;
|
|
@@ -18,30 +19,62 @@ exports.getChildren = getChildren;
|
|
|
18
19
|
exports.reorderTenant = reorderTenant;
|
|
19
20
|
const pool_helpers_js_1 = require("../pool-helpers.js");
|
|
20
21
|
const core_1 = require("@stratum-hq/core");
|
|
22
|
+
/**
|
|
23
|
+
* Serialize changes to the tree's shape on one advisory key. A move takes it
|
|
24
|
+
* exclusively: it checks for cycles against the destination's ancestry and
|
|
25
|
+
* rewrites the paths of a whole subtree, so nothing else may read or write
|
|
26
|
+
* ancestry while it runs. Creates and lifecycle transitions take it shared, so
|
|
27
|
+
* they still run alongside each other (row locks order those) but never read a
|
|
28
|
+
* parent path that an in-flight move is about to rewrite. Take it before
|
|
29
|
+
* reading any row the change depends on.
|
|
30
|
+
*/
|
|
31
|
+
async function lockTree(client, mode) {
|
|
32
|
+
const fn = mode === "exclusive" ? "pg_advisory_xact_lock" : "pg_advisory_xact_lock_shared";
|
|
33
|
+
await client.query(`SELECT ${fn}(('x' || substr(md5('stratum.tenant_tree'), 1, 16))::bit(64)::bigint)`);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Load the parent for a tenant being created or moved, and require it to be
|
|
37
|
+
* active: a suspended or archived subtree must not grow, so an active tenant
|
|
38
|
+
* always has an active parent. FOR SHARE holds the parent row until commit, so
|
|
39
|
+
* a concurrent suspend or archive of the parent (which locks it FOR UPDATE)
|
|
40
|
+
* waits for this transaction and then sees the new child.
|
|
41
|
+
*/
|
|
42
|
+
async function loadActiveParent(client, parentId) {
|
|
43
|
+
const res = await client.query(`SELECT * FROM tenants WHERE id = $1 FOR SHARE`, [parentId]);
|
|
44
|
+
if (res.rows.length === 0) {
|
|
45
|
+
throw new core_1.TenantNotFoundError(parentId);
|
|
46
|
+
}
|
|
47
|
+
const parent = res.rows[0];
|
|
48
|
+
if (parent.status === "archived") {
|
|
49
|
+
throw new core_1.TenantArchivedError(parentId);
|
|
50
|
+
}
|
|
51
|
+
if (parent.status === "suspended") {
|
|
52
|
+
throw new core_1.TenantSuspendedError(parentId);
|
|
53
|
+
}
|
|
54
|
+
if (parent.status === "pending") {
|
|
55
|
+
throw new core_1.TenantPendingError(parentId);
|
|
56
|
+
}
|
|
57
|
+
return parent;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* A tenant with its own schema or database starts `pending` and is activated
|
|
61
|
+
* (activateTenant) once that storage has been provisioned, so it is never
|
|
62
|
+
* usable without it. SHARED_RLS tenants need no storage and start `active`.
|
|
63
|
+
*/
|
|
64
|
+
function initialStatus(input) {
|
|
65
|
+
const strategy = input.isolation_strategy ?? "SHARED_RLS";
|
|
66
|
+
return strategy === "SCHEMA_PER_TENANT" || strategy === "DB_PER_TENANT" ? "pending" : "active";
|
|
67
|
+
}
|
|
21
68
|
async function createTenant(pool, input) {
|
|
22
69
|
return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
|
|
23
|
-
// Advisory lock on parent to serialize sibling inserts
|
|
24
70
|
if (input.parent_id) {
|
|
25
|
-
await client
|
|
26
|
-
|
|
27
|
-
const parentRes = await client.query(`SELECT * FROM tenants WHERE id = $1`, [input.parent_id]);
|
|
28
|
-
if (parentRes.rows.length === 0) {
|
|
29
|
-
throw new core_1.TenantNotFoundError(input.parent_id);
|
|
30
|
-
}
|
|
31
|
-
if (parentRes.rows[0].status === "archived") {
|
|
32
|
-
throw new core_1.TenantArchivedError(input.parent_id);
|
|
33
|
-
}
|
|
34
|
-
// A suspended or archived subtree must not grow: an active tenant always
|
|
35
|
-
// has an active parent. See suspendTenant/resumeTenant for the inverse.
|
|
36
|
-
if (parentRes.rows[0].status === "suspended") {
|
|
37
|
-
throw new core_1.TenantSuspendedError(input.parent_id);
|
|
38
|
-
}
|
|
39
|
-
const parent = parentRes.rows[0];
|
|
71
|
+
await lockTree(client, "shared");
|
|
72
|
+
const parent = await loadActiveParent(client, input.parent_id);
|
|
40
73
|
const ancestry_path = (0, core_1.appendToPath)(parent.ancestry_path, parent.id);
|
|
41
74
|
// Inherit region_id from parent if not explicitly provided
|
|
42
75
|
const regionId = input.region_id ?? parent.region_id ?? null;
|
|
43
76
|
const res = await client.query(`INSERT INTO tenants (parent_id, ancestry_path, depth, name, slug, config, metadata, isolation_strategy, status, region_id)
|
|
44
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8,
|
|
77
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
45
78
|
RETURNING *`, [
|
|
46
79
|
input.parent_id,
|
|
47
80
|
ancestry_path,
|
|
@@ -51,6 +84,7 @@ async function createTenant(pool, input) {
|
|
|
51
84
|
JSON.stringify(input.config ?? {}),
|
|
52
85
|
JSON.stringify(input.metadata ?? {}),
|
|
53
86
|
input.isolation_strategy ?? "SHARED_RLS",
|
|
87
|
+
initialStatus(input),
|
|
54
88
|
regionId,
|
|
55
89
|
]);
|
|
56
90
|
return res.rows[0];
|
|
@@ -59,13 +93,14 @@ async function createTenant(pool, input) {
|
|
|
59
93
|
// Root tenant — no parent lock needed
|
|
60
94
|
const rootRegionId = input.region_id ?? null;
|
|
61
95
|
const res = await client.query(`INSERT INTO tenants (parent_id, ancestry_path, depth, name, slug, config, metadata, isolation_strategy, status, region_id)
|
|
62
|
-
VALUES (NULL, '/', 0, $1, $2, $3, $4, $5,
|
|
96
|
+
VALUES (NULL, '/', 0, $1, $2, $3, $4, $5, $6, $7)
|
|
63
97
|
RETURNING *`, [
|
|
64
98
|
input.name,
|
|
65
99
|
input.slug,
|
|
66
100
|
JSON.stringify(input.config ?? {}),
|
|
67
101
|
JSON.stringify(input.metadata ?? {}),
|
|
68
102
|
input.isolation_strategy ?? "SHARED_RLS",
|
|
103
|
+
initialStatus(input),
|
|
69
104
|
rootRegionId,
|
|
70
105
|
]);
|
|
71
106
|
return res.rows[0];
|
|
@@ -90,12 +125,15 @@ async function getTenant(pool, id, includeArchived = false) {
|
|
|
90
125
|
if (!includeArchived && tenant.status === "archived") {
|
|
91
126
|
throw new core_1.TenantArchivedError(id);
|
|
92
127
|
}
|
|
93
|
-
// Suspended tenants are blocked from normal access too.
|
|
94
|
-
// is the "give me the row whatever its state" escape
|
|
95
|
-
//
|
|
128
|
+
// Suspended and pending tenants are blocked from normal access too.
|
|
129
|
+
// `includeArchived` is the "give me the row whatever its state" escape
|
|
130
|
+
// hatch and bypasses every non-active state.
|
|
96
131
|
if (!includeArchived && tenant.status === "suspended") {
|
|
97
132
|
throw new core_1.TenantSuspendedError(id);
|
|
98
133
|
}
|
|
134
|
+
if (!includeArchived && tenant.status === "pending") {
|
|
135
|
+
throw new core_1.TenantPendingError(id);
|
|
136
|
+
}
|
|
99
137
|
return tenant;
|
|
100
138
|
});
|
|
101
139
|
}
|
|
@@ -107,7 +145,7 @@ async function getTenant(pool, id, includeArchived = false) {
|
|
|
107
145
|
*
|
|
108
146
|
* Mirrors getTenant's not-found and state contract: throws TenantNotFoundError
|
|
109
147
|
* when no row matches, and — unless `includeArchived` is set — TenantArchivedError
|
|
110
|
-
* / TenantSuspendedError for a non-active row. `includeArchived` is the "give me
|
|
148
|
+
* / TenantSuspendedError / TenantPendingError for a non-active row. `includeArchived` is the "give me
|
|
111
149
|
* the row whatever its state" escape hatch and bypasses both non-active states.
|
|
112
150
|
*/
|
|
113
151
|
async function getTenantBySlug(pool, slug, includeArchived = false) {
|
|
@@ -124,24 +162,31 @@ async function getTenantBySlug(pool, slug, includeArchived = false) {
|
|
|
124
162
|
if (!includeArchived && tenant.status === "suspended") {
|
|
125
163
|
throw new core_1.TenantSuspendedError(tenant.id);
|
|
126
164
|
}
|
|
165
|
+
if (!includeArchived && tenant.status === "pending") {
|
|
166
|
+
throw new core_1.TenantPendingError(tenant.id);
|
|
167
|
+
}
|
|
127
168
|
return tenant;
|
|
128
169
|
});
|
|
129
170
|
}
|
|
130
|
-
|
|
171
|
+
/**
|
|
172
|
+
* List tenants in one status, `active` by default. Pending, suspended and
|
|
173
|
+
* archived tenants appear only when that status is asked for.
|
|
174
|
+
*/
|
|
175
|
+
async function listTenants(pool, pagination, status = "active") {
|
|
131
176
|
return (0, pool_helpers_js_1.withClient)(pool, async (client) => {
|
|
132
177
|
const limit = pagination.limit ?? 50;
|
|
133
178
|
let res;
|
|
134
179
|
if (pagination.cursor) {
|
|
135
180
|
res = await client.query(`SELECT * FROM tenants
|
|
136
|
-
WHERE status =
|
|
181
|
+
WHERE status = $3 AND id > $1
|
|
137
182
|
ORDER BY id ASC
|
|
138
|
-
LIMIT $2`, [pagination.cursor, limit + 1]);
|
|
183
|
+
LIMIT $2`, [pagination.cursor, limit + 1, status]);
|
|
139
184
|
}
|
|
140
185
|
else {
|
|
141
186
|
res = await client.query(`SELECT * FROM tenants
|
|
142
|
-
WHERE status =
|
|
187
|
+
WHERE status = $2
|
|
143
188
|
ORDER BY id ASC
|
|
144
|
-
LIMIT $1`, [limit + 1]);
|
|
189
|
+
LIMIT $1`, [limit + 1, status]);
|
|
145
190
|
}
|
|
146
191
|
const rows = res.rows;
|
|
147
192
|
const has_more = rows.length > limit;
|
|
@@ -164,6 +209,14 @@ async function updateTenant(pool, id, patch) {
|
|
|
164
209
|
values.push(patch.name);
|
|
165
210
|
}
|
|
166
211
|
if (patch.slug !== undefined) {
|
|
212
|
+
// Schema- and database-per-tenant storage is named from the slug, so
|
|
213
|
+
// changing it would detach the tenant from its storage and free the old
|
|
214
|
+
// name for another tenant.
|
|
215
|
+
const strategy = existing.rows[0].isolation_strategy;
|
|
216
|
+
if (patch.slug !== existing.rows[0].slug &&
|
|
217
|
+
(strategy === "SCHEMA_PER_TENANT" || strategy === "DB_PER_TENANT")) {
|
|
218
|
+
throw new core_1.ValidationError(`Slug cannot be changed for a ${strategy} tenant`, { tenant_id: id, isolation_strategy: strategy });
|
|
219
|
+
}
|
|
167
220
|
sets.push(`slug = $${idx++}`);
|
|
168
221
|
values.push(patch.slug);
|
|
169
222
|
}
|
|
@@ -194,8 +247,10 @@ async function updateTenant(pool, id, patch) {
|
|
|
194
247
|
// ---------------------------------------------------------------------------
|
|
195
248
|
// Tenant lifecycle
|
|
196
249
|
//
|
|
197
|
-
// States: active -> {suspended, archived} -> (purged). Transitions:
|
|
198
|
-
// createTenant (none) -> active
|
|
250
|
+
// States: [pending ->] active -> {suspended, archived} -> (purged). Transitions:
|
|
251
|
+
// createTenant (none) -> active, or pending for a tenant with
|
|
252
|
+
// its own schema or database
|
|
253
|
+
// activateTenant pending -> active (storage provisioned)
|
|
199
254
|
// suspendTenant active -> suspended (reversible, blocks access)
|
|
200
255
|
// resumeTenant suspended | archived -> active (reverses suspend/archive)
|
|
201
256
|
// archiveTenant active | suspended -> archived (soft delete, reversible)
|
|
@@ -208,8 +263,8 @@ async function updateTenant(pool, id, patch) {
|
|
|
208
263
|
// Descendant rules, tested against real Postgres in packages/integration-tests:
|
|
209
264
|
// * suspend / archive block when the tenant has an ACTIVE child
|
|
210
265
|
// (TenantHasChildrenError). They act leaf-first and never cascade.
|
|
211
|
-
// * resume / create require the parent to be ACTIVE, so
|
|
212
|
-
// active tenant's parent is active" always holds.
|
|
266
|
+
// * resume / activate / create / move require the parent to be ACTIVE, so
|
|
267
|
+
// the invariant "an active tenant's parent is active" always holds.
|
|
213
268
|
// * purge requires an empty subtree (no children of ANY status).
|
|
214
269
|
// ---------------------------------------------------------------------------
|
|
215
270
|
/**
|
|
@@ -218,7 +273,11 @@ async function updateTenant(pool, id, patch) {
|
|
|
218
273
|
* suspended rows, because lifecycle operations act on exactly those states.
|
|
219
274
|
*/
|
|
220
275
|
async function loadForTransition(client, id) {
|
|
221
|
-
|
|
276
|
+
// Transitions read the tree (children, parent) to keep "an active tenant's
|
|
277
|
+
// parent is active", so they must not interleave with a move. FOR UPDATE
|
|
278
|
+
// then orders them against creates and other transitions on this tenant.
|
|
279
|
+
await lockTree(client, "shared");
|
|
280
|
+
const res = await client.query(`SELECT * FROM tenants WHERE id = $1 FOR UPDATE`, [id]);
|
|
222
281
|
if (res.rows.length === 0) {
|
|
223
282
|
throw new core_1.TenantNotFoundError(id);
|
|
224
283
|
}
|
|
@@ -278,7 +337,9 @@ async function resumeTenant(pool, id) {
|
|
|
278
337
|
throw new core_1.InvalidTenantStateError(id, tenant.status, "resume", ["suspended", "archived"]);
|
|
279
338
|
}
|
|
280
339
|
if (tenant.parent_id) {
|
|
281
|
-
|
|
340
|
+
// FOR SHARE: a concurrent suspend or archive of the parent waits for
|
|
341
|
+
// this resume and then sees an active child.
|
|
342
|
+
const parentRes = await client.query(`SELECT status FROM tenants WHERE id = $1 FOR SHARE`, [tenant.parent_id]);
|
|
282
343
|
const parentStatus = parentRes.rows[0]?.status;
|
|
283
344
|
if (parentStatus === "archived") {
|
|
284
345
|
throw new core_1.TenantArchivedError(tenant.parent_id);
|
|
@@ -291,6 +352,23 @@ async function resumeTenant(pool, id) {
|
|
|
291
352
|
return res.rows[0];
|
|
292
353
|
});
|
|
293
354
|
}
|
|
355
|
+
/**
|
|
356
|
+
* Activate a pending tenant once its schema or database has been provisioned.
|
|
357
|
+
* Rejects if the tenant is not pending, or if its parent is not active.
|
|
358
|
+
*/
|
|
359
|
+
async function activateTenant(pool, id) {
|
|
360
|
+
return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
|
|
361
|
+
const tenant = await loadForTransition(client, id);
|
|
362
|
+
if (tenant.status !== "pending") {
|
|
363
|
+
throw new core_1.InvalidTenantStateError(id, tenant.status, "activate", ["pending"]);
|
|
364
|
+
}
|
|
365
|
+
if (tenant.parent_id) {
|
|
366
|
+
await loadActiveParent(client, tenant.parent_id);
|
|
367
|
+
}
|
|
368
|
+
const res = await client.query(`UPDATE tenants SET status = 'active', updated_at = now() WHERE id = $1 RETURNING *`, [id]);
|
|
369
|
+
return res.rows[0];
|
|
370
|
+
});
|
|
371
|
+
}
|
|
294
372
|
/**
|
|
295
373
|
* Soft-delete a tenant.
|
|
296
374
|
*
|
|
@@ -302,21 +380,14 @@ async function deleteTenant(pool, id) {
|
|
|
302
380
|
}
|
|
303
381
|
async function moveTenant(pool, id, newParentId) {
|
|
304
382
|
return (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
|
|
305
|
-
|
|
306
|
-
await client.query(`SELECT pg_advisory_xact_lock(('x' || substr(md5($1::text), 1, 16))::bit(64)::bigint)`, [id]);
|
|
307
|
-
await client.query(`SELECT pg_advisory_xact_lock(('x' || substr(md5($1::text), 1, 16))::bit(64)::bigint)`, [newParentId]);
|
|
383
|
+
await lockTree(client, "exclusive");
|
|
308
384
|
// Load tenant being moved
|
|
309
385
|
const tenantRes = await client.query(`SELECT * FROM tenants WHERE id = $1 AND status != 'archived'`, [id]);
|
|
310
386
|
if (tenantRes.rows.length === 0) {
|
|
311
387
|
throw new core_1.TenantNotFoundError(id);
|
|
312
388
|
}
|
|
313
389
|
const tenant = tenantRes.rows[0];
|
|
314
|
-
|
|
315
|
-
const newParentRes = await client.query(`SELECT * FROM tenants WHERE id = $1 AND status != 'archived'`, [newParentId]);
|
|
316
|
-
if (newParentRes.rows.length === 0) {
|
|
317
|
-
throw new core_1.TenantNotFoundError(newParentId);
|
|
318
|
-
}
|
|
319
|
-
const newParent = newParentRes.rows[0];
|
|
390
|
+
const newParent = await loadActiveParent(client, newParentId);
|
|
320
391
|
// Cycle detection: newParent must not be a descendant of tenant.
|
|
321
392
|
// Check if the moving tenant's ID appears in the new parent's ancestry_path
|
|
322
393
|
// (which would mean the new parent is a descendant of the moving tenant).
|
|
@@ -373,6 +444,7 @@ async function batchCreateTenants(pool, inputs) {
|
|
|
373
444
|
const errors = [];
|
|
374
445
|
try {
|
|
375
446
|
await (0, pool_helpers_js_1.withTransaction)(pool, async (client) => {
|
|
447
|
+
await lockTree(client, "shared");
|
|
376
448
|
// Map of slug → created tenant for intra-batch parent references
|
|
377
449
|
const slugMap = new Map();
|
|
378
450
|
for (let i = 0; i < inputs.length; i++) {
|
|
@@ -386,19 +458,12 @@ async function batchCreateTenants(pool, inputs) {
|
|
|
386
458
|
parentNode = batchParent;
|
|
387
459
|
}
|
|
388
460
|
else {
|
|
389
|
-
|
|
390
|
-
const parentRes = await client.query(`SELECT * FROM tenants WHERE id = $1 AND status != 'archived'`, [parentId]);
|
|
391
|
-
if (parentRes.rows.length === 0) {
|
|
392
|
-
throw new core_1.TenantNotFoundError(parentId);
|
|
393
|
-
}
|
|
394
|
-
parentNode = parentRes.rows[0];
|
|
461
|
+
parentNode = await loadActiveParent(client, parentId);
|
|
395
462
|
}
|
|
396
|
-
// Advisory lock on parent
|
|
397
|
-
await client.query(`SELECT pg_advisory_xact_lock(('x' || substr(md5($1::text), 1, 16))::bit(64)::bigint)`, [parentId]);
|
|
398
463
|
const ancestry_path = (0, core_1.appendToPath)(parentNode.ancestry_path, parentNode.id);
|
|
399
464
|
const regionId = input.region_id ?? parentNode.region_id ?? null;
|
|
400
465
|
const res = await client.query(`INSERT INTO tenants (parent_id, ancestry_path, depth, name, slug, config, metadata, isolation_strategy, status, region_id)
|
|
401
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8,
|
|
466
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
402
467
|
RETURNING *`, [
|
|
403
468
|
parentId,
|
|
404
469
|
ancestry_path,
|
|
@@ -408,6 +473,7 @@ async function batchCreateTenants(pool, inputs) {
|
|
|
408
473
|
JSON.stringify(input.config ?? {}),
|
|
409
474
|
JSON.stringify(input.metadata ?? {}),
|
|
410
475
|
input.isolation_strategy ?? "SHARED_RLS",
|
|
476
|
+
initialStatus(input),
|
|
411
477
|
regionId,
|
|
412
478
|
]);
|
|
413
479
|
const tenant = res.rows[0];
|
|
@@ -418,13 +484,14 @@ async function batchCreateTenants(pool, inputs) {
|
|
|
418
484
|
// Root tenant
|
|
419
485
|
const rootRegionId = input.region_id ?? null;
|
|
420
486
|
const res = await client.query(`INSERT INTO tenants (parent_id, ancestry_path, depth, name, slug, config, metadata, isolation_strategy, status, region_id)
|
|
421
|
-
VALUES (NULL, '/', 0, $1, $2, $3, $4, $5,
|
|
487
|
+
VALUES (NULL, '/', 0, $1, $2, $3, $4, $5, $6, $7)
|
|
422
488
|
RETURNING *`, [
|
|
423
489
|
input.name,
|
|
424
490
|
input.slug,
|
|
425
491
|
JSON.stringify(input.config ?? {}),
|
|
426
492
|
JSON.stringify(input.metadata ?? {}),
|
|
427
493
|
input.isolation_strategy ?? "SHARED_RLS",
|
|
494
|
+
initialStatus(input),
|
|
428
495
|
rootRegionId,
|
|
429
496
|
]);
|
|
430
497
|
const tenant = res.rows[0];
|