@openlup/platform 0.0.0-stage → 0.15.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/CHANGELOG.md +19 -0
- package/LICENSE +204 -0
- package/README.md +125 -2
- package/dist/bff/dispatch.d.ts +27 -0
- package/dist/bff/dispatch.js +47 -0
- package/dist/bff/envelope.d.ts +46 -0
- package/dist/bff/envelope.js +80 -0
- package/dist/bff/health.d.ts +24 -0
- package/dist/bff/health.js +19 -0
- package/dist/bff/index.d.ts +6 -0
- package/dist/bff/index.js +6 -0
- package/dist/bff/lazyRoute.d.ts +4 -0
- package/dist/bff/lazyRoute.js +13 -0
- package/dist/bff/requestContext.d.ts +17 -0
- package/dist/bff/requestContext.js +26 -0
- package/dist/bff/routerRewrite.d.ts +2 -0
- package/dist/bff/routerRewrite.js +31 -0
- package/dist/blob/binding.d.ts +16 -0
- package/dist/blob/binding.js +37 -0
- package/dist/blob/filesystem.d.ts +8 -0
- package/dist/blob/filesystem.js +71 -0
- package/dist/blob/index.d.ts +6 -0
- package/dist/blob/index.js +6 -0
- package/dist/blob/s3.d.ts +32 -0
- package/dist/blob/s3.js +56 -0
- package/dist/blob/s3Client.d.ts +25 -0
- package/dist/blob/s3Client.js +76 -0
- package/dist/blob/supabase.d.ts +40 -0
- package/dist/blob/supabase.js +59 -0
- package/dist/blob/vercel.d.ts +63 -0
- package/dist/blob/vercel.js +69 -0
- package/dist/composition/composeBundle.d.ts +47 -0
- package/dist/composition/composeBundle.js +45 -0
- package/dist/composition/httpBinding.d.ts +8 -0
- package/dist/composition/httpBinding.js +18 -0
- package/dist/composition/index.d.ts +3 -0
- package/dist/composition/index.js +3 -0
- package/dist/composition/nodeHost.d.ts +103 -0
- package/dist/composition/nodeHost.js +145 -0
- package/dist/data/binding.d.ts +34 -0
- package/dist/data/binding.js +92 -0
- package/dist/data/index.d.ts +5 -0
- package/dist/data/index.js +5 -0
- package/dist/data/postgresGateway.d.ts +42 -0
- package/dist/data/postgresGateway.js +135 -0
- package/dist/data/queryBuilder.d.ts +54 -0
- package/dist/data/queryBuilder.js +240 -0
- package/dist/data/queryBuilderSql.d.ts +76 -0
- package/dist/data/queryBuilderSql.js +145 -0
- package/dist/data/supabaseClientFactory.d.ts +34 -0
- package/dist/data/supabaseClientFactory.js +56 -0
- package/dist/data/supabaseGateway.d.ts +22 -0
- package/dist/data/supabaseGateway.js +49 -0
- package/dist/http/deadline.d.ts +20 -0
- package/dist/http/deadline.js +93 -0
- package/dist/http/index.d.ts +6 -0
- package/dist/http/index.js +5 -0
- package/dist/http/requestAdapter.d.ts +38 -0
- package/dist/http/requestAdapter.js +203 -0
- package/dist/http/runtime.d.ts +83 -0
- package/dist/http/runtime.js +199 -0
- package/dist/http/security.d.ts +44 -0
- package/dist/http/security.js +114 -0
- package/dist/http/staticAssets.d.ts +3 -0
- package/dist/http/staticAssets.js +42 -0
- package/dist/http/types.d.ts +23 -0
- package/dist/http/types.js +6 -0
- package/dist/job-ledger/index.d.ts +5 -0
- package/dist/job-ledger/index.js +3 -0
- package/dist/job-ledger/ports.d.ts +10 -0
- package/dist/job-ledger/ports.js +7 -0
- package/dist/job-ledger/postgres.d.ts +10 -0
- package/dist/job-ledger/postgres.js +101 -0
- package/dist/job-ledger/sql.d.ts +7 -0
- package/dist/job-ledger/sql.js +10 -0
- package/dist/job-ledger/supabase.d.ts +77 -0
- package/dist/job-ledger/supabase.js +202 -0
- package/dist/kernel/index.d.ts +1 -0
- package/dist/kernel/index.js +1 -0
- package/dist/kernel/platformKernel.d.ts +34 -0
- package/dist/kernel/platformKernel.js +115 -0
- package/dist/migrations/index.d.ts +1 -0
- package/dist/migrations/index.js +1 -0
- package/dist/migrations/postgresRunner.d.ts +50 -0
- package/dist/migrations/postgresRunner.js +176 -0
- package/dist/scheduler/advisoryLock.d.ts +44 -0
- package/dist/scheduler/advisoryLock.js +114 -0
- package/dist/scheduler/cronGuard.d.ts +10 -0
- package/dist/scheduler/cronGuard.js +19 -0
- package/dist/scheduler/cronSecret.d.ts +5 -0
- package/dist/scheduler/cronSecret.js +23 -0
- package/dist/scheduler/handlers.d.ts +9 -0
- package/dist/scheduler/handlers.js +10 -0
- package/dist/scheduler/httpEntry.d.ts +16 -0
- package/dist/scheduler/httpEntry.js +31 -0
- package/dist/scheduler/index.d.ts +9 -0
- package/dist/scheduler/index.js +9 -0
- package/dist/scheduler/jobRegistry.d.ts +21 -0
- package/dist/scheduler/jobRegistry.js +39 -0
- package/dist/scheduler/jobs.d.ts +48 -0
- package/dist/scheduler/jobs.js +92 -0
- package/dist/scheduler/nodeScheduler.d.ts +42 -0
- package/dist/scheduler/nodeScheduler.js +73 -0
- package/dist/scheduler/vercelScheduler.d.ts +29 -0
- package/dist/scheduler/vercelScheduler.js +51 -0
- package/package.json +108 -4
- package/release-gates.json +121 -0
- package/sql/20260809150000_job_run_ledger.sql +301 -0
- package/sql/20260809150001_job_run_ledger_v3.sql +70 -0
- package/src/bff/dispatch.ts +81 -0
- package/src/bff/envelope.ts +142 -0
- package/src/bff/health.ts +37 -0
- package/src/bff/index.ts +30 -0
- package/src/bff/lazyRoute.ts +28 -0
- package/src/bff/requestContext.ts +48 -0
- package/src/bff/routerRewrite.ts +38 -0
- package/src/blob/binding.ts +55 -0
- package/src/blob/filesystem.ts +86 -0
- package/src/blob/index.ts +17 -0
- package/src/blob/s3.ts +87 -0
- package/src/blob/s3Client.ts +91 -0
- package/src/blob/supabase.ts +88 -0
- package/src/blob/vercel.ts +127 -0
- package/src/composition/composeBundle.ts +109 -0
- package/src/composition/httpBinding.ts +30 -0
- package/src/composition/index.ts +17 -0
- package/src/composition/nodeHost.ts +289 -0
- package/src/data/binding.ts +157 -0
- package/src/data/index.ts +33 -0
- package/src/data/postgresGateway.ts +186 -0
- package/src/data/queryBuilder.ts +300 -0
- package/src/data/queryBuilderSql.ts +183 -0
- package/src/data/supabaseClientFactory.ts +92 -0
- package/src/data/supabaseGateway.ts +76 -0
- package/src/http/deadline.ts +117 -0
- package/src/http/index.ts +36 -0
- package/src/http/requestAdapter.ts +215 -0
- package/src/http/runtime.ts +319 -0
- package/src/http/security.ts +155 -0
- package/src/http/staticAssets.ts +42 -0
- package/src/http/types.ts +31 -0
- package/src/job-ledger/index.ts +25 -0
- package/src/job-ledger/ports.ts +27 -0
- package/src/job-ledger/postgres.ts +121 -0
- package/src/job-ledger/sql.ts +12 -0
- package/src/job-ledger/supabase.ts +294 -0
- package/src/kernel/index.ts +17 -0
- package/src/kernel/platformKernel.ts +153 -0
- package/src/migrations/index.ts +10 -0
- package/src/migrations/postgresRunner.ts +255 -0
- package/src/scheduler/advisoryLock.ts +145 -0
- package/src/scheduler/cronGuard.ts +30 -0
- package/src/scheduler/cronSecret.ts +26 -0
- package/src/scheduler/handlers.ts +24 -0
- package/src/scheduler/httpEntry.ts +49 -0
- package/src/scheduler/index.ts +47 -0
- package/src/scheduler/jobRegistry.ts +57 -0
- package/src/scheduler/jobs.ts +119 -0
- package/src/scheduler/nodeScheduler.ts +123 -0
- package/src/scheduler/vercelScheduler.ts +71 -0
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
// Managed (Supabase) adapter for the job-run ledger, and the managed v2/v3 compatibility calls.
|
|
2
|
+
//
|
|
3
|
+
// It speaks the managed job-control RPCs (`platform_claim_job_run`, `platform_finish_job_run_v2`
|
|
4
|
+
// and their v3 forms) through an injected client. Every claim and finish row carries the
|
|
5
|
+
// deployment's release correlation; the default failure reason and the two read-failure messages
|
|
6
|
+
// are the deployment's own values.
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
PlatformJobBackstopState,
|
|
10
|
+
PlatformJobFinishStatus,
|
|
11
|
+
PlatformJobFinishSummary,
|
|
12
|
+
PlatformJobInvocation,
|
|
13
|
+
PlatformJobRunLedgerPort,
|
|
14
|
+
} from "./ports.js";
|
|
15
|
+
|
|
16
|
+
export type PlatformJobDriver = "manual_admin" | "pg_cron" | "vercel_cron" | "worker";
|
|
17
|
+
|
|
18
|
+
type RpcResult<T> = {
|
|
19
|
+
data: T | null;
|
|
20
|
+
error: { message?: string } | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type PlatformJobMetadataQuery = {
|
|
24
|
+
select(columns: "metadata"): PlatformJobMetadataQuery;
|
|
25
|
+
eq(column: "job_name", value: string): PlatformJobMetadataQuery;
|
|
26
|
+
in(column: "status", values: readonly ["success", "failed"]): PlatformJobMetadataQuery;
|
|
27
|
+
not(column: "finished_at", operator: "is", value: null): PlatformJobMetadataQuery;
|
|
28
|
+
order(column: "started_at", options: { ascending: false }): PlatformJobMetadataQuery;
|
|
29
|
+
limit(count: 1): PlatformJobMetadataQuery;
|
|
30
|
+
maybeSingle(): PromiseLike<{
|
|
31
|
+
data: { metadata?: unknown } | null;
|
|
32
|
+
error: { message?: string } | null;
|
|
33
|
+
}>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type PlatformJobControlQuery = {
|
|
37
|
+
select(columns: "last_status,last_success_at"): PlatformJobControlQuery;
|
|
38
|
+
eq(column: "job_name", value: string): PlatformJobControlQuery;
|
|
39
|
+
maybeSingle(): PromiseLike<{
|
|
40
|
+
data: { last_status?: string | null; last_success_at?: string | null } | null;
|
|
41
|
+
error: { message?: string } | null;
|
|
42
|
+
}>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
export interface PlatformJobSupabaseClient {
|
|
46
|
+
rpc<T = unknown>(name: string, args: Record<string, unknown>): PromiseLike<RpcResult<T>>;
|
|
47
|
+
from(table: "platform_job_runs"): PlatformJobMetadataQuery;
|
|
48
|
+
from(table: "platform_job_controls"): PlatformJobControlQuery;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Minimal compatibility client for the legacy free-function surface. */
|
|
52
|
+
export type PlatformJobClient = Pick<PlatformJobSupabaseClient, "rpc">;
|
|
53
|
+
|
|
54
|
+
function triggerSourceForDriver(driver: PlatformJobDriver): string {
|
|
55
|
+
switch (driver) {
|
|
56
|
+
case "vercel_cron": return "vercel_cron";
|
|
57
|
+
case "pg_cron": return "pg_cron_scheduler";
|
|
58
|
+
case "worker": return "worker";
|
|
59
|
+
case "manual_admin": return "manual_admin";
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function platformJobInvocationForDriver(driver: PlatformJobDriver): PlatformJobInvocation {
|
|
64
|
+
switch (driver) {
|
|
65
|
+
case "manual_admin": return { triggerKind: "operator", invocationSource: driver };
|
|
66
|
+
case "pg_cron":
|
|
67
|
+
case "vercel_cron": return { triggerKind: "scheduler", invocationSource: driver };
|
|
68
|
+
case "worker": return { triggerKind: "worker", invocationSource: driver };
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function managedDriverForInvocation(invocation: PlatformJobInvocation): PlatformJobDriver {
|
|
73
|
+
if (!(["worker", "scheduler", "operator"] as const).includes(invocation.triggerKind)) {
|
|
74
|
+
throw new Error("platform_job_invalid_trigger_kind");
|
|
75
|
+
}
|
|
76
|
+
const source = invocation.invocationSource.trim();
|
|
77
|
+
const driver = source as PlatformJobDriver;
|
|
78
|
+
const expected = platformJobInvocationForDriver(driver);
|
|
79
|
+
if (!expected || expected.triggerKind !== invocation.triggerKind) {
|
|
80
|
+
throw new Error("platform_job_invalid_invocation");
|
|
81
|
+
}
|
|
82
|
+
return driver;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function claimResult(data: unknown): { acquired: boolean; runId: string | null; reason: string } {
|
|
86
|
+
const row = Array.isArray(data) ? data[0] : data;
|
|
87
|
+
const record = plainRecord(row);
|
|
88
|
+
return {
|
|
89
|
+
acquired: record?.acquired === true,
|
|
90
|
+
runId: typeof record?.run_id === "string" ? record.run_id : null,
|
|
91
|
+
reason: typeof record?.reason === "string" ? record.reason : "unknown",
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface ManagedJobRunOptions {
|
|
96
|
+
/** Release and deployment identity merged into every claim and finish row's metadata. */
|
|
97
|
+
releaseCorrelation: () => Record<string, string | null>;
|
|
98
|
+
/** Failure reason recorded when a failed run's summary carries none. */
|
|
99
|
+
defaultFailureReason: string;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export interface SupabasePlatformJobRunLedgerOptions extends ManagedJobRunOptions {
|
|
103
|
+
/** Message of the error thrown when the backstop state cannot be read. */
|
|
104
|
+
backstopReadError: string;
|
|
105
|
+
/** Message of the error thrown when the latest terminal run cannot be read. */
|
|
106
|
+
checkpointReadError: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** The managed v2/v3 compatibility calls, bound to the deployment's release correlation. */
|
|
110
|
+
export function createManagedJobRunCalls(options: ManagedJobRunOptions) {
|
|
111
|
+
/** Compatibility surface for existing managed v2 consumers. */
|
|
112
|
+
async function claimJobRun(
|
|
113
|
+
client: PlatformJobClient,
|
|
114
|
+
jobName: string,
|
|
115
|
+
driver: PlatformJobDriver,
|
|
116
|
+
leaseSeconds = 900,
|
|
117
|
+
) {
|
|
118
|
+
const { data, error } = await client.rpc("platform_claim_job_run", {
|
|
119
|
+
p_job_name: jobName,
|
|
120
|
+
p_driver: driver,
|
|
121
|
+
p_lease_seconds: leaseSeconds,
|
|
122
|
+
p_metadata: {
|
|
123
|
+
driver,
|
|
124
|
+
triggerSource: driver === "vercel_cron" ? "vercel_cron_manual" : driver,
|
|
125
|
+
...options.releaseCorrelation(),
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
if (error) throw new Error(error.message ?? "platform_claim_job_run failed");
|
|
129
|
+
return claimResult(data);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** Compatibility surface for existing managed v3 consumers. */
|
|
133
|
+
async function claimJobRunV3(
|
|
134
|
+
client: PlatformJobClient,
|
|
135
|
+
jobName: string,
|
|
136
|
+
invocation: PlatformJobInvocation,
|
|
137
|
+
leaseSeconds = 900,
|
|
138
|
+
) {
|
|
139
|
+
const invocationSource = invocation.invocationSource.trim();
|
|
140
|
+
if (!invocationSource) throw new Error("platform_job_invocation_source_required");
|
|
141
|
+
|
|
142
|
+
const { data, error } = await client.rpc("platform_claim_job_run_v3", {
|
|
143
|
+
p_job_name: jobName,
|
|
144
|
+
p_trigger_kind: invocation.triggerKind,
|
|
145
|
+
p_invocation_source: invocationSource,
|
|
146
|
+
p_lease_seconds: leaseSeconds,
|
|
147
|
+
p_metadata: {
|
|
148
|
+
triggerKind: invocation.triggerKind,
|
|
149
|
+
invocationSource,
|
|
150
|
+
...options.releaseCorrelation(),
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
if (error) throw new Error(error.message ?? "platform_claim_job_run_v3 failed");
|
|
154
|
+
return claimResult(data);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Compatibility surface for existing managed v2 consumers. */
|
|
158
|
+
async function finishJobRun(
|
|
159
|
+
client: PlatformJobClient,
|
|
160
|
+
jobName: string,
|
|
161
|
+
runId: string,
|
|
162
|
+
status: PlatformJobFinishStatus,
|
|
163
|
+
result: PlatformJobFinishSummary,
|
|
164
|
+
extraMetadata?: Record<string, unknown>,
|
|
165
|
+
) {
|
|
166
|
+
const { data, error } = await client.rpc<boolean>("platform_finish_job_run_v2", {
|
|
167
|
+
p_job_name: jobName,
|
|
168
|
+
p_run_id: runId,
|
|
169
|
+
p_status: status,
|
|
170
|
+
p_checked: result.checked,
|
|
171
|
+
p_updated: result.updated,
|
|
172
|
+
p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
|
|
173
|
+
p_support_code: null,
|
|
174
|
+
p_metadata: {
|
|
175
|
+
driver: "manual_admin",
|
|
176
|
+
failures: result.failures,
|
|
177
|
+
skipped: result.skipped,
|
|
178
|
+
reason: result.reason ?? null,
|
|
179
|
+
...options.releaseCorrelation(),
|
|
180
|
+
...(extraMetadata ?? {}),
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
if (error) throw new Error(error.message ?? "platform_finish_job_run_v2 failed");
|
|
184
|
+
return data === true;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** Compatibility surface for existing managed v3 consumers. */
|
|
188
|
+
async function finishJobRunV3(
|
|
189
|
+
client: PlatformJobClient,
|
|
190
|
+
jobName: string,
|
|
191
|
+
runId: string,
|
|
192
|
+
status: PlatformJobFinishStatus,
|
|
193
|
+
result: PlatformJobFinishSummary,
|
|
194
|
+
extraMetadata?: Record<string, unknown>,
|
|
195
|
+
) {
|
|
196
|
+
const { error } = await client.rpc("platform_finish_job_run_v3", {
|
|
197
|
+
p_job_name: jobName,
|
|
198
|
+
p_run_id: runId,
|
|
199
|
+
p_status: status,
|
|
200
|
+
p_checked: result.checked,
|
|
201
|
+
p_updated: result.updated,
|
|
202
|
+
p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
|
|
203
|
+
p_support_code: null,
|
|
204
|
+
p_metadata: {
|
|
205
|
+
failures: result.failures,
|
|
206
|
+
skipped: result.skipped,
|
|
207
|
+
reason: result.reason ?? null,
|
|
208
|
+
...options.releaseCorrelation(),
|
|
209
|
+
...(extraMetadata ?? {}),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
if (error) throw new Error(error.message ?? "platform_finish_job_run_v3 failed");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { claimJobRun, claimJobRunV3, finishJobRun, finishJobRunV3 };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Managed adapter for the semantic job-run port. */
|
|
219
|
+
export function createSupabasePlatformJobRunLedger(
|
|
220
|
+
client: PlatformJobSupabaseClient,
|
|
221
|
+
options: SupabasePlatformJobRunLedgerOptions,
|
|
222
|
+
): PlatformJobRunLedgerPort {
|
|
223
|
+
return {
|
|
224
|
+
async claimJobRun(jobName, invocation, leaseSeconds = 900) {
|
|
225
|
+
const driver = managedDriverForInvocation(invocation);
|
|
226
|
+
const { data, error } = await client.rpc("platform_claim_job_run", {
|
|
227
|
+
p_job_name: jobName,
|
|
228
|
+
p_driver: driver,
|
|
229
|
+
p_lease_seconds: leaseSeconds,
|
|
230
|
+
p_metadata: {
|
|
231
|
+
driver,
|
|
232
|
+
triggerSource: triggerSourceForDriver(driver),
|
|
233
|
+
...options.releaseCorrelation(),
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
if (error) throw new Error(error.message ?? "platform_claim_job_run failed");
|
|
237
|
+
return claimResult(data);
|
|
238
|
+
},
|
|
239
|
+
|
|
240
|
+
async finishJobRun(jobName, runId, invocation, status, result, extraMetadata) {
|
|
241
|
+
const driver = managedDriverForInvocation(invocation);
|
|
242
|
+
const { data, error } = await client.rpc<boolean>("platform_finish_job_run_v2", {
|
|
243
|
+
p_job_name: jobName,
|
|
244
|
+
p_run_id: runId,
|
|
245
|
+
p_status: status,
|
|
246
|
+
p_checked: result.checked,
|
|
247
|
+
p_updated: result.updated,
|
|
248
|
+
p_error: status === "failed" ? result.reason ?? options.defaultFailureReason : null,
|
|
249
|
+
p_support_code: null,
|
|
250
|
+
p_metadata: {
|
|
251
|
+
failures: result.failures,
|
|
252
|
+
skipped: result.skipped,
|
|
253
|
+
reason: result.reason ?? null,
|
|
254
|
+
...options.releaseCorrelation(),
|
|
255
|
+
...(extraMetadata ?? {}),
|
|
256
|
+
driver,
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
if (error) throw new Error(error.message ?? "platform_finish_job_run_v2 failed");
|
|
260
|
+
return data === true;
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
async readJobBackstopState(jobName): Promise<PlatformJobBackstopState | null> {
|
|
264
|
+
const { data, error } = await client
|
|
265
|
+
.from("platform_job_controls")
|
|
266
|
+
.select("last_status,last_success_at")
|
|
267
|
+
.eq("job_name", jobName)
|
|
268
|
+
.maybeSingle();
|
|
269
|
+
if (error) throw new Error(options.backstopReadError);
|
|
270
|
+
return data ? {
|
|
271
|
+
lastStatus: data.last_status ?? null,
|
|
272
|
+
lastSuccessAt: data.last_success_at ?? null,
|
|
273
|
+
} : null;
|
|
274
|
+
},
|
|
275
|
+
|
|
276
|
+
async readLatestTerminalRunMetadata(jobName) {
|
|
277
|
+
const { data, error } = await client
|
|
278
|
+
.from("platform_job_runs")
|
|
279
|
+
.select("metadata")
|
|
280
|
+
.eq("job_name", jobName)
|
|
281
|
+
.in("status", ["success", "failed"])
|
|
282
|
+
.order("started_at", { ascending: false })
|
|
283
|
+
.limit(1)
|
|
284
|
+
.maybeSingle();
|
|
285
|
+
if (error) throw new Error(options.checkpointReadError);
|
|
286
|
+
return plainRecord(data?.metadata);
|
|
287
|
+
},
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function plainRecord(value: unknown): Record<string, unknown> | null {
|
|
292
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return null;
|
|
293
|
+
return { ...(value as Record<string, unknown>) };
|
|
294
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export {
|
|
2
|
+
BLOB_CAPABILITIES,
|
|
3
|
+
DEFAULT_PLATFORM_BUNDLE,
|
|
4
|
+
ENV_MISSING_CODE,
|
|
5
|
+
PLATFORM_BUNDLE_DESCRIPTORS,
|
|
6
|
+
PLATFORM_BUNDLE_IDS,
|
|
7
|
+
getBundleDescriptor,
|
|
8
|
+
isPlatformBundleId,
|
|
9
|
+
platformEnvSchema,
|
|
10
|
+
resolveBlobCapability,
|
|
11
|
+
resolveBundleId,
|
|
12
|
+
type BlobCapability,
|
|
13
|
+
type PlatformBundleDescriptor,
|
|
14
|
+
type PlatformBundleId,
|
|
15
|
+
type PlatformBundleReadiness,
|
|
16
|
+
type PlatformEnv,
|
|
17
|
+
} from "./platformKernel.js";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
createPlatformBundleRegistry,
|
|
5
|
+
platformEnvSchema as corePlatformEnvSchema,
|
|
6
|
+
type PlatformBundleDescriptor as CorePlatformBundleDescriptor,
|
|
7
|
+
type PlatformBundleReadiness,
|
|
8
|
+
type PlatformEnvInput,
|
|
9
|
+
createPlatformBundleIdGuard,
|
|
10
|
+
} from "@openlup/core/platform-runtime";
|
|
11
|
+
|
|
12
|
+
/** The bundles a deployment can select with `PLATFORM_BUNDLE`. */
|
|
13
|
+
export const PLATFORM_BUNDLE_IDS = [
|
|
14
|
+
"vercel-supabase",
|
|
15
|
+
"node-supabase",
|
|
16
|
+
"node-postgres",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type PlatformBundleId = (typeof PLATFORM_BUNDLE_IDS)[number];
|
|
20
|
+
|
|
21
|
+
/** The bundle selected when `PLATFORM_BUNDLE` is absent or unknown. */
|
|
22
|
+
export const DEFAULT_PLATFORM_BUNDLE: PlatformBundleId = "vercel-supabase";
|
|
23
|
+
|
|
24
|
+
export const isPlatformBundleId = createPlatformBundleIdGuard(PLATFORM_BUNDLE_IDS);
|
|
25
|
+
|
|
26
|
+
export type PlatformBundleDescriptor = CorePlatformBundleDescriptor & { id: PlatformBundleId };
|
|
27
|
+
export type { PlatformBundleReadiness };
|
|
28
|
+
|
|
29
|
+
/** The readiness code a bundle reports when a selected capability lacks its configuration. */
|
|
30
|
+
export const ENV_MISSING_CODE = "OPENLUP_E_ENV_MISSING";
|
|
31
|
+
|
|
32
|
+
/** The blob capabilities a deployment can select with `PLATFORM_BLOB`. */
|
|
33
|
+
export const BLOB_CAPABILITIES = ["filesystem", "s3", "supabase", "vercel-blob"] as const;
|
|
34
|
+
|
|
35
|
+
export type BlobCapability = (typeof BLOB_CAPABILITIES)[number];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* The blob capability in effect: a valid `PLATFORM_BLOB` selection, else the bundle's own. An
|
|
39
|
+
* invalid selection keeps the bundle's own here and is refused by readiness.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveBlobCapability(
|
|
42
|
+
descriptor: Pick<PlatformBundleDescriptor, "capabilities">,
|
|
43
|
+
env: PlatformEnvInput,
|
|
44
|
+
): string {
|
|
45
|
+
const selected = env.PLATFORM_BLOB?.trim();
|
|
46
|
+
return selected && (BLOB_CAPABILITIES as readonly string[]).includes(selected) ? selected : descriptor.capabilities.blob;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The settings each blob capability needs; an alternative name may stand in for the first. */
|
|
50
|
+
const BLOB_SETTINGS: Readonly<Record<string, ReadonlyArray<readonly string[]>>> = {
|
|
51
|
+
"vercel-blob": [["BLOB_READ_WRITE_TOKEN"]],
|
|
52
|
+
supabase: [["SUPABASE_URL", "VITE_SUPABASE_URL"], ["SUPABASE_SERVICE_ROLE_KEY"], ["BLOB_SUPABASE_BUCKET"]],
|
|
53
|
+
filesystem: [["BLOB_FS_ROOT"], ["BLOB_FS_BASE_URL"]],
|
|
54
|
+
s3: [["BLOB_S3_BUCKET"], ["BLOB_S3_BASE_URL"]],
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function present(env: PlatformEnvInput, name: string): boolean {
|
|
58
|
+
return typeof env[name] === "string" && env[name]!.trim() !== "";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Per-capability readiness, in this order: an unknown PLATFORM_BUNDLE refuses (resolution itself
|
|
63
|
+
* keeps defaulting); a self-host bundle needs APP_BASE_URL; then every selected capability's
|
|
64
|
+
* missing settings are reported together under OPENLUP_E_ENV_MISSING: a scheduler needs
|
|
65
|
+
* CRON_SECRET, Node scheduling needs DATABASE_URL for its cluster lock (never a silent no-op lock),
|
|
66
|
+
* and the selected blob capability needs its settings. An unknown PLATFORM_BLOB refuses too.
|
|
67
|
+
*/
|
|
68
|
+
function capabilityReadiness(descriptor: Pick<PlatformBundleDescriptor, "capabilities">, selfHost: boolean) {
|
|
69
|
+
return (env: PlatformEnvInput): PlatformBundleReadiness => {
|
|
70
|
+
const raw = env.PLATFORM_BUNDLE;
|
|
71
|
+
if (raw !== undefined && raw.trim() !== "" && !isPlatformBundleId(raw)) {
|
|
72
|
+
return { ok: false, error: `unknown PLATFORM_BUNDLE "${raw}"; expected one of ${PLATFORM_BUNDLE_IDS.join(", ")}` };
|
|
73
|
+
}
|
|
74
|
+
const selectedBlob = env.PLATFORM_BLOB;
|
|
75
|
+
if (selectedBlob !== undefined && selectedBlob.trim() !== "" && !(BLOB_CAPABILITIES as readonly string[]).includes(selectedBlob.trim())) {
|
|
76
|
+
return { ok: false, error: `unknown PLATFORM_BLOB "${selectedBlob}"; expected one of ${BLOB_CAPABILITIES.join(", ")}` };
|
|
77
|
+
}
|
|
78
|
+
if (selfHost && !present(env, "APP_BASE_URL")) {
|
|
79
|
+
return { ok: false, error: "APP_BASE_URL is required for self-host bundles" };
|
|
80
|
+
}
|
|
81
|
+
const { scheduler } = descriptor.capabilities;
|
|
82
|
+
const blob = resolveBlobCapability(descriptor, env);
|
|
83
|
+
const missing: string[] = [];
|
|
84
|
+
if (scheduler === "vercel-cron" || scheduler === "node-cron") {
|
|
85
|
+
if (!present(env, "CRON_SECRET")) missing.push("CRON_SECRET (scheduler)");
|
|
86
|
+
}
|
|
87
|
+
if (scheduler === "node-cron" && !present(env, "DATABASE_URL")) missing.push("DATABASE_URL (scheduler lock)");
|
|
88
|
+
for (const names of BLOB_SETTINGS[blob] ?? []) {
|
|
89
|
+
if (!names.some((name) => present(env, name))) missing.push(`${names[0]} (blob: ${blob})`);
|
|
90
|
+
}
|
|
91
|
+
return missing.length === 0 ? { ok: true } : { ok: false, error: `${ENV_MISSING_CODE}: ${missing.join(", ")}` };
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function descriptor(
|
|
96
|
+
id: PlatformBundleId,
|
|
97
|
+
capabilities: PlatformBundleDescriptor["capabilities"],
|
|
98
|
+
selfHost: boolean,
|
|
99
|
+
): PlatformBundleDescriptor {
|
|
100
|
+
return { id, capabilities, readReadiness: capabilityReadiness({ capabilities }, selfHost) };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const DESCRIPTORS: readonly PlatformBundleDescriptor[] = [
|
|
104
|
+
descriptor("vercel-supabase", {
|
|
105
|
+
http: "vercel",
|
|
106
|
+
scheduler: "vercel-cron",
|
|
107
|
+
blob: "vercel-blob",
|
|
108
|
+
data: "supabase",
|
|
109
|
+
migrations: "supabase",
|
|
110
|
+
analytics: "vercel",
|
|
111
|
+
transactional: "application",
|
|
112
|
+
}, false),
|
|
113
|
+
descriptor("node-supabase", {
|
|
114
|
+
http: "node",
|
|
115
|
+
scheduler: "node-cron",
|
|
116
|
+
blob: "supabase",
|
|
117
|
+
data: "supabase",
|
|
118
|
+
migrations: "supabase",
|
|
119
|
+
analytics: "gtm",
|
|
120
|
+
transactional: "node",
|
|
121
|
+
}, true),
|
|
122
|
+
descriptor("node-postgres", {
|
|
123
|
+
http: "node",
|
|
124
|
+
scheduler: "node-cron",
|
|
125
|
+
blob: "filesystem",
|
|
126
|
+
data: "postgres",
|
|
127
|
+
migrations: "postgres",
|
|
128
|
+
analytics: "noop",
|
|
129
|
+
transactional: "node",
|
|
130
|
+
}, true),
|
|
131
|
+
];
|
|
132
|
+
|
|
133
|
+
const registry = createPlatformBundleRegistry({
|
|
134
|
+
descriptors: DESCRIPTORS,
|
|
135
|
+
defaultBundleId: DEFAULT_PLATFORM_BUNDLE,
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
export const PLATFORM_BUNDLE_DESCRIPTORS: readonly PlatformBundleDescriptor[] =
|
|
139
|
+
registry.descriptors as readonly PlatformBundleDescriptor[];
|
|
140
|
+
|
|
141
|
+
export function getBundleDescriptor(id: PlatformBundleId): PlatformBundleDescriptor {
|
|
142
|
+
return registry.getBundleDescriptor(id) as PlatformBundleDescriptor;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export function resolveBundleId(env: PlatformEnvInput): PlatformBundleId {
|
|
146
|
+
return registry.resolveBundleId(env) as PlatformBundleId;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const platformEnvSchema = corePlatformEnvSchema.extend({
|
|
150
|
+
PLATFORM_BUNDLE: z.enum(PLATFORM_BUNDLE_IDS).optional(),
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
export type PlatformEnv = z.infer<typeof platformEnvSchema>;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export {
|
|
2
|
+
createPostgresMigrationRunner,
|
|
3
|
+
resolvePostgresMigrationRunnerEnv,
|
|
4
|
+
type PlatformMigrationFile,
|
|
5
|
+
type PlatformMigrationManifest,
|
|
6
|
+
type PlatformMigrationManifestEntry,
|
|
7
|
+
type PlatformMigrationSource,
|
|
8
|
+
type PostgresMigrationRunnerEnv,
|
|
9
|
+
type PostgresMigrationRunnerOptions,
|
|
10
|
+
} from "./postgresRunner.js";
|