@vrplatform/kysely 1.3.45-stage.4904 → 1.3.45-stage.4925
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/build/main/error.d.ts +2 -1
- package/build/main/error.js +2 -1
- package/build/main/error.js.map +1 -1
- package/build/main/regional.js +16 -8
- package/build/main/regional.js.map +1 -1
- package/build/module/error.d.ts +2 -1
- package/build/module/error.js +3 -1
- package/build/module/error.js.map +1 -1
- package/build/module/regional.js +16 -8
- package/build/module/regional.js.map +1 -1
- package/package.json +1 -1
package/build/main/error.d.ts
CHANGED
package/build/main/error.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.KyselyError = void 0;
|
|
4
4
|
class KyselyError extends Error {
|
|
5
|
-
constructor(message) {
|
|
5
|
+
constructor(message, details) {
|
|
6
6
|
super(message);
|
|
7
|
+
this.details = details;
|
|
7
8
|
this.name = 'KyselyError';
|
|
8
9
|
}
|
|
9
10
|
}
|
package/build/main/error.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"src/","sources":["error.ts"],"names":[],"mappings":";;;AAAA,MAAa,WAAY,SAAQ,KAAK;IACpC,
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"src/","sources":["error.ts"],"names":[],"mappings":";;;AAAA,MAAa,WAAY,SAAQ,KAAK;IACpC,YACE,OAAe,EACC,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF;AARD,kCAQC","sourcesContent":["export class KyselyError extends Error {\n constructor(\n message: string,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'KyselyError';\n }\n}\n"]}
|
package/build/main/regional.js
CHANGED
|
@@ -41,6 +41,7 @@ exports.resolveConfiguredTenantDataRegions = resolveConfiguredTenantDataRegions;
|
|
|
41
41
|
exports.useTenantDatabaseKysely = useTenantDatabaseKysely;
|
|
42
42
|
exports.resolveTenantKysely = resolveTenantKysely;
|
|
43
43
|
exports.forEachConfiguredRegion = forEachConfiguredRegion;
|
|
44
|
+
const error_1 = require("./error");
|
|
44
45
|
// The tenant-data-region contract lives here — the region set plus how a
|
|
45
46
|
// region resolves to its database url, API base url, and how to fan a job out
|
|
46
47
|
// across regions. Every runtime resolves regions through these functions;
|
|
@@ -60,7 +61,7 @@ exports.tenantDataRegions = [
|
|
|
60
61
|
];
|
|
61
62
|
function assertTenantDataRegion(value) {
|
|
62
63
|
if (!exports.tenantDataRegions.includes(value)) {
|
|
63
|
-
throw new
|
|
64
|
+
throw new error_1.KyselyError('Missing or invalid tenant data region', { value });
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
function databaseUrlEnvKey(dataRegion) {
|
|
@@ -72,7 +73,10 @@ function apiBaseUrlEnvKey(dataRegion) {
|
|
|
72
73
|
function resolveTenantDatabaseUrl(dataRegion, env) {
|
|
73
74
|
const databaseUrl = env[databaseUrlEnvKey(dataRegion)];
|
|
74
75
|
if (!databaseUrl) {
|
|
75
|
-
throw new
|
|
76
|
+
throw new error_1.KyselyError('Tenant database URL is missing', {
|
|
77
|
+
dataRegion,
|
|
78
|
+
key: databaseUrlEnvKey(dataRegion),
|
|
79
|
+
});
|
|
76
80
|
}
|
|
77
81
|
return databaseUrl;
|
|
78
82
|
}
|
|
@@ -80,7 +84,10 @@ function resolveTenantApiBaseUrl(dataRegion, env) {
|
|
|
80
84
|
const key = apiBaseUrlEnvKey(dataRegion);
|
|
81
85
|
const baseUrl = env[key];
|
|
82
86
|
if (!baseUrl) {
|
|
83
|
-
throw new
|
|
87
|
+
throw new error_1.KyselyError('Tenant API base URL is missing', {
|
|
88
|
+
dataRegion,
|
|
89
|
+
key,
|
|
90
|
+
});
|
|
84
91
|
}
|
|
85
92
|
return baseUrl;
|
|
86
93
|
}
|
|
@@ -99,8 +106,9 @@ function resolveConfiguredTenantDataRegions(env) {
|
|
|
99
106
|
}
|
|
100
107
|
for (const aliased of regionsByUrl.values()) {
|
|
101
108
|
if (aliased.length > 1) {
|
|
102
|
-
throw new
|
|
103
|
-
|
|
109
|
+
throw new error_1.KyselyError('Tenant data regions share the same database URL', {
|
|
110
|
+
dataRegions: aliased,
|
|
111
|
+
});
|
|
104
112
|
}
|
|
105
113
|
}
|
|
106
114
|
return configured;
|
|
@@ -184,9 +192,9 @@ async function forEachConfiguredRegion(run, env) {
|
|
|
184
192
|
}
|
|
185
193
|
}
|
|
186
194
|
if (!results.length && errors.length) {
|
|
187
|
-
throw new
|
|
188
|
-
|
|
189
|
-
|
|
195
|
+
throw new error_1.KyselyError('All configured tenant data regions failed', {
|
|
196
|
+
errors,
|
|
197
|
+
});
|
|
190
198
|
}
|
|
191
199
|
return { results, errors };
|
|
192
200
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regional.js","sourceRoot":"src/","sources":["regional.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Kysely } from './index';\n\n// The tenant-data-region contract lives here — the region set plus how a\n// region resolves to its database url, API base url, and how to fan a job out\n// across regions. Every runtime resolves regions through these functions;\n// @vrplatform/common re-exports them for app-facing imports, and the trigger\n// runtime wraps them only to default `env` to process.env.\n//\n// Region → resource keys are derived from the region name\n// (`DATABASE_URL_VRTRUST_<REGION>`, `API_BASE_URL_<REGION>`), so adding a\n// region means extending this array plus its env values and migrations — not\n// editing per-region branches.\nexport const tenantDataRegions = [\n 'ap',\n 'crunchy',\n 'eu',\n 'hostaway',\n 'us',\n] as const;\nexport type TenantDataRegion = (typeof tenantDataRegions)[number];\n\nexport type RegionEnvironment = Record<string, string | undefined>;\n\nexport function assertTenantDataRegion(\n value: string | undefined\n): asserts value is TenantDataRegion {\n if (!tenantDataRegions.includes(value as TenantDataRegion)) {\n throw new Error(`Missing or invalid tenant data region: ${value ?? ''}`);\n }\n}\n\nfunction databaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `DATABASE_URL_VRTRUST_${dataRegion.toUpperCase()}`;\n}\n\nfunction apiBaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `API_BASE_URL_${dataRegion.toUpperCase()}`;\n}\n\nexport function resolveTenantDatabaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const databaseUrl = env[databaseUrlEnvKey(dataRegion)];\n if (!databaseUrl) {\n throw new Error(`Missing database url for ${dataRegion}`);\n }\n return databaseUrl;\n}\n\nexport function resolveTenantApiBaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const key = apiBaseUrlEnvKey(dataRegion);\n const baseUrl = env[key];\n if (!baseUrl) {\n throw new Error(`Missing ${key}`);\n }\n return baseUrl;\n}\n\nexport function resolveConfiguredTenantDataRegions(env: RegionEnvironment) {\n const configured = tenantDataRegions.filter((region) =>\n Boolean(env[databaseUrlEnvKey(region)])\n );\n\n if (!configured.length) {\n throw new Error('No tenant data region database urls configured');\n }\n\n // Two regions sharing one URL means every region-fan-out job scans the\n // same physical DB twice (e.g. a stale dashboard env var re-merged into a\n // deploy). Regions must stay unset until they point at distinct databases.\n const regionsByUrl = new Map<string, TenantDataRegion[]>();\n for (const region of configured) {\n const url = env[databaseUrlEnvKey(region)]!;\n regionsByUrl.set(url, [...(regionsByUrl.get(url) ?? []), region]);\n }\n for (const aliased of regionsByUrl.values()) {\n if (aliased.length > 1) {\n throw new Error(\n `Tenant data regions ${aliased.join(', ')} share the same database url; ` +\n 'each configured region must point at a distinct database'\n );\n }\n }\n\n return configured;\n}\n\ntype UseAsyncRegionalKysely = (\n connectionString: string,\n options?: {\n repositoryName?: string;\n probe?: true;\n }\n) => Promise<Kysely>;\n\nasync function defaultUseAsyncKysely(\n connectionString: string,\n options?: Parameters<UseAsyncRegionalKysely>[1]\n) {\n const { useAsyncKysely } = await import('./index');\n return await useAsyncKysely(connectionString, options);\n}\n\nexport async function useTenantDatabaseKysely(\n dataRegion: TenantDataRegion,\n repositoryName: string,\n env: RegionEnvironment,\n options: { useAsyncKysely?: UseAsyncRegionalKysely; probe?: true } = {}\n) {\n const open = options.useAsyncKysely ?? defaultUseAsyncKysely;\n return await open(resolveTenantDatabaseUrl(dataRegion, env), {\n repositoryName,\n probe: options.probe,\n });\n}\n\nexport type TenantKysely = {\n id: string;\n slug: string;\n dataRegion: TenantDataRegion;\n kysely: Kysely;\n};\n\n/**\n * Resolve a tenant's regional Kysely connection from a connection id, slug,\n * or tenant id. The control-plane connection is opened and destroyed\n * internally; the returned regional connection must be destroyed by the\n * caller.\n */\nexport async function resolveTenantKysely({\n connectionId,\n slug,\n tenantId,\n repositoryName,\n env,\n ...options\n}: {\n connectionId?: string;\n slug?: string;\n tenantId?: string;\n repositoryName: string;\n env: RegionEnvironment;\n useAsyncKysely?: UseAsyncRegionalKysely;\n probe?: true;\n}): Promise<TenantKysely> {\n if (!connectionId && !slug && !tenantId) {\n throw new Error('connectionId, slug, or tenantId is required');\n }\n\n const controlPlaneDatabaseUrl = env.CONTROL_PLANE_DATABASE_URL;\n if (!controlPlaneDatabaseUrl)\n throw new Error('CONTROL_PLANE_DATABASE_URL is required');\n\n const controlPlane = await (options.useAsyncKysely ?? defaultUseAsyncKysely)(\n controlPlaneDatabaseUrl,\n {\n repositoryName: `${repositoryName}/control-plane`,\n probe: options.probe,\n }\n );\n let tenant: { id: string; slug: string; dataRegion: string } | undefined;\n try {\n const query = controlPlane\n .selectFrom('public.tenant')\n .select([\n 'public.tenant.id as id',\n 'public.tenant.slug as slug',\n 'public.tenant.dataRegion as dataRegion',\n ]);\n tenant = await (\n slug\n ? query.where('public.tenant.slug', '=', slug)\n : connectionId\n ? query\n .innerJoin(\n 'public.connectionRoute',\n 'public.connectionRoute.tenantId',\n 'public.tenant.id'\n )\n .where('public.connectionRoute.connectionId', '=', connectionId)\n : query.where('public.tenant.id', '=', tenantId!)\n ).executeTakeFirst();\n } finally {\n await controlPlane.destroy();\n }\n if (!tenant) throw new Error('Tenant not found in control plane');\n\n assertTenantDataRegion(tenant.dataRegion);\n return {\n id: tenant.id,\n slug: tenant.slug,\n dataRegion: tenant.dataRegion,\n kysely: await useTenantDatabaseKysely(\n tenant.dataRegion,\n repositoryName,\n env,\n options\n ),\n };\n}\n\nexport type RegionRunError = {\n dataRegion: TenantDataRegion;\n error: string;\n};\n\n/**\n * Run a job once per configured tenant data region with per-region error\n * isolation: one region's outage must not stall the others (regions iterate\n * in a fixed order, so a failing first region would otherwise starve the\n * rest on every attempt). Throws only when every configured region failed.\n */\nexport async function forEachConfiguredRegion<T>(\n run: (dataRegion: TenantDataRegion) => Promise<T>,\n env: RegionEnvironment\n): Promise<{\n results: { dataRegion: TenantDataRegion; result: T }[];\n errors: RegionRunError[];\n}> {\n const results: { dataRegion: TenantDataRegion; result: T }[] = [];\n const errors: RegionRunError[] = [];\n\n for (const dataRegion of resolveConfiguredTenantDataRegions(env)) {\n try {\n results.push({ dataRegion, result: await run(dataRegion) });\n } catch (error) {\n errors.push({\n dataRegion,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (!results.length && errors.length) {\n throw new Error(\n `All configured tenant data regions failed: ${errors\n .map((regionError) => `${regionError.dataRegion}: ${regionError.error}`)\n .join('; ')}`\n );\n }\n\n return { results, errors };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"regional.js","sourceRoot":"src/","sources":["regional.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Kysely } from './index';\nimport { KyselyError } from './error';\n\n// The tenant-data-region contract lives here — the region set plus how a\n// region resolves to its database url, API base url, and how to fan a job out\n// across regions. Every runtime resolves regions through these functions;\n// @vrplatform/common re-exports them for app-facing imports, and the trigger\n// runtime wraps them only to default `env` to process.env.\n//\n// Region → resource keys are derived from the region name\n// (`DATABASE_URL_VRTRUST_<REGION>`, `API_BASE_URL_<REGION>`), so adding a\n// region means extending this array plus its env values and migrations — not\n// editing per-region branches.\nexport const tenantDataRegions = [\n 'ap',\n 'crunchy',\n 'eu',\n 'hostaway',\n 'us',\n] as const;\nexport type TenantDataRegion = (typeof tenantDataRegions)[number];\n\nexport type RegionEnvironment = Record<string, string | undefined>;\n\nexport function assertTenantDataRegion(\n value: string | undefined\n): asserts value is TenantDataRegion {\n if (!tenantDataRegions.includes(value as TenantDataRegion)) {\n throw new KyselyError('Missing or invalid tenant data region', { value });\n }\n}\n\nfunction databaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `DATABASE_URL_VRTRUST_${dataRegion.toUpperCase()}`;\n}\n\nfunction apiBaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `API_BASE_URL_${dataRegion.toUpperCase()}`;\n}\n\nexport function resolveTenantDatabaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const databaseUrl = env[databaseUrlEnvKey(dataRegion)];\n if (!databaseUrl) {\n throw new KyselyError('Tenant database URL is missing', {\n dataRegion,\n key: databaseUrlEnvKey(dataRegion),\n });\n }\n return databaseUrl;\n}\n\nexport function resolveTenantApiBaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const key = apiBaseUrlEnvKey(dataRegion);\n const baseUrl = env[key];\n if (!baseUrl) {\n throw new KyselyError('Tenant API base URL is missing', {\n dataRegion,\n key,\n });\n }\n return baseUrl;\n}\n\nexport function resolveConfiguredTenantDataRegions(env: RegionEnvironment) {\n const configured = tenantDataRegions.filter((region) =>\n Boolean(env[databaseUrlEnvKey(region)])\n );\n\n if (!configured.length) {\n throw new Error('No tenant data region database urls configured');\n }\n\n // Two regions sharing one URL means every region-fan-out job scans the\n // same physical DB twice (e.g. a stale dashboard env var re-merged into a\n // deploy). Regions must stay unset until they point at distinct databases.\n const regionsByUrl = new Map<string, TenantDataRegion[]>();\n for (const region of configured) {\n const url = env[databaseUrlEnvKey(region)]!;\n regionsByUrl.set(url, [...(regionsByUrl.get(url) ?? []), region]);\n }\n for (const aliased of regionsByUrl.values()) {\n if (aliased.length > 1) {\n throw new KyselyError('Tenant data regions share the same database URL', {\n dataRegions: aliased,\n });\n }\n }\n\n return configured;\n}\n\ntype UseAsyncRegionalKysely = (\n connectionString: string,\n options?: {\n repositoryName?: string;\n probe?: true;\n }\n) => Promise<Kysely>;\n\nasync function defaultUseAsyncKysely(\n connectionString: string,\n options?: Parameters<UseAsyncRegionalKysely>[1]\n) {\n const { useAsyncKysely } = await import('./index');\n return await useAsyncKysely(connectionString, options);\n}\n\nexport async function useTenantDatabaseKysely(\n dataRegion: TenantDataRegion,\n repositoryName: string,\n env: RegionEnvironment,\n options: { useAsyncKysely?: UseAsyncRegionalKysely; probe?: true } = {}\n) {\n const open = options.useAsyncKysely ?? defaultUseAsyncKysely;\n return await open(resolveTenantDatabaseUrl(dataRegion, env), {\n repositoryName,\n probe: options.probe,\n });\n}\n\nexport type TenantKysely = {\n id: string;\n slug: string;\n dataRegion: TenantDataRegion;\n kysely: Kysely;\n};\n\n/**\n * Resolve a tenant's regional Kysely connection from a connection id, slug,\n * or tenant id. The control-plane connection is opened and destroyed\n * internally; the returned regional connection must be destroyed by the\n * caller.\n */\nexport async function resolveTenantKysely({\n connectionId,\n slug,\n tenantId,\n repositoryName,\n env,\n ...options\n}: {\n connectionId?: string;\n slug?: string;\n tenantId?: string;\n repositoryName: string;\n env: RegionEnvironment;\n useAsyncKysely?: UseAsyncRegionalKysely;\n probe?: true;\n}): Promise<TenantKysely> {\n if (!connectionId && !slug && !tenantId) {\n throw new Error('connectionId, slug, or tenantId is required');\n }\n\n const controlPlaneDatabaseUrl = env.CONTROL_PLANE_DATABASE_URL;\n if (!controlPlaneDatabaseUrl)\n throw new Error('CONTROL_PLANE_DATABASE_URL is required');\n\n const controlPlane = await (options.useAsyncKysely ?? defaultUseAsyncKysely)(\n controlPlaneDatabaseUrl,\n {\n repositoryName: `${repositoryName}/control-plane`,\n probe: options.probe,\n }\n );\n let tenant: { id: string; slug: string; dataRegion: string } | undefined;\n try {\n const query = controlPlane\n .selectFrom('public.tenant')\n .select([\n 'public.tenant.id as id',\n 'public.tenant.slug as slug',\n 'public.tenant.dataRegion as dataRegion',\n ]);\n tenant = await (\n slug\n ? query.where('public.tenant.slug', '=', slug)\n : connectionId\n ? query\n .innerJoin(\n 'public.connectionRoute',\n 'public.connectionRoute.tenantId',\n 'public.tenant.id'\n )\n .where('public.connectionRoute.connectionId', '=', connectionId)\n : query.where('public.tenant.id', '=', tenantId!)\n ).executeTakeFirst();\n } finally {\n await controlPlane.destroy();\n }\n if (!tenant) throw new Error('Tenant not found in control plane');\n\n assertTenantDataRegion(tenant.dataRegion);\n return {\n id: tenant.id,\n slug: tenant.slug,\n dataRegion: tenant.dataRegion,\n kysely: await useTenantDatabaseKysely(\n tenant.dataRegion,\n repositoryName,\n env,\n options\n ),\n };\n}\n\nexport type RegionRunError = {\n dataRegion: TenantDataRegion;\n error: string;\n};\n\n/**\n * Run a job once per configured tenant data region with per-region error\n * isolation: one region's outage must not stall the others (regions iterate\n * in a fixed order, so a failing first region would otherwise starve the\n * rest on every attempt). Throws only when every configured region failed.\n */\nexport async function forEachConfiguredRegion<T>(\n run: (dataRegion: TenantDataRegion) => Promise<T>,\n env: RegionEnvironment\n): Promise<{\n results: { dataRegion: TenantDataRegion; result: T }[];\n errors: RegionRunError[];\n}> {\n const results: { dataRegion: TenantDataRegion; result: T }[] = [];\n const errors: RegionRunError[] = [];\n\n for (const dataRegion of resolveConfiguredTenantDataRegions(env)) {\n try {\n results.push({ dataRegion, result: await run(dataRegion) });\n } catch (error) {\n errors.push({\n dataRegion,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (!results.length && errors.length) {\n throw new KyselyError('All configured tenant data regions failed', {\n errors,\n });\n }\n\n return { results, errors };\n}\n"]}
|
package/build/module/error.d.ts
CHANGED
package/build/module/error.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error.js","sourceRoot":"src/","sources":["error.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,WAAY,SAAQ,KAAK;
|
|
1
|
+
{"version":3,"file":"error.js","sourceRoot":"src/","sources":["error.ts"],"names":[],"mappings":"AAAA,MAAM,OAAO,WAAY,SAAQ,KAAK;IAGlB;IAFlB,YACE,OAAe,EACC,OAAiB;QAEjC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFC,YAAO,GAAP,OAAO,CAAU;QAGjC,IAAI,CAAC,IAAI,GAAG,aAAa,CAAC;IAC5B,CAAC;CACF","sourcesContent":["export class KyselyError extends Error {\n constructor(\n message: string,\n public readonly details?: unknown\n ) {\n super(message);\n this.name = 'KyselyError';\n }\n}\n"]}
|
package/build/module/regional.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { KyselyError } from './error';
|
|
1
2
|
// The tenant-data-region contract lives here — the region set plus how a
|
|
2
3
|
// region resolves to its database url, API base url, and how to fan a job out
|
|
3
4
|
// across regions. Every runtime resolves regions through these functions;
|
|
@@ -17,7 +18,7 @@ export const tenantDataRegions = [
|
|
|
17
18
|
];
|
|
18
19
|
export function assertTenantDataRegion(value) {
|
|
19
20
|
if (!tenantDataRegions.includes(value)) {
|
|
20
|
-
throw new
|
|
21
|
+
throw new KyselyError('Missing or invalid tenant data region', { value });
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
function databaseUrlEnvKey(dataRegion) {
|
|
@@ -29,7 +30,10 @@ function apiBaseUrlEnvKey(dataRegion) {
|
|
|
29
30
|
export function resolveTenantDatabaseUrl(dataRegion, env) {
|
|
30
31
|
const databaseUrl = env[databaseUrlEnvKey(dataRegion)];
|
|
31
32
|
if (!databaseUrl) {
|
|
32
|
-
throw new
|
|
33
|
+
throw new KyselyError('Tenant database URL is missing', {
|
|
34
|
+
dataRegion,
|
|
35
|
+
key: databaseUrlEnvKey(dataRegion),
|
|
36
|
+
});
|
|
33
37
|
}
|
|
34
38
|
return databaseUrl;
|
|
35
39
|
}
|
|
@@ -37,7 +41,10 @@ export function resolveTenantApiBaseUrl(dataRegion, env) {
|
|
|
37
41
|
const key = apiBaseUrlEnvKey(dataRegion);
|
|
38
42
|
const baseUrl = env[key];
|
|
39
43
|
if (!baseUrl) {
|
|
40
|
-
throw new
|
|
44
|
+
throw new KyselyError('Tenant API base URL is missing', {
|
|
45
|
+
dataRegion,
|
|
46
|
+
key,
|
|
47
|
+
});
|
|
41
48
|
}
|
|
42
49
|
return baseUrl;
|
|
43
50
|
}
|
|
@@ -56,8 +63,9 @@ export function resolveConfiguredTenantDataRegions(env) {
|
|
|
56
63
|
}
|
|
57
64
|
for (const aliased of regionsByUrl.values()) {
|
|
58
65
|
if (aliased.length > 1) {
|
|
59
|
-
throw new
|
|
60
|
-
|
|
66
|
+
throw new KyselyError('Tenant data regions share the same database URL', {
|
|
67
|
+
dataRegions: aliased,
|
|
68
|
+
});
|
|
61
69
|
}
|
|
62
70
|
}
|
|
63
71
|
return configured;
|
|
@@ -141,9 +149,9 @@ export async function forEachConfiguredRegion(run, env) {
|
|
|
141
149
|
}
|
|
142
150
|
}
|
|
143
151
|
if (!results.length && errors.length) {
|
|
144
|
-
throw new
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
throw new KyselyError('All configured tenant data regions failed', {
|
|
153
|
+
errors,
|
|
154
|
+
});
|
|
147
155
|
}
|
|
148
156
|
return { results, errors };
|
|
149
157
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"regional.js","sourceRoot":"src/","sources":["regional.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Kysely } from './index';\n\n// The tenant-data-region contract lives here — the region set plus how a\n// region resolves to its database url, API base url, and how to fan a job out\n// across regions. Every runtime resolves regions through these functions;\n// @vrplatform/common re-exports them for app-facing imports, and the trigger\n// runtime wraps them only to default `env` to process.env.\n//\n// Region → resource keys are derived from the region name\n// (`DATABASE_URL_VRTRUST_<REGION>`, `API_BASE_URL_<REGION>`), so adding a\n// region means extending this array plus its env values and migrations — not\n// editing per-region branches.\nexport const tenantDataRegions = [\n 'ap',\n 'crunchy',\n 'eu',\n 'hostaway',\n 'us',\n] as const;\nexport type TenantDataRegion = (typeof tenantDataRegions)[number];\n\nexport type RegionEnvironment = Record<string, string | undefined>;\n\nexport function assertTenantDataRegion(\n value: string | undefined\n): asserts value is TenantDataRegion {\n if (!tenantDataRegions.includes(value as TenantDataRegion)) {\n throw new Error(`Missing or invalid tenant data region: ${value ?? ''}`);\n }\n}\n\nfunction databaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `DATABASE_URL_VRTRUST_${dataRegion.toUpperCase()}`;\n}\n\nfunction apiBaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `API_BASE_URL_${dataRegion.toUpperCase()}`;\n}\n\nexport function resolveTenantDatabaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const databaseUrl = env[databaseUrlEnvKey(dataRegion)];\n if (!databaseUrl) {\n throw new Error(`Missing database url for ${dataRegion}`);\n }\n return databaseUrl;\n}\n\nexport function resolveTenantApiBaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const key = apiBaseUrlEnvKey(dataRegion);\n const baseUrl = env[key];\n if (!baseUrl) {\n throw new Error(`Missing ${key}`);\n }\n return baseUrl;\n}\n\nexport function resolveConfiguredTenantDataRegions(env: RegionEnvironment) {\n const configured = tenantDataRegions.filter((region) =>\n Boolean(env[databaseUrlEnvKey(region)])\n );\n\n if (!configured.length) {\n throw new Error('No tenant data region database urls configured');\n }\n\n // Two regions sharing one URL means every region-fan-out job scans the\n // same physical DB twice (e.g. a stale dashboard env var re-merged into a\n // deploy). Regions must stay unset until they point at distinct databases.\n const regionsByUrl = new Map<string, TenantDataRegion[]>();\n for (const region of configured) {\n const url = env[databaseUrlEnvKey(region)]!;\n regionsByUrl.set(url, [...(regionsByUrl.get(url) ?? []), region]);\n }\n for (const aliased of regionsByUrl.values()) {\n if (aliased.length > 1) {\n throw new Error(\n `Tenant data regions ${aliased.join(', ')} share the same database url; ` +\n 'each configured region must point at a distinct database'\n );\n }\n }\n\n return configured;\n}\n\ntype UseAsyncRegionalKysely = (\n connectionString: string,\n options?: {\n repositoryName?: string;\n probe?: true;\n }\n) => Promise<Kysely>;\n\nasync function defaultUseAsyncKysely(\n connectionString: string,\n options?: Parameters<UseAsyncRegionalKysely>[1]\n) {\n const { useAsyncKysely } = await import('./index');\n return await useAsyncKysely(connectionString, options);\n}\n\nexport async function useTenantDatabaseKysely(\n dataRegion: TenantDataRegion,\n repositoryName: string,\n env: RegionEnvironment,\n options: { useAsyncKysely?: UseAsyncRegionalKysely; probe?: true } = {}\n) {\n const open = options.useAsyncKysely ?? defaultUseAsyncKysely;\n return await open(resolveTenantDatabaseUrl(dataRegion, env), {\n repositoryName,\n probe: options.probe,\n });\n}\n\nexport type TenantKysely = {\n id: string;\n slug: string;\n dataRegion: TenantDataRegion;\n kysely: Kysely;\n};\n\n/**\n * Resolve a tenant's regional Kysely connection from a connection id, slug,\n * or tenant id. The control-plane connection is opened and destroyed\n * internally; the returned regional connection must be destroyed by the\n * caller.\n */\nexport async function resolveTenantKysely({\n connectionId,\n slug,\n tenantId,\n repositoryName,\n env,\n ...options\n}: {\n connectionId?: string;\n slug?: string;\n tenantId?: string;\n repositoryName: string;\n env: RegionEnvironment;\n useAsyncKysely?: UseAsyncRegionalKysely;\n probe?: true;\n}): Promise<TenantKysely> {\n if (!connectionId && !slug && !tenantId) {\n throw new Error('connectionId, slug, or tenantId is required');\n }\n\n const controlPlaneDatabaseUrl = env.CONTROL_PLANE_DATABASE_URL;\n if (!controlPlaneDatabaseUrl)\n throw new Error('CONTROL_PLANE_DATABASE_URL is required');\n\n const controlPlane = await (options.useAsyncKysely ?? defaultUseAsyncKysely)(\n controlPlaneDatabaseUrl,\n {\n repositoryName: `${repositoryName}/control-plane`,\n probe: options.probe,\n }\n );\n let tenant: { id: string; slug: string; dataRegion: string } | undefined;\n try {\n const query = controlPlane\n .selectFrom('public.tenant')\n .select([\n 'public.tenant.id as id',\n 'public.tenant.slug as slug',\n 'public.tenant.dataRegion as dataRegion',\n ]);\n tenant = await (\n slug\n ? query.where('public.tenant.slug', '=', slug)\n : connectionId\n ? query\n .innerJoin(\n 'public.connectionRoute',\n 'public.connectionRoute.tenantId',\n 'public.tenant.id'\n )\n .where('public.connectionRoute.connectionId', '=', connectionId)\n : query.where('public.tenant.id', '=', tenantId!)\n ).executeTakeFirst();\n } finally {\n await controlPlane.destroy();\n }\n if (!tenant) throw new Error('Tenant not found in control plane');\n\n assertTenantDataRegion(tenant.dataRegion);\n return {\n id: tenant.id,\n slug: tenant.slug,\n dataRegion: tenant.dataRegion,\n kysely: await useTenantDatabaseKysely(\n tenant.dataRegion,\n repositoryName,\n env,\n options\n ),\n };\n}\n\nexport type RegionRunError = {\n dataRegion: TenantDataRegion;\n error: string;\n};\n\n/**\n * Run a job once per configured tenant data region with per-region error\n * isolation: one region's outage must not stall the others (regions iterate\n * in a fixed order, so a failing first region would otherwise starve the\n * rest on every attempt). Throws only when every configured region failed.\n */\nexport async function forEachConfiguredRegion<T>(\n run: (dataRegion: TenantDataRegion) => Promise<T>,\n env: RegionEnvironment\n): Promise<{\n results: { dataRegion: TenantDataRegion; result: T }[];\n errors: RegionRunError[];\n}> {\n const results: { dataRegion: TenantDataRegion; result: T }[] = [];\n const errors: RegionRunError[] = [];\n\n for (const dataRegion of resolveConfiguredTenantDataRegions(env)) {\n try {\n results.push({ dataRegion, result: await run(dataRegion) });\n } catch (error) {\n errors.push({\n dataRegion,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (!results.length && errors.length) {\n throw new Error(\n `All configured tenant data regions failed: ${errors\n .map((regionError) => `${regionError.dataRegion}: ${regionError.error}`)\n .join('; ')}`\n );\n }\n\n return { results, errors };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"regional.js","sourceRoot":"src/","sources":["regional.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Kysely } from './index';\nimport { KyselyError } from './error';\n\n// The tenant-data-region contract lives here — the region set plus how a\n// region resolves to its database url, API base url, and how to fan a job out\n// across regions. Every runtime resolves regions through these functions;\n// @vrplatform/common re-exports them for app-facing imports, and the trigger\n// runtime wraps them only to default `env` to process.env.\n//\n// Region → resource keys are derived from the region name\n// (`DATABASE_URL_VRTRUST_<REGION>`, `API_BASE_URL_<REGION>`), so adding a\n// region means extending this array plus its env values and migrations — not\n// editing per-region branches.\nexport const tenantDataRegions = [\n 'ap',\n 'crunchy',\n 'eu',\n 'hostaway',\n 'us',\n] as const;\nexport type TenantDataRegion = (typeof tenantDataRegions)[number];\n\nexport type RegionEnvironment = Record<string, string | undefined>;\n\nexport function assertTenantDataRegion(\n value: string | undefined\n): asserts value is TenantDataRegion {\n if (!tenantDataRegions.includes(value as TenantDataRegion)) {\n throw new KyselyError('Missing or invalid tenant data region', { value });\n }\n}\n\nfunction databaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `DATABASE_URL_VRTRUST_${dataRegion.toUpperCase()}`;\n}\n\nfunction apiBaseUrlEnvKey(dataRegion: TenantDataRegion) {\n return `API_BASE_URL_${dataRegion.toUpperCase()}`;\n}\n\nexport function resolveTenantDatabaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const databaseUrl = env[databaseUrlEnvKey(dataRegion)];\n if (!databaseUrl) {\n throw new KyselyError('Tenant database URL is missing', {\n dataRegion,\n key: databaseUrlEnvKey(dataRegion),\n });\n }\n return databaseUrl;\n}\n\nexport function resolveTenantApiBaseUrl(\n dataRegion: TenantDataRegion,\n env: RegionEnvironment\n) {\n const key = apiBaseUrlEnvKey(dataRegion);\n const baseUrl = env[key];\n if (!baseUrl) {\n throw new KyselyError('Tenant API base URL is missing', {\n dataRegion,\n key,\n });\n }\n return baseUrl;\n}\n\nexport function resolveConfiguredTenantDataRegions(env: RegionEnvironment) {\n const configured = tenantDataRegions.filter((region) =>\n Boolean(env[databaseUrlEnvKey(region)])\n );\n\n if (!configured.length) {\n throw new Error('No tenant data region database urls configured');\n }\n\n // Two regions sharing one URL means every region-fan-out job scans the\n // same physical DB twice (e.g. a stale dashboard env var re-merged into a\n // deploy). Regions must stay unset until they point at distinct databases.\n const regionsByUrl = new Map<string, TenantDataRegion[]>();\n for (const region of configured) {\n const url = env[databaseUrlEnvKey(region)]!;\n regionsByUrl.set(url, [...(regionsByUrl.get(url) ?? []), region]);\n }\n for (const aliased of regionsByUrl.values()) {\n if (aliased.length > 1) {\n throw new KyselyError('Tenant data regions share the same database URL', {\n dataRegions: aliased,\n });\n }\n }\n\n return configured;\n}\n\ntype UseAsyncRegionalKysely = (\n connectionString: string,\n options?: {\n repositoryName?: string;\n probe?: true;\n }\n) => Promise<Kysely>;\n\nasync function defaultUseAsyncKysely(\n connectionString: string,\n options?: Parameters<UseAsyncRegionalKysely>[1]\n) {\n const { useAsyncKysely } = await import('./index');\n return await useAsyncKysely(connectionString, options);\n}\n\nexport async function useTenantDatabaseKysely(\n dataRegion: TenantDataRegion,\n repositoryName: string,\n env: RegionEnvironment,\n options: { useAsyncKysely?: UseAsyncRegionalKysely; probe?: true } = {}\n) {\n const open = options.useAsyncKysely ?? defaultUseAsyncKysely;\n return await open(resolveTenantDatabaseUrl(dataRegion, env), {\n repositoryName,\n probe: options.probe,\n });\n}\n\nexport type TenantKysely = {\n id: string;\n slug: string;\n dataRegion: TenantDataRegion;\n kysely: Kysely;\n};\n\n/**\n * Resolve a tenant's regional Kysely connection from a connection id, slug,\n * or tenant id. The control-plane connection is opened and destroyed\n * internally; the returned regional connection must be destroyed by the\n * caller.\n */\nexport async function resolveTenantKysely({\n connectionId,\n slug,\n tenantId,\n repositoryName,\n env,\n ...options\n}: {\n connectionId?: string;\n slug?: string;\n tenantId?: string;\n repositoryName: string;\n env: RegionEnvironment;\n useAsyncKysely?: UseAsyncRegionalKysely;\n probe?: true;\n}): Promise<TenantKysely> {\n if (!connectionId && !slug && !tenantId) {\n throw new Error('connectionId, slug, or tenantId is required');\n }\n\n const controlPlaneDatabaseUrl = env.CONTROL_PLANE_DATABASE_URL;\n if (!controlPlaneDatabaseUrl)\n throw new Error('CONTROL_PLANE_DATABASE_URL is required');\n\n const controlPlane = await (options.useAsyncKysely ?? defaultUseAsyncKysely)(\n controlPlaneDatabaseUrl,\n {\n repositoryName: `${repositoryName}/control-plane`,\n probe: options.probe,\n }\n );\n let tenant: { id: string; slug: string; dataRegion: string } | undefined;\n try {\n const query = controlPlane\n .selectFrom('public.tenant')\n .select([\n 'public.tenant.id as id',\n 'public.tenant.slug as slug',\n 'public.tenant.dataRegion as dataRegion',\n ]);\n tenant = await (\n slug\n ? query.where('public.tenant.slug', '=', slug)\n : connectionId\n ? query\n .innerJoin(\n 'public.connectionRoute',\n 'public.connectionRoute.tenantId',\n 'public.tenant.id'\n )\n .where('public.connectionRoute.connectionId', '=', connectionId)\n : query.where('public.tenant.id', '=', tenantId!)\n ).executeTakeFirst();\n } finally {\n await controlPlane.destroy();\n }\n if (!tenant) throw new Error('Tenant not found in control plane');\n\n assertTenantDataRegion(tenant.dataRegion);\n return {\n id: tenant.id,\n slug: tenant.slug,\n dataRegion: tenant.dataRegion,\n kysely: await useTenantDatabaseKysely(\n tenant.dataRegion,\n repositoryName,\n env,\n options\n ),\n };\n}\n\nexport type RegionRunError = {\n dataRegion: TenantDataRegion;\n error: string;\n};\n\n/**\n * Run a job once per configured tenant data region with per-region error\n * isolation: one region's outage must not stall the others (regions iterate\n * in a fixed order, so a failing first region would otherwise starve the\n * rest on every attempt). Throws only when every configured region failed.\n */\nexport async function forEachConfiguredRegion<T>(\n run: (dataRegion: TenantDataRegion) => Promise<T>,\n env: RegionEnvironment\n): Promise<{\n results: { dataRegion: TenantDataRegion; result: T }[];\n errors: RegionRunError[];\n}> {\n const results: { dataRegion: TenantDataRegion; result: T }[] = [];\n const errors: RegionRunError[] = [];\n\n for (const dataRegion of resolveConfiguredTenantDataRegions(env)) {\n try {\n results.push({ dataRegion, result: await run(dataRegion) });\n } catch (error) {\n errors.push({\n dataRegion,\n error: error instanceof Error ? error.message : String(error),\n });\n }\n }\n\n if (!results.length && errors.length) {\n throw new KyselyError('All configured tenant data regions failed', {\n errors,\n });\n }\n\n return { results, errors };\n}\n"]}
|