@supacloud/lite 0.7.3 → 0.8.1
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 +20 -0
- package/LICENSES/POSTGRESQL-17-COPYRIGHT.txt +23 -0
- package/LICENSES/THESEUS-POSTGRESQL-LICENSE.txt +7 -0
- package/THIRD_PARTY_NOTICES.md +11 -1
- package/dist/cli.js +2674 -375
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2537 -305
- package/dist/project-runtime.d.ts +5 -0
- package/dist/project-runtime.d.ts.map +1 -1
- package/dist/runtime/auth/handler.d.ts +5 -2
- package/dist/runtime/auth/handler.d.ts.map +1 -1
- package/dist/runtime/db/data-dir-lock.d.ts +2 -0
- package/dist/runtime/db/data-dir-lock.d.ts.map +1 -1
- package/dist/runtime/db/database.d.ts.map +1 -1
- package/dist/runtime/db/emulated.d.ts +2 -1
- package/dist/runtime/db/emulated.d.ts.map +1 -1
- package/dist/runtime/db/pglite-engine.d.ts.map +1 -1
- package/dist/runtime/node/fs-driver.d.ts +2 -0
- package/dist/runtime/node/fs-driver.d.ts.map +1 -1
- package/dist/runtime/node/native/engine.d.ts +23 -0
- package/dist/runtime/node/native/engine.d.ts.map +1 -0
- package/dist/runtime/node/native/wire-engine.d.ts +9 -0
- package/dist/runtime/node/native/wire-engine.d.ts.map +1 -0
- package/dist/runtime/node/native/wire.d.ts +56 -0
- package/dist/runtime/node/native/wire.d.ts.map +1 -0
- package/dist/runtime/rest/handler.d.ts.map +1 -1
- package/dist/runtime/storage/handler.d.ts +5 -0
- package/dist/runtime/storage/handler.d.ts.map +1 -1
- package/dist/runtime/storage/image-transform-cache.d.ts +13 -0
- package/dist/runtime/storage/image-transform-cache.d.ts.map +1 -0
- package/dist/runtime/storage/image-transform.d.ts +1 -1
- package/dist/runtime/storage/image-transform.d.ts.map +1 -1
- package/dist/runtime/storage/s3-driver.d.ts +2 -0
- package/dist/runtime/storage/s3-driver.d.ts.map +1 -1
- package/dist/runtime/types.d.ts +2 -0
- package/dist/runtime/types.d.ts.map +1 -1
- package/dist/snapshot.d.ts +5 -1
- package/dist/snapshot.d.ts.map +1 -1
- package/package.json +6 -2
package/dist/cli.js
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
var __require = import.meta.require;
|
|
4
4
|
|
|
5
5
|
// src/cli.ts
|
|
6
|
-
import { mkdir as mkdir7, rm as
|
|
7
|
-
import { dirname as
|
|
6
|
+
import { mkdir as mkdir7, rm as rm6, writeFile as writeFile7 } from "fs/promises";
|
|
7
|
+
import { dirname as dirname7, join as join10, resolve as resolve4 } from "path";
|
|
8
8
|
// package.json
|
|
9
9
|
var package_default = {
|
|
10
10
|
name: "@supacloud/lite",
|
|
11
|
-
version: "0.
|
|
12
|
-
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite",
|
|
11
|
+
version: "0.8.1",
|
|
12
|
+
description: "Bun-native, single-project Supabase-compatible backend powered by PGlite or native PostgreSQL",
|
|
13
13
|
type: "module",
|
|
14
14
|
license: "Apache-2.0",
|
|
15
15
|
bin: {
|
|
@@ -45,6 +45,10 @@ var package_default = {
|
|
|
45
45
|
dev: "bun run src/cli.ts start",
|
|
46
46
|
start: "bun run src/cli.ts start",
|
|
47
47
|
test: "bun test --timeout 20000",
|
|
48
|
+
"test:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun test test/native-engine.test.ts --timeout 180000",
|
|
49
|
+
parity: "bun run parity/harness.ts",
|
|
50
|
+
"parity:native": "SUPACLOUD_LITE_TEST_NATIVE=1 bun run parity/harness.ts --engine native",
|
|
51
|
+
"check:native": "bun run test:native && bun run parity:native",
|
|
48
52
|
"test:package": "bun run scripts/package-smoke.ts",
|
|
49
53
|
"test:standalone": "bun run scripts/standalone-smoke.ts",
|
|
50
54
|
prepack: "bun run build",
|
|
@@ -1292,7 +1296,8 @@ class AuthHandler {
|
|
|
1292
1296
|
return await this.oauth.authorize(url);
|
|
1293
1297
|
}
|
|
1294
1298
|
if (path === "callback" && (method === "GET" || method === "POST")) {
|
|
1295
|
-
|
|
1299
|
+
const callbackUrl = method === "POST" ? await this.oauthCallbackUrl(req, url) : url;
|
|
1300
|
+
return await this.oauth.callback(callbackUrl, (userId) => this.oauthSessionTokensFor(userId));
|
|
1296
1301
|
}
|
|
1297
1302
|
if (path.startsWith("admin/"))
|
|
1298
1303
|
return await this.admin(req, ctx, path, method);
|
|
@@ -1302,6 +1307,16 @@ class AuthHandler {
|
|
|
1302
1307
|
return authError(500, "unexpected_failure", msg);
|
|
1303
1308
|
}
|
|
1304
1309
|
}
|
|
1310
|
+
async oauthCallbackUrl(req, url) {
|
|
1311
|
+
const callbackUrl = new URL(url);
|
|
1312
|
+
const form = await req.formData();
|
|
1313
|
+
for (const field of ["code", "state"]) {
|
|
1314
|
+
const value = form.get(field);
|
|
1315
|
+
if (typeof value === "string" && value)
|
|
1316
|
+
callbackUrl.searchParams.set(field, value);
|
|
1317
|
+
}
|
|
1318
|
+
return callbackUrl;
|
|
1319
|
+
}
|
|
1305
1320
|
async signup(req) {
|
|
1306
1321
|
const body = await req.json().catch(() => ({}));
|
|
1307
1322
|
if (!body.email && !body.password) {
|
|
@@ -1379,7 +1394,7 @@ class AuthHandler {
|
|
|
1379
1394
|
if (!userId)
|
|
1380
1395
|
return authError(403, "flow_state_not_found", "invalid or expired auth code");
|
|
1381
1396
|
const ures = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
1382
|
-
return json(200, await this.
|
|
1397
|
+
return json(200, await this.sessionForOAuth(ures.rows[0]));
|
|
1383
1398
|
}
|
|
1384
1399
|
return authError(400, "invalid_grant", `unsupported grant_type: ${grantType}`);
|
|
1385
1400
|
}
|
|
@@ -1424,17 +1439,18 @@ class AuthHandler {
|
|
|
1424
1439
|
sets.push(`is_anonymous = false`);
|
|
1425
1440
|
sets.push(`raw_app_meta_data = coalesce(raw_app_meta_data, '{}'::jsonb) || '{"provider":"email","providers":["email"]}'::jsonb`);
|
|
1426
1441
|
}
|
|
1427
|
-
if (sets.length === 0)
|
|
1428
|
-
return json(200, this.userJson(user));
|
|
1429
|
-
params.push(user.id);
|
|
1430
|
-
const res = await this.db.query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1431
|
-
const updated = res.rows[0];
|
|
1432
|
-
if (upgradingAnon) {
|
|
1433
|
-
await this.db.query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
1434
|
-
values ($1, 'email', $2, $3)
|
|
1435
|
-
on conflict (provider, provider_id) do nothing`, [updated.id, updated.id, JSON.stringify({ sub: updated.id, email: updated.email })]);
|
|
1442
|
+
if (sets.length === 0) {
|
|
1443
|
+
return json(200, this.userJson(user, await this.getUserFactors(user.id), await this.getUserIdentities(user.id)));
|
|
1436
1444
|
}
|
|
1437
|
-
|
|
1445
|
+
params.push(user.id);
|
|
1446
|
+
const payload = await this.db.transaction(async (query) => {
|
|
1447
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1448
|
+
const updated = res.rows[0];
|
|
1449
|
+
if (body.email)
|
|
1450
|
+
await this.ensureEmailIdentity(updated, query);
|
|
1451
|
+
return this.userJsonWithRelations(updated, query);
|
|
1452
|
+
});
|
|
1453
|
+
return json(200, payload);
|
|
1438
1454
|
}
|
|
1439
1455
|
async logout(req, url) {
|
|
1440
1456
|
const claims = await this.claimsFromBearer(req);
|
|
@@ -1464,7 +1480,7 @@ class AuthHandler {
|
|
|
1464
1480
|
let user = res.rows[0];
|
|
1465
1481
|
if (!user) {
|
|
1466
1482
|
if (!createUser)
|
|
1467
|
-
return
|
|
1483
|
+
return json(200, {});
|
|
1468
1484
|
if (this.settings.disableSignup)
|
|
1469
1485
|
return authError(422, "signup_disabled", "Signups not allowed for this instance");
|
|
1470
1486
|
res = await this.db.query(`insert into auth.users (aud, role, email, raw_app_meta_data, raw_user_meta_data)
|
|
@@ -1796,6 +1812,8 @@ Or sign in with this link: ${link}`
|
|
|
1796
1812
|
}
|
|
1797
1813
|
const idMatch = path.match(/^admin\/users\/([0-9a-f-]{36})$/);
|
|
1798
1814
|
const exportMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/export$/);
|
|
1815
|
+
const factorsMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors$/);
|
|
1816
|
+
const factorMatch = path.match(/^admin\/users\/([0-9a-f-]{36})\/factors\/([0-9a-f-]{36})$/);
|
|
1799
1817
|
if (path === "admin/generate_link" && method === "POST") {
|
|
1800
1818
|
return await this.generateAdminMagicLink(req);
|
|
1801
1819
|
}
|
|
@@ -1807,32 +1825,49 @@ Or sign in with this link: ${link}`
|
|
|
1807
1825
|
if (exportMatch && method === "GET") {
|
|
1808
1826
|
return await this.exportUser(exportMatch[1]);
|
|
1809
1827
|
}
|
|
1828
|
+
if (factorsMatch && method === "GET") {
|
|
1829
|
+
const user = await this.db.query(`select 1 from auth.users where id = $1`, [factorsMatch[1]]);
|
|
1830
|
+
if (user.rows.length === 0)
|
|
1831
|
+
return authError(404, "user_not_found", "User not found");
|
|
1832
|
+
return json(200, await this.getUserFactors(factorsMatch[1]));
|
|
1833
|
+
}
|
|
1834
|
+
if (factorMatch && method === "DELETE") {
|
|
1835
|
+
const deleted = await this.db.query(`delete from auth.mfa_factors where user_id = $1 and id = $2 returning id`, [factorMatch[1], factorMatch[2]]);
|
|
1836
|
+
if (deleted.rows.length === 0)
|
|
1837
|
+
return authError(404, "mfa_factor_not_found", "MFA factor not found");
|
|
1838
|
+
return json(200, { id: factorMatch[2] });
|
|
1839
|
+
}
|
|
1810
1840
|
if (path === "admin/users" && method === "GET") {
|
|
1811
1841
|
const res = await this.db.query(`select * from auth.users order by created_at desc limit 1000`);
|
|
1812
|
-
return json(200, { users: res.rows.map((
|
|
1842
|
+
return json(200, { users: res.rows.map((user) => this.userJson(user)), aud: "authenticated" });
|
|
1813
1843
|
}
|
|
1814
1844
|
if (path === "admin/users" && method === "POST") {
|
|
1815
1845
|
const body = await req.json().catch(() => ({}));
|
|
1816
1846
|
if (!body.email)
|
|
1817
1847
|
return authError(400, "validation_failed", "email is required");
|
|
1818
1848
|
const hashed = body.password ? await hashPassword(body.password) : null;
|
|
1819
|
-
const
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1849
|
+
const created = await this.db.transaction(async (query) => {
|
|
1850
|
+
const res = await query(`insert into auth.users
|
|
1851
|
+
(aud, role, email, encrypted_password, email_confirmed_at, raw_app_meta_data, raw_user_meta_data)
|
|
1852
|
+
values ('authenticated', 'authenticated', $1, $2, case when $3 then now() else null end, $4, $5)
|
|
1853
|
+
returning *`, [
|
|
1854
|
+
body.email.toLowerCase().trim(),
|
|
1855
|
+
hashed,
|
|
1856
|
+
body.email_confirm ?? true,
|
|
1857
|
+
JSON.stringify({ provider: "email", providers: ["email"], ...body.app_metadata ?? {} }),
|
|
1858
|
+
JSON.stringify(body.user_metadata ?? {})
|
|
1859
|
+
]);
|
|
1860
|
+
const user = res.rows[0];
|
|
1861
|
+
await this.ensureEmailIdentity(user, query);
|
|
1862
|
+
return this.userJsonWithRelations(user, query);
|
|
1863
|
+
});
|
|
1864
|
+
return json(200, created);
|
|
1830
1865
|
}
|
|
1831
1866
|
if (idMatch && method === "GET") {
|
|
1832
1867
|
const res = await this.db.query(`select * from auth.users where id = $1`, [idMatch[1]]);
|
|
1833
1868
|
if (res.rows.length === 0)
|
|
1834
1869
|
return authError(404, "user_not_found", "User not found");
|
|
1835
|
-
return json(200, this.
|
|
1870
|
+
return json(200, await this.userJsonWithRelations(res.rows[0]));
|
|
1836
1871
|
}
|
|
1837
1872
|
if (idMatch && method === "PUT") {
|
|
1838
1873
|
const body = await req.json().catch(() => ({}));
|
|
@@ -1859,10 +1894,18 @@ Or sign in with this link: ${link}`
|
|
|
1859
1894
|
if (sets.length === 0)
|
|
1860
1895
|
return authError(400, "validation_failed", "nothing to update");
|
|
1861
1896
|
params.push(idMatch[1]);
|
|
1862
|
-
const
|
|
1863
|
-
|
|
1897
|
+
const updated = await this.db.transaction(async (query) => {
|
|
1898
|
+
const res = await query(`update auth.users set ${sets.join(", ")}, updated_at = now() where id = $${params.length} returning *`, params);
|
|
1899
|
+
const user = res.rows[0];
|
|
1900
|
+
if (!user)
|
|
1901
|
+
return null;
|
|
1902
|
+
if (typeof body.email === "string")
|
|
1903
|
+
await this.ensureEmailIdentity(user, query);
|
|
1904
|
+
return this.userJsonWithRelations(user, query);
|
|
1905
|
+
});
|
|
1906
|
+
if (!updated)
|
|
1864
1907
|
return authError(404, "user_not_found", "User not found");
|
|
1865
|
-
return json(200,
|
|
1908
|
+
return json(200, updated);
|
|
1866
1909
|
}
|
|
1867
1910
|
if (idMatch && method === "DELETE") {
|
|
1868
1911
|
return await this.eraseUser(idMatch[1]);
|
|
@@ -2058,17 +2101,26 @@ Or sign in with this link: ${link}`
|
|
|
2058
2101
|
if (!await verifyTotp(factor.secret, body.code ?? "")) {
|
|
2059
2102
|
return authError(422, "mfa_verification_failed", "Invalid TOTP code entered");
|
|
2060
2103
|
}
|
|
2061
|
-
await this.db.
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2104
|
+
const session = await this.db.transaction(async (query) => {
|
|
2105
|
+
const claimed = await query(`update auth.mfa_challenges set verified_at = now()
|
|
2106
|
+
where id = $1 and factor_id = $2 and verified_at is null and expires_at >= now()
|
|
2107
|
+
returning id`, [challenge.id, factorId]);
|
|
2108
|
+
if (claimed.rows.length === 0)
|
|
2109
|
+
return null;
|
|
2110
|
+
if (factor.status !== "verified") {
|
|
2111
|
+
await query(`update auth.mfa_factors set status = 'verified', updated_at = now() where id = $1`, [factorId]);
|
|
2112
|
+
}
|
|
2113
|
+
return this.sessionFor(user, undefined, {
|
|
2114
|
+
aal: "aal2",
|
|
2115
|
+
amr: [
|
|
2116
|
+
{ method: "password", timestamp: Math.floor(Date.now() / 1000) },
|
|
2117
|
+
{ method: "totp", timestamp: Math.floor(Date.now() / 1000) }
|
|
2118
|
+
]
|
|
2119
|
+
}, query);
|
|
2071
2120
|
});
|
|
2121
|
+
if (!session) {
|
|
2122
|
+
return authError(422, "mfa_verification_failed", "This challenge has already been verified");
|
|
2123
|
+
}
|
|
2072
2124
|
return json(200, session);
|
|
2073
2125
|
}
|
|
2074
2126
|
async unenrollFactor(req, factorId) {
|
|
@@ -2106,6 +2158,17 @@ Or sign in with this link: ${link}`
|
|
|
2106
2158
|
updated_at: iso(r.updated_at)
|
|
2107
2159
|
}));
|
|
2108
2160
|
}
|
|
2161
|
+
async ensureEmailIdentity(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2162
|
+
if (!user.email)
|
|
2163
|
+
return;
|
|
2164
|
+
await query(`insert into auth.identities (user_id, provider, provider_id, identity_data)
|
|
2165
|
+
values ($1::uuid, 'email', $1::text, $2::jsonb)
|
|
2166
|
+
on conflict (provider, provider_id) do update
|
|
2167
|
+
set identity_data = excluded.identity_data, updated_at = now()`, [user.id, JSON.stringify({ sub: user.id, email: user.email })]);
|
|
2168
|
+
}
|
|
2169
|
+
async userJsonWithRelations(user, query = (sql, params) => this.db.query(sql, params)) {
|
|
2170
|
+
return this.userJson(user, await this.getUserFactors(user.id, query), await this.getUserIdentities(user.id, query));
|
|
2171
|
+
}
|
|
2109
2172
|
async rotateRefreshToken(token) {
|
|
2110
2173
|
return this.db.transaction(async (query) => {
|
|
2111
2174
|
const claimedToken = await this.claimRefreshToken(query, token);
|
|
@@ -2179,9 +2242,14 @@ Or sign in with this link: ${link}`
|
|
|
2179
2242
|
is_anonymous: u.is_anonymous ?? false
|
|
2180
2243
|
};
|
|
2181
2244
|
}
|
|
2182
|
-
|
|
2245
|
+
sessionForOAuth(user) {
|
|
2246
|
+
return this.sessionFor(user, undefined, {
|
|
2247
|
+
amr: [{ method: "oauth", timestamp: Math.floor(Date.now() / 1000) }]
|
|
2248
|
+
});
|
|
2249
|
+
}
|
|
2250
|
+
async oauthSessionTokensFor(userId) {
|
|
2183
2251
|
const res = await this.db.query(`select * from auth.users where id = $1`, [userId]);
|
|
2184
|
-
const session = await this.
|
|
2252
|
+
const session = await this.sessionForOAuth(res.rows[0]);
|
|
2185
2253
|
return { access_token: session.access_token, refresh_token: session.refresh_token, expires_in: session.expires_in };
|
|
2186
2254
|
}
|
|
2187
2255
|
async sessionFor(user, parentToken, opts, query = (sql, params) => this.db.query(sql, params)) {
|
|
@@ -3545,41 +3613,1158 @@ $pgmq$;
|
|
|
3545
3613
|
revoke all on all functions in schema pgmq from public, anon, authenticated;
|
|
3546
3614
|
grant execute on all functions in schema pgmq to service_role;
|
|
3547
3615
|
|
|
3548
|
-
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
3564
|
-
|
|
3565
|
-
|
|
3566
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3569
|
-
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3616
|
+
-- supacloud:sql-module:pgmq-public:start
|
|
3617
|
+
DO $pgmq_extension$
|
|
3618
|
+
BEGIN
|
|
3619
|
+
IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
|
|
3620
|
+
EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
|
|
3621
|
+
END IF;
|
|
3622
|
+
END
|
|
3623
|
+
$pgmq_extension$;
|
|
3624
|
+
|
|
3625
|
+
CREATE SCHEMA IF NOT EXISTS pgmq_public;
|
|
3626
|
+
GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3627
|
+
|
|
3628
|
+
CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
|
|
3629
|
+
RETURNS text
|
|
3630
|
+
LANGUAGE plpgsql
|
|
3631
|
+
IMMUTABLE
|
|
3632
|
+
SET search_path = ''
|
|
3633
|
+
AS $$
|
|
3634
|
+
DECLARE
|
|
3635
|
+
normalized_queue_name text := lower(btrim(queue_name));
|
|
3636
|
+
BEGIN
|
|
3637
|
+
IF normalized_queue_name IS NULL
|
|
3638
|
+
OR left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN
|
|
3639
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';
|
|
3640
|
+
END IF;
|
|
3641
|
+
RETURN normalized_queue_name;
|
|
3642
|
+
END;
|
|
3643
|
+
$$;
|
|
3644
|
+
|
|
3645
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)
|
|
3646
|
+
RETURNS SETOF bigint
|
|
3647
|
+
LANGUAGE sql
|
|
3648
|
+
VOLATILE
|
|
3649
|
+
SECURITY DEFINER
|
|
3650
|
+
SET search_path = ''
|
|
3651
|
+
AS $$ SELECT * FROM pgmq.send(pgmq_public.require_public_queue(queue_name), message, sleep_seconds); $$;
|
|
3652
|
+
|
|
3653
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)
|
|
3654
|
+
RETURNS SETOF bigint
|
|
3655
|
+
LANGUAGE sql
|
|
3656
|
+
VOLATILE
|
|
3657
|
+
SECURITY DEFINER
|
|
3658
|
+
SET search_path = ''
|
|
3659
|
+
AS $$ SELECT * FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), messages, sleep_seconds); $$;
|
|
3660
|
+
|
|
3661
|
+
CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
|
|
3662
|
+
RETURNS SETOF pgmq.message_record
|
|
3663
|
+
LANGUAGE sql
|
|
3664
|
+
VOLATILE
|
|
3665
|
+
SECURITY DEFINER
|
|
3666
|
+
SET search_path = ''
|
|
3667
|
+
AS $$ SELECT * FROM pgmq.read(pgmq_public.require_public_queue(queue_name), sleep_seconds, n); $$;
|
|
3668
|
+
|
|
3669
|
+
CREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)
|
|
3670
|
+
RETURNS SETOF pgmq.message_record
|
|
3671
|
+
LANGUAGE sql
|
|
3672
|
+
VOLATILE
|
|
3673
|
+
SECURITY DEFINER
|
|
3674
|
+
SET search_path = ''
|
|
3675
|
+
AS $$ SELECT * FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)); $$;
|
|
3676
|
+
|
|
3677
|
+
CREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)
|
|
3678
|
+
RETURNS boolean
|
|
3679
|
+
LANGUAGE sql
|
|
3680
|
+
VOLATILE
|
|
3681
|
+
SECURITY DEFINER
|
|
3682
|
+
SET search_path = ''
|
|
3683
|
+
AS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), message_id); $$;
|
|
3684
|
+
|
|
3685
|
+
CREATE OR REPLACE FUNCTION pgmq_public."delete"(queue_name text, message_id bigint)
|
|
3686
|
+
RETURNS boolean
|
|
3687
|
+
LANGUAGE sql
|
|
3688
|
+
VOLATILE
|
|
3689
|
+
SECURITY DEFINER
|
|
3690
|
+
SET search_path = ''
|
|
3691
|
+
AS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), message_id); $$;
|
|
3692
|
+
|
|
3693
|
+
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;
|
|
3694
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3695
|
+
-- supacloud:sql-module:pgmq-public:end
|
|
3696
|
+
`;
|
|
3697
|
+
var WORKFLOWS_SQL = `
|
|
3698
|
+
-- supacloud:sql-module:workflows-public:start
|
|
3699
|
+
DO $pgmq_extension$
|
|
3700
|
+
BEGIN
|
|
3701
|
+
IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
|
|
3702
|
+
EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
|
|
3703
|
+
END IF;
|
|
3704
|
+
END
|
|
3705
|
+
$pgmq_extension$;
|
|
3706
|
+
CREATE SCHEMA IF NOT EXISTS supacloud_workflows;
|
|
3707
|
+
REVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;
|
|
3708
|
+
GRANT USAGE ON SCHEMA supacloud_workflows TO service_role;
|
|
3709
|
+
|
|
3710
|
+
SELECT pgmq.create('supacloud_internal_workflows');
|
|
3711
|
+
|
|
3712
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.runs (
|
|
3713
|
+
id uuid PRIMARY KEY,
|
|
3714
|
+
workflow_name text NOT NULL
|
|
3715
|
+
CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
|
|
3716
|
+
workflow_version text NOT NULL
|
|
3717
|
+
CHECK (char_length(workflow_version) BETWEEN 1 AND 80),
|
|
3718
|
+
status text NOT NULL DEFAULT 'queued'
|
|
3719
|
+
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
|
|
3720
|
+
input jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3721
|
+
CHECK (jsonb_typeof(input) = 'object'),
|
|
3722
|
+
output jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3723
|
+
CHECK (jsonb_typeof(output) = 'object'),
|
|
3724
|
+
error_message text NOT NULL DEFAULT ''
|
|
3725
|
+
CHECK (char_length(error_message) <= 4000),
|
|
3726
|
+
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),
|
|
3727
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3728
|
+
started_at timestamptz,
|
|
3729
|
+
completed_at timestamptz,
|
|
3730
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3731
|
+
);
|
|
3732
|
+
|
|
3733
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.steps (
|
|
3734
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3735
|
+
run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
|
|
3736
|
+
step_key text NOT NULL
|
|
3737
|
+
CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
|
|
3738
|
+
status text NOT NULL DEFAULT 'queued'
|
|
3739
|
+
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),
|
|
3740
|
+
input jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3741
|
+
CHECK (jsonb_typeof(input) = 'object'),
|
|
3742
|
+
output jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3743
|
+
CHECK (jsonb_typeof(output) = 'object'),
|
|
3744
|
+
error_message text NOT NULL DEFAULT ''
|
|
3745
|
+
CHECK (char_length(error_message) <= 4000),
|
|
3746
|
+
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
3747
|
+
max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),
|
|
3748
|
+
retry_delay_seconds integer NOT NULL DEFAULT 0
|
|
3749
|
+
CHECK (retry_delay_seconds BETWEEN 0 AND 86400),
|
|
3750
|
+
queue_message_id bigint NOT NULL UNIQUE,
|
|
3751
|
+
claimed_by text,
|
|
3752
|
+
claimed_at timestamptz,
|
|
3753
|
+
completed_at timestamptz,
|
|
3754
|
+
next_step_key text,
|
|
3755
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3756
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
3757
|
+
UNIQUE (run_id, step_key)
|
|
3758
|
+
);
|
|
3759
|
+
|
|
3760
|
+
ALTER TABLE supacloud_workflows.steps
|
|
3761
|
+
ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0
|
|
3762
|
+
CHECK (retry_delay_seconds BETWEEN 0 AND 86400);
|
|
3763
|
+
|
|
3764
|
+
CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx
|
|
3765
|
+
ON supacloud_workflows.steps (run_id)
|
|
3766
|
+
WHERE status IN ('queued', 'running');
|
|
3767
|
+
|
|
3768
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx
|
|
3769
|
+
ON supacloud_workflows.runs (status, updated_at DESC, id);
|
|
3770
|
+
|
|
3771
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx
|
|
3772
|
+
ON supacloud_workflows.steps (run_id, created_at, id);
|
|
3773
|
+
|
|
3774
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.events (
|
|
3775
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
3776
|
+
run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
|
|
3777
|
+
step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,
|
|
3778
|
+
event_type text NOT NULL
|
|
3779
|
+
CHECK (event_type IN (
|
|
3780
|
+
'run_started', 'step_claimed', 'step_retried', 'step_completed',
|
|
3781
|
+
'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'
|
|
3782
|
+
)),
|
|
3783
|
+
attempt integer CHECK (attempt IS NULL OR attempt > 0),
|
|
3784
|
+
details jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3785
|
+
CHECK (jsonb_typeof(details) = 'object'),
|
|
3786
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3787
|
+
);
|
|
3788
|
+
|
|
3789
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx
|
|
3790
|
+
ON supacloud_workflows.events (run_id, id);
|
|
3791
|
+
|
|
3792
|
+
CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx
|
|
3793
|
+
ON supacloud_workflows.events (step_id, attempt)
|
|
3794
|
+
WHERE event_type IN ('step_retried', 'step_dead_lettered')
|
|
3795
|
+
AND details ->> 'operation' = 'retry';
|
|
3796
|
+
|
|
3797
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(
|
|
3798
|
+
p_run_id uuid,
|
|
3799
|
+
p_idempotent boolean DEFAULT false
|
|
3800
|
+
) RETURNS jsonb
|
|
3801
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
3802
|
+
SELECT jsonb_build_object(
|
|
3803
|
+
'runId', run.id,
|
|
3804
|
+
'workflowName', run.workflow_name,
|
|
3805
|
+
'workflowVersion', run.workflow_version,
|
|
3806
|
+
'status', run.status,
|
|
3807
|
+
'input', run.input,
|
|
3808
|
+
'output', run.output,
|
|
3809
|
+
'errorMessage', run.error_message,
|
|
3810
|
+
'rowVersion', run.row_version::text,
|
|
3811
|
+
'createdAt', run.created_at,
|
|
3812
|
+
'startedAt', run.started_at,
|
|
3813
|
+
'completedAt', run.completed_at,
|
|
3814
|
+
'updatedAt', run.updated_at,
|
|
3815
|
+
'idempotent', p_idempotent,
|
|
3816
|
+
'steps', coalesce((
|
|
3817
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
3818
|
+
'stepId', step.id,
|
|
3819
|
+
'stepKey', step.step_key,
|
|
3820
|
+
'status', step.status,
|
|
3821
|
+
'input', step.input,
|
|
3822
|
+
'output', step.output,
|
|
3823
|
+
'errorMessage', step.error_message,
|
|
3824
|
+
'attempts', step.attempts,
|
|
3825
|
+
'maxAttempts', step.max_attempts,
|
|
3826
|
+
'retryDelaySeconds', step.retry_delay_seconds,
|
|
3827
|
+
'queueMessageId', step.queue_message_id::text,
|
|
3828
|
+
'claimedBy', step.claimed_by,
|
|
3829
|
+
'claimedAt', step.claimed_at,
|
|
3830
|
+
'completedAt', step.completed_at,
|
|
3831
|
+
'nextStepKey', step.next_step_key,
|
|
3832
|
+
'createdAt', step.created_at,
|
|
3833
|
+
'updatedAt', step.updated_at
|
|
3834
|
+
) ORDER BY step.created_at, step.id)
|
|
3835
|
+
FROM supacloud_workflows.steps step
|
|
3836
|
+
WHERE step.run_id = run.id
|
|
3837
|
+
), '[]'::jsonb)
|
|
3838
|
+
)
|
|
3839
|
+
FROM supacloud_workflows.runs run
|
|
3840
|
+
WHERE run.id = p_run_id
|
|
3841
|
+
$$;
|
|
3842
|
+
|
|
3843
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(
|
|
3844
|
+
p_run_id uuid,
|
|
3845
|
+
p_step_key text,
|
|
3846
|
+
p_input jsonb,
|
|
3847
|
+
p_max_attempts integer
|
|
3848
|
+
) RETURNS supacloud_workflows.steps
|
|
3849
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3850
|
+
DECLARE
|
|
3851
|
+
normalized_step_key text := nullif(btrim(p_step_key), '');
|
|
3852
|
+
step_id uuid := gen_random_uuid();
|
|
3853
|
+
message_id bigint;
|
|
3854
|
+
created_step supacloud_workflows.steps%ROWTYPE;
|
|
3855
|
+
BEGIN
|
|
3856
|
+
IF normalized_step_key IS NULL
|
|
3857
|
+
OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3858
|
+
OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
|
|
3859
|
+
OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
3860
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';
|
|
3861
|
+
END IF;
|
|
3862
|
+
|
|
3863
|
+
IF NOT EXISTS (
|
|
3864
|
+
SELECT 1 FROM supacloud_workflows.runs run
|
|
3865
|
+
WHERE run.id = p_run_id AND run.status IN ('queued', 'running')
|
|
3866
|
+
) THEN
|
|
3867
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
3868
|
+
END IF;
|
|
3869
|
+
|
|
3870
|
+
SELECT queued_id INTO message_id
|
|
3871
|
+
FROM pgmq.send(
|
|
3872
|
+
'supacloud_internal_workflows',
|
|
3873
|
+
jsonb_build_object('run_id', p_run_id, 'step_id', step_id),
|
|
3874
|
+
0
|
|
3875
|
+
) AS queued_id;
|
|
3876
|
+
|
|
3877
|
+
INSERT INTO supacloud_workflows.steps (
|
|
3878
|
+
id, run_id, step_key, input, max_attempts, queue_message_id
|
|
3879
|
+
) VALUES (
|
|
3880
|
+
step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
|
|
3881
|
+
) RETURNING * INTO created_step;
|
|
3882
|
+
|
|
3883
|
+
RETURN created_step;
|
|
3884
|
+
END;
|
|
3885
|
+
$$;
|
|
3886
|
+
|
|
3887
|
+
-- Clean-code exception: private transitions keep typed PostgreSQL arguments and
|
|
3888
|
+
-- the complete lock/queue/ledger/event mutation in one transaction. The public
|
|
3889
|
+
-- contract already uses one JSON request; revisit if a private routine gains a
|
|
3890
|
+
-- second caller or any transition can be decomposed without weakening atomicity.
|
|
3891
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.start_run(
|
|
3892
|
+
p_run_id uuid,
|
|
3893
|
+
p_workflow_name text,
|
|
3894
|
+
p_workflow_version text,
|
|
3895
|
+
p_first_step_key text,
|
|
3896
|
+
p_input jsonb,
|
|
3897
|
+
p_max_attempts integer
|
|
3898
|
+
) RETURNS jsonb
|
|
3899
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3900
|
+
DECLARE
|
|
3901
|
+
normalized_name text := nullif(btrim(p_workflow_name), '');
|
|
3902
|
+
normalized_version text := nullif(btrim(p_workflow_version), '');
|
|
3903
|
+
normalized_first_step_key text := nullif(btrim(p_first_step_key), '');
|
|
3904
|
+
existing_run supacloud_workflows.runs%ROWTYPE;
|
|
3905
|
+
existing_step supacloud_workflows.steps%ROWTYPE;
|
|
3906
|
+
first_step supacloud_workflows.steps%ROWTYPE;
|
|
3907
|
+
BEGIN
|
|
3908
|
+
IF p_run_id IS NULL
|
|
3909
|
+
OR normalized_name IS NULL
|
|
3910
|
+
OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3911
|
+
OR normalized_version IS NULL
|
|
3912
|
+
OR char_length(normalized_version) > 80
|
|
3913
|
+
OR normalized_first_step_key IS NULL
|
|
3914
|
+
OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3915
|
+
OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
|
|
3916
|
+
OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
3917
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
|
|
3918
|
+
END IF;
|
|
3919
|
+
|
|
3920
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
3921
|
+
SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
|
|
3922
|
+
IF FOUND THEN
|
|
3923
|
+
SELECT * INTO existing_step
|
|
3924
|
+
FROM supacloud_workflows.steps
|
|
3925
|
+
WHERE run_id = p_run_id
|
|
3926
|
+
ORDER BY created_at, id
|
|
3927
|
+
LIMIT 1;
|
|
3928
|
+
IF NOT FOUND THEN
|
|
3929
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
3930
|
+
END IF;
|
|
3931
|
+
IF existing_run.workflow_name <> normalized_name
|
|
3932
|
+
OR existing_run.workflow_version <> normalized_version
|
|
3933
|
+
OR existing_run.input <> p_input
|
|
3934
|
+
OR existing_step.step_key <> normalized_first_step_key
|
|
3935
|
+
OR existing_step.input <> p_input
|
|
3936
|
+
OR existing_step.max_attempts <> p_max_attempts THEN
|
|
3937
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
3938
|
+
END IF;
|
|
3939
|
+
RETURN supacloud_workflows.snapshot(p_run_id, true);
|
|
3940
|
+
END IF;
|
|
3941
|
+
|
|
3942
|
+
INSERT INTO supacloud_workflows.runs (
|
|
3943
|
+
id, workflow_name, workflow_version, input
|
|
3944
|
+
) VALUES (
|
|
3945
|
+
p_run_id, normalized_name, normalized_version, p_input
|
|
3946
|
+
);
|
|
3947
|
+
first_step := supacloud_workflows.enqueue_step(
|
|
3948
|
+
p_run_id, normalized_first_step_key, p_input, p_max_attempts
|
|
3949
|
+
);
|
|
3950
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)
|
|
3951
|
+
VALUES (p_run_id, first_step.id, 'run_started');
|
|
3952
|
+
RETURN supacloud_workflows.snapshot(p_run_id, false);
|
|
3953
|
+
END;
|
|
3954
|
+
$$;
|
|
3955
|
+
|
|
3956
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(
|
|
3957
|
+
p_worker_id text,
|
|
3958
|
+
p_visibility_timeout_seconds integer
|
|
3959
|
+
) RETURNS jsonb
|
|
3960
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3961
|
+
DECLARE
|
|
3962
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
3963
|
+
queued_message pgmq.message_record;
|
|
3964
|
+
message_run_id text;
|
|
3965
|
+
message_step_id text;
|
|
3966
|
+
candidate_run_id uuid;
|
|
3967
|
+
claimed_step supacloud_workflows.steps%ROWTYPE;
|
|
3968
|
+
claimed_run supacloud_workflows.runs%ROWTYPE;
|
|
3969
|
+
BEGIN
|
|
3970
|
+
IF normalized_worker_id IS NULL
|
|
3971
|
+
OR char_length(normalized_worker_id) > 200
|
|
3972
|
+
OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN
|
|
3973
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
3974
|
+
END IF;
|
|
3975
|
+
|
|
3976
|
+
SELECT * INTO queued_message
|
|
3977
|
+
FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)
|
|
3978
|
+
LIMIT 1;
|
|
3979
|
+
IF NOT FOUND THEN RETURN NULL; END IF;
|
|
3980
|
+
|
|
3981
|
+
message_run_id := queued_message.message ->> 'run_id';
|
|
3982
|
+
message_step_id := queued_message.message ->> 'step_id';
|
|
3983
|
+
IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'
|
|
3984
|
+
OR message_run_id IS NULL
|
|
3985
|
+
OR message_step_id IS NULL
|
|
3986
|
+
OR message_run_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$'
|
|
3987
|
+
OR message_step_id !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
|
|
3988
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3989
|
+
RETURN jsonb_build_object(
|
|
3990
|
+
'status', 'discarded',
|
|
3991
|
+
'reason', 'invalid_message',
|
|
3992
|
+
'messageId', queued_message.msg_id::text
|
|
3993
|
+
);
|
|
3994
|
+
END IF;
|
|
3995
|
+
|
|
3996
|
+
SELECT step.run_id INTO candidate_run_id
|
|
3997
|
+
FROM supacloud_workflows.steps step
|
|
3998
|
+
WHERE step.id::text = lower(message_step_id)
|
|
3999
|
+
AND step.run_id::text = lower(message_run_id)
|
|
4000
|
+
AND step.queue_message_id = queued_message.msg_id;
|
|
4001
|
+
IF NOT FOUND THEN
|
|
4002
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
4003
|
+
RETURN jsonb_build_object(
|
|
4004
|
+
'status', 'discarded',
|
|
4005
|
+
'reason', 'orphaned_message',
|
|
4006
|
+
'messageId', queued_message.msg_id::text
|
|
4007
|
+
);
|
|
4008
|
+
END IF;
|
|
4009
|
+
|
|
4010
|
+
IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
|
|
4011
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
|
|
4012
|
+
END IF;
|
|
4013
|
+
SELECT * INTO claimed_step
|
|
4014
|
+
FROM supacloud_workflows.steps
|
|
4015
|
+
WHERE id::text = lower(message_step_id)
|
|
4016
|
+
AND run_id = candidate_run_id
|
|
4017
|
+
AND queue_message_id = queued_message.msg_id
|
|
4018
|
+
FOR UPDATE;
|
|
4019
|
+
IF NOT FOUND THEN
|
|
4020
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
4021
|
+
RETURN jsonb_build_object(
|
|
4022
|
+
'status', 'discarded',
|
|
4023
|
+
'reason', 'orphaned_message',
|
|
4024
|
+
'messageId', queued_message.msg_id::text
|
|
4025
|
+
);
|
|
4026
|
+
END IF;
|
|
4027
|
+
|
|
4028
|
+
SELECT * INTO claimed_run
|
|
4029
|
+
FROM supacloud_workflows.runs
|
|
4030
|
+
WHERE id = claimed_step.run_id
|
|
4031
|
+
FOR UPDATE;
|
|
4032
|
+
IF claimed_run.status NOT IN ('queued', 'running')
|
|
4033
|
+
OR claimed_step.status NOT IN ('queued', 'running') THEN
|
|
4034
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
4035
|
+
RETURN jsonb_build_object(
|
|
4036
|
+
'status', 'discarded',
|
|
4037
|
+
'reason', 'step_not_claimable',
|
|
4038
|
+
'runId', claimed_step.run_id,
|
|
4039
|
+
'stepId', claimed_step.id,
|
|
4040
|
+
'messageId', queued_message.msg_id::text
|
|
4041
|
+
);
|
|
4042
|
+
END IF;
|
|
4043
|
+
|
|
4044
|
+
IF queued_message.read_ct > claimed_step.max_attempts THEN
|
|
4045
|
+
UPDATE supacloud_workflows.steps
|
|
4046
|
+
SET status = 'dead_lettered', attempts = queued_message.read_ct,
|
|
4047
|
+
error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()
|
|
4048
|
+
WHERE id = claimed_step.id;
|
|
4049
|
+
UPDATE supacloud_workflows.runs
|
|
4050
|
+
SET status = 'failed', error_message = 'maximum attempts exceeded',
|
|
4051
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4052
|
+
WHERE id = claimed_step.run_id;
|
|
4053
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
4054
|
+
INSERT INTO supacloud_workflows.events (
|
|
4055
|
+
run_id, step_id, event_type, attempt, details
|
|
4056
|
+
) VALUES (
|
|
4057
|
+
claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,
|
|
4058
|
+
jsonb_build_object('errorMessage', 'maximum attempts exceeded')
|
|
4059
|
+
);
|
|
4060
|
+
RETURN jsonb_build_object(
|
|
4061
|
+
'status', 'dead_lettered',
|
|
4062
|
+
'runId', claimed_step.run_id,
|
|
4063
|
+
'stepId', claimed_step.id,
|
|
4064
|
+
'stepKey', claimed_step.step_key,
|
|
4065
|
+
'messageId', queued_message.msg_id::text,
|
|
4066
|
+
'attempt', queued_message.read_ct,
|
|
4067
|
+
'maxAttempts', claimed_step.max_attempts
|
|
4068
|
+
);
|
|
4069
|
+
END IF;
|
|
4070
|
+
|
|
4071
|
+
UPDATE supacloud_workflows.steps
|
|
4072
|
+
SET status = 'running', attempts = queued_message.read_ct,
|
|
4073
|
+
retry_delay_seconds = 0, claimed_by = normalized_worker_id,
|
|
4074
|
+
claimed_at = now(), updated_at = now()
|
|
4075
|
+
WHERE id = claimed_step.id;
|
|
4076
|
+
UPDATE supacloud_workflows.runs
|
|
4077
|
+
SET status = 'running', started_at = coalesce(started_at, now()),
|
|
4078
|
+
updated_at = now(), row_version = row_version + 1
|
|
4079
|
+
WHERE id = claimed_step.run_id;
|
|
4080
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)
|
|
4081
|
+
VALUES (
|
|
4082
|
+
claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,
|
|
4083
|
+
jsonb_build_object('workerId', normalized_worker_id)
|
|
4084
|
+
);
|
|
4085
|
+
|
|
4086
|
+
RETURN jsonb_build_object(
|
|
4087
|
+
'status', 'claimed',
|
|
4088
|
+
'runId', claimed_step.run_id,
|
|
4089
|
+
'workflowName', claimed_run.workflow_name,
|
|
4090
|
+
'workflowVersion', claimed_run.workflow_version,
|
|
4091
|
+
'stepId', claimed_step.id,
|
|
4092
|
+
'stepKey', claimed_step.step_key,
|
|
4093
|
+
'input', claimed_step.input,
|
|
4094
|
+
'messageId', queued_message.msg_id::text,
|
|
4095
|
+
'attempt', queued_message.read_ct,
|
|
4096
|
+
'maxAttempts', claimed_step.max_attempts,
|
|
4097
|
+
'workerId', normalized_worker_id
|
|
4098
|
+
);
|
|
4099
|
+
END;
|
|
4100
|
+
$$;
|
|
4101
|
+
|
|
4102
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(
|
|
4103
|
+
p_step_id uuid
|
|
4104
|
+
) RETURNS supacloud_workflows.steps
|
|
4105
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4106
|
+
DECLARE
|
|
4107
|
+
candidate_run_id uuid;
|
|
4108
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4109
|
+
BEGIN
|
|
4110
|
+
SELECT step.run_id INTO candidate_run_id
|
|
4111
|
+
FROM supacloud_workflows.steps step
|
|
4112
|
+
WHERE step.id = p_step_id;
|
|
4113
|
+
IF NOT FOUND THEN
|
|
4114
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4115
|
+
END IF;
|
|
4116
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
|
|
4117
|
+
SELECT * INTO active_step
|
|
4118
|
+
FROM supacloud_workflows.steps
|
|
4119
|
+
WHERE id = p_step_id
|
|
4120
|
+
FOR UPDATE;
|
|
4121
|
+
IF NOT FOUND THEN
|
|
4122
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4123
|
+
END IF;
|
|
4124
|
+
RETURN active_step;
|
|
4125
|
+
END;
|
|
4126
|
+
$$;
|
|
4127
|
+
|
|
4128
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(
|
|
4129
|
+
p_step_id uuid,
|
|
4130
|
+
p_message_id bigint,
|
|
4131
|
+
p_attempt integer,
|
|
4132
|
+
p_worker_id text
|
|
4133
|
+
) RETURNS supacloud_workflows.steps
|
|
4134
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4135
|
+
DECLARE
|
|
4136
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4137
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
4138
|
+
BEGIN
|
|
4139
|
+
active_step := supacloud_workflows.lock_step(p_step_id);
|
|
4140
|
+
IF normalized_worker_id IS NULL
|
|
4141
|
+
OR p_message_id IS NULL
|
|
4142
|
+
OR p_attempt IS NULL
|
|
4143
|
+
OR active_step.queue_message_id IS DISTINCT FROM p_message_id
|
|
4144
|
+
OR active_step.attempts IS DISTINCT FROM p_attempt
|
|
4145
|
+
OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN
|
|
4146
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4147
|
+
END IF;
|
|
4148
|
+
RETURN active_step;
|
|
4149
|
+
END;
|
|
4150
|
+
$$;
|
|
4151
|
+
|
|
4152
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(
|
|
4153
|
+
p_step_id uuid,
|
|
4154
|
+
p_message_id bigint,
|
|
4155
|
+
p_attempt integer,
|
|
4156
|
+
p_worker_id text,
|
|
4157
|
+
p_output jsonb,
|
|
4158
|
+
p_next_step_key text,
|
|
4159
|
+
p_next_input jsonb,
|
|
4160
|
+
p_next_max_attempts integer
|
|
4161
|
+
) RETURNS jsonb
|
|
4162
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4163
|
+
DECLARE
|
|
4164
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4165
|
+
next_step supacloud_workflows.steps%ROWTYPE;
|
|
4166
|
+
normalized_next_step_key text := nullif(btrim(p_next_step_key), '');
|
|
4167
|
+
archived boolean;
|
|
4168
|
+
BEGIN
|
|
4169
|
+
IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'
|
|
4170
|
+
OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'
|
|
4171
|
+
OR normalized_next_step_key IS NULL
|
|
4172
|
+
OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
4173
|
+
OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
4174
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4175
|
+
END IF;
|
|
4176
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4177
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4178
|
+
);
|
|
4179
|
+
|
|
4180
|
+
IF current_step.status = 'completed' THEN
|
|
4181
|
+
SELECT * INTO next_step
|
|
4182
|
+
FROM supacloud_workflows.steps
|
|
4183
|
+
WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
|
|
4184
|
+
IF NOT FOUND
|
|
4185
|
+
OR current_step.output IS DISTINCT FROM p_output
|
|
4186
|
+
OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
|
|
4187
|
+
OR next_step.input IS DISTINCT FROM p_next_input
|
|
4188
|
+
OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN
|
|
4189
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4190
|
+
END IF;
|
|
4191
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4192
|
+
END IF;
|
|
4193
|
+
IF current_step.status <> 'running' THEN
|
|
4194
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4195
|
+
END IF;
|
|
4196
|
+
|
|
4197
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4198
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4199
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4200
|
+
END IF;
|
|
4201
|
+
UPDATE supacloud_workflows.steps
|
|
4202
|
+
SET status = 'completed', output = p_output, error_message = '',
|
|
4203
|
+
completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()
|
|
4204
|
+
WHERE id = current_step.id;
|
|
4205
|
+
INSERT INTO supacloud_workflows.events (
|
|
4206
|
+
run_id, step_id, event_type, attempt, details
|
|
4207
|
+
) VALUES (
|
|
4208
|
+
current_step.run_id, current_step.id, 'step_completed', p_attempt,
|
|
4209
|
+
jsonb_build_object('nextStepKey', normalized_next_step_key)
|
|
4210
|
+
);
|
|
4211
|
+
next_step := supacloud_workflows.enqueue_step(
|
|
4212
|
+
current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts
|
|
4213
|
+
);
|
|
4214
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4215
|
+
END;
|
|
4216
|
+
$$;
|
|
4217
|
+
|
|
4218
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(
|
|
4219
|
+
p_step_id uuid,
|
|
4220
|
+
p_message_id bigint,
|
|
4221
|
+
p_attempt integer,
|
|
4222
|
+
p_worker_id text,
|
|
4223
|
+
p_step_output jsonb,
|
|
4224
|
+
p_run_output jsonb
|
|
4225
|
+
) RETURNS jsonb
|
|
4226
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4227
|
+
DECLARE
|
|
4228
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4229
|
+
current_run supacloud_workflows.runs%ROWTYPE;
|
|
4230
|
+
archived boolean;
|
|
4231
|
+
BEGIN
|
|
4232
|
+
IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'
|
|
4233
|
+
OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN
|
|
4234
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4235
|
+
END IF;
|
|
4236
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4237
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4238
|
+
);
|
|
4239
|
+
SELECT * INTO current_run
|
|
4240
|
+
FROM supacloud_workflows.runs
|
|
4241
|
+
WHERE id = current_step.run_id
|
|
4242
|
+
FOR UPDATE;
|
|
4243
|
+
|
|
4244
|
+
IF current_step.status = 'completed' THEN
|
|
4245
|
+
IF current_step.next_step_key IS NOT NULL
|
|
4246
|
+
OR current_step.output IS DISTINCT FROM p_step_output
|
|
4247
|
+
OR current_run.status IS DISTINCT FROM 'completed'
|
|
4248
|
+
OR current_run.output IS DISTINCT FROM p_run_output THEN
|
|
4249
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4250
|
+
END IF;
|
|
4251
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4252
|
+
END IF;
|
|
4253
|
+
IF current_step.status <> 'running' THEN
|
|
4254
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4255
|
+
END IF;
|
|
4256
|
+
IF current_run.status <> 'running' THEN
|
|
4257
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4258
|
+
END IF;
|
|
4259
|
+
|
|
4260
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4261
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4262
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4263
|
+
END IF;
|
|
4264
|
+
UPDATE supacloud_workflows.steps
|
|
4265
|
+
SET status = 'completed', output = p_step_output, error_message = '',
|
|
4266
|
+
completed_at = now(), updated_at = now()
|
|
4267
|
+
WHERE id = current_step.id;
|
|
4268
|
+
UPDATE supacloud_workflows.runs
|
|
4269
|
+
SET status = 'completed', output = p_run_output, error_message = '',
|
|
4270
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4271
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4272
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
|
|
4273
|
+
VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);
|
|
4274
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
|
|
4275
|
+
VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);
|
|
4276
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4277
|
+
END;
|
|
4278
|
+
$$;
|
|
4279
|
+
|
|
4280
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(
|
|
4281
|
+
p_step_id uuid,
|
|
4282
|
+
p_message_id bigint,
|
|
4283
|
+
p_attempt integer,
|
|
4284
|
+
p_worker_id text,
|
|
4285
|
+
p_error_message text,
|
|
4286
|
+
p_delay_seconds integer
|
|
4287
|
+
) RETURNS jsonb
|
|
4288
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4289
|
+
DECLARE
|
|
4290
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4291
|
+
normalized_error text := nullif(btrim(p_error_message), '');
|
|
4292
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
4293
|
+
retry_receipt jsonb;
|
|
4294
|
+
queue_message_updated boolean;
|
|
4295
|
+
archived boolean;
|
|
4296
|
+
BEGIN
|
|
4297
|
+
IF normalized_error IS NULL OR char_length(normalized_error) > 4000
|
|
4298
|
+
OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN
|
|
4299
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4300
|
+
END IF;
|
|
4301
|
+
current_step := supacloud_workflows.lock_step(p_step_id);
|
|
4302
|
+
SELECT event.details INTO retry_receipt
|
|
4303
|
+
FROM supacloud_workflows.events event
|
|
4304
|
+
WHERE event.step_id = current_step.id
|
|
4305
|
+
AND event.attempt = p_attempt
|
|
4306
|
+
AND event.event_type IN ('step_retried', 'step_dead_lettered')
|
|
4307
|
+
AND event.details ->> 'operation' = 'retry'
|
|
4308
|
+
ORDER BY event.id DESC
|
|
4309
|
+
LIMIT 1;
|
|
4310
|
+
IF FOUND THEN
|
|
4311
|
+
IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text
|
|
4312
|
+
OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id
|
|
4313
|
+
OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error
|
|
4314
|
+
OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN
|
|
4315
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4316
|
+
END IF;
|
|
4317
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4318
|
+
END IF;
|
|
4319
|
+
IF normalized_worker_id IS NULL
|
|
4320
|
+
OR p_message_id IS NULL
|
|
4321
|
+
OR p_attempt IS NULL
|
|
4322
|
+
OR current_step.queue_message_id IS DISTINCT FROM p_message_id
|
|
4323
|
+
OR current_step.attempts IS DISTINCT FROM p_attempt
|
|
4324
|
+
OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id
|
|
4325
|
+
OR current_step.status <> 'running' THEN
|
|
4326
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4327
|
+
END IF;
|
|
4328
|
+
|
|
4329
|
+
IF current_step.attempts >= current_step.max_attempts THEN
|
|
4330
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4331
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4332
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4333
|
+
END IF;
|
|
4334
|
+
UPDATE supacloud_workflows.steps
|
|
4335
|
+
SET status = 'dead_lettered', error_message = normalized_error,
|
|
4336
|
+
completed_at = now(), updated_at = now()
|
|
4337
|
+
WHERE id = current_step.id;
|
|
4338
|
+
UPDATE supacloud_workflows.runs
|
|
4339
|
+
SET status = 'failed', error_message = normalized_error,
|
|
4340
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4341
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4342
|
+
IF NOT FOUND THEN
|
|
4343
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4344
|
+
END IF;
|
|
4345
|
+
INSERT INTO supacloud_workflows.events (
|
|
4346
|
+
run_id, step_id, event_type, attempt, details
|
|
4347
|
+
) VALUES (
|
|
4348
|
+
current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt,
|
|
4349
|
+
jsonb_build_object(
|
|
4350
|
+
'operation', 'retry',
|
|
4351
|
+
'messageId', p_message_id::text,
|
|
4352
|
+
'workerId', normalized_worker_id,
|
|
4353
|
+
'errorMessage', normalized_error,
|
|
4354
|
+
'delaySeconds', p_delay_seconds
|
|
4355
|
+
)
|
|
4356
|
+
);
|
|
4357
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4358
|
+
END IF;
|
|
4359
|
+
|
|
4360
|
+
SELECT EXISTS (
|
|
4361
|
+
SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)
|
|
4362
|
+
) INTO queue_message_updated;
|
|
4363
|
+
IF queue_message_updated IS DISTINCT FROM true THEN
|
|
4364
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4365
|
+
END IF;
|
|
4366
|
+
UPDATE supacloud_workflows.steps
|
|
4367
|
+
SET status = 'queued', error_message = normalized_error,
|
|
4368
|
+
retry_delay_seconds = p_delay_seconds, updated_at = now()
|
|
4369
|
+
WHERE id = current_step.id;
|
|
4370
|
+
INSERT INTO supacloud_workflows.events (
|
|
4371
|
+
run_id, step_id, event_type, attempt, details
|
|
4372
|
+
) VALUES (
|
|
4373
|
+
current_step.run_id, current_step.id, 'step_retried', p_attempt,
|
|
4374
|
+
jsonb_build_object(
|
|
4375
|
+
'operation', 'retry',
|
|
4376
|
+
'messageId', p_message_id::text,
|
|
4377
|
+
'workerId', normalized_worker_id,
|
|
4378
|
+
'errorMessage', normalized_error,
|
|
4379
|
+
'delaySeconds', p_delay_seconds
|
|
4380
|
+
)
|
|
4381
|
+
);
|
|
4382
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4383
|
+
END;
|
|
4384
|
+
$$;
|
|
4385
|
+
|
|
4386
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(
|
|
4387
|
+
p_step_id uuid,
|
|
4388
|
+
p_message_id bigint,
|
|
4389
|
+
p_attempt integer,
|
|
4390
|
+
p_worker_id text,
|
|
4391
|
+
p_error_message text
|
|
4392
|
+
) RETURNS jsonb
|
|
4393
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4394
|
+
DECLARE
|
|
4395
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4396
|
+
current_run supacloud_workflows.runs%ROWTYPE;
|
|
4397
|
+
normalized_error text := nullif(btrim(p_error_message), '');
|
|
4398
|
+
archived boolean;
|
|
4399
|
+
BEGIN
|
|
4400
|
+
IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN
|
|
4401
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4402
|
+
END IF;
|
|
4403
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4404
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4405
|
+
);
|
|
4406
|
+
SELECT * INTO current_run
|
|
4407
|
+
FROM supacloud_workflows.runs
|
|
4408
|
+
WHERE id = current_step.run_id
|
|
4409
|
+
FOR UPDATE;
|
|
4410
|
+
|
|
4411
|
+
IF current_step.status = 'failed' THEN
|
|
4412
|
+
IF current_step.error_message IS DISTINCT FROM normalized_error
|
|
4413
|
+
OR current_run.status IS DISTINCT FROM 'failed'
|
|
4414
|
+
OR current_run.error_message IS DISTINCT FROM normalized_error THEN
|
|
4415
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4416
|
+
END IF;
|
|
4417
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4418
|
+
END IF;
|
|
4419
|
+
IF current_step.status <> 'running' THEN
|
|
4420
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4421
|
+
END IF;
|
|
4422
|
+
IF current_run.status <> 'running' THEN
|
|
4423
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4424
|
+
END IF;
|
|
4425
|
+
|
|
4426
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4427
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4428
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4429
|
+
END IF;
|
|
4430
|
+
UPDATE supacloud_workflows.steps
|
|
4431
|
+
SET status = 'failed', error_message = normalized_error,
|
|
4432
|
+
completed_at = now(), updated_at = now()
|
|
4433
|
+
WHERE id = current_step.id;
|
|
4434
|
+
UPDATE supacloud_workflows.runs
|
|
4435
|
+
SET status = 'failed', error_message = normalized_error,
|
|
4436
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4437
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4438
|
+
INSERT INTO supacloud_workflows.events (
|
|
4439
|
+
run_id, step_id, event_type, attempt, details
|
|
4440
|
+
) VALUES (
|
|
4441
|
+
current_step.run_id, current_step.id, 'step_failed', p_attempt,
|
|
4442
|
+
jsonb_build_object('errorMessage', normalized_error)
|
|
4443
|
+
);
|
|
4444
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4445
|
+
END;
|
|
4446
|
+
$$;
|
|
4447
|
+
|
|
4448
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(
|
|
4449
|
+
p_run_id uuid,
|
|
4450
|
+
p_reason text
|
|
4451
|
+
) RETURNS jsonb
|
|
4452
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4453
|
+
DECLARE
|
|
4454
|
+
normalized_reason text := nullif(btrim(p_reason), '');
|
|
4455
|
+
locked_run supacloud_workflows.runs%ROWTYPE;
|
|
4456
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4457
|
+
BEGIN
|
|
4458
|
+
IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN
|
|
4459
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
4460
|
+
END IF;
|
|
4461
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
4462
|
+
SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
|
|
4463
|
+
IF NOT FOUND THEN
|
|
4464
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4465
|
+
END IF;
|
|
4466
|
+
IF locked_run.status = 'cancelled' THEN
|
|
4467
|
+
IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN
|
|
4468
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4469
|
+
END IF;
|
|
4470
|
+
RETURN supacloud_workflows.snapshot(p_run_id, true);
|
|
4471
|
+
END IF;
|
|
4472
|
+
IF locked_run.status NOT IN ('queued', 'running') THEN
|
|
4473
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4474
|
+
END IF;
|
|
4475
|
+
SELECT * INTO active_step
|
|
4476
|
+
FROM supacloud_workflows.steps
|
|
4477
|
+
WHERE run_id = p_run_id AND status IN ('queued', 'running')
|
|
4478
|
+
FOR UPDATE;
|
|
4479
|
+
IF FOUND THEN
|
|
4480
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
|
|
4481
|
+
UPDATE supacloud_workflows.steps
|
|
4482
|
+
SET status = 'cancelled', error_message = normalized_reason,
|
|
4483
|
+
completed_at = now(), updated_at = now()
|
|
4484
|
+
WHERE id = active_step.id;
|
|
4485
|
+
END IF;
|
|
4486
|
+
UPDATE supacloud_workflows.runs
|
|
4487
|
+
SET status = 'cancelled', error_message = normalized_reason,
|
|
4488
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4489
|
+
WHERE id = p_run_id;
|
|
4490
|
+
INSERT INTO supacloud_workflows.events (
|
|
4491
|
+
run_id, step_id, event_type, details
|
|
4492
|
+
) VALUES (
|
|
4493
|
+
p_run_id, active_step.id, 'run_cancelled',
|
|
4494
|
+
jsonb_build_object('reason', normalized_reason)
|
|
4495
|
+
);
|
|
4496
|
+
RETURN supacloud_workflows.snapshot(p_run_id, false);
|
|
4497
|
+
END;
|
|
4498
|
+
$$;
|
|
4499
|
+
|
|
4500
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.run_events(
|
|
4501
|
+
p_run_id uuid,
|
|
4502
|
+
p_after_event_id bigint,
|
|
4503
|
+
p_limit integer
|
|
4504
|
+
) RETURNS jsonb
|
|
4505
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4506
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
4507
|
+
'eventId', page.id::text,
|
|
4508
|
+
'runId', page.run_id,
|
|
4509
|
+
'stepId', page.step_id,
|
|
4510
|
+
'eventType', page.event_type,
|
|
4511
|
+
'attempt', page.attempt,
|
|
4512
|
+
'details', page.details,
|
|
4513
|
+
'createdAt', page.created_at
|
|
4514
|
+
) ORDER BY page.id), '[]'::jsonb)
|
|
4515
|
+
FROM (
|
|
4516
|
+
SELECT event.*
|
|
4517
|
+
FROM supacloud_workflows.events event
|
|
4518
|
+
WHERE event.run_id = p_run_id AND event.id > p_after_event_id
|
|
4519
|
+
ORDER BY event.id
|
|
4520
|
+
LIMIT p_limit
|
|
4521
|
+
) page
|
|
4522
|
+
$$;
|
|
4523
|
+
|
|
4524
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(
|
|
4525
|
+
request jsonb,
|
|
4526
|
+
key text
|
|
4527
|
+
) RETURNS uuid
|
|
4528
|
+
LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$
|
|
4529
|
+
DECLARE
|
|
4530
|
+
uuid_text text;
|
|
4531
|
+
BEGIN
|
|
4532
|
+
IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
|
|
4533
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
|
|
4534
|
+
END IF;
|
|
4535
|
+
uuid_text := request ->> key;
|
|
4536
|
+
IF uuid_text IS NULL
|
|
4537
|
+
OR uuid_text !~* '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$' THEN
|
|
4538
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
|
|
4539
|
+
END IF;
|
|
4540
|
+
RETURN uuid_text::uuid;
|
|
4541
|
+
END;
|
|
4542
|
+
$$;
|
|
4543
|
+
|
|
4544
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)
|
|
4545
|
+
RETURNS jsonb
|
|
4546
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4547
|
+
DECLARE
|
|
4548
|
+
max_attempts integer;
|
|
4549
|
+
BEGIN
|
|
4550
|
+
max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
|
|
4551
|
+
RETURN supacloud_workflows.start_run(
|
|
4552
|
+
supacloud_workflows.request_uuid(request, 'runId'),
|
|
4553
|
+
request ->> 'workflowName',
|
|
4554
|
+
request ->> 'workflowVersion',
|
|
4555
|
+
request ->> 'firstStepKey',
|
|
4556
|
+
coalesce(request -> 'input', '{}'::jsonb),
|
|
4557
|
+
max_attempts
|
|
4558
|
+
);
|
|
4559
|
+
EXCEPTION
|
|
4560
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4561
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
|
|
4562
|
+
END;
|
|
4563
|
+
$$;
|
|
4564
|
+
|
|
4565
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)
|
|
4566
|
+
RETURNS jsonb
|
|
4567
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4568
|
+
DECLARE
|
|
4569
|
+
visibility_timeout_seconds integer;
|
|
4570
|
+
BEGIN
|
|
4571
|
+
IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
|
|
4572
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
4573
|
+
END IF;
|
|
4574
|
+
visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);
|
|
4575
|
+
RETURN supacloud_workflows.claim_step(
|
|
4576
|
+
request ->> 'workerId', visibility_timeout_seconds
|
|
4577
|
+
);
|
|
4578
|
+
EXCEPTION
|
|
4579
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4580
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
4581
|
+
END;
|
|
4582
|
+
$$;
|
|
4583
|
+
|
|
4584
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)
|
|
4585
|
+
RETURNS jsonb
|
|
4586
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4587
|
+
DECLARE
|
|
4588
|
+
message_id bigint;
|
|
4589
|
+
attempt integer;
|
|
4590
|
+
next_max_attempts integer;
|
|
4591
|
+
BEGIN
|
|
4592
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4593
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4594
|
+
next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);
|
|
4595
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4596
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4597
|
+
END IF;
|
|
4598
|
+
RETURN supacloud_workflows.advance_step(
|
|
4599
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4600
|
+
message_id,
|
|
4601
|
+
attempt,
|
|
4602
|
+
request ->> 'workerId',
|
|
4603
|
+
coalesce(request -> 'output', '{}'::jsonb),
|
|
4604
|
+
request ->> 'nextStepKey',
|
|
4605
|
+
coalesce(request -> 'nextInput', '{}'::jsonb),
|
|
4606
|
+
next_max_attempts
|
|
4607
|
+
);
|
|
4608
|
+
EXCEPTION
|
|
4609
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4610
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4611
|
+
END;
|
|
4612
|
+
$$;
|
|
4613
|
+
|
|
4614
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)
|
|
4615
|
+
RETURNS jsonb
|
|
4616
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4617
|
+
DECLARE
|
|
4618
|
+
message_id bigint;
|
|
4619
|
+
attempt integer;
|
|
4620
|
+
BEGIN
|
|
4621
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4622
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4623
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4624
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4625
|
+
END IF;
|
|
4626
|
+
RETURN supacloud_workflows.complete_run(
|
|
4627
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4628
|
+
message_id,
|
|
4629
|
+
attempt,
|
|
4630
|
+
request ->> 'workerId',
|
|
4631
|
+
coalesce(request -> 'stepOutput', '{}'::jsonb),
|
|
4632
|
+
coalesce(request -> 'runOutput', '{}'::jsonb)
|
|
4633
|
+
);
|
|
4634
|
+
EXCEPTION
|
|
4635
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4636
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4637
|
+
END;
|
|
4638
|
+
$$;
|
|
4639
|
+
|
|
4640
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)
|
|
4641
|
+
RETURNS jsonb
|
|
4642
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4643
|
+
DECLARE
|
|
4644
|
+
message_id bigint;
|
|
4645
|
+
attempt integer;
|
|
4646
|
+
delay_seconds integer;
|
|
4647
|
+
BEGIN
|
|
4648
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4649
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4650
|
+
delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);
|
|
4651
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4652
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4653
|
+
END IF;
|
|
4654
|
+
RETURN supacloud_workflows.retry_step(
|
|
4655
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4656
|
+
message_id,
|
|
4657
|
+
attempt,
|
|
4658
|
+
request ->> 'workerId',
|
|
4659
|
+
request ->> 'errorMessage',
|
|
4660
|
+
delay_seconds
|
|
4661
|
+
);
|
|
4662
|
+
EXCEPTION
|
|
4663
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4664
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4665
|
+
END;
|
|
4666
|
+
$$;
|
|
4667
|
+
|
|
4668
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)
|
|
4669
|
+
RETURNS jsonb
|
|
4670
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4671
|
+
DECLARE
|
|
4672
|
+
message_id bigint;
|
|
4673
|
+
attempt integer;
|
|
4674
|
+
BEGIN
|
|
4675
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4676
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4677
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4678
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4679
|
+
END IF;
|
|
4680
|
+
RETURN supacloud_workflows.fail_step(
|
|
4681
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4682
|
+
message_id,
|
|
4683
|
+
attempt,
|
|
4684
|
+
request ->> 'workerId',
|
|
4685
|
+
request ->> 'errorMessage'
|
|
4686
|
+
);
|
|
4687
|
+
EXCEPTION
|
|
4688
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4689
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4690
|
+
END;
|
|
4691
|
+
$$;
|
|
4692
|
+
|
|
4693
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)
|
|
4694
|
+
RETURNS jsonb
|
|
4695
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4696
|
+
BEGIN
|
|
4697
|
+
RETURN supacloud_workflows.cancel_run(
|
|
4698
|
+
supacloud_workflows.request_uuid(request, 'runId'),
|
|
4699
|
+
request ->> 'reason'
|
|
4700
|
+
);
|
|
4701
|
+
EXCEPTION
|
|
4702
|
+
WHEN invalid_parameter_value OR invalid_text_representation THEN
|
|
4703
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
4704
|
+
END;
|
|
4705
|
+
$$;
|
|
4706
|
+
|
|
4707
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)
|
|
4708
|
+
RETURNS jsonb
|
|
4709
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4710
|
+
BEGIN
|
|
4711
|
+
RETURN supacloud_workflows.snapshot(
|
|
4712
|
+
supacloud_workflows.request_uuid(request, 'runId'), false
|
|
4713
|
+
);
|
|
4714
|
+
EXCEPTION
|
|
4715
|
+
WHEN invalid_parameter_value OR invalid_text_representation THEN
|
|
4716
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';
|
|
4717
|
+
END;
|
|
4718
|
+
$$;
|
|
4719
|
+
|
|
4720
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)
|
|
4721
|
+
RETURNS jsonb
|
|
4722
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4723
|
+
DECLARE
|
|
4724
|
+
after_event_id bigint;
|
|
4725
|
+
event_limit integer;
|
|
4726
|
+
BEGIN
|
|
4727
|
+
after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);
|
|
4728
|
+
event_limit := coalesce((request ->> 'limit')::integer, 100);
|
|
4729
|
+
IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN
|
|
4730
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
|
|
4731
|
+
END IF;
|
|
4732
|
+
RETURN supacloud_workflows.run_events(
|
|
4733
|
+
supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit
|
|
4734
|
+
);
|
|
4735
|
+
EXCEPTION
|
|
4736
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4737
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
|
|
4738
|
+
END;
|
|
4739
|
+
$$;
|
|
4740
|
+
|
|
4741
|
+
REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows
|
|
4742
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4743
|
+
REVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows
|
|
4744
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4745
|
+
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows
|
|
4746
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4747
|
+
|
|
4748
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4749
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4750
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4751
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4752
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4753
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4754
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4755
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4756
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4757
|
+
|
|
4758
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;
|
|
4759
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;
|
|
4760
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;
|
|
4761
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;
|
|
4762
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;
|
|
4763
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;
|
|
4764
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;
|
|
4765
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
|
|
4766
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
|
|
4767
|
+
-- supacloud:sql-module:workflows-public:end
|
|
3583
4768
|
`;
|
|
3584
4769
|
var CRON_SQL = `
|
|
3585
4770
|
create schema if not exists cron;
|
|
@@ -3906,12 +5091,32 @@ function pickTag(text, base) {
|
|
|
3906
5091
|
return tag;
|
|
3907
5092
|
}
|
|
3908
5093
|
|
|
3909
|
-
// src/runtime/db/pglite-engine.ts
|
|
3910
|
-
import { mkdir, open, unlink as unlink2 } from "fs/promises";
|
|
3911
|
-
import { dirname, resolve } from "path";
|
|
3912
|
-
|
|
3913
5094
|
// src/runtime/db/data-dir-lock.ts
|
|
3914
|
-
import { readFile, unlink } from "fs/promises";
|
|
5095
|
+
import { mkdir, open, readFile, unlink } from "fs/promises";
|
|
5096
|
+
import { dirname, resolve } from "path";
|
|
5097
|
+
async function acquireDataDirLock(dataDir, engineName = "database") {
|
|
5098
|
+
if (!dataDir || dataDir.includes("://"))
|
|
5099
|
+
return async () => {};
|
|
5100
|
+
const absoluteDataDir = resolve(dataDir);
|
|
5101
|
+
const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
|
|
5102
|
+
await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
|
|
5103
|
+
const nonce = crypto.randomUUID();
|
|
5104
|
+
const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce, engineName);
|
|
5105
|
+
let released = false;
|
|
5106
|
+
return async () => {
|
|
5107
|
+
if (released)
|
|
5108
|
+
return;
|
|
5109
|
+
released = true;
|
|
5110
|
+
await handle.close();
|
|
5111
|
+
const owner = await readDataDirLockOwner(lockPath);
|
|
5112
|
+
if (owner?.nonce !== nonce)
|
|
5113
|
+
return;
|
|
5114
|
+
await unlink(lockPath).catch((error) => {
|
|
5115
|
+
if (error.code !== "ENOENT")
|
|
5116
|
+
throw error;
|
|
5117
|
+
});
|
|
5118
|
+
};
|
|
5119
|
+
}
|
|
3915
5120
|
async function recoverStaleDataDirLock(lockPath) {
|
|
3916
5121
|
const lockState = await inspectDataDirLock(lockPath);
|
|
3917
5122
|
if (lockState.kind !== "stale")
|
|
@@ -3929,6 +5134,50 @@ async function readDataDirLockOwner(lockPath) {
|
|
|
3929
5134
|
const lockState = await inspectDataDirLock(lockPath);
|
|
3930
5135
|
return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
|
|
3931
5136
|
}
|
|
5137
|
+
async function assertDataDirUnlocked(dataDir) {
|
|
5138
|
+
if (!dataDir)
|
|
5139
|
+
return;
|
|
5140
|
+
const lockPath = `${resolve(dataDir)}.supacloud-lite.lock`;
|
|
5141
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
5142
|
+
if (lockState.kind === "missing")
|
|
5143
|
+
return;
|
|
5144
|
+
if (lockState.kind === "active") {
|
|
5145
|
+
throw new Error(`database data directory is already in use: ${resolve(dataDir)} (pid ${lockState.owner.pid})`);
|
|
5146
|
+
}
|
|
5147
|
+
throw unreadableLockError(lockPath, "database");
|
|
5148
|
+
}
|
|
5149
|
+
async function createDataDirLock(absoluteDataDir, lockPath, nonce, engineName) {
|
|
5150
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
5151
|
+
try {
|
|
5152
|
+
return await writeDataDirLock(lockPath, nonce);
|
|
5153
|
+
} catch (error) {
|
|
5154
|
+
if (error.code !== "EEXIST")
|
|
5155
|
+
throw error;
|
|
5156
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
5157
|
+
if (lockState.kind === "active") {
|
|
5158
|
+
throw new Error(`${engineName} data directory is already in use: ${absoluteDataDir} (pid ${lockState.owner.pid})`);
|
|
5159
|
+
}
|
|
5160
|
+
if (lockState.kind === "unreadable")
|
|
5161
|
+
throw unreadableLockError(lockPath, engineName);
|
|
5162
|
+
}
|
|
5163
|
+
}
|
|
5164
|
+
throw unreadableLockError(lockPath, engineName);
|
|
5165
|
+
}
|
|
5166
|
+
async function writeDataDirLock(lockPath, nonce) {
|
|
5167
|
+
const handle = await open(lockPath, "wx", 384);
|
|
5168
|
+
try {
|
|
5169
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
5170
|
+
`);
|
|
5171
|
+
return handle;
|
|
5172
|
+
} catch (error) {
|
|
5173
|
+
await handle.close().catch(() => {});
|
|
5174
|
+
await unlink(lockPath).catch(() => {});
|
|
5175
|
+
throw error;
|
|
5176
|
+
}
|
|
5177
|
+
}
|
|
5178
|
+
function unreadableLockError(lockPath, engineName) {
|
|
5179
|
+
return new Error(`${engineName} data directory has an unreadable lock: ${lockPath}. ` + "Confirm no SupaCloud Lite process is using it, then remove the lock manually.");
|
|
5180
|
+
}
|
|
3932
5181
|
async function inspectDataDirLock(lockPath) {
|
|
3933
5182
|
let contents;
|
|
3934
5183
|
try {
|
|
@@ -3982,7 +5231,7 @@ begin
|
|
|
3982
5231
|
end $$;
|
|
3983
5232
|
`;
|
|
3984
5233
|
async function createPgliteEngine(dataDir) {
|
|
3985
|
-
const releaseLock = await acquireDataDirLock(dataDir);
|
|
5234
|
+
const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
|
|
3986
5235
|
let PGlite, extensions;
|
|
3987
5236
|
const standaloneAssets = getStandaloneAssets();
|
|
3988
5237
|
let cleanupStandaloneBundles = async () => {};
|
|
@@ -4106,63 +5355,6 @@ async function removePreparedBundles(cleanup) {
|
|
|
4106
5355
|
console.error("Unable to remove temporary PGlite extension bundles:", error);
|
|
4107
5356
|
}
|
|
4108
5357
|
}
|
|
4109
|
-
async function acquireDataDirLock(dataDir) {
|
|
4110
|
-
if (!dataDir || dataDir.includes("://"))
|
|
4111
|
-
return async () => {};
|
|
4112
|
-
const absoluteDataDir = resolve(dataDir);
|
|
4113
|
-
const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
|
|
4114
|
-
await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
|
|
4115
|
-
const nonce = crypto.randomUUID();
|
|
4116
|
-
const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce);
|
|
4117
|
-
let released = false;
|
|
4118
|
-
return async () => {
|
|
4119
|
-
if (released)
|
|
4120
|
-
return;
|
|
4121
|
-
released = true;
|
|
4122
|
-
await handle?.close();
|
|
4123
|
-
const owner = await readDataDirLockOwner(lockPath);
|
|
4124
|
-
if (owner?.nonce !== nonce)
|
|
4125
|
-
return;
|
|
4126
|
-
await unlink2(lockPath).catch((error) => {
|
|
4127
|
-
if (error.code !== "ENOENT")
|
|
4128
|
-
throw error;
|
|
4129
|
-
});
|
|
4130
|
-
};
|
|
4131
|
-
}
|
|
4132
|
-
async function createDataDirLock(absoluteDataDir, lockPath, nonce) {
|
|
4133
|
-
for (let attempt = 0;attempt < 3; attempt++) {
|
|
4134
|
-
try {
|
|
4135
|
-
return await writeDataDirLock(lockPath, nonce);
|
|
4136
|
-
} catch (error) {
|
|
4137
|
-
if (error.code !== "EEXIST")
|
|
4138
|
-
throw error;
|
|
4139
|
-
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
4140
|
-
if (lockState.kind === "active")
|
|
4141
|
-
throw lockInUseError(absoluteDataDir, lockState.owner.pid);
|
|
4142
|
-
if (lockState.kind === "unreadable")
|
|
4143
|
-
throw unreadableLockError(lockPath);
|
|
4144
|
-
}
|
|
4145
|
-
}
|
|
4146
|
-
throw unreadableLockError(lockPath);
|
|
4147
|
-
}
|
|
4148
|
-
async function writeDataDirLock(lockPath, nonce) {
|
|
4149
|
-
const handle = await open(lockPath, "wx", 384);
|
|
4150
|
-
try {
|
|
4151
|
-
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
4152
|
-
`);
|
|
4153
|
-
return handle;
|
|
4154
|
-
} catch (error) {
|
|
4155
|
-
await handle.close().catch(() => {});
|
|
4156
|
-
await unlink2(lockPath).catch(() => {});
|
|
4157
|
-
throw error;
|
|
4158
|
-
}
|
|
4159
|
-
}
|
|
4160
|
-
function lockInUseError(dataDir, pid) {
|
|
4161
|
-
return new Error(`PGlite data directory is already in use: ${dataDir} (pid ${pid})`);
|
|
4162
|
-
}
|
|
4163
|
-
function unreadableLockError(lockPath) {
|
|
4164
|
-
return new Error(`PGlite data directory has an unreadable lock: ${lockPath}. Confirm no SupaCloud Lite process is using it, then remove the lock manually.`);
|
|
4165
|
-
}
|
|
4166
5358
|
|
|
4167
5359
|
// src/runtime/db/database.ts
|
|
4168
5360
|
var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
|
|
@@ -4180,20 +5372,30 @@ class Database {
|
|
|
4180
5372
|
}
|
|
4181
5373
|
static async create(dataDirOrEngine, opts) {
|
|
4182
5374
|
const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
await engine.
|
|
5375
|
+
try {
|
|
5376
|
+
if (engine.minimalBootstrap) {
|
|
5377
|
+
await engine.exec(MINIMAL_BOOTSTRAP_SQL);
|
|
5378
|
+
} else {
|
|
5379
|
+
await engine.exec(BOOTSTRAP_SQL);
|
|
5380
|
+
await engine.exec(PGMQ_SQL);
|
|
5381
|
+
await engine.exec(WORKFLOWS_SQL);
|
|
5382
|
+
await engine.exec(CRON_SQL);
|
|
5383
|
+
await engine.exec(NET_SQL);
|
|
5384
|
+
await engine.exec(EXT_COMPAT_SQL);
|
|
5385
|
+
await engine.exec(VAULT_SQL);
|
|
5386
|
+
if (opts?.vaultKey) {
|
|
5387
|
+
await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
|
|
5388
|
+
}
|
|
5389
|
+
}
|
|
5390
|
+
return new Database(engine);
|
|
5391
|
+
} catch (error) {
|
|
5392
|
+
try {
|
|
5393
|
+
await engine.close();
|
|
5394
|
+
} catch (cleanupError) {
|
|
5395
|
+
throw new AggregateError([error, cleanupError], "database bootstrap and cleanup failed");
|
|
4194
5396
|
}
|
|
5397
|
+
throw error;
|
|
4195
5398
|
}
|
|
4196
|
-
return new Database(engine);
|
|
4197
5399
|
}
|
|
4198
5400
|
query(sql, params) {
|
|
4199
5401
|
return this.engine.query(sql, params);
|
|
@@ -5932,6 +7134,25 @@ function parsePrefer(header) {
|
|
|
5932
7134
|
}
|
|
5933
7135
|
return prefer;
|
|
5934
7136
|
}
|
|
7137
|
+
function applyRequestRange(query, request) {
|
|
7138
|
+
const range = request.headers.get("range");
|
|
7139
|
+
if (!range || query.limits.has("") || query.offsets.has(""))
|
|
7140
|
+
return;
|
|
7141
|
+
const unit = request.headers.get("range-unit");
|
|
7142
|
+
if (unit !== null && unit.toLowerCase() !== "items")
|
|
7143
|
+
throw new ParseError(`unsupported range unit: ${unit}`);
|
|
7144
|
+
const match = range.match(/^(\d+)-(\d*)$/);
|
|
7145
|
+
if (!match)
|
|
7146
|
+
throw new ParseError(`invalid range: ${range}`);
|
|
7147
|
+
const start = Number(match[1]);
|
|
7148
|
+
const end = match[2] ? Number(match[2]) : undefined;
|
|
7149
|
+
if (!Number.isSafeInteger(start) || end !== undefined && (!Number.isSafeInteger(end) || end < start)) {
|
|
7150
|
+
throw new ParseError(`invalid range: ${range}`);
|
|
7151
|
+
}
|
|
7152
|
+
query.offsets.set("", start);
|
|
7153
|
+
if (end !== undefined)
|
|
7154
|
+
query.limits.set("", end - start + 1);
|
|
7155
|
+
}
|
|
5935
7156
|
var OBJECT_MEDIA = "application/vnd.pgrst.object+json";
|
|
5936
7157
|
var CSV_MEDIA = "text/csv";
|
|
5937
7158
|
var PLAN_MEDIA = "application/vnd.pgrst.plan";
|
|
@@ -6008,6 +7229,8 @@ class RestHandler {
|
|
|
6008
7229
|
const wantsObject = accept.includes(OBJECT_MEDIA);
|
|
6009
7230
|
const wantsCsv = accept.includes(CSV_MEDIA);
|
|
6010
7231
|
const q = parseQuery(url.searchParams);
|
|
7232
|
+
if (method === "GET" || method === "HEAD")
|
|
7233
|
+
applyRequestRange(q, req);
|
|
6011
7234
|
if (this.maxRows !== undefined && (method === "GET" || method === "HEAD")) {
|
|
6012
7235
|
const requested = q.limits.get("");
|
|
6013
7236
|
q.limits.set("", requested === undefined ? this.maxRows : Math.min(requested, this.maxRows));
|
|
@@ -6050,10 +7273,11 @@ class RestHandler {
|
|
|
6050
7273
|
}
|
|
6051
7274
|
return { rows: res.rows[0].body, count: count2 };
|
|
6052
7275
|
});
|
|
7276
|
+
const offset = q.offsets.get("") ?? 0;
|
|
6053
7277
|
return this.dataResponse(rows, {
|
|
6054
|
-
status: 200,
|
|
7278
|
+
status: count !== null && (offset > 0 || rows.length < count) ? 206 : 200,
|
|
6055
7279
|
count,
|
|
6056
|
-
offset
|
|
7280
|
+
offset,
|
|
6057
7281
|
wantsObject,
|
|
6058
7282
|
wantsCsv,
|
|
6059
7283
|
head: method === "HEAD"
|
|
@@ -6484,8 +7708,10 @@ function applyResize(image, metadata, options) {
|
|
|
6484
7708
|
const proportionalWidth = Math.max(1, Math.round(metadata.width * options.height / metadata.height));
|
|
6485
7709
|
image.resize(proportionalWidth);
|
|
6486
7710
|
}
|
|
6487
|
-
async function transformImage(
|
|
6488
|
-
|
|
7711
|
+
async function transformImage(source, options, knownSourceSize) {
|
|
7712
|
+
const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
|
|
7713
|
+
const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
|
|
7714
|
+
if (sourceSize > MAX_TRANSFORM_BYTES) {
|
|
6489
7715
|
return {
|
|
6490
7716
|
ok: false,
|
|
6491
7717
|
status: 413,
|
|
@@ -6493,9 +7719,10 @@ async function transformImage(bytes, options) {
|
|
|
6493
7719
|
message: "The source image exceeds the 25MB transformation limit"
|
|
6494
7720
|
};
|
|
6495
7721
|
}
|
|
7722
|
+
const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
|
|
6496
7723
|
let metadata;
|
|
6497
7724
|
try {
|
|
6498
|
-
metadata = await
|
|
7725
|
+
metadata = await image.metadata();
|
|
6499
7726
|
} catch (error) {
|
|
6500
7727
|
return mapImageError(error);
|
|
6501
7728
|
}
|
|
@@ -6512,7 +7739,6 @@ async function transformImage(bytes, options) {
|
|
|
6512
7739
|
message: `The source format ${outputFormat} is not supported by this runtime`
|
|
6513
7740
|
};
|
|
6514
7741
|
}
|
|
6515
|
-
const image = new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS });
|
|
6516
7742
|
applyResize(image, metadata, options);
|
|
6517
7743
|
if (options.format === "jpeg" || options.format === "origin" && outputFormat === "jpeg" && options.quality !== undefined) {
|
|
6518
7744
|
image.jpeg(options.quality === undefined ? undefined : { quality: options.quality });
|
|
@@ -6522,12 +7748,127 @@ async function transformImage(bytes, options) {
|
|
|
6522
7748
|
image.webp(options.quality === undefined ? undefined : { quality: options.quality });
|
|
6523
7749
|
}
|
|
6524
7750
|
try {
|
|
6525
|
-
|
|
7751
|
+
const bytes = await image.bytes();
|
|
7752
|
+
if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
|
|
7753
|
+
return {
|
|
7754
|
+
ok: false,
|
|
7755
|
+
status: 413,
|
|
7756
|
+
error: "ImageTooLarge",
|
|
7757
|
+
message: "The transformed image exceeds the 25MB transformation limit"
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
return { ok: true, bytes, contentType };
|
|
6526
7761
|
} catch (error) {
|
|
6527
7762
|
return mapImageError(error);
|
|
6528
7763
|
}
|
|
6529
7764
|
}
|
|
6530
7765
|
|
|
7766
|
+
// src/runtime/storage/image-transform-cache.ts
|
|
7767
|
+
var DEFAULT_MAX_CACHE_BYTES = 64 * 1024 * 1024;
|
|
7768
|
+
var DEFAULT_MAX_CACHE_ENTRIES = 128;
|
|
7769
|
+
var OBJECT_VERSION_PREFIX = "v2-";
|
|
7770
|
+
|
|
7771
|
+
class Semaphore {
|
|
7772
|
+
limit;
|
|
7773
|
+
active = 0;
|
|
7774
|
+
waiters = [];
|
|
7775
|
+
constructor(limit) {
|
|
7776
|
+
this.limit = limit;
|
|
7777
|
+
}
|
|
7778
|
+
async run(operation) {
|
|
7779
|
+
await this.acquire();
|
|
7780
|
+
try {
|
|
7781
|
+
return await operation();
|
|
7782
|
+
} finally {
|
|
7783
|
+
this.release();
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
async acquire() {
|
|
7787
|
+
if (this.active < this.limit) {
|
|
7788
|
+
this.active += 1;
|
|
7789
|
+
return;
|
|
7790
|
+
}
|
|
7791
|
+
await new Promise((resolve2) => this.waiters.push(resolve2));
|
|
7792
|
+
}
|
|
7793
|
+
release() {
|
|
7794
|
+
const next = this.waiters.shift();
|
|
7795
|
+
if (next) {
|
|
7796
|
+
next();
|
|
7797
|
+
return;
|
|
7798
|
+
}
|
|
7799
|
+
this.active -= 1;
|
|
7800
|
+
}
|
|
7801
|
+
}
|
|
7802
|
+
var globalImageTransformSemaphore = new Semaphore(1);
|
|
7803
|
+
function imageTransformCacheKey(version, options) {
|
|
7804
|
+
if (!version?.startsWith(OBJECT_VERSION_PREFIX))
|
|
7805
|
+
return null;
|
|
7806
|
+
return [
|
|
7807
|
+
version,
|
|
7808
|
+
options.width ?? "",
|
|
7809
|
+
options.height ?? "",
|
|
7810
|
+
options.resize,
|
|
7811
|
+
options.quality ?? "",
|
|
7812
|
+
options.format
|
|
7813
|
+
].join("\x00");
|
|
7814
|
+
}
|
|
7815
|
+
|
|
7816
|
+
class ImageTransformCache {
|
|
7817
|
+
maxBytes;
|
|
7818
|
+
maxEntries;
|
|
7819
|
+
entries = new Map;
|
|
7820
|
+
inFlight = new Map;
|
|
7821
|
+
cachedBytes = 0;
|
|
7822
|
+
constructor(maxBytes = DEFAULT_MAX_CACHE_BYTES, maxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
|
|
7823
|
+
this.maxBytes = maxBytes;
|
|
7824
|
+
this.maxEntries = maxEntries;
|
|
7825
|
+
}
|
|
7826
|
+
async getOrTransform(key, operation) {
|
|
7827
|
+
if (key === null)
|
|
7828
|
+
return globalImageTransformSemaphore.run(operation);
|
|
7829
|
+
const cached = this.entries.get(key);
|
|
7830
|
+
if (cached) {
|
|
7831
|
+
this.entries.delete(key);
|
|
7832
|
+
this.entries.set(key, cached);
|
|
7833
|
+
return cached.transform;
|
|
7834
|
+
}
|
|
7835
|
+
const pending = this.inFlight.get(key);
|
|
7836
|
+
if (pending)
|
|
7837
|
+
return pending;
|
|
7838
|
+
const transform = globalImageTransformSemaphore.run(operation);
|
|
7839
|
+
this.inFlight.set(key, transform);
|
|
7840
|
+
try {
|
|
7841
|
+
const transformResult = await transform;
|
|
7842
|
+
if (transformResult.ok)
|
|
7843
|
+
this.store(key, transformResult);
|
|
7844
|
+
return transformResult;
|
|
7845
|
+
} finally {
|
|
7846
|
+
if (this.inFlight.get(key) === transform)
|
|
7847
|
+
this.inFlight.delete(key);
|
|
7848
|
+
}
|
|
7849
|
+
}
|
|
7850
|
+
store(key, transform) {
|
|
7851
|
+
const size = transform.bytes.byteLength;
|
|
7852
|
+
if (size > this.maxBytes || this.maxEntries === 0)
|
|
7853
|
+
return;
|
|
7854
|
+
const previous = this.entries.get(key);
|
|
7855
|
+
if (previous) {
|
|
7856
|
+
this.cachedBytes -= previous.size;
|
|
7857
|
+
this.entries.delete(key);
|
|
7858
|
+
}
|
|
7859
|
+
this.entries.set(key, { transform, size });
|
|
7860
|
+
this.cachedBytes += size;
|
|
7861
|
+
while (this.entries.size > this.maxEntries || this.cachedBytes > this.maxBytes) {
|
|
7862
|
+
const oldestKey = this.entries.keys().next().value;
|
|
7863
|
+
if (oldestKey === undefined)
|
|
7864
|
+
break;
|
|
7865
|
+
const oldest = this.entries.get(oldestKey);
|
|
7866
|
+
this.entries.delete(oldestKey);
|
|
7867
|
+
this.cachedBytes -= oldest.size;
|
|
7868
|
+
}
|
|
7869
|
+
}
|
|
7870
|
+
}
|
|
7871
|
+
|
|
6531
7872
|
// src/runtime/storage/handler.ts
|
|
6532
7873
|
var MAX_SIGNED_URL_EXPIRY = 7 * 24 * 60 * 60;
|
|
6533
7874
|
function clampExpiry(expiresIn) {
|
|
@@ -6606,7 +7947,7 @@ var MAX_COMPLETED_TUS_UPLOADS = 64;
|
|
|
6606
7947
|
var COMPLETED_TUS_RETENTION_MS = 60 * 60 * 1000;
|
|
6607
7948
|
var PREFLIGHT_ROLLBACK = Symbol("storage-preflight-rollback");
|
|
6608
7949
|
var INTERNAL_STORAGE_BUCKET = ".supacloud-lite";
|
|
6609
|
-
var
|
|
7950
|
+
var OBJECT_VERSION_PREFIX2 = "v2-";
|
|
6610
7951
|
|
|
6611
7952
|
class StorageHandler {
|
|
6612
7953
|
db;
|
|
@@ -6614,6 +7955,7 @@ class StorageHandler {
|
|
|
6614
7955
|
config;
|
|
6615
7956
|
tusUploads = new Map;
|
|
6616
7957
|
mutationTail = Promise.resolve();
|
|
7958
|
+
imageTransforms = new ImageTransformCache;
|
|
6617
7959
|
constructor(db, driver, config) {
|
|
6618
7960
|
this.db = db;
|
|
6619
7961
|
this.driver = driver;
|
|
@@ -6656,15 +7998,21 @@ class StorageHandler {
|
|
|
6656
7998
|
const bucket2 = parts[3];
|
|
6657
7999
|
const key2 = parts.slice(4).join("/");
|
|
6658
8000
|
if (kind === "public" && (method === "GET" || method === "HEAD")) {
|
|
6659
|
-
const source = await this.
|
|
8001
|
+
const source = await this.loadPublicObject(bucket2, key2);
|
|
8002
|
+
if (source instanceof Response)
|
|
8003
|
+
return source;
|
|
6660
8004
|
return await this.transformImageResponse(source, url, method === "HEAD");
|
|
6661
8005
|
}
|
|
6662
8006
|
if (kind === "authenticated" && (method === "GET" || method === "HEAD")) {
|
|
6663
|
-
const source = await this.
|
|
8007
|
+
const source = await this.loadAuthenticatedObject(ctx, bucket2, key2);
|
|
8008
|
+
if (source instanceof Response)
|
|
8009
|
+
return source;
|
|
6664
8010
|
return await this.transformImageResponse(source, url, method === "HEAD");
|
|
6665
8011
|
}
|
|
6666
8012
|
if (kind === "sign" && method === "GET") {
|
|
6667
|
-
const source = await this.
|
|
8013
|
+
const source = await this.loadSignedObject(url, bucket2, key2);
|
|
8014
|
+
if (source instanceof Response)
|
|
8015
|
+
return source;
|
|
6668
8016
|
return await this.transformImageResponse(source, url, false);
|
|
6669
8017
|
}
|
|
6670
8018
|
return storageError(404, "not_found", `unknown render endpoint: ${rest}`);
|
|
@@ -6877,21 +8225,31 @@ class StorageHandler {
|
|
|
6877
8225
|
throw e;
|
|
6878
8226
|
}
|
|
6879
8227
|
}
|
|
6880
|
-
async transformImageResponse(
|
|
6881
|
-
if (!source.ok)
|
|
6882
|
-
return source;
|
|
8228
|
+
async transformImageResponse(row, url, head) {
|
|
6883
8229
|
const parsed = parseImageTransform(url.searchParams);
|
|
6884
8230
|
if (!parsed.ok)
|
|
6885
8231
|
return storageError(parsed.status, parsed.error, parsed.message);
|
|
6886
|
-
|
|
8232
|
+
let result;
|
|
8233
|
+
try {
|
|
8234
|
+
result = await this.imageTransforms.getOrTransform(imageTransformCacheKey(row.version, parsed.value), async () => {
|
|
8235
|
+
const source = await this.readObjectSource(row);
|
|
8236
|
+
if (source === null)
|
|
8237
|
+
throw new StorageObjectMissingError;
|
|
8238
|
+
return transformImage(source, parsed.value, objectSize(row));
|
|
8239
|
+
});
|
|
8240
|
+
} catch (error) {
|
|
8241
|
+
if (error instanceof StorageObjectMissingError) {
|
|
8242
|
+
return storageError(404, "not_found", "Object not found");
|
|
8243
|
+
}
|
|
8244
|
+
throw error;
|
|
8245
|
+
}
|
|
6887
8246
|
if (!result.ok)
|
|
6888
8247
|
return storageError(result.status, result.error, result.message);
|
|
6889
|
-
const headers =
|
|
8248
|
+
const headers = objectHeaders(row, result.bytes.length);
|
|
6890
8249
|
headers.delete("content-disposition");
|
|
6891
8250
|
headers.delete("etag");
|
|
6892
8251
|
headers.set("content-type", result.contentType);
|
|
6893
|
-
|
|
6894
|
-
return new Response(head ? null : result.bytes, { status: source.status, headers });
|
|
8252
|
+
return new Response(head ? null : result.bytes, { status: 200, headers });
|
|
6895
8253
|
}
|
|
6896
8254
|
async persistObject(ctx, bucketId, key, bytes, contentType, cacheControl, upsert) {
|
|
6897
8255
|
const metadata = objectMetadata(bytes.length, contentType, cacheControl);
|
|
@@ -7161,13 +8519,25 @@ class StorageHandler {
|
|
|
7161
8519
|
return null;
|
|
7162
8520
|
}
|
|
7163
8521
|
async download(ctx, bucketId, key, head) {
|
|
8522
|
+
const row = await this.loadAuthenticatedObject(ctx, bucketId, key);
|
|
8523
|
+
if (row instanceof Response)
|
|
8524
|
+
return row;
|
|
8525
|
+
return this.serveObject(row, head);
|
|
8526
|
+
}
|
|
8527
|
+
async loadAuthenticatedObject(ctx, bucketId, key) {
|
|
7164
8528
|
const res = await this.db.withContext(ctx, (q) => q(`select * from storage.objects where bucket_id = $1 and name = $2`, [bucketId, key]));
|
|
7165
8529
|
const row = res.rows[0];
|
|
7166
8530
|
if (!row)
|
|
7167
8531
|
return storageError(404, "not_found", "Object not found");
|
|
7168
|
-
return
|
|
8532
|
+
return row;
|
|
7169
8533
|
}
|
|
7170
8534
|
async downloadPublic(bucketId, key, head) {
|
|
8535
|
+
const row = await this.loadPublicObject(bucketId, key);
|
|
8536
|
+
if (row instanceof Response)
|
|
8537
|
+
return row;
|
|
8538
|
+
return this.serveObject(row, head);
|
|
8539
|
+
}
|
|
8540
|
+
async loadPublicObject(bucketId, key) {
|
|
7171
8541
|
const bucket = await this.loadBucket(bucketId);
|
|
7172
8542
|
if (!bucket?.public)
|
|
7173
8543
|
return storageError(400, "not_found", "Bucket is not public");
|
|
@@ -7178,24 +8548,16 @@ class StorageHandler {
|
|
|
7178
8548
|
const row = res.rows[0];
|
|
7179
8549
|
if (!row)
|
|
7180
8550
|
return storageError(404, "not_found", "Object not found");
|
|
7181
|
-
return
|
|
8551
|
+
return row;
|
|
7182
8552
|
}
|
|
7183
8553
|
async serveObject(row, head) {
|
|
7184
8554
|
const bytes = await this.readObjectBytes(row);
|
|
7185
8555
|
if (bytes === null)
|
|
7186
8556
|
return storageError(404, "not_found", "Object not found");
|
|
7187
|
-
const
|
|
7188
|
-
const
|
|
7189
|
-
const headers = {
|
|
7190
|
-
"content-type": contentType,
|
|
7191
|
-
"content-length": String(bytes.length),
|
|
7192
|
-
"cache-control": String(meta.cacheControl ?? "no-cache"),
|
|
7193
|
-
etag: String(meta.eTag ?? '""'),
|
|
7194
|
-
"last-modified": new Date(String(meta.lastModified ?? Date.now())).toUTCString(),
|
|
7195
|
-
"x-content-type-options": "nosniff"
|
|
7196
|
-
};
|
|
8557
|
+
const contentType = String(row.metadata?.mimetype ?? "application/octet-stream");
|
|
8558
|
+
const headers = objectHeaders(row, bytes.length);
|
|
7197
8559
|
if (isRenderableActiveType(contentType))
|
|
7198
|
-
headers
|
|
8560
|
+
headers.set("content-disposition", "attachment");
|
|
7199
8561
|
return new Response(head ? null : bytes, { status: 200, headers });
|
|
7200
8562
|
}
|
|
7201
8563
|
async removeObjects(req, ctx, bucketId) {
|
|
@@ -7306,9 +8668,11 @@ class StorageHandler {
|
|
|
7306
8668
|
throw error;
|
|
7307
8669
|
}
|
|
7308
8670
|
async readObjectBytes(row) {
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
8671
|
+
return this.driver.get(storageKey(row));
|
|
8672
|
+
}
|
|
8673
|
+
async readObjectSource(row) {
|
|
8674
|
+
const key = storageKey(row);
|
|
8675
|
+
return this.driver.getBlob ? this.driver.getBlob(key) : this.driver.get(key);
|
|
7312
8676
|
}
|
|
7313
8677
|
async cleanupObjectRows(rows) {
|
|
7314
8678
|
const keys = rows.flatMap((row) => [
|
|
@@ -7411,6 +8775,12 @@ class StorageHandler {
|
|
|
7411
8775
|
return json3(200, out);
|
|
7412
8776
|
}
|
|
7413
8777
|
async redeemSignedUrl(url, bucketId, key) {
|
|
8778
|
+
const row = await this.loadSignedObject(url, bucketId, key);
|
|
8779
|
+
if (row instanceof Response)
|
|
8780
|
+
return row;
|
|
8781
|
+
return this.serveObject(row, false);
|
|
8782
|
+
}
|
|
8783
|
+
async loadSignedObject(url, bucketId, key) {
|
|
7414
8784
|
const token = url.searchParams.get("token") ?? "";
|
|
7415
8785
|
const claims = await verifyJwt(token, this.config.jwtSecret);
|
|
7416
8786
|
if (!claims || claims.url !== `${bucketId}/${key}` || claims.type !== "download") {
|
|
@@ -7423,7 +8793,7 @@ class StorageHandler {
|
|
|
7423
8793
|
const row = res.rows[0];
|
|
7424
8794
|
if (!row)
|
|
7425
8795
|
return storageError(404, "not_found", "Object not found");
|
|
7426
|
-
return
|
|
8796
|
+
return row;
|
|
7427
8797
|
}
|
|
7428
8798
|
async signUploadUrl(ctx, bucketId, key) {
|
|
7429
8799
|
const keyErr = invalidObjectKey(key);
|
|
@@ -7481,6 +8851,9 @@ function objectJson(r) {
|
|
|
7481
8851
|
|
|
7482
8852
|
class StorageValidationError extends Error {
|
|
7483
8853
|
}
|
|
8854
|
+
|
|
8855
|
+
class StorageObjectMissingError extends Error {
|
|
8856
|
+
}
|
|
7484
8857
|
function parseSizeLimit(v) {
|
|
7485
8858
|
if (v === null || v === undefined || v === "")
|
|
7486
8859
|
return null;
|
|
@@ -7503,14 +8876,32 @@ function objectMetadata(size, contentType, cacheControl) {
|
|
|
7503
8876
|
httpStatusCode: 200
|
|
7504
8877
|
};
|
|
7505
8878
|
}
|
|
8879
|
+
function objectHeaders(row, contentLength) {
|
|
8880
|
+
const metadata = row.metadata ?? {};
|
|
8881
|
+
return new Headers({
|
|
8882
|
+
"content-type": String(metadata.mimetype ?? "application/octet-stream"),
|
|
8883
|
+
"content-length": String(contentLength),
|
|
8884
|
+
"cache-control": String(metadata.cacheControl ?? "no-cache"),
|
|
8885
|
+
etag: String(metadata.eTag ?? '""'),
|
|
8886
|
+
"last-modified": new Date(String(metadata.lastModified ?? Date.now())).toUTCString(),
|
|
8887
|
+
"x-content-type-options": "nosniff"
|
|
8888
|
+
});
|
|
8889
|
+
}
|
|
8890
|
+
function objectSize(row) {
|
|
8891
|
+
const size = Number(row.metadata?.size);
|
|
8892
|
+
return Number.isFinite(size) && size >= 0 ? size : undefined;
|
|
8893
|
+
}
|
|
7506
8894
|
function objectVersionKey(version) {
|
|
7507
8895
|
return `.supacloud-lite/objects/${version}`;
|
|
7508
8896
|
}
|
|
8897
|
+
function storageKey(row) {
|
|
8898
|
+
return isVersionedObjectVersion(row.version) ? objectVersionKey(row.version) : legacyObjectKey(row);
|
|
8899
|
+
}
|
|
7509
8900
|
function createObjectVersion() {
|
|
7510
|
-
return `${
|
|
8901
|
+
return `${OBJECT_VERSION_PREFIX2}${crypto.randomUUID()}`;
|
|
7511
8902
|
}
|
|
7512
8903
|
function isVersionedObjectVersion(version) {
|
|
7513
|
-
return version?.startsWith(
|
|
8904
|
+
return version?.startsWith(OBJECT_VERSION_PREFIX2) ?? false;
|
|
7514
8905
|
}
|
|
7515
8906
|
function isInternalStorageBucket(bucketId) {
|
|
7516
8907
|
return bucketId === INTERNAL_STORAGE_BUCKET || bucketId.startsWith(`${INTERNAL_STORAGE_BUCKET}/`);
|
|
@@ -8859,21 +10250,797 @@ function withCors(res) {
|
|
|
8859
10250
|
}
|
|
8860
10251
|
|
|
8861
10252
|
// src/runtime/node/db-diff.ts
|
|
8862
|
-
import {
|
|
10253
|
+
import { mkdtempSync as mkdtempSync2 } from "fs";
|
|
10254
|
+
import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
10255
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
10256
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
10257
|
+
|
|
10258
|
+
// src/runtime/node/native/engine.ts
|
|
10259
|
+
import { execFileSync, spawn } from "child_process";
|
|
10260
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
10261
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
|
|
10262
|
+
import { writeFile } from "fs/promises";
|
|
10263
|
+
import { homedir, tmpdir } from "os";
|
|
8863
10264
|
import { join } from "path";
|
|
10265
|
+
import { extract as extractTar } from "tar";
|
|
10266
|
+
|
|
10267
|
+
// src/runtime/node/native/wire.ts
|
|
10268
|
+
import { createConnection } from "net";
|
|
10269
|
+
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
|
|
10270
|
+
|
|
10271
|
+
class PgWireError extends Error {
|
|
10272
|
+
code;
|
|
10273
|
+
detail;
|
|
10274
|
+
hint;
|
|
10275
|
+
severity;
|
|
10276
|
+
constructor(fields) {
|
|
10277
|
+
super(fields.get("M") ?? "postgres error");
|
|
10278
|
+
this.code = fields.get("C");
|
|
10279
|
+
this.detail = fields.get("D");
|
|
10280
|
+
this.hint = fields.get("H");
|
|
10281
|
+
this.severity = fields.get("S");
|
|
10282
|
+
}
|
|
10283
|
+
}
|
|
10284
|
+
|
|
10285
|
+
class PgWireClient {
|
|
10286
|
+
socket;
|
|
10287
|
+
buffer = Buffer.alloc(0);
|
|
10288
|
+
pending = null;
|
|
10289
|
+
queue = Promise.resolve();
|
|
10290
|
+
closed = false;
|
|
10291
|
+
onNotification = null;
|
|
10292
|
+
static async connect(opts) {
|
|
10293
|
+
const client = new PgWireClient;
|
|
10294
|
+
await client.open(opts);
|
|
10295
|
+
return client;
|
|
10296
|
+
}
|
|
10297
|
+
open(opts) {
|
|
10298
|
+
return new Promise((resolve2, reject) => {
|
|
10299
|
+
this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
|
|
10300
|
+
this.socket.on("error", (e) => {
|
|
10301
|
+
if (this.pending)
|
|
10302
|
+
this.pending.reject(e);
|
|
10303
|
+
reject(e);
|
|
10304
|
+
});
|
|
10305
|
+
this.socket.on("close", () => {
|
|
10306
|
+
this.closed = true;
|
|
10307
|
+
this.pending?.reject(new Error("connection closed"));
|
|
10308
|
+
});
|
|
10309
|
+
this.socket.on("connect", () => {
|
|
10310
|
+
const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
|
|
10311
|
+
const body = Buffer.from(params, "utf8");
|
|
10312
|
+
const msg = Buffer.alloc(8 + body.length);
|
|
10313
|
+
msg.writeInt32BE(8 + body.length, 0);
|
|
10314
|
+
msg.writeInt32BE(196608, 4);
|
|
10315
|
+
body.copy(msg, 8);
|
|
10316
|
+
this.socket.write(msg);
|
|
10317
|
+
});
|
|
10318
|
+
let clientNonce = "";
|
|
10319
|
+
let clientFirstBare = "";
|
|
10320
|
+
let serverSignature = "";
|
|
10321
|
+
const needPassword = () => {
|
|
10322
|
+
if (opts.password == null) {
|
|
10323
|
+
reject(new Error("the server requested a password but none was provided"));
|
|
10324
|
+
return false;
|
|
10325
|
+
}
|
|
10326
|
+
return true;
|
|
10327
|
+
};
|
|
10328
|
+
const startupHandler = (chunk) => {
|
|
10329
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
10330
|
+
let msg;
|
|
10331
|
+
while ((msg = this.nextMessage()) !== null) {
|
|
10332
|
+
const [type, payload] = msg;
|
|
10333
|
+
if (type === 82) {
|
|
10334
|
+
const code = payload.readInt32BE(0);
|
|
10335
|
+
if (code === 0) {} else if (code === 3) {
|
|
10336
|
+
if (!needPassword())
|
|
10337
|
+
return;
|
|
10338
|
+
this.socket.write(message(112, cstring(opts.password)));
|
|
10339
|
+
} else if (code === 5) {
|
|
10340
|
+
if (!needPassword())
|
|
10341
|
+
return;
|
|
10342
|
+
const salt = payload.subarray(4, 8);
|
|
10343
|
+
const inner = md5Hex(Buffer.from(opts.password + opts.user, "utf8"));
|
|
10344
|
+
const token = "md5" + md5Hex(Buffer.concat([Buffer.from(inner, "utf8"), salt]));
|
|
10345
|
+
this.socket.write(message(112, cstring(token)));
|
|
10346
|
+
} else if (code === 10) {
|
|
10347
|
+
if (!needPassword())
|
|
10348
|
+
return;
|
|
10349
|
+
const mechs = payload.subarray(4).toString("utf8").split("\x00").filter(Boolean);
|
|
10350
|
+
if (!mechs.includes("SCRAM-SHA-256")) {
|
|
10351
|
+
reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(", ")})`));
|
|
10352
|
+
return;
|
|
10353
|
+
}
|
|
10354
|
+
clientNonce = randomBytes(18).toString("base64");
|
|
10355
|
+
clientFirstBare = `n=,r=${clientNonce}`;
|
|
10356
|
+
const initial = Buffer.from(`n,,${clientFirstBare}`, "utf8");
|
|
10357
|
+
this.socket.write(message(112, Buffer.concat([cstring("SCRAM-SHA-256"), int32(initial.length), initial])));
|
|
10358
|
+
} else if (code === 11) {
|
|
10359
|
+
const serverFirst = payload.subarray(4).toString("utf8");
|
|
10360
|
+
const attrs = scramAttrs(serverFirst);
|
|
10361
|
+
if (!attrs.r?.startsWith(clientNonce)) {
|
|
10362
|
+
reject(new Error("SCRAM: server nonce does not extend client nonce"));
|
|
10363
|
+
return;
|
|
10364
|
+
}
|
|
10365
|
+
const salt = Buffer.from(attrs.s, "base64");
|
|
10366
|
+
const iterations = parseInt(attrs.i, 10);
|
|
10367
|
+
const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, "sha256");
|
|
10368
|
+
const clientKey = hmac(saltedPassword, "Client Key");
|
|
10369
|
+
const storedKey = sha256(clientKey);
|
|
10370
|
+
const finalNoProof = `c=biws,r=${attrs.r}`;
|
|
10371
|
+
const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
|
|
10372
|
+
const clientSignature = hmac(storedKey, authMessage);
|
|
10373
|
+
const proof = xorBuffers(clientKey, clientSignature);
|
|
10374
|
+
serverSignature = hmac(hmac(saltedPassword, "Server Key"), authMessage).toString("base64");
|
|
10375
|
+
const clientFinal = `${finalNoProof},p=${proof.toString("base64")}`;
|
|
10376
|
+
this.socket.write(message(112, Buffer.from(clientFinal, "utf8")));
|
|
10377
|
+
} else if (code === 12) {
|
|
10378
|
+
const v = scramAttrs(payload.subarray(4).toString("utf8")).v;
|
|
10379
|
+
if (v && serverSignature && v !== serverSignature) {
|
|
10380
|
+
reject(new Error("SCRAM: server signature verification failed"));
|
|
10381
|
+
return;
|
|
10382
|
+
}
|
|
10383
|
+
} else {
|
|
10384
|
+
reject(new Error(`unsupported auth method ${code}`));
|
|
10385
|
+
return;
|
|
10386
|
+
}
|
|
10387
|
+
} else if (type === 69) {
|
|
10388
|
+
reject(new PgWireError(parseErrorFields(payload)));
|
|
10389
|
+
return;
|
|
10390
|
+
} else if (type === 90) {
|
|
10391
|
+
this.socket.off("data", startupHandler);
|
|
10392
|
+
this.socket.on("data", (c) => {
|
|
10393
|
+
this.buffer = Buffer.concat([this.buffer, c]);
|
|
10394
|
+
this.processMessages();
|
|
10395
|
+
});
|
|
10396
|
+
resolve2();
|
|
10397
|
+
return;
|
|
10398
|
+
}
|
|
10399
|
+
}
|
|
10400
|
+
};
|
|
10401
|
+
this.socket.on("data", startupHandler);
|
|
10402
|
+
});
|
|
10403
|
+
}
|
|
10404
|
+
run(send) {
|
|
10405
|
+
const op = this.queue.then(() => new Promise((resolve2, reject) => {
|
|
10406
|
+
if (this.closed)
|
|
10407
|
+
return reject(new Error("connection closed"));
|
|
10408
|
+
this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
|
|
10409
|
+
send();
|
|
10410
|
+
}));
|
|
10411
|
+
this.queue = op.catch(() => {});
|
|
10412
|
+
return op;
|
|
10413
|
+
}
|
|
10414
|
+
async exec(sql) {
|
|
10415
|
+
return this.run(() => this.socket.write(message(81, cstring(sql))));
|
|
10416
|
+
}
|
|
10417
|
+
async query(sql, params = []) {
|
|
10418
|
+
const results = await this.run(() => {
|
|
10419
|
+
const parse = message(80, Buffer.concat([cstring(""), cstring(sql), int16(0)]));
|
|
10420
|
+
const paramBufs = [int16(0), int16(params.length)];
|
|
10421
|
+
for (const p of params) {
|
|
10422
|
+
if (p === null || p === undefined) {
|
|
10423
|
+
paramBufs.push(int32(-1));
|
|
10424
|
+
} else {
|
|
10425
|
+
const b = Buffer.from(String(p), "utf8");
|
|
10426
|
+
paramBufs.push(int32(b.length), b);
|
|
10427
|
+
}
|
|
10428
|
+
}
|
|
10429
|
+
paramBufs.push(int16(0));
|
|
10430
|
+
const bind = message(66, Buffer.concat([cstring(""), cstring(""), ...paramBufs]));
|
|
10431
|
+
const describe = message(68, Buffer.concat([Buffer.from("P"), cstring("")]));
|
|
10432
|
+
const execute = message(69, Buffer.concat([cstring(""), int32(0)]));
|
|
10433
|
+
const sync = message(83, Buffer.alloc(0));
|
|
10434
|
+
this.socket.write(Buffer.concat([parse, bind, describe, execute, sync]));
|
|
10435
|
+
});
|
|
10436
|
+
return results[0] ?? { rows: [] };
|
|
10437
|
+
}
|
|
10438
|
+
close() {
|
|
10439
|
+
return new Promise((resolve2) => {
|
|
10440
|
+
if (this.closed)
|
|
10441
|
+
return resolve2();
|
|
10442
|
+
this.socket.write(message(88, Buffer.alloc(0)));
|
|
10443
|
+
this.socket.end(() => resolve2());
|
|
10444
|
+
});
|
|
10445
|
+
}
|
|
10446
|
+
nextMessage() {
|
|
10447
|
+
if (this.buffer.length < 5)
|
|
10448
|
+
return null;
|
|
10449
|
+
const type = this.buffer[0];
|
|
10450
|
+
const length = this.buffer.readInt32BE(1);
|
|
10451
|
+
if (this.buffer.length < 1 + length)
|
|
10452
|
+
return null;
|
|
10453
|
+
const payload = this.buffer.subarray(5, 1 + length);
|
|
10454
|
+
this.buffer = this.buffer.subarray(1 + length);
|
|
10455
|
+
return [type, Buffer.from(payload)];
|
|
10456
|
+
}
|
|
10457
|
+
processMessages() {
|
|
10458
|
+
let msg;
|
|
10459
|
+
while ((msg = this.nextMessage()) !== null) {
|
|
10460
|
+
const [type, payload] = msg;
|
|
10461
|
+
const p = this.pending;
|
|
10462
|
+
switch (type) {
|
|
10463
|
+
case 84: {
|
|
10464
|
+
if (!p)
|
|
10465
|
+
break;
|
|
10466
|
+
const count = payload.readInt16BE(0);
|
|
10467
|
+
let off = 2;
|
|
10468
|
+
const columns = [];
|
|
10469
|
+
for (let i = 0;i < count; i++) {
|
|
10470
|
+
const end = payload.indexOf(0, off);
|
|
10471
|
+
const name = payload.toString("utf8", off, end);
|
|
10472
|
+
off = end + 1;
|
|
10473
|
+
const typeOid = payload.readInt32BE(off + 6);
|
|
10474
|
+
off += 18;
|
|
10475
|
+
columns.push({ name, typeOid });
|
|
10476
|
+
}
|
|
10477
|
+
p.columns = columns;
|
|
10478
|
+
break;
|
|
10479
|
+
}
|
|
10480
|
+
case 68: {
|
|
10481
|
+
if (!p)
|
|
10482
|
+
break;
|
|
10483
|
+
const count = payload.readInt16BE(0);
|
|
10484
|
+
let off = 2;
|
|
10485
|
+
const row = {};
|
|
10486
|
+
for (let i = 0;i < count; i++) {
|
|
10487
|
+
const len = payload.readInt32BE(off);
|
|
10488
|
+
off += 4;
|
|
10489
|
+
let value = null;
|
|
10490
|
+
if (len >= 0) {
|
|
10491
|
+
value = decodeValue(payload.toString("utf8", off, off + len), p.columns[i]?.typeOid ?? 25);
|
|
10492
|
+
off += len;
|
|
10493
|
+
}
|
|
10494
|
+
row[p.columns[i]?.name ?? `col${i}`] = value;
|
|
10495
|
+
}
|
|
10496
|
+
if (p.results.length === 0)
|
|
10497
|
+
p.results.push({ rows: [] });
|
|
10498
|
+
p.results[p.results.length - 1].rows.push(row);
|
|
10499
|
+
break;
|
|
10500
|
+
}
|
|
10501
|
+
case 67: {
|
|
10502
|
+
if (!p)
|
|
10503
|
+
break;
|
|
10504
|
+
const tag = payload.toString("utf8", 0, payload.length - 1);
|
|
10505
|
+
const parts = tag.split(" ");
|
|
10506
|
+
const affected = parseInt(parts[parts.length - 1], 10);
|
|
10507
|
+
if (p.results.length === 0)
|
|
10508
|
+
p.results.push({ rows: [] });
|
|
10509
|
+
const current = p.results[p.results.length - 1];
|
|
10510
|
+
if (!Number.isNaN(affected))
|
|
10511
|
+
current.affectedRows = affected;
|
|
10512
|
+
p.results.push({ rows: [] });
|
|
10513
|
+
p.columns = [];
|
|
10514
|
+
break;
|
|
10515
|
+
}
|
|
10516
|
+
case 69: {
|
|
10517
|
+
if (p)
|
|
10518
|
+
p.error = new PgWireError(parseErrorFields(payload));
|
|
10519
|
+
break;
|
|
10520
|
+
}
|
|
10521
|
+
case 65: {
|
|
10522
|
+
payload.readInt32BE(0);
|
|
10523
|
+
const channelEnd = payload.indexOf(0, 4);
|
|
10524
|
+
const channel = payload.toString("utf8", 4, channelEnd);
|
|
10525
|
+
const payloadEnd = payload.indexOf(0, channelEnd + 1);
|
|
10526
|
+
const body = payload.toString("utf8", channelEnd + 1, payloadEnd);
|
|
10527
|
+
this.onNotification?.(channel, body);
|
|
10528
|
+
break;
|
|
10529
|
+
}
|
|
10530
|
+
case 90: {
|
|
10531
|
+
if (!p)
|
|
10532
|
+
break;
|
|
10533
|
+
this.pending = null;
|
|
10534
|
+
if (p.error)
|
|
10535
|
+
p.reject(p.error);
|
|
10536
|
+
else {
|
|
10537
|
+
const results = p.results.filter((r, i) => i < p.results.length - 1 || r.rows.length > 0 || r.affectedRows !== undefined);
|
|
10538
|
+
p.resolve(results.length > 0 ? results : [{ rows: [] }]);
|
|
10539
|
+
}
|
|
10540
|
+
break;
|
|
10541
|
+
}
|
|
10542
|
+
}
|
|
10543
|
+
}
|
|
10544
|
+
}
|
|
10545
|
+
}
|
|
10546
|
+
var hmac = (key, data) => createHmac("sha256", key).update(data, "utf8").digest();
|
|
10547
|
+
var sha256 = (b) => createHash("sha256").update(b).digest();
|
|
10548
|
+
var md5Hex = (b) => createHash("md5").update(b).digest("hex");
|
|
10549
|
+
function xorBuffers(a, b) {
|
|
10550
|
+
const out = Buffer.alloc(a.length);
|
|
10551
|
+
for (let i = 0;i < a.length; i++)
|
|
10552
|
+
out[i] = a[i] ^ b[i];
|
|
10553
|
+
return out;
|
|
10554
|
+
}
|
|
10555
|
+
function scramAttrs(s) {
|
|
10556
|
+
const out = {};
|
|
10557
|
+
for (const part of s.split(",")) {
|
|
10558
|
+
const eq = part.indexOf("=");
|
|
10559
|
+
if (eq > 0)
|
|
10560
|
+
out[part.slice(0, eq)] = part.slice(eq + 1);
|
|
10561
|
+
}
|
|
10562
|
+
return out;
|
|
10563
|
+
}
|
|
10564
|
+
function message(type, body) {
|
|
10565
|
+
const out = Buffer.alloc(5 + body.length);
|
|
10566
|
+
out[0] = type;
|
|
10567
|
+
out.writeInt32BE(4 + body.length, 1);
|
|
10568
|
+
body.copy(out, 5);
|
|
10569
|
+
return out;
|
|
10570
|
+
}
|
|
10571
|
+
var cstring = (s) => Buffer.from(s + "\x00", "utf8");
|
|
10572
|
+
var int16 = (n) => {
|
|
10573
|
+
const b = Buffer.alloc(2);
|
|
10574
|
+
b.writeInt16BE(n);
|
|
10575
|
+
return b;
|
|
10576
|
+
};
|
|
10577
|
+
var int32 = (n) => {
|
|
10578
|
+
const b = Buffer.alloc(4);
|
|
10579
|
+
b.writeInt32BE(n);
|
|
10580
|
+
return b;
|
|
10581
|
+
};
|
|
10582
|
+
function parseErrorFields(payload) {
|
|
10583
|
+
const fields = new Map;
|
|
10584
|
+
let off = 0;
|
|
10585
|
+
while (off < payload.length && payload[off] !== 0) {
|
|
10586
|
+
const key = String.fromCharCode(payload[off]);
|
|
10587
|
+
const end = payload.indexOf(0, off + 1);
|
|
10588
|
+
fields.set(key, payload.toString("utf8", off + 1, end));
|
|
10589
|
+
off = end + 1;
|
|
10590
|
+
}
|
|
10591
|
+
return fields;
|
|
10592
|
+
}
|
|
10593
|
+
function decodeValue(text, oid) {
|
|
10594
|
+
switch (oid) {
|
|
10595
|
+
case 16:
|
|
10596
|
+
return text === "t";
|
|
10597
|
+
case 20: {
|
|
10598
|
+
const n = Number(text);
|
|
10599
|
+
return Number.isSafeInteger(n) ? n : text;
|
|
10600
|
+
}
|
|
10601
|
+
case 21:
|
|
10602
|
+
case 23:
|
|
10603
|
+
case 26:
|
|
10604
|
+
return Number(text);
|
|
10605
|
+
case 700:
|
|
10606
|
+
case 701:
|
|
10607
|
+
return Number(text);
|
|
10608
|
+
case 114:
|
|
10609
|
+
case 3802:
|
|
10610
|
+
return JSON.parse(text);
|
|
10611
|
+
case 1114:
|
|
10612
|
+
return new Date(text.replace(" ", "T") + "Z");
|
|
10613
|
+
case 1184: {
|
|
10614
|
+
let iso3 = text.replace(" ", "T");
|
|
10615
|
+
if (/[+-]\d\d$/.test(iso3))
|
|
10616
|
+
iso3 += ":00";
|
|
10617
|
+
return new Date(iso3);
|
|
10618
|
+
}
|
|
10619
|
+
case 1000:
|
|
10620
|
+
return parsePgArray(text).map((v) => v === "t");
|
|
10621
|
+
case 1007:
|
|
10622
|
+
return parsePgArray(text).map((v) => v === null ? null : Number(v));
|
|
10623
|
+
case 1016:
|
|
10624
|
+
return parsePgArray(text).map((v) => {
|
|
10625
|
+
if (v === null)
|
|
10626
|
+
return null;
|
|
10627
|
+
const n = Number(v);
|
|
10628
|
+
return Number.isSafeInteger(n) ? n : v;
|
|
10629
|
+
});
|
|
10630
|
+
case 1003:
|
|
10631
|
+
case 1009:
|
|
10632
|
+
case 1015:
|
|
10633
|
+
return parsePgArray(text);
|
|
10634
|
+
default:
|
|
10635
|
+
return text;
|
|
10636
|
+
}
|
|
10637
|
+
}
|
|
10638
|
+
function parsePgArray(text) {
|
|
10639
|
+
const out = [];
|
|
10640
|
+
if (text.length < 2)
|
|
10641
|
+
return out;
|
|
10642
|
+
let i = 1;
|
|
10643
|
+
while (i < text.length - 1) {
|
|
10644
|
+
if (text[i] === ",") {
|
|
10645
|
+
i++;
|
|
10646
|
+
continue;
|
|
10647
|
+
}
|
|
10648
|
+
if (text[i] === '"') {
|
|
10649
|
+
let value = "";
|
|
10650
|
+
i++;
|
|
10651
|
+
while (text[i] !== '"') {
|
|
10652
|
+
if (text[i] === "\\")
|
|
10653
|
+
i++;
|
|
10654
|
+
value += text[i++];
|
|
10655
|
+
}
|
|
10656
|
+
i++;
|
|
10657
|
+
out.push(value);
|
|
10658
|
+
} else {
|
|
10659
|
+
let value = "";
|
|
10660
|
+
while (i < text.length - 1 && text[i] !== ",")
|
|
10661
|
+
value += text[i++];
|
|
10662
|
+
out.push(value === "NULL" ? null : value);
|
|
10663
|
+
}
|
|
10664
|
+
}
|
|
10665
|
+
return out;
|
|
10666
|
+
}
|
|
10667
|
+
|
|
10668
|
+
// src/runtime/db/engine.ts
|
|
10669
|
+
class Mutex {
|
|
10670
|
+
tail = Promise.resolve();
|
|
10671
|
+
async lock() {
|
|
10672
|
+
let release;
|
|
10673
|
+
const next = new Promise((r) => release = r);
|
|
10674
|
+
const prev = this.tail;
|
|
10675
|
+
this.tail = this.tail.then(() => next);
|
|
10676
|
+
await prev;
|
|
10677
|
+
return release;
|
|
10678
|
+
}
|
|
10679
|
+
async run(fn) {
|
|
10680
|
+
const release = await this.lock();
|
|
10681
|
+
try {
|
|
10682
|
+
return await fn();
|
|
10683
|
+
} finally {
|
|
10684
|
+
release();
|
|
10685
|
+
}
|
|
10686
|
+
}
|
|
10687
|
+
}
|
|
10688
|
+
|
|
10689
|
+
// src/runtime/node/native/wire-engine.ts
|
|
10690
|
+
async function buildWireEngine(options) {
|
|
10691
|
+
const queryClient = await options.connect();
|
|
10692
|
+
let listenerClient;
|
|
10693
|
+
try {
|
|
10694
|
+
listenerClient = await options.connect();
|
|
10695
|
+
} catch (error) {
|
|
10696
|
+
await queryClient.close().catch(() => {});
|
|
10697
|
+
throw error;
|
|
10698
|
+
}
|
|
10699
|
+
const queryMutex = new Mutex;
|
|
10700
|
+
const listenerMutex = new Mutex;
|
|
10701
|
+
const listeners = new Map;
|
|
10702
|
+
listenerClient.onNotification = (channel, payload) => {
|
|
10703
|
+
for (const listener of listeners.get(channel) ?? [])
|
|
10704
|
+
listener(payload);
|
|
10705
|
+
};
|
|
10706
|
+
const transactionClient = {
|
|
10707
|
+
async query(sql, params) {
|
|
10708
|
+
const queryResult = await queryClient.query(sql, normalizeParams(params));
|
|
10709
|
+
return { rows: queryResult.rows, affectedRows: queryResult.affectedRows };
|
|
10710
|
+
},
|
|
10711
|
+
async exec(sql) {
|
|
10712
|
+
await queryClient.exec(sql);
|
|
10713
|
+
}
|
|
10714
|
+
};
|
|
10715
|
+
let closePromise = null;
|
|
10716
|
+
return {
|
|
10717
|
+
query(sql, params) {
|
|
10718
|
+
return queryMutex.run(() => transactionClient.query(sql, params));
|
|
10719
|
+
},
|
|
10720
|
+
exec(sql) {
|
|
10721
|
+
return queryMutex.run(() => transactionClient.exec(sql));
|
|
10722
|
+
},
|
|
10723
|
+
transaction(callback) {
|
|
10724
|
+
return queryMutex.run(async () => {
|
|
10725
|
+
await queryClient.exec("begin");
|
|
10726
|
+
try {
|
|
10727
|
+
const response = await callback(transactionClient);
|
|
10728
|
+
await queryClient.exec("commit");
|
|
10729
|
+
return response;
|
|
10730
|
+
} catch (error) {
|
|
10731
|
+
await queryClient.exec("rollback").catch(() => {});
|
|
10732
|
+
throw error;
|
|
10733
|
+
}
|
|
10734
|
+
});
|
|
10735
|
+
},
|
|
10736
|
+
async listen(channel, listener) {
|
|
10737
|
+
return listenerMutex.run(async () => {
|
|
10738
|
+
let channelListeners = listeners.get(channel);
|
|
10739
|
+
if (!channelListeners) {
|
|
10740
|
+
channelListeners = new Set;
|
|
10741
|
+
await listenerClient.exec(`listen "${channel.replaceAll('"', '""')}"`);
|
|
10742
|
+
listeners.set(channel, channelListeners);
|
|
10743
|
+
}
|
|
10744
|
+
channelListeners.add(listener);
|
|
10745
|
+
return () => {
|
|
10746
|
+
channelListeners.delete(listener);
|
|
10747
|
+
};
|
|
10748
|
+
});
|
|
10749
|
+
},
|
|
10750
|
+
close() {
|
|
10751
|
+
closePromise ??= closeWireEngine(queryClient, listenerClient, options.onClose);
|
|
10752
|
+
return closePromise;
|
|
10753
|
+
}
|
|
10754
|
+
};
|
|
10755
|
+
}
|
|
10756
|
+
async function closeWireEngine(queryClient, listenerClient, onClose) {
|
|
10757
|
+
const closeResults = await Promise.allSettled([queryClient.close(), listenerClient.close()]);
|
|
10758
|
+
let engineCleanupError;
|
|
10759
|
+
try {
|
|
10760
|
+
await onClose?.();
|
|
10761
|
+
} catch (error) {
|
|
10762
|
+
engineCleanupError = error;
|
|
10763
|
+
}
|
|
10764
|
+
const connectionErrors = closeResults.flatMap((closeResult) => closeResult.status === "rejected" ? [closeResult.reason] : []);
|
|
10765
|
+
if (engineCleanupError !== undefined)
|
|
10766
|
+
connectionErrors.push(engineCleanupError);
|
|
10767
|
+
if (connectionErrors.length > 0)
|
|
10768
|
+
throw new AggregateError(connectionErrors, "native database cleanup failed");
|
|
10769
|
+
}
|
|
10770
|
+
function normalizeParams(params) {
|
|
10771
|
+
return params?.map((parameter) => {
|
|
10772
|
+
if (parameter === null || parameter === undefined)
|
|
10773
|
+
return null;
|
|
10774
|
+
if (Array.isArray(parameter))
|
|
10775
|
+
return toPgArrayLiteral(parameter);
|
|
10776
|
+
if (parameter instanceof Date)
|
|
10777
|
+
return parameter.toISOString();
|
|
10778
|
+
if (typeof parameter === "object")
|
|
10779
|
+
return JSON.stringify(parameter);
|
|
10780
|
+
return parameter;
|
|
10781
|
+
});
|
|
10782
|
+
}
|
|
10783
|
+
function toPgArrayLiteral(array) {
|
|
10784
|
+
const encoded = array.map((element) => {
|
|
10785
|
+
if (element === null || element === undefined)
|
|
10786
|
+
return "NULL";
|
|
10787
|
+
if (Array.isArray(element))
|
|
10788
|
+
return toPgArrayLiteral(element);
|
|
10789
|
+
if (typeof element === "number" || typeof element === "boolean")
|
|
10790
|
+
return String(element);
|
|
10791
|
+
const text = typeof element === "object" ? JSON.stringify(element) : String(element);
|
|
10792
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
10793
|
+
});
|
|
10794
|
+
return `{${encoded.join(",")}}`;
|
|
10795
|
+
}
|
|
10796
|
+
|
|
10797
|
+
// src/runtime/node/native/engine.ts
|
|
10798
|
+
var DEFAULT_PG_VERSION = "17.7.0";
|
|
10799
|
+
var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
|
|
10800
|
+
function isNativeEngineSupported() {
|
|
10801
|
+
return (process.platform === "darwin" || process.platform === "linux") && (process.arch === "arm64" || process.arch === "x64") && (process.platform !== "linux" || isGlibcLinux());
|
|
10802
|
+
}
|
|
10803
|
+
function isGlibcLinux() {
|
|
10804
|
+
try {
|
|
10805
|
+
const version = execFileSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
10806
|
+
return /glibc|gnu libc/i.test(version);
|
|
10807
|
+
} catch {
|
|
10808
|
+
return false;
|
|
10809
|
+
}
|
|
10810
|
+
}
|
|
10811
|
+
function target() {
|
|
10812
|
+
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
|
|
10813
|
+
if (!arch)
|
|
10814
|
+
throw new Error(`unsupported architecture for native engine: ${process.arch}`);
|
|
10815
|
+
if (process.platform === "darwin")
|
|
10816
|
+
return `${arch}-apple-darwin`;
|
|
10817
|
+
if (process.platform === "linux")
|
|
10818
|
+
return `${arch}-unknown-linux-gnu`;
|
|
10819
|
+
throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
|
|
10820
|
+
}
|
|
10821
|
+
function isCompleteInstall(dir) {
|
|
10822
|
+
return existsSync(join(dir, "bin", "postgres")) && existsSync(join(dir, "share", "postgres.bki"));
|
|
10823
|
+
}
|
|
10824
|
+
var PINNED_SHA256 = {
|
|
10825
|
+
"postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
|
|
10826
|
+
"postgresql-17.7.0-aarch64-unknown-linux-gnu": "89cc2f089880cc8e5e6b7a29387829ec4e4779427855bc0b9fa187c8fce33c8b",
|
|
10827
|
+
"postgresql-17.7.0-x86_64-apple-darwin": "0dd8c25173524bad4ae8ef6b970da1ac40f4c1f231150c416ccb8cd06feff8f2",
|
|
10828
|
+
"postgresql-17.7.0-aarch64-apple-darwin": "727ac08d20a704014a0d51eb3300aa0c8e292c1cf0a1c99d4f4b1002e1420220"
|
|
10829
|
+
};
|
|
10830
|
+
async function verifyTarball(tarball, key, url) {
|
|
10831
|
+
const actual = createHash2("sha256").update(readFileSync(tarball)).digest("hex");
|
|
10832
|
+
const pinned = PINNED_SHA256[key];
|
|
10833
|
+
if (pinned) {
|
|
10834
|
+
if (actual !== pinned) {
|
|
10835
|
+
throw new Error(`postgres binary checksum mismatch for ${key}: expected ${pinned}, got ${actual}`);
|
|
10836
|
+
}
|
|
10837
|
+
return;
|
|
10838
|
+
}
|
|
10839
|
+
const res = await fetchRelease(`${url}.sha256`);
|
|
10840
|
+
if (!res.ok)
|
|
10841
|
+
throw new Error(`could not fetch checksum for ${key}: HTTP ${res.status}`);
|
|
10842
|
+
const expected = (await res.text()).trim().split(/\s+/)[0].toLowerCase();
|
|
10843
|
+
if (!/^[0-9a-f]{64}$/.test(expected))
|
|
10844
|
+
throw new Error(`malformed published checksum for ${key}`);
|
|
10845
|
+
if (actual !== expected) {
|
|
10846
|
+
throw new Error(`postgres binary checksum mismatch for ${key}: expected ${expected}, got ${actual}`);
|
|
10847
|
+
}
|
|
10848
|
+
}
|
|
10849
|
+
async function ensurePostgres(version = DEFAULT_PG_VERSION, cacheDir, log) {
|
|
10850
|
+
const t = target();
|
|
10851
|
+
const root = cacheDir ?? join(homedir(), ".cache", "supacloud-lite");
|
|
10852
|
+
const dir = join(root, `postgresql-${version}-${t}`);
|
|
10853
|
+
if (isCompleteInstall(dir))
|
|
10854
|
+
return dir;
|
|
10855
|
+
const url = `https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/postgresql-${version}-${t}.tar.gz`;
|
|
10856
|
+
mkdirSync(root, { recursive: true });
|
|
10857
|
+
const uniq = `${process.pid}-${randomBytes2(6).toString("hex")}`;
|
|
10858
|
+
const tarball = join(root, `pg-${version}-${uniq}.tar.gz`);
|
|
10859
|
+
const tmpDir = join(root, `.tmp-${version}-${t}-${uniq}`);
|
|
10860
|
+
try {
|
|
10861
|
+
if (isCompleteInstall(dir))
|
|
10862
|
+
return dir;
|
|
10863
|
+
log?.(`downloading postgres ${version} (${t})\u2026`);
|
|
10864
|
+
const res = await fetchRelease(url);
|
|
10865
|
+
if (!res.ok)
|
|
10866
|
+
throw new Error(`failed to download ${url}: HTTP ${res.status}`);
|
|
10867
|
+
await writeFile(tarball, Buffer.from(await res.arrayBuffer()));
|
|
10868
|
+
await verifyTarball(tarball, `postgresql-${version}-${t}`, url);
|
|
10869
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
10870
|
+
await extractTar({ cwd: tmpDir, file: tarball, gzip: true, preserveOwner: false, strict: true, strip: 1 });
|
|
10871
|
+
if (!isCompleteInstall(tmpDir))
|
|
10872
|
+
throw new Error("postgres archive extracted incompletely");
|
|
10873
|
+
try {
|
|
10874
|
+
renameSync(tmpDir, dir);
|
|
10875
|
+
} catch {
|
|
10876
|
+
if (!isCompleteInstall(dir)) {
|
|
10877
|
+
rmSync(dir, { recursive: true, force: true });
|
|
10878
|
+
renameSync(tmpDir, dir);
|
|
10879
|
+
}
|
|
10880
|
+
}
|
|
10881
|
+
log?.(`postgres installed to ${dir}`);
|
|
10882
|
+
return dir;
|
|
10883
|
+
} finally {
|
|
10884
|
+
rmSync(tarball, { force: true });
|
|
10885
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
10886
|
+
}
|
|
10887
|
+
}
|
|
10888
|
+
async function fetchRelease(url) {
|
|
10889
|
+
let lastError;
|
|
10890
|
+
for (let attempt = 1;attempt <= 3; attempt++) {
|
|
10891
|
+
try {
|
|
10892
|
+
const response = await fetch(url);
|
|
10893
|
+
if (response.ok || response.status < 500)
|
|
10894
|
+
return response;
|
|
10895
|
+
lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
|
|
10896
|
+
} catch (error) {
|
|
10897
|
+
lastError = error;
|
|
10898
|
+
}
|
|
10899
|
+
if (attempt < 3)
|
|
10900
|
+
await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
|
|
10901
|
+
}
|
|
10902
|
+
throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
|
|
10903
|
+
}
|
|
10904
|
+
var TUNED_CONF = `
|
|
10905
|
+
# supacloud-lite: memory-lean settings for an embedded, single-app Postgres
|
|
10906
|
+
listen_addresses = ''
|
|
10907
|
+
shared_buffers = 16MB
|
|
10908
|
+
dynamic_shared_memory_type = posix
|
|
10909
|
+
max_connections = 10
|
|
10910
|
+
wal_level = minimal
|
|
10911
|
+
max_wal_senders = 0
|
|
10912
|
+
logging_collector = off
|
|
10913
|
+
`;
|
|
10914
|
+
async function createNativeEngine(opts) {
|
|
10915
|
+
const releaseLock = await acquireDataDirLock(opts.dataDir, "native PostgreSQL");
|
|
10916
|
+
let socketDirectory;
|
|
10917
|
+
let postgres;
|
|
10918
|
+
let removeExitHandler;
|
|
10919
|
+
try {
|
|
10920
|
+
const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log);
|
|
10921
|
+
const bin = (name) => join(installDir, "bin", name);
|
|
10922
|
+
if (!existsSync(join(opts.dataDir, "PG_VERSION"))) {
|
|
10923
|
+
mkdirSync(opts.dataDir, { recursive: true });
|
|
10924
|
+
try {
|
|
10925
|
+
execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
|
|
10926
|
+
stdio: "pipe"
|
|
10927
|
+
});
|
|
10928
|
+
} catch (error) {
|
|
10929
|
+
const stderr = error.stderr?.toString() ?? "";
|
|
10930
|
+
throw new Error(`initdb failed:
|
|
10931
|
+
${stderr || error.message}`);
|
|
10932
|
+
}
|
|
10933
|
+
appendFileSync(join(opts.dataDir, "postgresql.conf"), TUNED_CONF);
|
|
10934
|
+
}
|
|
10935
|
+
removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
|
|
10936
|
+
socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
|
|
10937
|
+
chmodSync(socketDirectory, 448);
|
|
10938
|
+
postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
|
|
10939
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
10940
|
+
detached: false
|
|
10941
|
+
});
|
|
10942
|
+
let postgresExited = false;
|
|
10943
|
+
let postgresStderr = "";
|
|
10944
|
+
postgres.stderr?.on("data", (chunk) => {
|
|
10945
|
+
postgresStderr = (postgresStderr + chunk.toString()).slice(-4000);
|
|
10946
|
+
});
|
|
10947
|
+
postgres.on("exit", () => postgresExited = true);
|
|
10948
|
+
const killPostgres = () => {
|
|
10949
|
+
if (!postgresExited)
|
|
10950
|
+
postgres?.kill("SIGTERM");
|
|
10951
|
+
};
|
|
10952
|
+
process.once("exit", killPostgres);
|
|
10953
|
+
removeExitHandler = () => process.off("exit", killPostgres);
|
|
10954
|
+
const socketPath = join(socketDirectory, ".s.PGSQL.5432");
|
|
10955
|
+
const connect = async () => {
|
|
10956
|
+
const deadline = Date.now() + 20000;
|
|
10957
|
+
while (Date.now() <= deadline) {
|
|
10958
|
+
try {
|
|
10959
|
+
return await PgWireClient.connect({ socketPath, user: "postgres", database: "postgres" });
|
|
10960
|
+
} catch (error) {
|
|
10961
|
+
if (postgresExited) {
|
|
10962
|
+
const detail = postgresStderr.trim();
|
|
10963
|
+
throw new Error(`embedded postgres failed to start${detail ? `:
|
|
10964
|
+
${detail}` : " (no output)"}
|
|
10965
|
+
|
|
10966
|
+
` + `data dir: ${opts.dataDir}
|
|
10967
|
+
` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
|
|
10968
|
+
}
|
|
10969
|
+
await new Promise((resolve2) => setTimeout(resolve2, 150));
|
|
10970
|
+
}
|
|
10971
|
+
}
|
|
10972
|
+
throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
|
|
10973
|
+
};
|
|
10974
|
+
return await buildWireEngine({
|
|
10975
|
+
connect,
|
|
10976
|
+
onClose: async () => {
|
|
10977
|
+
removeExitHandler?.();
|
|
10978
|
+
await stopPostgres(postgres, () => postgresExited);
|
|
10979
|
+
rmSync(socketDirectory, { recursive: true, force: true });
|
|
10980
|
+
await releaseLock();
|
|
10981
|
+
}
|
|
10982
|
+
});
|
|
10983
|
+
} catch (error) {
|
|
10984
|
+
removeExitHandler?.();
|
|
10985
|
+
if (postgres)
|
|
10986
|
+
await stopPostgres(postgres, () => postgres.exitCode !== null);
|
|
10987
|
+
if (socketDirectory)
|
|
10988
|
+
rmSync(socketDirectory, { recursive: true, force: true });
|
|
10989
|
+
await releaseLock();
|
|
10990
|
+
throw error;
|
|
10991
|
+
}
|
|
10992
|
+
}
|
|
10993
|
+
async function stopPostgres(postgres, hasExited) {
|
|
10994
|
+
if (hasExited())
|
|
10995
|
+
return;
|
|
10996
|
+
postgres.kill("SIGINT");
|
|
10997
|
+
await new Promise((resolve2) => {
|
|
10998
|
+
const killTimeout = setTimeout(() => {
|
|
10999
|
+
postgres.kill("SIGKILL");
|
|
11000
|
+
resolve2();
|
|
11001
|
+
}, 5000);
|
|
11002
|
+
postgres.once("exit", () => {
|
|
11003
|
+
clearTimeout(killTimeout);
|
|
11004
|
+
resolve2();
|
|
11005
|
+
});
|
|
11006
|
+
});
|
|
11007
|
+
}
|
|
11008
|
+
function removeStalePidFile(pidPath) {
|
|
11009
|
+
if (!existsSync(pidPath))
|
|
11010
|
+
return;
|
|
11011
|
+
try {
|
|
11012
|
+
const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
|
|
11013
|
+
`)[0]?.trim() ?? "", 10);
|
|
11014
|
+
if (!pid) {
|
|
11015
|
+
rmSync(pidPath, { force: true });
|
|
11016
|
+
return;
|
|
11017
|
+
}
|
|
11018
|
+
try {
|
|
11019
|
+
process.kill(pid, 0);
|
|
11020
|
+
} catch {
|
|
11021
|
+
rmSync(pidPath, { force: true });
|
|
11022
|
+
}
|
|
11023
|
+
} catch {}
|
|
11024
|
+
}
|
|
11025
|
+
|
|
11026
|
+
// src/runtime/node/db-diff.ts
|
|
8864
11027
|
async function computeDbDiff(opts) {
|
|
8865
11028
|
const schema = opts.schema ?? "public";
|
|
8866
|
-
|
|
8867
|
-
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
8868
|
-
migrations: opts.migrations,
|
|
8869
|
-
startRuntimeServices: false
|
|
8870
|
-
});
|
|
11029
|
+
let shadow;
|
|
8871
11030
|
let live;
|
|
11031
|
+
let unclaimedLiveEngine = opts.liveEngine;
|
|
8872
11032
|
let operationFailed = false;
|
|
8873
11033
|
try {
|
|
11034
|
+
shadow = await createBackend({
|
|
11035
|
+
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
11036
|
+
migrations: opts.migrations,
|
|
11037
|
+
startRuntimeServices: false
|
|
11038
|
+
});
|
|
11039
|
+
const liveEngine = unclaimedLiveEngine;
|
|
11040
|
+
unclaimedLiveEngine = undefined;
|
|
8874
11041
|
live = await createBackend({
|
|
8875
|
-
engine:
|
|
8876
|
-
dataDir:
|
|
11042
|
+
engine: liveEngine,
|
|
11043
|
+
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
8877
11044
|
migrations: opts.migrations,
|
|
8878
11045
|
startRuntimeServices: false
|
|
8879
11046
|
});
|
|
@@ -8885,26 +11052,57 @@ async function computeDbDiff(opts) {
|
|
|
8885
11052
|
throw error;
|
|
8886
11053
|
} finally {
|
|
8887
11054
|
try {
|
|
8888
|
-
await
|
|
11055
|
+
await closeResources(unclaimedLiveEngine, live, shadow);
|
|
8889
11056
|
} catch (error) {
|
|
8890
11057
|
if (!operationFailed)
|
|
8891
11058
|
throw error;
|
|
8892
11059
|
}
|
|
8893
11060
|
}
|
|
8894
11061
|
}
|
|
11062
|
+
function shadowNativeDataDir() {
|
|
11063
|
+
return join2(mkdtempSync2(join2(tmpdir2(), "supacloud-lite-shadow-")), "pg");
|
|
11064
|
+
}
|
|
11065
|
+
async function createTemporaryNativeEngine() {
|
|
11066
|
+
const dataDir = shadowNativeDataDir();
|
|
11067
|
+
let engine;
|
|
11068
|
+
try {
|
|
11069
|
+
engine = await createNativeEngine({ dataDir });
|
|
11070
|
+
} catch (error) {
|
|
11071
|
+
try {
|
|
11072
|
+
await rm(dirname2(dataDir), { recursive: true, force: true });
|
|
11073
|
+
} catch (cleanupError) {
|
|
11074
|
+
throw new AggregateError([error, cleanupError], "temporary native database initialization cleanup failed");
|
|
11075
|
+
}
|
|
11076
|
+
throw error;
|
|
11077
|
+
}
|
|
11078
|
+
return {
|
|
11079
|
+
...engine,
|
|
11080
|
+
async close() {
|
|
11081
|
+
try {
|
|
11082
|
+
await engine.close();
|
|
11083
|
+
} finally {
|
|
11084
|
+
await rm(dirname2(dataDir), { recursive: true, force: true });
|
|
11085
|
+
}
|
|
11086
|
+
}
|
|
11087
|
+
};
|
|
11088
|
+
}
|
|
8895
11089
|
async function pullSchema(opts) {
|
|
8896
11090
|
const schema = opts.schema ?? "public";
|
|
8897
|
-
|
|
8898
|
-
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
8899
|
-
migrations: opts.migrations,
|
|
8900
|
-
startRuntimeServices: false
|
|
8901
|
-
});
|
|
11091
|
+
let shadow;
|
|
8902
11092
|
let live;
|
|
11093
|
+
let unclaimedLiveEngine = opts.liveEngine;
|
|
8903
11094
|
let operationFailed = false;
|
|
8904
11095
|
try {
|
|
11096
|
+
shadow = await createBackend({
|
|
11097
|
+
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
11098
|
+
migrations: opts.migrations,
|
|
11099
|
+
startRuntimeServices: false
|
|
11100
|
+
});
|
|
11101
|
+
const liveEngine = unclaimedLiveEngine;
|
|
11102
|
+
unclaimedLiveEngine = undefined;
|
|
8905
11103
|
live = await createBackend({
|
|
8906
|
-
engine:
|
|
8907
|
-
dataDir:
|
|
11104
|
+
engine: liveEngine,
|
|
11105
|
+
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
8908
11106
|
migrations: opts.migrations,
|
|
8909
11107
|
startRuntimeServices: false
|
|
8910
11108
|
});
|
|
@@ -8920,8 +11118,8 @@ async function pullSchema(opts) {
|
|
|
8920
11118
|
let path = null;
|
|
8921
11119
|
if (opts.migrationsDir) {
|
|
8922
11120
|
await mkdir2(opts.migrationsDir, { recursive: true });
|
|
8923
|
-
path =
|
|
8924
|
-
await
|
|
11121
|
+
path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
|
|
11122
|
+
await writeFile2(path, body);
|
|
8925
11123
|
}
|
|
8926
11124
|
await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
|
|
8927
11125
|
values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
|
|
@@ -8931,15 +11129,15 @@ async function pullSchema(opts) {
|
|
|
8931
11129
|
throw error;
|
|
8932
11130
|
} finally {
|
|
8933
11131
|
try {
|
|
8934
|
-
await
|
|
11132
|
+
await closeResources(unclaimedLiveEngine, live, shadow);
|
|
8935
11133
|
} catch (error) {
|
|
8936
11134
|
if (!operationFailed)
|
|
8937
11135
|
throw error;
|
|
8938
11136
|
}
|
|
8939
11137
|
}
|
|
8940
11138
|
}
|
|
8941
|
-
async function
|
|
8942
|
-
const results = await Promise.allSettled(
|
|
11139
|
+
async function closeResources(...resources) {
|
|
11140
|
+
const results = await Promise.allSettled(resources.filter((resource) => resource !== undefined).map(async (resource) => await resource.close()));
|
|
8943
11141
|
const failed = results.find((result) => result.status === "rejected");
|
|
8944
11142
|
if (failed?.status === "rejected")
|
|
8945
11143
|
throw failed.reason;
|
|
@@ -8947,9 +11145,9 @@ async function closeBackends(...backends) {
|
|
|
8947
11145
|
|
|
8948
11146
|
// src/runtime/node/project.ts
|
|
8949
11147
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
8950
|
-
import { join as
|
|
11148
|
+
import { join as join3 } from "path";
|
|
8951
11149
|
async function loadSupabaseProject(projectDir, seed = {}) {
|
|
8952
|
-
const migrationsDir =
|
|
11150
|
+
const migrationsDir = join3(projectDir, "supabase", "migrations");
|
|
8953
11151
|
const migrations = [];
|
|
8954
11152
|
let entries = [];
|
|
8955
11153
|
try {
|
|
@@ -8961,19 +11159,19 @@ async function loadSupabaseProject(projectDir, seed = {}) {
|
|
|
8961
11159
|
for (const entry of entries.sort()) {
|
|
8962
11160
|
if (!entry.endsWith(".sql"))
|
|
8963
11161
|
continue;
|
|
8964
|
-
const sql = await readFile2(
|
|
11162
|
+
const sql = await readFile2(join3(migrationsDir, entry), "utf8");
|
|
8965
11163
|
migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
|
|
8966
11164
|
}
|
|
8967
11165
|
let seedSql;
|
|
8968
11166
|
if (seed.enabled !== false) {
|
|
8969
11167
|
const parts = [];
|
|
8970
|
-
const supabaseDir =
|
|
11168
|
+
const supabaseDir = join3(projectDir, "supabase");
|
|
8971
11169
|
for (const configuredPath of seed.paths ?? ["seed.sql"]) {
|
|
8972
11170
|
const pattern = configuredPath.replace(/^\.\//, "");
|
|
8973
11171
|
const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
|
|
8974
11172
|
for (const relativePath of matches) {
|
|
8975
11173
|
try {
|
|
8976
|
-
parts.push(await readFile2(
|
|
11174
|
+
parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
|
|
8977
11175
|
} catch (error) {
|
|
8978
11176
|
if (!isNotFound(error))
|
|
8979
11177
|
throw error;
|
|
@@ -8991,8 +11189,8 @@ function isNotFound(error) {
|
|
|
8991
11189
|
}
|
|
8992
11190
|
|
|
8993
11191
|
// src/project-runtime.ts
|
|
8994
|
-
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as
|
|
8995
|
-
import { dirname as
|
|
11192
|
+
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
11193
|
+
import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve2 } from "path";
|
|
8996
11194
|
|
|
8997
11195
|
// src/runtime/node/bun-server.ts
|
|
8998
11196
|
async function serveBun(backend, opts = {}) {
|
|
@@ -9023,8 +11221,8 @@ async function serveBun(backend, opts = {}) {
|
|
|
9023
11221
|
}, { vsn: ws.data.vsn });
|
|
9024
11222
|
ws.data.session = session;
|
|
9025
11223
|
},
|
|
9026
|
-
message(ws,
|
|
9027
|
-
ws.data.session?.onMessage(typeof
|
|
11224
|
+
message(ws, message2) {
|
|
11225
|
+
ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
|
|
9028
11226
|
},
|
|
9029
11227
|
close(ws) {
|
|
9030
11228
|
ws.data.session?.onClose();
|
|
@@ -9042,8 +11240,8 @@ async function serveBun(backend, opts = {}) {
|
|
|
9042
11240
|
}
|
|
9043
11241
|
|
|
9044
11242
|
// src/runtime/node/fs-driver.ts
|
|
9045
|
-
import { mkdir as mkdir3, readFile as readFile3, rename, rm, writeFile as
|
|
9046
|
-
import { dirname as
|
|
11243
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
11244
|
+
import { dirname as dirname3, join as join4, normalize, sep } from "path";
|
|
9047
11245
|
|
|
9048
11246
|
class FsStorageDriver {
|
|
9049
11247
|
root;
|
|
@@ -9051,7 +11249,7 @@ class FsStorageDriver {
|
|
|
9051
11249
|
this.root = root;
|
|
9052
11250
|
}
|
|
9053
11251
|
resolve(key) {
|
|
9054
|
-
const path = normalize(
|
|
11252
|
+
const path = normalize(join4(this.root, key));
|
|
9055
11253
|
if (!path.startsWith(normalize(this.root) + sep)) {
|
|
9056
11254
|
throw new Error(`invalid storage key: ${key}`);
|
|
9057
11255
|
}
|
|
@@ -9059,13 +11257,13 @@ class FsStorageDriver {
|
|
|
9059
11257
|
}
|
|
9060
11258
|
async put(key, data) {
|
|
9061
11259
|
const path = this.resolve(key);
|
|
9062
|
-
await mkdir3(
|
|
11260
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
9063
11261
|
const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
9064
11262
|
try {
|
|
9065
|
-
await
|
|
11263
|
+
await writeFile3(temporaryPath, data);
|
|
9066
11264
|
await rename(temporaryPath, path);
|
|
9067
11265
|
} catch (error) {
|
|
9068
|
-
await
|
|
11266
|
+
await rm2(temporaryPath, { force: true }).catch(() => {});
|
|
9069
11267
|
throw error;
|
|
9070
11268
|
}
|
|
9071
11269
|
}
|
|
@@ -9078,8 +11276,12 @@ class FsStorageDriver {
|
|
|
9078
11276
|
throw e;
|
|
9079
11277
|
}
|
|
9080
11278
|
}
|
|
11279
|
+
async getBlob(key) {
|
|
11280
|
+
const file = Bun.file(this.resolve(key));
|
|
11281
|
+
return await file.exists() ? file : null;
|
|
11282
|
+
}
|
|
9081
11283
|
async delete(key) {
|
|
9082
|
-
await
|
|
11284
|
+
await rm2(this.resolve(key), { force: true });
|
|
9083
11285
|
}
|
|
9084
11286
|
async deleteMany(keys) {
|
|
9085
11287
|
for (const k of keys)
|
|
@@ -9088,15 +11290,15 @@ class FsStorageDriver {
|
|
|
9088
11290
|
}
|
|
9089
11291
|
|
|
9090
11292
|
// src/runtime/node/config-toml.ts
|
|
9091
|
-
import { readFileSync } from "fs";
|
|
9092
|
-
import { join as
|
|
11293
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
11294
|
+
import { join as join5 } from "path";
|
|
9093
11295
|
function emptyTable() {
|
|
9094
11296
|
return { values: new Map, children: new Map };
|
|
9095
11297
|
}
|
|
9096
11298
|
function loadConfigToml(projectDir, env = process.env) {
|
|
9097
11299
|
let text;
|
|
9098
11300
|
try {
|
|
9099
|
-
text =
|
|
11301
|
+
text = readFileSync2(join5(projectDir, "supabase", "config.toml"), "utf8");
|
|
9100
11302
|
} catch {
|
|
9101
11303
|
return emptyTable();
|
|
9102
11304
|
}
|
|
@@ -9461,16 +11663,16 @@ function readFunctions(root) {
|
|
|
9461
11663
|
}
|
|
9462
11664
|
|
|
9463
11665
|
// src/runtime/node/load-functions.ts
|
|
9464
|
-
import { readdir as readdir2, readFile as readFile5, realpath, rm as
|
|
9465
|
-
import { dirname as
|
|
11666
|
+
import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
|
|
11667
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
9466
11668
|
import { pathToFileURL } from "url";
|
|
9467
11669
|
|
|
9468
11670
|
// src/runtime/node/bundle-function.ts
|
|
9469
|
-
import { createHash } from "crypto";
|
|
9470
|
-
import { mkdir as mkdir4, readFile as readFile4, rm as
|
|
9471
|
-
import { existsSync } from "fs";
|
|
9472
|
-
import { tmpdir } from "os";
|
|
9473
|
-
import { join as
|
|
11671
|
+
import { createHash as createHash3 } from "crypto";
|
|
11672
|
+
import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
11673
|
+
import { existsSync as existsSync2 } from "fs";
|
|
11674
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
11675
|
+
import { join as join6 } from "path";
|
|
9474
11676
|
function rewriteRemoteSpecifier(spec) {
|
|
9475
11677
|
if (spec.startsWith("npm:"))
|
|
9476
11678
|
return `https://esm.sh/${spec.slice(4)}`;
|
|
@@ -9478,18 +11680,18 @@ function rewriteRemoteSpecifier(spec) {
|
|
|
9478
11680
|
return `https://esm.sh/jsr/${spec.slice(4)}`;
|
|
9479
11681
|
return spec;
|
|
9480
11682
|
}
|
|
9481
|
-
var HTTP_CACHE =
|
|
11683
|
+
var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
|
|
9482
11684
|
async function fetchModule(url) {
|
|
9483
|
-
const key =
|
|
9484
|
-
const cached =
|
|
9485
|
-
if (
|
|
11685
|
+
const key = createHash3("sha256").update(url).digest("hex");
|
|
11686
|
+
const cached = join6(HTTP_CACHE, key);
|
|
11687
|
+
if (existsSync2(cached))
|
|
9486
11688
|
return readFile4(cached, "utf8");
|
|
9487
11689
|
const res = await fetch(url, { redirect: "follow" });
|
|
9488
11690
|
if (!res.ok)
|
|
9489
11691
|
throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
|
|
9490
11692
|
const body = await res.text();
|
|
9491
11693
|
await mkdir4(HTTP_CACHE, { recursive: true });
|
|
9492
|
-
await
|
|
11694
|
+
await writeFile4(cached, body);
|
|
9493
11695
|
return body;
|
|
9494
11696
|
}
|
|
9495
11697
|
function remotePlugin() {
|
|
@@ -9512,7 +11714,7 @@ function remotePlugin() {
|
|
|
9512
11714
|
};
|
|
9513
11715
|
}
|
|
9514
11716
|
async function bundleFunction(entryPath, name) {
|
|
9515
|
-
const outDir =
|
|
11717
|
+
const outDir = join6(tmpdir3(), "supacloud-lite-fn-bundle", name);
|
|
9516
11718
|
await mkdir4(outDir, { recursive: true });
|
|
9517
11719
|
try {
|
|
9518
11720
|
const buildOutput = await Bun.build({
|
|
@@ -9530,7 +11732,7 @@ async function bundleFunction(entryPath, name) {
|
|
|
9530
11732
|
return buildOutput.outputs[0].path;
|
|
9531
11733
|
} catch (buildError) {
|
|
9532
11734
|
try {
|
|
9533
|
-
await
|
|
11735
|
+
await rm3(outDir, { recursive: true, force: true });
|
|
9534
11736
|
} catch (cleanupError) {
|
|
9535
11737
|
throw new AggregateError([buildError, cleanupError], `failed to clean function bundle directory ${outDir}`);
|
|
9536
11738
|
}
|
|
@@ -9542,7 +11744,7 @@ async function bundleFunction(entryPath, name) {
|
|
|
9542
11744
|
async function loadFunctionEnv(projectDir) {
|
|
9543
11745
|
let text;
|
|
9544
11746
|
try {
|
|
9545
|
-
text = await readFile5(
|
|
11747
|
+
text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
|
|
9546
11748
|
} catch {
|
|
9547
11749
|
return {};
|
|
9548
11750
|
}
|
|
@@ -9581,7 +11783,7 @@ async function loadFunctions2(projectDir, options = {}) {
|
|
|
9581
11783
|
}
|
|
9582
11784
|
async function loadFunctionsUnlocked(projectDir, options) {
|
|
9583
11785
|
const functions = new Map;
|
|
9584
|
-
const root =
|
|
11786
|
+
const root = join7(projectDir, "supabase", "functions");
|
|
9585
11787
|
let entries = [];
|
|
9586
11788
|
try {
|
|
9587
11789
|
entries = await readdir2(root);
|
|
@@ -9594,10 +11796,10 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
9594
11796
|
continue;
|
|
9595
11797
|
if (options[name]?.enabled === false)
|
|
9596
11798
|
continue;
|
|
9597
|
-
const dir =
|
|
11799
|
+
const dir = join7(root, name);
|
|
9598
11800
|
if (!(await stat(dir)).isDirectory())
|
|
9599
11801
|
continue;
|
|
9600
|
-
const candidates = options[name]?.entrypoint ? [
|
|
11802
|
+
const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
|
|
9601
11803
|
for (const path of candidates) {
|
|
9602
11804
|
try {
|
|
9603
11805
|
await stat(path);
|
|
@@ -9634,7 +11836,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
9634
11836
|
}
|
|
9635
11837
|
} finally {
|
|
9636
11838
|
if (bundledPath)
|
|
9637
|
-
await
|
|
11839
|
+
await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
|
|
9638
11840
|
}
|
|
9639
11841
|
break;
|
|
9640
11842
|
}
|
|
@@ -9708,6 +11910,21 @@ class S3StorageDriver {
|
|
|
9708
11910
|
throw error;
|
|
9709
11911
|
}
|
|
9710
11912
|
}
|
|
11913
|
+
async getBlob(key) {
|
|
11914
|
+
const file = this.client.file(this.objectKey(key));
|
|
11915
|
+
if (!await file.exists())
|
|
11916
|
+
return null;
|
|
11917
|
+
if (typeof file.arrayBuffer === "function")
|
|
11918
|
+
return file;
|
|
11919
|
+
try {
|
|
11920
|
+
const bytes = Uint8Array.from(await file.bytes());
|
|
11921
|
+
return new Blob([bytes.buffer]);
|
|
11922
|
+
} catch (error) {
|
|
11923
|
+
if (isNotFoundError(error))
|
|
11924
|
+
return null;
|
|
11925
|
+
throw error;
|
|
11926
|
+
}
|
|
11927
|
+
}
|
|
9711
11928
|
async delete(key) {
|
|
9712
11929
|
await this.client.file(this.objectKey(key)).delete();
|
|
9713
11930
|
}
|
|
@@ -9727,14 +11944,16 @@ var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets mar
|
|
|
9727
11944
|
function resolveProjectPaths(options = {}) {
|
|
9728
11945
|
const projectDir = resolve2(options.projectDir ?? process.cwd());
|
|
9729
11946
|
const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
|
|
9730
|
-
const
|
|
9731
|
-
const
|
|
11947
|
+
const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
|
|
11948
|
+
const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
|
|
11949
|
+
const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join8(stateDir, "storage"));
|
|
9732
11950
|
return {
|
|
9733
11951
|
projectDir,
|
|
9734
11952
|
stateDir,
|
|
9735
11953
|
dataDir,
|
|
9736
11954
|
storageDir,
|
|
9737
|
-
secretsFile:
|
|
11955
|
+
secretsFile: join8(stateDir, "secrets.json"),
|
|
11956
|
+
databaseEngine
|
|
9738
11957
|
};
|
|
9739
11958
|
}
|
|
9740
11959
|
async function assertResetPathsSafe(paths) {
|
|
@@ -9747,7 +11966,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
9747
11966
|
}
|
|
9748
11967
|
const canonicalStateDir = await realpath2(stateDir);
|
|
9749
11968
|
const secretsFile = resolve2(paths.secretsFile);
|
|
9750
|
-
if (secretsFile !==
|
|
11969
|
+
if (secretsFile !== join8(stateDir, "secrets.json")) {
|
|
9751
11970
|
throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
|
|
9752
11971
|
}
|
|
9753
11972
|
const markerInfo = await requiredResetEntry(secretsFile);
|
|
@@ -9760,12 +11979,12 @@ async function assertResetPathsSafe(paths) {
|
|
|
9760
11979
|
["storage", paths.storageDir]
|
|
9761
11980
|
];
|
|
9762
11981
|
for (const [label, targetPath] of targets) {
|
|
9763
|
-
const
|
|
9764
|
-
const relativePath = relative(stateDir,
|
|
11982
|
+
const target2 = resolve2(targetPath);
|
|
11983
|
+
const relativePath = relative(stateDir, target2);
|
|
9765
11984
|
if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
9766
|
-
throw new Error(`refusing to reset ${label} path outside the state directory: ${
|
|
11985
|
+
throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
|
|
9767
11986
|
}
|
|
9768
|
-
await assertResetTargetCanonical(stateDir, canonicalStateDir,
|
|
11987
|
+
await assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label);
|
|
9769
11988
|
}
|
|
9770
11989
|
}
|
|
9771
11990
|
async function requiredResetEntry(path) {
|
|
@@ -9792,8 +12011,8 @@ async function assertResetSecretsValid(path) {
|
|
|
9792
12011
|
throw new Error(RESET_INVALID_SECRETS_ERROR);
|
|
9793
12012
|
}
|
|
9794
12013
|
}
|
|
9795
|
-
async function assertResetTargetCanonical(stateDir, canonicalStateDir,
|
|
9796
|
-
let current =
|
|
12014
|
+
async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label) {
|
|
12015
|
+
let current = target2;
|
|
9797
12016
|
while (current !== stateDir) {
|
|
9798
12017
|
try {
|
|
9799
12018
|
if ((await lstat(current)).isSymbolicLink()) {
|
|
@@ -9803,20 +12022,20 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
|
|
|
9803
12022
|
if (error.code !== "ENOENT")
|
|
9804
12023
|
throw error;
|
|
9805
12024
|
}
|
|
9806
|
-
const parent =
|
|
12025
|
+
const parent = dirname5(current);
|
|
9807
12026
|
if (parent === current)
|
|
9808
|
-
throw new Error(`refusing to reset ${label} path outside the state directory: ${
|
|
12027
|
+
throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
|
|
9809
12028
|
current = parent;
|
|
9810
12029
|
}
|
|
9811
|
-
const existingAncestor = await nearestExistingAncestor(
|
|
9812
|
-
const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor,
|
|
12030
|
+
const existingAncestor = await nearestExistingAncestor(target2);
|
|
12031
|
+
const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target2));
|
|
9813
12032
|
const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
|
|
9814
12033
|
if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
|
|
9815
|
-
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${
|
|
12034
|
+
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
|
|
9816
12035
|
}
|
|
9817
12036
|
}
|
|
9818
|
-
async function nearestExistingAncestor(
|
|
9819
|
-
let current =
|
|
12037
|
+
async function nearestExistingAncestor(target2) {
|
|
12038
|
+
let current = target2;
|
|
9820
12039
|
while (true) {
|
|
9821
12040
|
try {
|
|
9822
12041
|
await lstat(current);
|
|
@@ -9825,9 +12044,9 @@ async function nearestExistingAncestor(target) {
|
|
|
9825
12044
|
if (error.code !== "ENOENT")
|
|
9826
12045
|
throw error;
|
|
9827
12046
|
}
|
|
9828
|
-
const parent =
|
|
12047
|
+
const parent = dirname5(current);
|
|
9829
12048
|
if (parent === current)
|
|
9830
|
-
throw new Error(`unable to resolve an existing ancestor for ${
|
|
12049
|
+
throw new Error(`unable to resolve an existing ancestor for ${target2}`);
|
|
9831
12050
|
current = parent;
|
|
9832
12051
|
}
|
|
9833
12052
|
}
|
|
@@ -9851,7 +12070,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
9851
12070
|
};
|
|
9852
12071
|
const temporaryFile = `${paths.secretsFile}.${crypto.randomUUID()}.tmp`;
|
|
9853
12072
|
try {
|
|
9854
|
-
await
|
|
12073
|
+
await writeFile5(temporaryFile, `${JSON.stringify(candidate, null, 2)}
|
|
9855
12074
|
`, { mode: 384, flag: "wx" });
|
|
9856
12075
|
await link(temporaryFile, paths.secretsFile);
|
|
9857
12076
|
stored = candidate;
|
|
@@ -9860,7 +12079,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
9860
12079
|
throw error2;
|
|
9861
12080
|
stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
|
|
9862
12081
|
} finally {
|
|
9863
|
-
await
|
|
12082
|
+
await unlink2(temporaryFile).catch((error2) => {
|
|
9864
12083
|
if (error2.code !== "ENOENT")
|
|
9865
12084
|
throw error2;
|
|
9866
12085
|
});
|
|
@@ -9892,42 +12111,58 @@ async function createProjectBackend(options = {}) {
|
|
|
9892
12111
|
const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
|
|
9893
12112
|
const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
|
|
9894
12113
|
const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
|
|
12114
|
+
const databaseEngine = paths.databaseEngine;
|
|
9895
12115
|
if (paths.dataDir) {
|
|
9896
12116
|
await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
|
|
9897
12117
|
await chmod(paths.dataDir, 448);
|
|
9898
12118
|
}
|
|
9899
12119
|
await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
|
|
9900
12120
|
await chmod(paths.storageDir, 448);
|
|
9901
|
-
const
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
9921
|
-
|
|
9922
|
-
|
|
9923
|
-
|
|
9924
|
-
|
|
9925
|
-
|
|
9926
|
-
|
|
9927
|
-
|
|
9928
|
-
|
|
9929
|
-
|
|
9930
|
-
|
|
12121
|
+
const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
|
|
12122
|
+
const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
|
|
12123
|
+
let backend;
|
|
12124
|
+
try {
|
|
12125
|
+
backend = await createBackend({
|
|
12126
|
+
engine,
|
|
12127
|
+
dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12128
|
+
jwtSecret: secrets.jwtSecret,
|
|
12129
|
+
vaultKey: secrets.vaultKey,
|
|
12130
|
+
apiUrl: url,
|
|
12131
|
+
siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
|
|
12132
|
+
host,
|
|
12133
|
+
jwtExpiry: config.auth.jwtExpiry,
|
|
12134
|
+
uriAllowList: config.auth.uriAllowList,
|
|
12135
|
+
authEnabled: config.auth.enabled,
|
|
12136
|
+
authSettings: config.auth.settings,
|
|
12137
|
+
authRateLimits: config.auth.rateLimits,
|
|
12138
|
+
sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
|
|
12139
|
+
sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
|
|
12140
|
+
oauthProviders: config.auth.oauthProviders,
|
|
12141
|
+
smsSender: options.smsSender,
|
|
12142
|
+
dbSchemas: config.api.schemas,
|
|
12143
|
+
maxRows: config.api.maxRows,
|
|
12144
|
+
storageFileSizeLimit: config.storage.fileSizeLimit,
|
|
12145
|
+
buckets: config.storage.buckets,
|
|
12146
|
+
migrations: options.applyMigrations === false ? [] : project.migrations,
|
|
12147
|
+
seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
|
|
12148
|
+
functions,
|
|
12149
|
+
functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
|
|
12150
|
+
functionEnv,
|
|
12151
|
+
webhooks,
|
|
12152
|
+
startRuntimeServices: options.startRuntimeServices,
|
|
12153
|
+
storageDriver,
|
|
12154
|
+
log: options.log
|
|
12155
|
+
});
|
|
12156
|
+
} catch (error) {
|
|
12157
|
+
if (engine) {
|
|
12158
|
+
try {
|
|
12159
|
+
await engine.close();
|
|
12160
|
+
} catch (cleanupError) {
|
|
12161
|
+
throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
|
|
12162
|
+
}
|
|
12163
|
+
}
|
|
12164
|
+
throw error;
|
|
12165
|
+
}
|
|
9931
12166
|
return {
|
|
9932
12167
|
backend,
|
|
9933
12168
|
config,
|
|
@@ -9938,9 +12173,22 @@ async function createProjectBackend(options = {}) {
|
|
|
9938
12173
|
migrationCount: project.migrations.length,
|
|
9939
12174
|
functionNames: [...functions.keys()],
|
|
9940
12175
|
webhookCount: webhooks.length,
|
|
9941
|
-
storageBackend
|
|
12176
|
+
storageBackend,
|
|
12177
|
+
databaseEngine
|
|
9942
12178
|
};
|
|
9943
12179
|
}
|
|
12180
|
+
function resolveDatabaseEngine(value, memory = false) {
|
|
12181
|
+
const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
|
|
12182
|
+
if (configured !== "pglite" && configured !== "native") {
|
|
12183
|
+
throw new Error(`unsupported SUPACLOUD_LITE_ENGINE: ${configured}`);
|
|
12184
|
+
}
|
|
12185
|
+
if (configured === "native" && memory)
|
|
12186
|
+
throw new Error("--memory is only supported by the pglite engine");
|
|
12187
|
+
if (configured === "native" && !isNativeEngineSupported()) {
|
|
12188
|
+
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ` + `${process.platform}/${process.arch} must use --engine pglite`);
|
|
12189
|
+
}
|
|
12190
|
+
return configured;
|
|
12191
|
+
}
|
|
9944
12192
|
function resolveStorageBackend(value) {
|
|
9945
12193
|
const configured = value ?? process.env.SUPACLOUD_LITE_STORAGE_BACKEND ?? "fs";
|
|
9946
12194
|
if (configured === "fs" || configured === "memory" || configured === "s3")
|
|
@@ -9995,7 +12243,7 @@ async function startProjectServer(options = {}) {
|
|
|
9995
12243
|
}
|
|
9996
12244
|
async function loadWebhooks(projectDir) {
|
|
9997
12245
|
try {
|
|
9998
|
-
const parsed = JSON.parse(await readFile6(
|
|
12246
|
+
const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
|
|
9999
12247
|
return Array.isArray(parsed) ? parsed : [];
|
|
10000
12248
|
} catch (error) {
|
|
10001
12249
|
if (error.code === "ENOENT")
|
|
@@ -10046,9 +12294,9 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
10046
12294
|
}
|
|
10047
12295
|
|
|
10048
12296
|
// src/snapshot.ts
|
|
10049
|
-
import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as
|
|
10050
|
-
import { dirname as
|
|
10051
|
-
import { create as createTar, extract as
|
|
12297
|
+
import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as rm5, writeFile as writeFile6 } from "fs/promises";
|
|
12298
|
+
import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
12299
|
+
import { create as createTar, extract as extractTar2 } from "tar";
|
|
10052
12300
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
10053
12301
|
var SNAPSHOT_VERSION = 1;
|
|
10054
12302
|
async function createSnapshot(options) {
|
|
@@ -10063,21 +12311,27 @@ async function createSnapshot(options) {
|
|
|
10063
12311
|
storageBackend: options.storageBackend,
|
|
10064
12312
|
includesDatabase: Boolean(paths.dataDir),
|
|
10065
12313
|
includesLocalStorage: options.storageBackend === "fs",
|
|
10066
|
-
includesSecrets: true
|
|
12314
|
+
includesSecrets: true,
|
|
12315
|
+
databaseEngine: paths.databaseEngine,
|
|
12316
|
+
...paths.databaseEngine === "native" ? {
|
|
12317
|
+
platform: process.platform,
|
|
12318
|
+
architecture: process.arch,
|
|
12319
|
+
postgresMajor: await readPostgresMajor(paths.dataDir)
|
|
12320
|
+
} : {}
|
|
10067
12321
|
};
|
|
10068
12322
|
const output = resolve3(options.output);
|
|
10069
12323
|
if (await existingInfo(output))
|
|
10070
12324
|
throw new Error(`snapshot output already exists: ${output}`);
|
|
10071
|
-
await mkdir6(
|
|
10072
|
-
const stagingRoot = await mkdtemp(
|
|
12325
|
+
await mkdir6(dirname6(output), { recursive: true });
|
|
12326
|
+
const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
|
|
10073
12327
|
try {
|
|
10074
|
-
await
|
|
12328
|
+
await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
10075
12329
|
`);
|
|
10076
|
-
await stageFile(paths.secretsFile,
|
|
12330
|
+
await stageFile(paths.secretsFile, join9(stagingRoot, "secrets.json"));
|
|
10077
12331
|
if (paths.dataDir)
|
|
10078
|
-
await stageDirectory(paths.dataDir,
|
|
12332
|
+
await stageDirectory(paths.dataDir, join9(stagingRoot, "database"));
|
|
10079
12333
|
if (options.storageBackend === "fs")
|
|
10080
|
-
await stageDirectory(paths.storageDir,
|
|
12334
|
+
await stageDirectory(paths.storageDir, join9(stagingRoot, "storage"));
|
|
10081
12335
|
const entries = ["manifest.json", "secrets.json"];
|
|
10082
12336
|
if (paths.dataDir)
|
|
10083
12337
|
entries.push("database");
|
|
@@ -10088,23 +12342,23 @@ async function createSnapshot(options) {
|
|
|
10088
12342
|
await chmod2(output, 384);
|
|
10089
12343
|
return manifest;
|
|
10090
12344
|
} catch (error) {
|
|
10091
|
-
await
|
|
12345
|
+
await rm5(output, { force: true });
|
|
10092
12346
|
throw error;
|
|
10093
12347
|
} finally {
|
|
10094
|
-
await
|
|
12348
|
+
await rm5(stagingRoot, { recursive: true, force: true });
|
|
10095
12349
|
}
|
|
10096
12350
|
}
|
|
10097
12351
|
async function restoreSnapshot(options) {
|
|
10098
12352
|
const paths = normalizePaths(options.paths);
|
|
10099
12353
|
await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
|
|
10100
12354
|
await assertNoDataDirectoryLock(paths);
|
|
10101
|
-
const stagingRoot = await mkdtemp(
|
|
10102
|
-
const payloadRoot =
|
|
12355
|
+
const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
|
|
12356
|
+
const payloadRoot = join9(stagingRoot, "payload");
|
|
10103
12357
|
const rollbackId = crypto.randomUUID();
|
|
10104
12358
|
const rollbackPaths = [];
|
|
10105
12359
|
try {
|
|
10106
12360
|
await mkdir6(payloadRoot, { recursive: true });
|
|
10107
|
-
await
|
|
12361
|
+
await extractTar2({
|
|
10108
12362
|
cwd: payloadRoot,
|
|
10109
12363
|
file: resolve3(options.input),
|
|
10110
12364
|
preserveOwner: false,
|
|
@@ -10129,6 +12383,7 @@ async function restoreSnapshot(options) {
|
|
|
10129
12383
|
if (manifest.storageBackend !== options.storageBackend) {
|
|
10130
12384
|
throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
|
|
10131
12385
|
}
|
|
12386
|
+
assertDatabaseSnapshotCompatible(manifest, paths);
|
|
10132
12387
|
if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
|
|
10133
12388
|
throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
|
|
10134
12389
|
}
|
|
@@ -10137,27 +12392,27 @@ async function restoreSnapshot(options) {
|
|
|
10137
12392
|
}
|
|
10138
12393
|
await assertSnapshotPayload(payloadRoot, manifest);
|
|
10139
12394
|
if (manifest.includesDatabase)
|
|
10140
|
-
await mkdir6(
|
|
12395
|
+
await mkdir6(join9(payloadRoot, "database"), { recursive: true });
|
|
10141
12396
|
if (manifest.includesLocalStorage)
|
|
10142
|
-
await mkdir6(
|
|
12397
|
+
await mkdir6(join9(payloadRoot, "storage"), { recursive: true });
|
|
10143
12398
|
await assertRestoreTargets(paths, manifest, options.force === true);
|
|
10144
|
-
const stateStage =
|
|
12399
|
+
const stateStage = join9(stagingRoot, "state");
|
|
10145
12400
|
await mkdir6(stateStage, { recursive: true });
|
|
10146
|
-
await copyEntry(
|
|
12401
|
+
await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
|
|
10147
12402
|
if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
|
|
10148
|
-
await copyEntry(
|
|
12403
|
+
await copyEntry(join9(payloadRoot, "database"), join9(stateStage, relative2(paths.stateDir, paths.dataDir)));
|
|
10149
12404
|
}
|
|
10150
12405
|
if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
|
|
10151
|
-
await copyEntry(
|
|
12406
|
+
await copyEntry(join9(payloadRoot, "storage"), join9(stateStage, relative2(paths.stateDir, paths.storageDir)));
|
|
10152
12407
|
}
|
|
10153
12408
|
const swaps = [];
|
|
10154
12409
|
try {
|
|
10155
12410
|
await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
|
|
10156
12411
|
if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
|
|
10157
|
-
await applyDirectorySwap(
|
|
12412
|
+
await applyDirectorySwap(join9(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
|
|
10158
12413
|
}
|
|
10159
12414
|
if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
|
|
10160
|
-
await applyDirectorySwap(
|
|
12415
|
+
await applyDirectorySwap(join9(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
|
|
10161
12416
|
}
|
|
10162
12417
|
} catch (error) {
|
|
10163
12418
|
await rollbackDirectorySwaps(swaps);
|
|
@@ -10176,7 +12431,7 @@ async function restoreSnapshot(options) {
|
|
|
10176
12431
|
} catch (error) {
|
|
10177
12432
|
throw error instanceof Error ? error : new Error(String(error));
|
|
10178
12433
|
} finally {
|
|
10179
|
-
await
|
|
12434
|
+
await rm5(stagingRoot, { recursive: true, force: true });
|
|
10180
12435
|
}
|
|
10181
12436
|
}
|
|
10182
12437
|
function normalizePaths(paths) {
|
|
@@ -10193,7 +12448,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
10193
12448
|
try {
|
|
10194
12449
|
if (paths.stateDir === parse2(paths.stateDir).root)
|
|
10195
12450
|
throw new Error("snapshot state directory must not be the filesystem root");
|
|
10196
|
-
if (paths.secretsFile !==
|
|
12451
|
+
if (paths.secretsFile !== join9(paths.stateDir, "secrets.json"))
|
|
10197
12452
|
throw new Error("snapshot secrets path must be inside the state directory");
|
|
10198
12453
|
const stateInfo = await lstat2(paths.stateDir);
|
|
10199
12454
|
if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
|
|
@@ -10251,10 +12506,10 @@ async function stageDirectory(root, destination) {
|
|
|
10251
12506
|
throw error;
|
|
10252
12507
|
}
|
|
10253
12508
|
await mkdir6(destination, { recursive: true });
|
|
10254
|
-
const walk = async (current,
|
|
12509
|
+
const walk = async (current, target2) => {
|
|
10255
12510
|
for (const entry of await readdir3(current, { withFileTypes: true })) {
|
|
10256
|
-
const fullPath =
|
|
10257
|
-
const targetPath =
|
|
12511
|
+
const fullPath = join9(current, entry.name);
|
|
12512
|
+
const targetPath = join9(target2, entry.name);
|
|
10258
12513
|
if (entry.isSymbolicLink())
|
|
10259
12514
|
throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
|
|
10260
12515
|
if (entry.isDirectory()) {
|
|
@@ -10269,14 +12524,14 @@ async function stageDirectory(root, destination) {
|
|
|
10269
12524
|
};
|
|
10270
12525
|
await walk(root, destination);
|
|
10271
12526
|
}
|
|
10272
|
-
async function stageFile(source,
|
|
10273
|
-
await mkdir6(
|
|
10274
|
-
await copyFile(source,
|
|
12527
|
+
async function stageFile(source, target2) {
|
|
12528
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12529
|
+
await copyFile(source, target2);
|
|
10275
12530
|
}
|
|
10276
12531
|
async function readManifest(payloadRoot) {
|
|
10277
12532
|
let parsed;
|
|
10278
12533
|
try {
|
|
10279
|
-
parsed = JSON.parse(await readFile7(
|
|
12534
|
+
parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
|
|
10280
12535
|
} catch (error) {
|
|
10281
12536
|
throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
|
|
10282
12537
|
}
|
|
@@ -10288,13 +12543,38 @@ function isSnapshotManifest(value) {
|
|
|
10288
12543
|
if (!value || typeof value !== "object")
|
|
10289
12544
|
return false;
|
|
10290
12545
|
const candidate = value;
|
|
10291
|
-
return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true;
|
|
12546
|
+
return candidate.format === SNAPSHOT_FORMAT && candidate.version === SNAPSHOT_VERSION && typeof candidate.createdAt === "string" && typeof candidate.packageVersion === "string" && (candidate.storageBackend === "fs" || candidate.storageBackend === "s3" || candidate.storageBackend === "memory") && typeof candidate.includesDatabase === "boolean" && typeof candidate.includesLocalStorage === "boolean" && candidate.includesSecrets === true && (candidate.databaseEngine === undefined || candidate.databaseEngine === "pglite" || candidate.databaseEngine === "native") && (candidate.platform === undefined || typeof candidate.platform === "string") && (candidate.architecture === undefined || typeof candidate.architecture === "string") && (candidate.postgresMajor === undefined || typeof candidate.postgresMajor === "string");
|
|
12547
|
+
}
|
|
12548
|
+
function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
12549
|
+
const sourceEngine = manifest.databaseEngine ?? "pglite";
|
|
12550
|
+
if (sourceEngine !== paths.databaseEngine) {
|
|
12551
|
+
throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
|
|
12552
|
+
}
|
|
12553
|
+
if (sourceEngine !== "native")
|
|
12554
|
+
return;
|
|
12555
|
+
if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
|
|
12556
|
+
throw new Error(`native PostgreSQL snapshots require the same platform and architecture; ` + `source is ${manifest.platform ?? "unknown"}/${manifest.architecture ?? "unknown"}, ` + `target is ${process.platform}/${process.arch}`);
|
|
12557
|
+
}
|
|
12558
|
+
if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
|
|
12559
|
+
throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
|
|
12560
|
+
}
|
|
12561
|
+
}
|
|
12562
|
+
async function readPostgresMajor(dataDir) {
|
|
12563
|
+
if (!dataDir)
|
|
12564
|
+
return;
|
|
12565
|
+
try {
|
|
12566
|
+
return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
|
|
12567
|
+
} catch (error) {
|
|
12568
|
+
if (error.code === "ENOENT")
|
|
12569
|
+
return;
|
|
12570
|
+
throw error;
|
|
12571
|
+
}
|
|
10292
12572
|
}
|
|
10293
12573
|
async function assertSnapshotPayload(payloadRoot, manifest) {
|
|
10294
12574
|
const required = ["manifest.json", "secrets.json"];
|
|
10295
12575
|
for (const path of required) {
|
|
10296
12576
|
try {
|
|
10297
|
-
await lstat2(
|
|
12577
|
+
await lstat2(join9(payloadRoot, path));
|
|
10298
12578
|
} catch {
|
|
10299
12579
|
throw new Error(`snapshot is missing required payload: ${path}`);
|
|
10300
12580
|
}
|
|
@@ -10317,9 +12597,9 @@ async function assertRestoreTargets(paths, manifest, force) {
|
|
|
10317
12597
|
if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
|
|
10318
12598
|
targets.push(paths.storageDir);
|
|
10319
12599
|
if (!force) {
|
|
10320
|
-
for (const
|
|
10321
|
-
if (await directoryHasEntries(
|
|
10322
|
-
throw new Error(`restore target is not empty: ${
|
|
12600
|
+
for (const target2 of targets) {
|
|
12601
|
+
if (await directoryHasEntries(target2))
|
|
12602
|
+
throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
|
|
10323
12603
|
}
|
|
10324
12604
|
}
|
|
10325
12605
|
}
|
|
@@ -10332,34 +12612,34 @@ async function directoryHasEntries(path) {
|
|
|
10332
12612
|
throw error;
|
|
10333
12613
|
}
|
|
10334
12614
|
}
|
|
10335
|
-
async function applyDirectorySwap(source,
|
|
10336
|
-
const targetInfo = await existingInfo(
|
|
12615
|
+
async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
|
|
12616
|
+
const targetInfo = await existingInfo(target2);
|
|
10337
12617
|
if (targetInfo && !targetInfo.isDirectory())
|
|
10338
|
-
throw new Error(`restore target is not a directory: ${
|
|
10339
|
-
const swap = { target };
|
|
12618
|
+
throw new Error(`restore target is not a directory: ${target2}`);
|
|
12619
|
+
const swap = { target: target2 };
|
|
10340
12620
|
if (targetInfo) {
|
|
10341
12621
|
if (!force) {
|
|
10342
|
-
if (await directoryHasEntries(
|
|
10343
|
-
throw new Error(`restore target is not empty: ${
|
|
10344
|
-
await
|
|
12622
|
+
if (await directoryHasEntries(target2))
|
|
12623
|
+
throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
|
|
12624
|
+
await rm5(target2, { recursive: true, force: true });
|
|
10345
12625
|
} else {
|
|
10346
|
-
swap.rollbackPath =
|
|
10347
|
-
await rename2(
|
|
12626
|
+
swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
|
|
12627
|
+
await rename2(target2, swap.rollbackPath);
|
|
10348
12628
|
}
|
|
10349
12629
|
}
|
|
10350
12630
|
try {
|
|
10351
|
-
await mkdir6(
|
|
10352
|
-
await rename2(source,
|
|
12631
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12632
|
+
await rename2(source, target2);
|
|
10353
12633
|
swaps.push(swap);
|
|
10354
12634
|
} catch (error) {
|
|
10355
12635
|
if (swap.rollbackPath)
|
|
10356
|
-
await rename2(swap.rollbackPath,
|
|
12636
|
+
await rename2(swap.rollbackPath, target2).catch(() => {});
|
|
10357
12637
|
throw error;
|
|
10358
12638
|
}
|
|
10359
12639
|
}
|
|
10360
12640
|
async function rollbackDirectorySwaps(swaps) {
|
|
10361
12641
|
for (const swap of [...swaps].reverse()) {
|
|
10362
|
-
await
|
|
12642
|
+
await rm5(swap.target, { recursive: true, force: true });
|
|
10363
12643
|
if (swap.rollbackPath)
|
|
10364
12644
|
await rename2(swap.rollbackPath, swap.target);
|
|
10365
12645
|
}
|
|
@@ -10373,17 +12653,17 @@ async function existingInfo(path) {
|
|
|
10373
12653
|
throw error;
|
|
10374
12654
|
}
|
|
10375
12655
|
}
|
|
10376
|
-
async function copyEntry(source,
|
|
12656
|
+
async function copyEntry(source, target2) {
|
|
10377
12657
|
const info = await lstat2(source);
|
|
10378
12658
|
if (info.isSymbolicLink())
|
|
10379
12659
|
throw new Error(`snapshot refuses symbolic link: ${source}`);
|
|
10380
12660
|
if (info.isDirectory()) {
|
|
10381
|
-
await mkdir6(
|
|
12661
|
+
await mkdir6(target2, { recursive: true });
|
|
10382
12662
|
for (const entry of await readdir3(source))
|
|
10383
|
-
await copyEntry(
|
|
12663
|
+
await copyEntry(join9(source, entry), join9(target2, entry));
|
|
10384
12664
|
} else if (info.isFile()) {
|
|
10385
|
-
await mkdir6(
|
|
10386
|
-
await Bun.write(
|
|
12665
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12666
|
+
await Bun.write(target2, Bun.file(source));
|
|
10387
12667
|
} else
|
|
10388
12668
|
throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
|
|
10389
12669
|
}
|
|
@@ -10394,7 +12674,7 @@ async function hardenRestoredTree(root) {
|
|
|
10394
12674
|
if (info.isDirectory()) {
|
|
10395
12675
|
await chmod2(root, 448);
|
|
10396
12676
|
for (const entry of await readdir3(root))
|
|
10397
|
-
await hardenRestoredTree(
|
|
12677
|
+
await hardenRestoredTree(join9(root, entry));
|
|
10398
12678
|
return;
|
|
10399
12679
|
}
|
|
10400
12680
|
if (info.isFile()) {
|
|
@@ -10405,7 +12685,7 @@ async function hardenRestoredTree(root) {
|
|
|
10405
12685
|
}
|
|
10406
12686
|
async function assertNoSymlinks(root) {
|
|
10407
12687
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
10408
|
-
const fullPath =
|
|
12688
|
+
const fullPath = join9(root, entry.name);
|
|
10409
12689
|
if (entry.isSymbolicLink())
|
|
10410
12690
|
throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
|
|
10411
12691
|
if (entry.isDirectory())
|
|
@@ -10478,6 +12758,8 @@ function parseArgs(argv) {
|
|
|
10478
12758
|
options.storageBackend = next();
|
|
10479
12759
|
else if (argument === "--s3-prefix")
|
|
10480
12760
|
options.s3 = { ...options.s3, prefix: next() };
|
|
12761
|
+
else if (argument === "--engine")
|
|
12762
|
+
options.engine = next();
|
|
10481
12763
|
else if (argument === "--memory")
|
|
10482
12764
|
options.memory = true;
|
|
10483
12765
|
else if (argument === "--output" || argument === "-o")
|
|
@@ -10540,6 +12822,7 @@ ${privilegedKey}
|
|
|
10540
12822
|
}
|
|
10541
12823
|
const project2 = await createProjectBackend({
|
|
10542
12824
|
...options,
|
|
12825
|
+
applyMigrations: false,
|
|
10543
12826
|
includeFunctions: false,
|
|
10544
12827
|
includeWebhooks: false,
|
|
10545
12828
|
startRuntimeServices: false,
|
|
@@ -10548,8 +12831,8 @@ ${privilegedKey}
|
|
|
10548
12831
|
try {
|
|
10549
12832
|
const source = await generateTypes(project2.backend.db, "public");
|
|
10550
12833
|
if (options.output) {
|
|
10551
|
-
await mkdir7(
|
|
10552
|
-
await
|
|
12834
|
+
await mkdir7(dirname7(options.output), { recursive: true });
|
|
12835
|
+
await writeFile7(options.output, source);
|
|
10553
12836
|
await writeStandardOutput(`Wrote ${options.output}
|
|
10554
12837
|
`);
|
|
10555
12838
|
} else
|
|
@@ -10562,6 +12845,7 @@ ${privilegedKey}
|
|
|
10562
12845
|
if (options.command === "inspect") {
|
|
10563
12846
|
const project2 = await createProjectBackend({
|
|
10564
12847
|
...options,
|
|
12848
|
+
applyMigrations: false,
|
|
10565
12849
|
includeFunctions: false,
|
|
10566
12850
|
includeWebhooks: false,
|
|
10567
12851
|
startRuntimeServices: false,
|
|
@@ -10597,13 +12881,13 @@ ${privilegedKey}
|
|
|
10597
12881
|
}
|
|
10598
12882
|
if (options.command !== "start")
|
|
10599
12883
|
throw new Error(`unknown command: ${options.command}`);
|
|
10600
|
-
const project = await startProjectServer({ ...options, log: (
|
|
12884
|
+
const project = await startProjectServer({ ...options, log: (message2) => console.log(` ${message2}`) });
|
|
10601
12885
|
const shutdown = waitForShutdown(() => project.close());
|
|
10602
12886
|
await writeStandardOutput(`
|
|
10603
12887
|
SupaCloud Lite running
|
|
10604
12888
|
|
|
10605
12889
|
API URL: ${project.url}
|
|
10606
|
-
Engine:
|
|
12890
|
+
Engine: ${formatDatabaseEngine(project.databaseEngine, paths.dataDir)}
|
|
10607
12891
|
Storage: ${formatStorage(project.storageBackend, paths.storageDir)}
|
|
10608
12892
|
Migrations: ${project.migrationCount} file(s)
|
|
10609
12893
|
Functions: ${project.functionNames.length ? project.functionNames.join(", ") : "none"}
|
|
@@ -10625,9 +12909,10 @@ async function runDbCommand(options) {
|
|
|
10625
12909
|
throw new Error("db reset refuses the s3 storage backend because remote objects cannot be deleted atomically");
|
|
10626
12910
|
}
|
|
10627
12911
|
await assertResetPathsSafe(paths);
|
|
12912
|
+
await assertDataDirUnlocked(paths.dataDir);
|
|
10628
12913
|
if (paths.dataDir)
|
|
10629
|
-
await
|
|
10630
|
-
await
|
|
12914
|
+
await rm6(paths.dataDir, { recursive: true, force: true });
|
|
12915
|
+
await rm6(paths.storageDir, { recursive: true, force: true });
|
|
10631
12916
|
const project2 = await createProjectBackend({
|
|
10632
12917
|
...options,
|
|
10633
12918
|
includeFunctions: false,
|
|
@@ -10646,7 +12931,13 @@ async function runDbCommand(options) {
|
|
|
10646
12931
|
}
|
|
10647
12932
|
const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
|
|
10648
12933
|
if (subcommand === "diff") {
|
|
10649
|
-
const
|
|
12934
|
+
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
|
|
12935
|
+
const ddl = await computeDbDiff({
|
|
12936
|
+
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12937
|
+
liveEngine,
|
|
12938
|
+
migrations: project.migrations,
|
|
12939
|
+
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined
|
|
12940
|
+
});
|
|
10650
12941
|
if (ddl.length === 0) {
|
|
10651
12942
|
await writeStandardError(`No schema changes found.
|
|
10652
12943
|
`);
|
|
@@ -10658,9 +12949,9 @@ async function runDbCommand(options) {
|
|
|
10658
12949
|
`;
|
|
10659
12950
|
if (options.diffFile) {
|
|
10660
12951
|
const stamp = timestamp();
|
|
10661
|
-
const output =
|
|
10662
|
-
await mkdir7(
|
|
10663
|
-
await
|
|
12952
|
+
const output = join10(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
|
|
12953
|
+
await mkdir7(join10(paths.projectDir, "supabase", "migrations"), { recursive: true });
|
|
12954
|
+
await writeFile7(output, source);
|
|
10664
12955
|
await writeStandardOutput(`Wrote ${output}
|
|
10665
12956
|
`);
|
|
10666
12957
|
} else
|
|
@@ -10668,10 +12959,13 @@ async function runDbCommand(options) {
|
|
|
10668
12959
|
return;
|
|
10669
12960
|
}
|
|
10670
12961
|
if (subcommand === "pull") {
|
|
12962
|
+
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
|
|
10671
12963
|
const result = await pullSchema({
|
|
10672
|
-
liveDataDir: paths.dataDir,
|
|
12964
|
+
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12965
|
+
liveEngine,
|
|
10673
12966
|
migrations: project.migrations,
|
|
10674
|
-
|
|
12967
|
+
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined,
|
|
12968
|
+
migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
|
|
10675
12969
|
name: options.positionals[1] ?? "remote_schema"
|
|
10676
12970
|
});
|
|
10677
12971
|
if (!result.path)
|
|
@@ -10692,7 +12986,7 @@ async function runSnapshotCommand(options) {
|
|
|
10692
12986
|
throw new Error("snapshot does not support --memory because the database is not durable");
|
|
10693
12987
|
if (subcommand === "create") {
|
|
10694
12988
|
await ensureProjectSecrets(paths);
|
|
10695
|
-
const output = options.output ??
|
|
12989
|
+
const output = options.output ?? join10(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
|
|
10696
12990
|
const manifest = await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
|
|
10697
12991
|
await writeStandardOutput(`Snapshot created: ${output}
|
|
10698
12992
|
`);
|
|
@@ -10726,7 +13020,7 @@ async function runUpgradeCommand(options) {
|
|
|
10726
13020
|
const paths = resolveProjectPaths(options);
|
|
10727
13021
|
const storageBackend = resolveStorageBackend(options.storageBackend);
|
|
10728
13022
|
await ensureProjectSecrets(paths);
|
|
10729
|
-
const output = options.output ??
|
|
13023
|
+
const output = options.output ?? join10(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
|
|
10730
13024
|
await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
|
|
10731
13025
|
await writeStandardOutput(`Pre-upgrade snapshot: ${output}
|
|
10732
13026
|
`);
|
|
@@ -10810,6 +13104,7 @@ Options:
|
|
|
10810
13104
|
--storage-dir <p> object storage directory
|
|
10811
13105
|
--storage-backend <b> fs, memory, or s3 (default fs)
|
|
10812
13106
|
--s3-prefix <p> optional key prefix for the s3 backend
|
|
13107
|
+
--engine <e> pglite (default) or native (macOS/glibc Linux x64/arm64)
|
|
10813
13108
|
--memory use an in-memory PGlite database
|
|
10814
13109
|
-o, --output <p> output file for gen types
|
|
10815
13110
|
-f, --file <name> migration suffix for db diff
|
|
@@ -10825,6 +13120,10 @@ function formatStorage(backend, storageDir) {
|
|
|
10825
13120
|
return "custom driver";
|
|
10826
13121
|
return storageDir;
|
|
10827
13122
|
}
|
|
13123
|
+
function formatDatabaseEngine(engine, dataDir) {
|
|
13124
|
+
const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
|
|
13125
|
+
return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
|
|
13126
|
+
}
|
|
10828
13127
|
var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
|
|
10829
13128
|
var exitCode = 0;
|
|
10830
13129
|
try {
|