@supacloud/lite 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -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 +2592 -347
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2463 -285
- package/dist/project-runtime.d.ts +5 -0
- package/dist/project-runtime.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/bundle-function.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/load-functions.d.ts +3 -2
- package/dist/runtime/node/load-functions.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.0",
|
|
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",
|
|
@@ -3545,41 +3549,1158 @@ $pgmq$;
|
|
|
3545
3549
|
revoke all on all functions in schema pgmq from public, anon, authenticated;
|
|
3546
3550
|
grant execute on all functions in schema pgmq to service_role;
|
|
3547
3551
|
|
|
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
|
-
|
|
3552
|
+
-- supacloud:sql-module:pgmq-public:start
|
|
3553
|
+
DO $pgmq_extension$
|
|
3554
|
+
BEGIN
|
|
3555
|
+
IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
|
|
3556
|
+
EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
|
|
3557
|
+
END IF;
|
|
3558
|
+
END
|
|
3559
|
+
$pgmq_extension$;
|
|
3560
|
+
|
|
3561
|
+
CREATE SCHEMA IF NOT EXISTS pgmq_public;
|
|
3562
|
+
GRANT USAGE ON SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3563
|
+
|
|
3564
|
+
CREATE OR REPLACE FUNCTION pgmq_public.require_public_queue(queue_name text)
|
|
3565
|
+
RETURNS text
|
|
3566
|
+
LANGUAGE plpgsql
|
|
3567
|
+
IMMUTABLE
|
|
3568
|
+
SET search_path = ''
|
|
3569
|
+
AS $$
|
|
3570
|
+
DECLARE
|
|
3571
|
+
normalized_queue_name text := lower(btrim(queue_name));
|
|
3572
|
+
BEGIN
|
|
3573
|
+
IF normalized_queue_name IS NULL
|
|
3574
|
+
OR left(normalized_queue_name, char_length('supacloud_internal_')) = 'supacloud_internal_' THEN
|
|
3575
|
+
RAISE EXCEPTION 'SUPACLOUD_QUEUE_NAME_RESERVED' USING ERRCODE = '42501';
|
|
3576
|
+
END IF;
|
|
3577
|
+
RETURN normalized_queue_name;
|
|
3578
|
+
END;
|
|
3579
|
+
$$;
|
|
3580
|
+
|
|
3581
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send(queue_name text, message jsonb, sleep_seconds integer DEFAULT 0)
|
|
3582
|
+
RETURNS SETOF bigint
|
|
3583
|
+
LANGUAGE sql
|
|
3584
|
+
VOLATILE
|
|
3585
|
+
SECURITY DEFINER
|
|
3586
|
+
SET search_path = ''
|
|
3587
|
+
AS $$ SELECT * FROM pgmq.send(pgmq_public.require_public_queue(queue_name), message, sleep_seconds); $$;
|
|
3588
|
+
|
|
3589
|
+
CREATE OR REPLACE FUNCTION pgmq_public.send_batch(queue_name text, messages jsonb[], sleep_seconds integer DEFAULT 0)
|
|
3590
|
+
RETURNS SETOF bigint
|
|
3591
|
+
LANGUAGE sql
|
|
3592
|
+
VOLATILE
|
|
3593
|
+
SECURITY DEFINER
|
|
3594
|
+
SET search_path = ''
|
|
3595
|
+
AS $$ SELECT * FROM pgmq.send_batch(pgmq_public.require_public_queue(queue_name), messages, sleep_seconds); $$;
|
|
3596
|
+
|
|
3597
|
+
CREATE OR REPLACE FUNCTION pgmq_public.read(queue_name text, sleep_seconds integer, n integer)
|
|
3598
|
+
RETURNS SETOF pgmq.message_record
|
|
3599
|
+
LANGUAGE sql
|
|
3600
|
+
VOLATILE
|
|
3601
|
+
SECURITY DEFINER
|
|
3602
|
+
SET search_path = ''
|
|
3603
|
+
AS $$ SELECT * FROM pgmq.read(pgmq_public.require_public_queue(queue_name), sleep_seconds, n); $$;
|
|
3604
|
+
|
|
3605
|
+
CREATE OR REPLACE FUNCTION pgmq_public.pop(queue_name text)
|
|
3606
|
+
RETURNS SETOF pgmq.message_record
|
|
3607
|
+
LANGUAGE sql
|
|
3608
|
+
VOLATILE
|
|
3609
|
+
SECURITY DEFINER
|
|
3610
|
+
SET search_path = ''
|
|
3611
|
+
AS $$ SELECT * FROM pgmq.pop(pgmq_public.require_public_queue(queue_name)); $$;
|
|
3612
|
+
|
|
3613
|
+
CREATE OR REPLACE FUNCTION pgmq_public.archive(queue_name text, message_id bigint)
|
|
3614
|
+
RETURNS boolean
|
|
3615
|
+
LANGUAGE sql
|
|
3616
|
+
VOLATILE
|
|
3617
|
+
SECURITY DEFINER
|
|
3618
|
+
SET search_path = ''
|
|
3619
|
+
AS $$ SELECT pgmq.archive(pgmq_public.require_public_queue(queue_name), message_id); $$;
|
|
3620
|
+
|
|
3621
|
+
CREATE OR REPLACE FUNCTION pgmq_public."delete"(queue_name text, message_id bigint)
|
|
3622
|
+
RETURNS boolean
|
|
3623
|
+
LANGUAGE sql
|
|
3624
|
+
VOLATILE
|
|
3625
|
+
SECURITY DEFINER
|
|
3626
|
+
SET search_path = ''
|
|
3627
|
+
AS $$ SELECT pgmq.delete(pgmq_public.require_public_queue(queue_name), message_id); $$;
|
|
3628
|
+
|
|
3629
|
+
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA pgmq_public FROM PUBLIC;
|
|
3630
|
+
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA pgmq_public TO anon, authenticated, service_role;
|
|
3631
|
+
-- supacloud:sql-module:pgmq-public:end
|
|
3632
|
+
`;
|
|
3633
|
+
var WORKFLOWS_SQL = `
|
|
3634
|
+
-- supacloud:sql-module:workflows-public:start
|
|
3635
|
+
DO $pgmq_extension$
|
|
3636
|
+
BEGIN
|
|
3637
|
+
IF to_regprocedure('pgmq.send(text,jsonb,integer)') IS NULL THEN
|
|
3638
|
+
EXECUTE 'CREATE EXTENSION IF NOT EXISTS pgmq';
|
|
3639
|
+
END IF;
|
|
3640
|
+
END
|
|
3641
|
+
$pgmq_extension$;
|
|
3642
|
+
CREATE SCHEMA IF NOT EXISTS supacloud_workflows;
|
|
3643
|
+
REVOKE ALL ON SCHEMA supacloud_workflows FROM PUBLIC, anon, authenticated;
|
|
3644
|
+
GRANT USAGE ON SCHEMA supacloud_workflows TO service_role;
|
|
3645
|
+
|
|
3646
|
+
SELECT pgmq.create('supacloud_internal_workflows');
|
|
3647
|
+
|
|
3648
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.runs (
|
|
3649
|
+
id uuid PRIMARY KEY,
|
|
3650
|
+
workflow_name text NOT NULL
|
|
3651
|
+
CHECK (workflow_name ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
|
|
3652
|
+
workflow_version text NOT NULL
|
|
3653
|
+
CHECK (char_length(workflow_version) BETWEEN 1 AND 80),
|
|
3654
|
+
status text NOT NULL DEFAULT 'queued'
|
|
3655
|
+
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'cancelled')),
|
|
3656
|
+
input jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3657
|
+
CHECK (jsonb_typeof(input) = 'object'),
|
|
3658
|
+
output jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3659
|
+
CHECK (jsonb_typeof(output) = 'object'),
|
|
3660
|
+
error_message text NOT NULL DEFAULT ''
|
|
3661
|
+
CHECK (char_length(error_message) <= 4000),
|
|
3662
|
+
row_version bigint NOT NULL DEFAULT 1 CHECK (row_version > 0),
|
|
3663
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3664
|
+
started_at timestamptz,
|
|
3665
|
+
completed_at timestamptz,
|
|
3666
|
+
updated_at timestamptz NOT NULL DEFAULT now()
|
|
3667
|
+
);
|
|
3668
|
+
|
|
3669
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.steps (
|
|
3670
|
+
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
3671
|
+
run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
|
|
3672
|
+
step_key text NOT NULL
|
|
3673
|
+
CHECK (step_key ~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'),
|
|
3674
|
+
status text NOT NULL DEFAULT 'queued'
|
|
3675
|
+
CHECK (status IN ('queued', 'running', 'completed', 'failed', 'dead_lettered', 'cancelled')),
|
|
3676
|
+
input jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3677
|
+
CHECK (jsonb_typeof(input) = 'object'),
|
|
3678
|
+
output jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3679
|
+
CHECK (jsonb_typeof(output) = 'object'),
|
|
3680
|
+
error_message text NOT NULL DEFAULT ''
|
|
3681
|
+
CHECK (char_length(error_message) <= 4000),
|
|
3682
|
+
attempts integer NOT NULL DEFAULT 0 CHECK (attempts >= 0),
|
|
3683
|
+
max_attempts integer NOT NULL DEFAULT 3 CHECK (max_attempts BETWEEN 1 AND 100),
|
|
3684
|
+
retry_delay_seconds integer NOT NULL DEFAULT 0
|
|
3685
|
+
CHECK (retry_delay_seconds BETWEEN 0 AND 86400),
|
|
3686
|
+
queue_message_id bigint NOT NULL UNIQUE,
|
|
3687
|
+
claimed_by text,
|
|
3688
|
+
claimed_at timestamptz,
|
|
3689
|
+
completed_at timestamptz,
|
|
3690
|
+
next_step_key text,
|
|
3691
|
+
created_at timestamptz NOT NULL DEFAULT now(),
|
|
3692
|
+
updated_at timestamptz NOT NULL DEFAULT now(),
|
|
3693
|
+
UNIQUE (run_id, step_key)
|
|
3694
|
+
);
|
|
3695
|
+
|
|
3696
|
+
ALTER TABLE supacloud_workflows.steps
|
|
3697
|
+
ADD COLUMN IF NOT EXISTS retry_delay_seconds integer NOT NULL DEFAULT 0
|
|
3698
|
+
CHECK (retry_delay_seconds BETWEEN 0 AND 86400);
|
|
3699
|
+
|
|
3700
|
+
CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_one_active_step_idx
|
|
3701
|
+
ON supacloud_workflows.steps (run_id)
|
|
3702
|
+
WHERE status IN ('queued', 'running');
|
|
3703
|
+
|
|
3704
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_runs_status_idx
|
|
3705
|
+
ON supacloud_workflows.runs (status, updated_at DESC, id);
|
|
3706
|
+
|
|
3707
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_steps_run_idx
|
|
3708
|
+
ON supacloud_workflows.steps (run_id, created_at, id);
|
|
3709
|
+
|
|
3710
|
+
CREATE TABLE IF NOT EXISTS supacloud_workflows.events (
|
|
3711
|
+
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
3712
|
+
run_id uuid NOT NULL REFERENCES supacloud_workflows.runs(id) ON DELETE CASCADE,
|
|
3713
|
+
step_id uuid REFERENCES supacloud_workflows.steps(id) ON DELETE CASCADE,
|
|
3714
|
+
event_type text NOT NULL
|
|
3715
|
+
CHECK (event_type IN (
|
|
3716
|
+
'run_started', 'step_claimed', 'step_retried', 'step_completed',
|
|
3717
|
+
'step_failed', 'step_dead_lettered', 'run_completed', 'run_cancelled'
|
|
3718
|
+
)),
|
|
3719
|
+
attempt integer CHECK (attempt IS NULL OR attempt > 0),
|
|
3720
|
+
details jsonb NOT NULL DEFAULT '{}'::jsonb
|
|
3721
|
+
CHECK (jsonb_typeof(details) = 'object'),
|
|
3722
|
+
created_at timestamptz NOT NULL DEFAULT now()
|
|
3723
|
+
);
|
|
3724
|
+
|
|
3725
|
+
CREATE INDEX IF NOT EXISTS supacloud_workflows_events_run_idx
|
|
3726
|
+
ON supacloud_workflows.events (run_id, id);
|
|
3727
|
+
|
|
3728
|
+
CREATE UNIQUE INDEX IF NOT EXISTS supacloud_workflows_retry_receipt_idx
|
|
3729
|
+
ON supacloud_workflows.events (step_id, attempt)
|
|
3730
|
+
WHERE event_type IN ('step_retried', 'step_dead_lettered')
|
|
3731
|
+
AND details ->> 'operation' = 'retry';
|
|
3732
|
+
|
|
3733
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.snapshot(
|
|
3734
|
+
p_run_id uuid,
|
|
3735
|
+
p_idempotent boolean DEFAULT false
|
|
3736
|
+
) RETURNS jsonb
|
|
3737
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
3738
|
+
SELECT jsonb_build_object(
|
|
3739
|
+
'runId', run.id,
|
|
3740
|
+
'workflowName', run.workflow_name,
|
|
3741
|
+
'workflowVersion', run.workflow_version,
|
|
3742
|
+
'status', run.status,
|
|
3743
|
+
'input', run.input,
|
|
3744
|
+
'output', run.output,
|
|
3745
|
+
'errorMessage', run.error_message,
|
|
3746
|
+
'rowVersion', run.row_version::text,
|
|
3747
|
+
'createdAt', run.created_at,
|
|
3748
|
+
'startedAt', run.started_at,
|
|
3749
|
+
'completedAt', run.completed_at,
|
|
3750
|
+
'updatedAt', run.updated_at,
|
|
3751
|
+
'idempotent', p_idempotent,
|
|
3752
|
+
'steps', coalesce((
|
|
3753
|
+
SELECT jsonb_agg(jsonb_build_object(
|
|
3754
|
+
'stepId', step.id,
|
|
3755
|
+
'stepKey', step.step_key,
|
|
3756
|
+
'status', step.status,
|
|
3757
|
+
'input', step.input,
|
|
3758
|
+
'output', step.output,
|
|
3759
|
+
'errorMessage', step.error_message,
|
|
3760
|
+
'attempts', step.attempts,
|
|
3761
|
+
'maxAttempts', step.max_attempts,
|
|
3762
|
+
'retryDelaySeconds', step.retry_delay_seconds,
|
|
3763
|
+
'queueMessageId', step.queue_message_id::text,
|
|
3764
|
+
'claimedBy', step.claimed_by,
|
|
3765
|
+
'claimedAt', step.claimed_at,
|
|
3766
|
+
'completedAt', step.completed_at,
|
|
3767
|
+
'nextStepKey', step.next_step_key,
|
|
3768
|
+
'createdAt', step.created_at,
|
|
3769
|
+
'updatedAt', step.updated_at
|
|
3770
|
+
) ORDER BY step.created_at, step.id)
|
|
3771
|
+
FROM supacloud_workflows.steps step
|
|
3772
|
+
WHERE step.run_id = run.id
|
|
3773
|
+
), '[]'::jsonb)
|
|
3774
|
+
)
|
|
3775
|
+
FROM supacloud_workflows.runs run
|
|
3776
|
+
WHERE run.id = p_run_id
|
|
3777
|
+
$$;
|
|
3778
|
+
|
|
3779
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.enqueue_step(
|
|
3780
|
+
p_run_id uuid,
|
|
3781
|
+
p_step_key text,
|
|
3782
|
+
p_input jsonb,
|
|
3783
|
+
p_max_attempts integer
|
|
3784
|
+
) RETURNS supacloud_workflows.steps
|
|
3785
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3786
|
+
DECLARE
|
|
3787
|
+
normalized_step_key text := nullif(btrim(p_step_key), '');
|
|
3788
|
+
step_id uuid := gen_random_uuid();
|
|
3789
|
+
message_id bigint;
|
|
3790
|
+
created_step supacloud_workflows.steps%ROWTYPE;
|
|
3791
|
+
BEGIN
|
|
3792
|
+
IF normalized_step_key IS NULL
|
|
3793
|
+
OR normalized_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3794
|
+
OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
|
|
3795
|
+
OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
3796
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_INVALID' USING ERRCODE = '22023';
|
|
3797
|
+
END IF;
|
|
3798
|
+
|
|
3799
|
+
IF NOT EXISTS (
|
|
3800
|
+
SELECT 1 FROM supacloud_workflows.runs run
|
|
3801
|
+
WHERE run.id = p_run_id AND run.status IN ('queued', 'running')
|
|
3802
|
+
) THEN
|
|
3803
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
3804
|
+
END IF;
|
|
3805
|
+
|
|
3806
|
+
SELECT queued_id INTO message_id
|
|
3807
|
+
FROM pgmq.send(
|
|
3808
|
+
'supacloud_internal_workflows',
|
|
3809
|
+
jsonb_build_object('run_id', p_run_id, 'step_id', step_id),
|
|
3810
|
+
0
|
|
3811
|
+
) AS queued_id;
|
|
3812
|
+
|
|
3813
|
+
INSERT INTO supacloud_workflows.steps (
|
|
3814
|
+
id, run_id, step_key, input, max_attempts, queue_message_id
|
|
3815
|
+
) VALUES (
|
|
3816
|
+
step_id, p_run_id, normalized_step_key, p_input, p_max_attempts, message_id
|
|
3817
|
+
) RETURNING * INTO created_step;
|
|
3818
|
+
|
|
3819
|
+
RETURN created_step;
|
|
3820
|
+
END;
|
|
3821
|
+
$$;
|
|
3822
|
+
|
|
3823
|
+
-- Clean-code exception: private transitions keep typed PostgreSQL arguments and
|
|
3824
|
+
-- the complete lock/queue/ledger/event mutation in one transaction. The public
|
|
3825
|
+
-- contract already uses one JSON request; revisit if a private routine gains a
|
|
3826
|
+
-- second caller or any transition can be decomposed without weakening atomicity.
|
|
3827
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.start_run(
|
|
3828
|
+
p_run_id uuid,
|
|
3829
|
+
p_workflow_name text,
|
|
3830
|
+
p_workflow_version text,
|
|
3831
|
+
p_first_step_key text,
|
|
3832
|
+
p_input jsonb,
|
|
3833
|
+
p_max_attempts integer
|
|
3834
|
+
) RETURNS jsonb
|
|
3835
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3836
|
+
DECLARE
|
|
3837
|
+
normalized_name text := nullif(btrim(p_workflow_name), '');
|
|
3838
|
+
normalized_version text := nullif(btrim(p_workflow_version), '');
|
|
3839
|
+
normalized_first_step_key text := nullif(btrim(p_first_step_key), '');
|
|
3840
|
+
existing_run supacloud_workflows.runs%ROWTYPE;
|
|
3841
|
+
existing_step supacloud_workflows.steps%ROWTYPE;
|
|
3842
|
+
first_step supacloud_workflows.steps%ROWTYPE;
|
|
3843
|
+
BEGIN
|
|
3844
|
+
IF p_run_id IS NULL
|
|
3845
|
+
OR normalized_name IS NULL
|
|
3846
|
+
OR normalized_name !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3847
|
+
OR normalized_version IS NULL
|
|
3848
|
+
OR char_length(normalized_version) > 80
|
|
3849
|
+
OR normalized_first_step_key IS NULL
|
|
3850
|
+
OR normalized_first_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
3851
|
+
OR jsonb_typeof(p_input) IS DISTINCT FROM 'object'
|
|
3852
|
+
OR p_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
3853
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
|
|
3854
|
+
END IF;
|
|
3855
|
+
|
|
3856
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
3857
|
+
SELECT * INTO existing_run FROM supacloud_workflows.runs WHERE id = p_run_id;
|
|
3858
|
+
IF FOUND THEN
|
|
3859
|
+
SELECT * INTO existing_step
|
|
3860
|
+
FROM supacloud_workflows.steps
|
|
3861
|
+
WHERE run_id = p_run_id
|
|
3862
|
+
ORDER BY created_at, id
|
|
3863
|
+
LIMIT 1;
|
|
3864
|
+
IF NOT FOUND THEN
|
|
3865
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
3866
|
+
END IF;
|
|
3867
|
+
IF existing_run.workflow_name <> normalized_name
|
|
3868
|
+
OR existing_run.workflow_version <> normalized_version
|
|
3869
|
+
OR existing_run.input <> p_input
|
|
3870
|
+
OR existing_step.step_key <> normalized_first_step_key
|
|
3871
|
+
OR existing_step.input <> p_input
|
|
3872
|
+
OR existing_step.max_attempts <> p_max_attempts THEN
|
|
3873
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
3874
|
+
END IF;
|
|
3875
|
+
RETURN supacloud_workflows.snapshot(p_run_id, true);
|
|
3876
|
+
END IF;
|
|
3877
|
+
|
|
3878
|
+
INSERT INTO supacloud_workflows.runs (
|
|
3879
|
+
id, workflow_name, workflow_version, input
|
|
3880
|
+
) VALUES (
|
|
3881
|
+
p_run_id, normalized_name, normalized_version, p_input
|
|
3882
|
+
);
|
|
3883
|
+
first_step := supacloud_workflows.enqueue_step(
|
|
3884
|
+
p_run_id, normalized_first_step_key, p_input, p_max_attempts
|
|
3885
|
+
);
|
|
3886
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type)
|
|
3887
|
+
VALUES (p_run_id, first_step.id, 'run_started');
|
|
3888
|
+
RETURN supacloud_workflows.snapshot(p_run_id, false);
|
|
3889
|
+
END;
|
|
3890
|
+
$$;
|
|
3891
|
+
|
|
3892
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.claim_step(
|
|
3893
|
+
p_worker_id text,
|
|
3894
|
+
p_visibility_timeout_seconds integer
|
|
3895
|
+
) RETURNS jsonb
|
|
3896
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
3897
|
+
DECLARE
|
|
3898
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
3899
|
+
queued_message pgmq.message_record;
|
|
3900
|
+
message_run_id text;
|
|
3901
|
+
message_step_id text;
|
|
3902
|
+
candidate_run_id uuid;
|
|
3903
|
+
claimed_step supacloud_workflows.steps%ROWTYPE;
|
|
3904
|
+
claimed_run supacloud_workflows.runs%ROWTYPE;
|
|
3905
|
+
BEGIN
|
|
3906
|
+
IF normalized_worker_id IS NULL
|
|
3907
|
+
OR char_length(normalized_worker_id) > 200
|
|
3908
|
+
OR p_visibility_timeout_seconds NOT BETWEEN 15 AND 3600 THEN
|
|
3909
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
3910
|
+
END IF;
|
|
3911
|
+
|
|
3912
|
+
SELECT * INTO queued_message
|
|
3913
|
+
FROM pgmq.read('supacloud_internal_workflows', p_visibility_timeout_seconds, 1)
|
|
3914
|
+
LIMIT 1;
|
|
3915
|
+
IF NOT FOUND THEN RETURN NULL; END IF;
|
|
3916
|
+
|
|
3917
|
+
message_run_id := queued_message.message ->> 'run_id';
|
|
3918
|
+
message_step_id := queued_message.message ->> 'step_id';
|
|
3919
|
+
IF jsonb_typeof(queued_message.message) IS DISTINCT FROM 'object'
|
|
3920
|
+
OR message_run_id IS NULL
|
|
3921
|
+
OR message_step_id IS NULL
|
|
3922
|
+
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}$'
|
|
3923
|
+
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
|
|
3924
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3925
|
+
RETURN jsonb_build_object(
|
|
3926
|
+
'status', 'discarded',
|
|
3927
|
+
'reason', 'invalid_message',
|
|
3928
|
+
'messageId', queued_message.msg_id::text
|
|
3929
|
+
);
|
|
3930
|
+
END IF;
|
|
3931
|
+
|
|
3932
|
+
SELECT step.run_id INTO candidate_run_id
|
|
3933
|
+
FROM supacloud_workflows.steps step
|
|
3934
|
+
WHERE step.id::text = lower(message_step_id)
|
|
3935
|
+
AND step.run_id::text = lower(message_run_id)
|
|
3936
|
+
AND step.queue_message_id = queued_message.msg_id;
|
|
3937
|
+
IF NOT FOUND THEN
|
|
3938
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3939
|
+
RETURN jsonb_build_object(
|
|
3940
|
+
'status', 'discarded',
|
|
3941
|
+
'reason', 'orphaned_message',
|
|
3942
|
+
'messageId', queued_message.msg_id::text
|
|
3943
|
+
);
|
|
3944
|
+
END IF;
|
|
3945
|
+
|
|
3946
|
+
IF NOT pg_try_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0)) THEN
|
|
3947
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_RETRY' USING ERRCODE = '40001';
|
|
3948
|
+
END IF;
|
|
3949
|
+
SELECT * INTO claimed_step
|
|
3950
|
+
FROM supacloud_workflows.steps
|
|
3951
|
+
WHERE id::text = lower(message_step_id)
|
|
3952
|
+
AND run_id = candidate_run_id
|
|
3953
|
+
AND queue_message_id = queued_message.msg_id
|
|
3954
|
+
FOR UPDATE;
|
|
3955
|
+
IF NOT FOUND THEN
|
|
3956
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3957
|
+
RETURN jsonb_build_object(
|
|
3958
|
+
'status', 'discarded',
|
|
3959
|
+
'reason', 'orphaned_message',
|
|
3960
|
+
'messageId', queued_message.msg_id::text
|
|
3961
|
+
);
|
|
3962
|
+
END IF;
|
|
3963
|
+
|
|
3964
|
+
SELECT * INTO claimed_run
|
|
3965
|
+
FROM supacloud_workflows.runs
|
|
3966
|
+
WHERE id = claimed_step.run_id
|
|
3967
|
+
FOR UPDATE;
|
|
3968
|
+
IF claimed_run.status NOT IN ('queued', 'running')
|
|
3969
|
+
OR claimed_step.status NOT IN ('queued', 'running') THEN
|
|
3970
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3971
|
+
RETURN jsonb_build_object(
|
|
3972
|
+
'status', 'discarded',
|
|
3973
|
+
'reason', 'step_not_claimable',
|
|
3974
|
+
'runId', claimed_step.run_id,
|
|
3975
|
+
'stepId', claimed_step.id,
|
|
3976
|
+
'messageId', queued_message.msg_id::text
|
|
3977
|
+
);
|
|
3978
|
+
END IF;
|
|
3979
|
+
|
|
3980
|
+
IF queued_message.read_ct > claimed_step.max_attempts THEN
|
|
3981
|
+
UPDATE supacloud_workflows.steps
|
|
3982
|
+
SET status = 'dead_lettered', attempts = queued_message.read_ct,
|
|
3983
|
+
error_message = 'maximum attempts exceeded', completed_at = now(), updated_at = now()
|
|
3984
|
+
WHERE id = claimed_step.id;
|
|
3985
|
+
UPDATE supacloud_workflows.runs
|
|
3986
|
+
SET status = 'failed', error_message = 'maximum attempts exceeded',
|
|
3987
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
3988
|
+
WHERE id = claimed_step.run_id;
|
|
3989
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', queued_message.msg_id);
|
|
3990
|
+
INSERT INTO supacloud_workflows.events (
|
|
3991
|
+
run_id, step_id, event_type, attempt, details
|
|
3992
|
+
) VALUES (
|
|
3993
|
+
claimed_step.run_id, claimed_step.id, 'step_dead_lettered', queued_message.read_ct,
|
|
3994
|
+
jsonb_build_object('errorMessage', 'maximum attempts exceeded')
|
|
3995
|
+
);
|
|
3996
|
+
RETURN jsonb_build_object(
|
|
3997
|
+
'status', 'dead_lettered',
|
|
3998
|
+
'runId', claimed_step.run_id,
|
|
3999
|
+
'stepId', claimed_step.id,
|
|
4000
|
+
'stepKey', claimed_step.step_key,
|
|
4001
|
+
'messageId', queued_message.msg_id::text,
|
|
4002
|
+
'attempt', queued_message.read_ct,
|
|
4003
|
+
'maxAttempts', claimed_step.max_attempts
|
|
4004
|
+
);
|
|
4005
|
+
END IF;
|
|
4006
|
+
|
|
4007
|
+
UPDATE supacloud_workflows.steps
|
|
4008
|
+
SET status = 'running', attempts = queued_message.read_ct,
|
|
4009
|
+
retry_delay_seconds = 0, claimed_by = normalized_worker_id,
|
|
4010
|
+
claimed_at = now(), updated_at = now()
|
|
4011
|
+
WHERE id = claimed_step.id;
|
|
4012
|
+
UPDATE supacloud_workflows.runs
|
|
4013
|
+
SET status = 'running', started_at = coalesce(started_at, now()),
|
|
4014
|
+
updated_at = now(), row_version = row_version + 1
|
|
4015
|
+
WHERE id = claimed_step.run_id;
|
|
4016
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt, details)
|
|
4017
|
+
VALUES (
|
|
4018
|
+
claimed_step.run_id, claimed_step.id, 'step_claimed', queued_message.read_ct,
|
|
4019
|
+
jsonb_build_object('workerId', normalized_worker_id)
|
|
4020
|
+
);
|
|
4021
|
+
|
|
4022
|
+
RETURN jsonb_build_object(
|
|
4023
|
+
'status', 'claimed',
|
|
4024
|
+
'runId', claimed_step.run_id,
|
|
4025
|
+
'workflowName', claimed_run.workflow_name,
|
|
4026
|
+
'workflowVersion', claimed_run.workflow_version,
|
|
4027
|
+
'stepId', claimed_step.id,
|
|
4028
|
+
'stepKey', claimed_step.step_key,
|
|
4029
|
+
'input', claimed_step.input,
|
|
4030
|
+
'messageId', queued_message.msg_id::text,
|
|
4031
|
+
'attempt', queued_message.read_ct,
|
|
4032
|
+
'maxAttempts', claimed_step.max_attempts,
|
|
4033
|
+
'workerId', normalized_worker_id
|
|
4034
|
+
);
|
|
4035
|
+
END;
|
|
4036
|
+
$$;
|
|
4037
|
+
|
|
4038
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step(
|
|
4039
|
+
p_step_id uuid
|
|
4040
|
+
) RETURNS supacloud_workflows.steps
|
|
4041
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4042
|
+
DECLARE
|
|
4043
|
+
candidate_run_id uuid;
|
|
4044
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4045
|
+
BEGIN
|
|
4046
|
+
SELECT step.run_id INTO candidate_run_id
|
|
4047
|
+
FROM supacloud_workflows.steps step
|
|
4048
|
+
WHERE step.id = p_step_id;
|
|
4049
|
+
IF NOT FOUND THEN
|
|
4050
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4051
|
+
END IF;
|
|
4052
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(candidate_run_id::text, 0));
|
|
4053
|
+
SELECT * INTO active_step
|
|
4054
|
+
FROM supacloud_workflows.steps
|
|
4055
|
+
WHERE id = p_step_id
|
|
4056
|
+
FOR UPDATE;
|
|
4057
|
+
IF NOT FOUND THEN
|
|
4058
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STEP_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4059
|
+
END IF;
|
|
4060
|
+
RETURN active_step;
|
|
4061
|
+
END;
|
|
4062
|
+
$$;
|
|
4063
|
+
|
|
4064
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.lock_step_attempt(
|
|
4065
|
+
p_step_id uuid,
|
|
4066
|
+
p_message_id bigint,
|
|
4067
|
+
p_attempt integer,
|
|
4068
|
+
p_worker_id text
|
|
4069
|
+
) RETURNS supacloud_workflows.steps
|
|
4070
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4071
|
+
DECLARE
|
|
4072
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4073
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
4074
|
+
BEGIN
|
|
4075
|
+
active_step := supacloud_workflows.lock_step(p_step_id);
|
|
4076
|
+
IF normalized_worker_id IS NULL
|
|
4077
|
+
OR p_message_id IS NULL
|
|
4078
|
+
OR p_attempt IS NULL
|
|
4079
|
+
OR active_step.queue_message_id IS DISTINCT FROM p_message_id
|
|
4080
|
+
OR active_step.attempts IS DISTINCT FROM p_attempt
|
|
4081
|
+
OR active_step.claimed_by IS DISTINCT FROM normalized_worker_id THEN
|
|
4082
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4083
|
+
END IF;
|
|
4084
|
+
RETURN active_step;
|
|
4085
|
+
END;
|
|
4086
|
+
$$;
|
|
4087
|
+
|
|
4088
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.advance_step(
|
|
4089
|
+
p_step_id uuid,
|
|
4090
|
+
p_message_id bigint,
|
|
4091
|
+
p_attempt integer,
|
|
4092
|
+
p_worker_id text,
|
|
4093
|
+
p_output jsonb,
|
|
4094
|
+
p_next_step_key text,
|
|
4095
|
+
p_next_input jsonb,
|
|
4096
|
+
p_next_max_attempts integer
|
|
4097
|
+
) RETURNS jsonb
|
|
4098
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4099
|
+
DECLARE
|
|
4100
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4101
|
+
next_step supacloud_workflows.steps%ROWTYPE;
|
|
4102
|
+
normalized_next_step_key text := nullif(btrim(p_next_step_key), '');
|
|
4103
|
+
archived boolean;
|
|
4104
|
+
BEGIN
|
|
4105
|
+
IF jsonb_typeof(p_output) IS DISTINCT FROM 'object'
|
|
4106
|
+
OR jsonb_typeof(p_next_input) IS DISTINCT FROM 'object'
|
|
4107
|
+
OR normalized_next_step_key IS NULL
|
|
4108
|
+
OR normalized_next_step_key !~ '^[A-Za-z0-9][A-Za-z0-9._:-]{0,119}$'
|
|
4109
|
+
OR p_next_max_attempts NOT BETWEEN 1 AND 100 THEN
|
|
4110
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4111
|
+
END IF;
|
|
4112
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4113
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4114
|
+
);
|
|
4115
|
+
|
|
4116
|
+
IF current_step.status = 'completed' THEN
|
|
4117
|
+
SELECT * INTO next_step
|
|
4118
|
+
FROM supacloud_workflows.steps
|
|
4119
|
+
WHERE run_id = current_step.run_id AND step_key = normalized_next_step_key;
|
|
4120
|
+
IF NOT FOUND
|
|
4121
|
+
OR current_step.output IS DISTINCT FROM p_output
|
|
4122
|
+
OR current_step.next_step_key IS DISTINCT FROM normalized_next_step_key
|
|
4123
|
+
OR next_step.input IS DISTINCT FROM p_next_input
|
|
4124
|
+
OR next_step.max_attempts IS DISTINCT FROM p_next_max_attempts THEN
|
|
4125
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4126
|
+
END IF;
|
|
4127
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4128
|
+
END IF;
|
|
4129
|
+
IF current_step.status <> 'running' THEN
|
|
4130
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4131
|
+
END IF;
|
|
4132
|
+
|
|
4133
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4134
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4135
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4136
|
+
END IF;
|
|
4137
|
+
UPDATE supacloud_workflows.steps
|
|
4138
|
+
SET status = 'completed', output = p_output, error_message = '',
|
|
4139
|
+
completed_at = now(), next_step_key = normalized_next_step_key, updated_at = now()
|
|
4140
|
+
WHERE id = current_step.id;
|
|
4141
|
+
INSERT INTO supacloud_workflows.events (
|
|
4142
|
+
run_id, step_id, event_type, attempt, details
|
|
4143
|
+
) VALUES (
|
|
4144
|
+
current_step.run_id, current_step.id, 'step_completed', p_attempt,
|
|
4145
|
+
jsonb_build_object('nextStepKey', normalized_next_step_key)
|
|
4146
|
+
);
|
|
4147
|
+
next_step := supacloud_workflows.enqueue_step(
|
|
4148
|
+
current_step.run_id, normalized_next_step_key, p_next_input, p_next_max_attempts
|
|
4149
|
+
);
|
|
4150
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4151
|
+
END;
|
|
4152
|
+
$$;
|
|
4153
|
+
|
|
4154
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.complete_run(
|
|
4155
|
+
p_step_id uuid,
|
|
4156
|
+
p_message_id bigint,
|
|
4157
|
+
p_attempt integer,
|
|
4158
|
+
p_worker_id text,
|
|
4159
|
+
p_step_output jsonb,
|
|
4160
|
+
p_run_output jsonb
|
|
4161
|
+
) RETURNS jsonb
|
|
4162
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4163
|
+
DECLARE
|
|
4164
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4165
|
+
current_run supacloud_workflows.runs%ROWTYPE;
|
|
4166
|
+
archived boolean;
|
|
4167
|
+
BEGIN
|
|
4168
|
+
IF jsonb_typeof(p_step_output) IS DISTINCT FROM 'object'
|
|
4169
|
+
OR jsonb_typeof(p_run_output) IS DISTINCT FROM 'object' THEN
|
|
4170
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4171
|
+
END IF;
|
|
4172
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4173
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4174
|
+
);
|
|
4175
|
+
SELECT * INTO current_run
|
|
4176
|
+
FROM supacloud_workflows.runs
|
|
4177
|
+
WHERE id = current_step.run_id
|
|
4178
|
+
FOR UPDATE;
|
|
4179
|
+
|
|
4180
|
+
IF current_step.status = 'completed' THEN
|
|
4181
|
+
IF current_step.next_step_key IS NOT NULL
|
|
4182
|
+
OR current_step.output IS DISTINCT FROM p_step_output
|
|
4183
|
+
OR current_run.status IS DISTINCT FROM 'completed'
|
|
4184
|
+
OR current_run.output IS DISTINCT FROM p_run_output THEN
|
|
4185
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4186
|
+
END IF;
|
|
4187
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4188
|
+
END IF;
|
|
4189
|
+
IF current_step.status <> 'running' THEN
|
|
4190
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4191
|
+
END IF;
|
|
4192
|
+
IF current_run.status <> 'running' THEN
|
|
4193
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4194
|
+
END IF;
|
|
4195
|
+
|
|
4196
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4197
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4198
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4199
|
+
END IF;
|
|
4200
|
+
UPDATE supacloud_workflows.steps
|
|
4201
|
+
SET status = 'completed', output = p_step_output, error_message = '',
|
|
4202
|
+
completed_at = now(), updated_at = now()
|
|
4203
|
+
WHERE id = current_step.id;
|
|
4204
|
+
UPDATE supacloud_workflows.runs
|
|
4205
|
+
SET status = 'completed', output = p_run_output, error_message = '',
|
|
4206
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4207
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4208
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
|
|
4209
|
+
VALUES (current_step.run_id, current_step.id, 'step_completed', p_attempt);
|
|
4210
|
+
INSERT INTO supacloud_workflows.events (run_id, step_id, event_type, attempt)
|
|
4211
|
+
VALUES (current_step.run_id, current_step.id, 'run_completed', p_attempt);
|
|
4212
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4213
|
+
END;
|
|
4214
|
+
$$;
|
|
4215
|
+
|
|
4216
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.retry_step(
|
|
4217
|
+
p_step_id uuid,
|
|
4218
|
+
p_message_id bigint,
|
|
4219
|
+
p_attempt integer,
|
|
4220
|
+
p_worker_id text,
|
|
4221
|
+
p_error_message text,
|
|
4222
|
+
p_delay_seconds integer
|
|
4223
|
+
) RETURNS jsonb
|
|
4224
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4225
|
+
DECLARE
|
|
4226
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4227
|
+
normalized_error text := nullif(btrim(p_error_message), '');
|
|
4228
|
+
normalized_worker_id text := nullif(btrim(p_worker_id), '');
|
|
4229
|
+
retry_receipt jsonb;
|
|
4230
|
+
queue_message_updated boolean;
|
|
4231
|
+
archived boolean;
|
|
4232
|
+
BEGIN
|
|
4233
|
+
IF normalized_error IS NULL OR char_length(normalized_error) > 4000
|
|
4234
|
+
OR p_delay_seconds NOT BETWEEN 0 AND 86400 THEN
|
|
4235
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4236
|
+
END IF;
|
|
4237
|
+
current_step := supacloud_workflows.lock_step(p_step_id);
|
|
4238
|
+
SELECT event.details INTO retry_receipt
|
|
4239
|
+
FROM supacloud_workflows.events event
|
|
4240
|
+
WHERE event.step_id = current_step.id
|
|
4241
|
+
AND event.attempt = p_attempt
|
|
4242
|
+
AND event.event_type IN ('step_retried', 'step_dead_lettered')
|
|
4243
|
+
AND event.details ->> 'operation' = 'retry'
|
|
4244
|
+
ORDER BY event.id DESC
|
|
4245
|
+
LIMIT 1;
|
|
4246
|
+
IF FOUND THEN
|
|
4247
|
+
IF retry_receipt ->> 'messageId' IS DISTINCT FROM p_message_id::text
|
|
4248
|
+
OR retry_receipt ->> 'workerId' IS DISTINCT FROM normalized_worker_id
|
|
4249
|
+
OR retry_receipt ->> 'errorMessage' IS DISTINCT FROM normalized_error
|
|
4250
|
+
OR (retry_receipt ->> 'delaySeconds')::integer IS DISTINCT FROM p_delay_seconds THEN
|
|
4251
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4252
|
+
END IF;
|
|
4253
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4254
|
+
END IF;
|
|
4255
|
+
IF normalized_worker_id IS NULL
|
|
4256
|
+
OR p_message_id IS NULL
|
|
4257
|
+
OR p_attempt IS NULL
|
|
4258
|
+
OR current_step.queue_message_id IS DISTINCT FROM p_message_id
|
|
4259
|
+
OR current_step.attempts IS DISTINCT FROM p_attempt
|
|
4260
|
+
OR current_step.claimed_by IS DISTINCT FROM normalized_worker_id
|
|
4261
|
+
OR current_step.status <> 'running' THEN
|
|
4262
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4263
|
+
END IF;
|
|
4264
|
+
|
|
4265
|
+
IF current_step.attempts >= current_step.max_attempts THEN
|
|
4266
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4267
|
+
IF archived IS DISTINCT FROM true THEN
|
|
4268
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4269
|
+
END IF;
|
|
4270
|
+
UPDATE supacloud_workflows.steps
|
|
4271
|
+
SET status = 'dead_lettered', error_message = normalized_error,
|
|
4272
|
+
completed_at = now(), updated_at = now()
|
|
4273
|
+
WHERE id = current_step.id;
|
|
4274
|
+
UPDATE supacloud_workflows.runs
|
|
4275
|
+
SET status = 'failed', error_message = normalized_error,
|
|
4276
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4277
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4278
|
+
IF NOT FOUND THEN
|
|
4279
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4280
|
+
END IF;
|
|
4281
|
+
INSERT INTO supacloud_workflows.events (
|
|
4282
|
+
run_id, step_id, event_type, attempt, details
|
|
4283
|
+
) VALUES (
|
|
4284
|
+
current_step.run_id, current_step.id, 'step_dead_lettered', p_attempt,
|
|
4285
|
+
jsonb_build_object(
|
|
4286
|
+
'operation', 'retry',
|
|
4287
|
+
'messageId', p_message_id::text,
|
|
4288
|
+
'workerId', normalized_worker_id,
|
|
4289
|
+
'errorMessage', normalized_error,
|
|
4290
|
+
'delaySeconds', p_delay_seconds
|
|
4291
|
+
)
|
|
4292
|
+
);
|
|
4293
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4294
|
+
END IF;
|
|
4295
|
+
|
|
4296
|
+
SELECT EXISTS (
|
|
4297
|
+
SELECT 1 FROM pgmq.set_vt('supacloud_internal_workflows', p_message_id, p_delay_seconds)
|
|
4298
|
+
) INTO queue_message_updated;
|
|
4299
|
+
IF queue_message_updated IS DISTINCT FROM true THEN
|
|
4300
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_QUEUE_MESSAGE_MISSING' USING ERRCODE = '55000';
|
|
4301
|
+
END IF;
|
|
4302
|
+
UPDATE supacloud_workflows.steps
|
|
4303
|
+
SET status = 'queued', error_message = normalized_error,
|
|
4304
|
+
retry_delay_seconds = p_delay_seconds, updated_at = now()
|
|
4305
|
+
WHERE id = current_step.id;
|
|
4306
|
+
INSERT INTO supacloud_workflows.events (
|
|
4307
|
+
run_id, step_id, event_type, attempt, details
|
|
4308
|
+
) VALUES (
|
|
4309
|
+
current_step.run_id, current_step.id, 'step_retried', p_attempt,
|
|
4310
|
+
jsonb_build_object(
|
|
4311
|
+
'operation', 'retry',
|
|
4312
|
+
'messageId', p_message_id::text,
|
|
4313
|
+
'workerId', normalized_worker_id,
|
|
4314
|
+
'errorMessage', normalized_error,
|
|
4315
|
+
'delaySeconds', p_delay_seconds
|
|
4316
|
+
)
|
|
4317
|
+
);
|
|
4318
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4319
|
+
END;
|
|
4320
|
+
$$;
|
|
4321
|
+
|
|
4322
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.fail_step(
|
|
4323
|
+
p_step_id uuid,
|
|
4324
|
+
p_message_id bigint,
|
|
4325
|
+
p_attempt integer,
|
|
4326
|
+
p_worker_id text,
|
|
4327
|
+
p_error_message text
|
|
4328
|
+
) RETURNS jsonb
|
|
4329
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4330
|
+
DECLARE
|
|
4331
|
+
current_step supacloud_workflows.steps%ROWTYPE;
|
|
4332
|
+
current_run supacloud_workflows.runs%ROWTYPE;
|
|
4333
|
+
normalized_error text := nullif(btrim(p_error_message), '');
|
|
4334
|
+
archived boolean;
|
|
4335
|
+
BEGIN
|
|
4336
|
+
IF normalized_error IS NULL OR char_length(normalized_error) > 4000 THEN
|
|
4337
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4338
|
+
END IF;
|
|
4339
|
+
current_step := supacloud_workflows.lock_step_attempt(
|
|
4340
|
+
p_step_id, p_message_id, p_attempt, p_worker_id
|
|
4341
|
+
);
|
|
4342
|
+
SELECT * INTO current_run
|
|
4343
|
+
FROM supacloud_workflows.runs
|
|
4344
|
+
WHERE id = current_step.run_id
|
|
4345
|
+
FOR UPDATE;
|
|
4346
|
+
|
|
4347
|
+
IF current_step.status = 'failed' THEN
|
|
4348
|
+
IF current_step.error_message IS DISTINCT FROM normalized_error
|
|
4349
|
+
OR current_run.status IS DISTINCT FROM 'failed'
|
|
4350
|
+
OR current_run.error_message IS DISTINCT FROM normalized_error THEN
|
|
4351
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4352
|
+
END IF;
|
|
4353
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, true);
|
|
4354
|
+
END IF;
|
|
4355
|
+
IF current_step.status <> 'running' THEN
|
|
4356
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_STALE_ATTEMPT' USING ERRCODE = '40001';
|
|
4357
|
+
END IF;
|
|
4358
|
+
IF current_run.status <> 'running' THEN
|
|
4359
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4360
|
+
END IF;
|
|
4361
|
+
|
|
4362
|
+
SELECT pgmq.archive('supacloud_internal_workflows', p_message_id) INTO archived;
|
|
4363
|
+
IF archived 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 = 'failed', error_message = normalized_error,
|
|
4368
|
+
completed_at = now(), updated_at = now()
|
|
4369
|
+
WHERE id = current_step.id;
|
|
4370
|
+
UPDATE supacloud_workflows.runs
|
|
4371
|
+
SET status = 'failed', error_message = normalized_error,
|
|
4372
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4373
|
+
WHERE id = current_step.run_id AND status = 'running';
|
|
4374
|
+
INSERT INTO supacloud_workflows.events (
|
|
4375
|
+
run_id, step_id, event_type, attempt, details
|
|
4376
|
+
) VALUES (
|
|
4377
|
+
current_step.run_id, current_step.id, 'step_failed', p_attempt,
|
|
4378
|
+
jsonb_build_object('errorMessage', normalized_error)
|
|
4379
|
+
);
|
|
4380
|
+
RETURN supacloud_workflows.snapshot(current_step.run_id, false);
|
|
4381
|
+
END;
|
|
4382
|
+
$$;
|
|
4383
|
+
|
|
4384
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.cancel_run(
|
|
4385
|
+
p_run_id uuid,
|
|
4386
|
+
p_reason text
|
|
4387
|
+
) RETURNS jsonb
|
|
4388
|
+
LANGUAGE plpgsql SECURITY DEFINER SET search_path = '' AS $$
|
|
4389
|
+
DECLARE
|
|
4390
|
+
normalized_reason text := nullif(btrim(p_reason), '');
|
|
4391
|
+
locked_run supacloud_workflows.runs%ROWTYPE;
|
|
4392
|
+
active_step supacloud_workflows.steps%ROWTYPE;
|
|
4393
|
+
BEGIN
|
|
4394
|
+
IF p_run_id IS NULL OR normalized_reason IS NULL OR char_length(normalized_reason) > 4000 THEN
|
|
4395
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
4396
|
+
END IF;
|
|
4397
|
+
PERFORM pg_advisory_xact_lock(hashtextextended(p_run_id::text, 0));
|
|
4398
|
+
SELECT * INTO locked_run FROM supacloud_workflows.runs WHERE id = p_run_id FOR UPDATE;
|
|
4399
|
+
IF NOT FOUND THEN
|
|
4400
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_FOUND' USING ERRCODE = 'P0002';
|
|
4401
|
+
END IF;
|
|
4402
|
+
IF locked_run.status = 'cancelled' THEN
|
|
4403
|
+
IF locked_run.error_message IS DISTINCT FROM normalized_reason THEN
|
|
4404
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_IDEMPOTENCY_CONFLICT' USING ERRCODE = '23505';
|
|
4405
|
+
END IF;
|
|
4406
|
+
RETURN supacloud_workflows.snapshot(p_run_id, true);
|
|
4407
|
+
END IF;
|
|
4408
|
+
IF locked_run.status NOT IN ('queued', 'running') THEN
|
|
4409
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RUN_NOT_ACTIVE' USING ERRCODE = '55000';
|
|
4410
|
+
END IF;
|
|
4411
|
+
SELECT * INTO active_step
|
|
4412
|
+
FROM supacloud_workflows.steps
|
|
4413
|
+
WHERE run_id = p_run_id AND status IN ('queued', 'running')
|
|
4414
|
+
FOR UPDATE;
|
|
4415
|
+
IF FOUND THEN
|
|
4416
|
+
PERFORM pgmq.archive('supacloud_internal_workflows', active_step.queue_message_id);
|
|
4417
|
+
UPDATE supacloud_workflows.steps
|
|
4418
|
+
SET status = 'cancelled', error_message = normalized_reason,
|
|
4419
|
+
completed_at = now(), updated_at = now()
|
|
4420
|
+
WHERE id = active_step.id;
|
|
4421
|
+
END IF;
|
|
4422
|
+
UPDATE supacloud_workflows.runs
|
|
4423
|
+
SET status = 'cancelled', error_message = normalized_reason,
|
|
4424
|
+
completed_at = now(), updated_at = now(), row_version = row_version + 1
|
|
4425
|
+
WHERE id = p_run_id;
|
|
4426
|
+
INSERT INTO supacloud_workflows.events (
|
|
4427
|
+
run_id, step_id, event_type, details
|
|
4428
|
+
) VALUES (
|
|
4429
|
+
p_run_id, active_step.id, 'run_cancelled',
|
|
4430
|
+
jsonb_build_object('reason', normalized_reason)
|
|
4431
|
+
);
|
|
4432
|
+
RETURN supacloud_workflows.snapshot(p_run_id, false);
|
|
4433
|
+
END;
|
|
4434
|
+
$$;
|
|
4435
|
+
|
|
4436
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.run_events(
|
|
4437
|
+
p_run_id uuid,
|
|
4438
|
+
p_after_event_id bigint,
|
|
4439
|
+
p_limit integer
|
|
4440
|
+
) RETURNS jsonb
|
|
4441
|
+
LANGUAGE sql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4442
|
+
SELECT coalesce(jsonb_agg(jsonb_build_object(
|
|
4443
|
+
'eventId', page.id::text,
|
|
4444
|
+
'runId', page.run_id,
|
|
4445
|
+
'stepId', page.step_id,
|
|
4446
|
+
'eventType', page.event_type,
|
|
4447
|
+
'attempt', page.attempt,
|
|
4448
|
+
'details', page.details,
|
|
4449
|
+
'createdAt', page.created_at
|
|
4450
|
+
) ORDER BY page.id), '[]'::jsonb)
|
|
4451
|
+
FROM (
|
|
4452
|
+
SELECT event.*
|
|
4453
|
+
FROM supacloud_workflows.events event
|
|
4454
|
+
WHERE event.run_id = p_run_id AND event.id > p_after_event_id
|
|
4455
|
+
ORDER BY event.id
|
|
4456
|
+
LIMIT p_limit
|
|
4457
|
+
) page
|
|
4458
|
+
$$;
|
|
4459
|
+
|
|
4460
|
+
CREATE OR REPLACE FUNCTION supacloud_workflows.request_uuid(
|
|
4461
|
+
request jsonb,
|
|
4462
|
+
key text
|
|
4463
|
+
) RETURNS uuid
|
|
4464
|
+
LANGUAGE plpgsql IMMUTABLE SET search_path = '' AS $$
|
|
4465
|
+
DECLARE
|
|
4466
|
+
uuid_text text;
|
|
4467
|
+
BEGIN
|
|
4468
|
+
IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
|
|
4469
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
|
|
4470
|
+
END IF;
|
|
4471
|
+
uuid_text := request ->> key;
|
|
4472
|
+
IF uuid_text IS NULL
|
|
4473
|
+
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
|
|
4474
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_REQUEST_INVALID' USING ERRCODE = '22023';
|
|
4475
|
+
END IF;
|
|
4476
|
+
RETURN uuid_text::uuid;
|
|
4477
|
+
END;
|
|
4478
|
+
$$;
|
|
4479
|
+
|
|
4480
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_start(request jsonb)
|
|
4481
|
+
RETURNS jsonb
|
|
4482
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4483
|
+
DECLARE
|
|
4484
|
+
max_attempts integer;
|
|
4485
|
+
BEGIN
|
|
4486
|
+
max_attempts := coalesce((request ->> 'maxAttempts')::integer, 3);
|
|
4487
|
+
RETURN supacloud_workflows.start_run(
|
|
4488
|
+
supacloud_workflows.request_uuid(request, 'runId'),
|
|
4489
|
+
request ->> 'workflowName',
|
|
4490
|
+
request ->> 'workflowVersion',
|
|
4491
|
+
request ->> 'firstStepKey',
|
|
4492
|
+
coalesce(request -> 'input', '{}'::jsonb),
|
|
4493
|
+
max_attempts
|
|
4494
|
+
);
|
|
4495
|
+
EXCEPTION
|
|
4496
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4497
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_START_INVALID' USING ERRCODE = '22023';
|
|
4498
|
+
END;
|
|
4499
|
+
$$;
|
|
4500
|
+
|
|
4501
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_claim(request jsonb)
|
|
4502
|
+
RETURNS jsonb
|
|
4503
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4504
|
+
DECLARE
|
|
4505
|
+
visibility_timeout_seconds integer;
|
|
4506
|
+
BEGIN
|
|
4507
|
+
IF jsonb_typeof(request) IS DISTINCT FROM 'object' THEN
|
|
4508
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
4509
|
+
END IF;
|
|
4510
|
+
visibility_timeout_seconds := coalesce((request ->> 'visibilityTimeoutSeconds')::integer, 300);
|
|
4511
|
+
RETURN supacloud_workflows.claim_step(
|
|
4512
|
+
request ->> 'workerId', visibility_timeout_seconds
|
|
4513
|
+
);
|
|
4514
|
+
EXCEPTION
|
|
4515
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4516
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CLAIM_INVALID' USING ERRCODE = '22023';
|
|
4517
|
+
END;
|
|
4518
|
+
$$;
|
|
4519
|
+
|
|
4520
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_advance(request jsonb)
|
|
4521
|
+
RETURNS jsonb
|
|
4522
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4523
|
+
DECLARE
|
|
4524
|
+
message_id bigint;
|
|
4525
|
+
attempt integer;
|
|
4526
|
+
next_max_attempts integer;
|
|
4527
|
+
BEGIN
|
|
4528
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4529
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4530
|
+
next_max_attempts := coalesce((request ->> 'nextMaxAttempts')::integer, 3);
|
|
4531
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4532
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4533
|
+
END IF;
|
|
4534
|
+
RETURN supacloud_workflows.advance_step(
|
|
4535
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4536
|
+
message_id,
|
|
4537
|
+
attempt,
|
|
4538
|
+
request ->> 'workerId',
|
|
4539
|
+
coalesce(request -> 'output', '{}'::jsonb),
|
|
4540
|
+
request ->> 'nextStepKey',
|
|
4541
|
+
coalesce(request -> 'nextInput', '{}'::jsonb),
|
|
4542
|
+
next_max_attempts
|
|
4543
|
+
);
|
|
4544
|
+
EXCEPTION
|
|
4545
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4546
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_ADVANCE_INVALID' USING ERRCODE = '22023';
|
|
4547
|
+
END;
|
|
4548
|
+
$$;
|
|
4549
|
+
|
|
4550
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_complete(request jsonb)
|
|
4551
|
+
RETURNS jsonb
|
|
4552
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4553
|
+
DECLARE
|
|
4554
|
+
message_id bigint;
|
|
4555
|
+
attempt integer;
|
|
4556
|
+
BEGIN
|
|
4557
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4558
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4559
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4560
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4561
|
+
END IF;
|
|
4562
|
+
RETURN supacloud_workflows.complete_run(
|
|
4563
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4564
|
+
message_id,
|
|
4565
|
+
attempt,
|
|
4566
|
+
request ->> 'workerId',
|
|
4567
|
+
coalesce(request -> 'stepOutput', '{}'::jsonb),
|
|
4568
|
+
coalesce(request -> 'runOutput', '{}'::jsonb)
|
|
4569
|
+
);
|
|
4570
|
+
EXCEPTION
|
|
4571
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4572
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_COMPLETION_INVALID' USING ERRCODE = '22023';
|
|
4573
|
+
END;
|
|
4574
|
+
$$;
|
|
4575
|
+
|
|
4576
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_retry(request jsonb)
|
|
4577
|
+
RETURNS jsonb
|
|
4578
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4579
|
+
DECLARE
|
|
4580
|
+
message_id bigint;
|
|
4581
|
+
attempt integer;
|
|
4582
|
+
delay_seconds integer;
|
|
4583
|
+
BEGIN
|
|
4584
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4585
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4586
|
+
delay_seconds := coalesce((request ->> 'delaySeconds')::integer, 0);
|
|
4587
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4588
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4589
|
+
END IF;
|
|
4590
|
+
RETURN supacloud_workflows.retry_step(
|
|
4591
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4592
|
+
message_id,
|
|
4593
|
+
attempt,
|
|
4594
|
+
request ->> 'workerId',
|
|
4595
|
+
request ->> 'errorMessage',
|
|
4596
|
+
delay_seconds
|
|
4597
|
+
);
|
|
4598
|
+
EXCEPTION
|
|
4599
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4600
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_RETRY_INVALID' USING ERRCODE = '22023';
|
|
4601
|
+
END;
|
|
4602
|
+
$$;
|
|
4603
|
+
|
|
4604
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_fail(request jsonb)
|
|
4605
|
+
RETURNS jsonb
|
|
4606
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4607
|
+
DECLARE
|
|
4608
|
+
message_id bigint;
|
|
4609
|
+
attempt integer;
|
|
4610
|
+
BEGIN
|
|
4611
|
+
message_id := (request ->> 'messageId')::bigint;
|
|
4612
|
+
attempt := (request ->> 'attempt')::integer;
|
|
4613
|
+
IF message_id <= 0 OR attempt <= 0 THEN
|
|
4614
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4615
|
+
END IF;
|
|
4616
|
+
RETURN supacloud_workflows.fail_step(
|
|
4617
|
+
supacloud_workflows.request_uuid(request, 'stepId'),
|
|
4618
|
+
message_id,
|
|
4619
|
+
attempt,
|
|
4620
|
+
request ->> 'workerId',
|
|
4621
|
+
request ->> 'errorMessage'
|
|
4622
|
+
);
|
|
4623
|
+
EXCEPTION
|
|
4624
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4625
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_FAILURE_INVALID' USING ERRCODE = '22023';
|
|
4626
|
+
END;
|
|
4627
|
+
$$;
|
|
4628
|
+
|
|
4629
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_cancel(request jsonb)
|
|
4630
|
+
RETURNS jsonb
|
|
4631
|
+
LANGUAGE plpgsql VOLATILE SECURITY DEFINER SET search_path = '' AS $$
|
|
4632
|
+
BEGIN
|
|
4633
|
+
RETURN supacloud_workflows.cancel_run(
|
|
4634
|
+
supacloud_workflows.request_uuid(request, 'runId'),
|
|
4635
|
+
request ->> 'reason'
|
|
4636
|
+
);
|
|
4637
|
+
EXCEPTION
|
|
4638
|
+
WHEN invalid_parameter_value OR invalid_text_representation THEN
|
|
4639
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_CANCEL_INVALID' USING ERRCODE = '22023';
|
|
4640
|
+
END;
|
|
4641
|
+
$$;
|
|
4642
|
+
|
|
4643
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_get(request jsonb)
|
|
4644
|
+
RETURNS jsonb
|
|
4645
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4646
|
+
BEGIN
|
|
4647
|
+
RETURN supacloud_workflows.snapshot(
|
|
4648
|
+
supacloud_workflows.request_uuid(request, 'runId'), false
|
|
4649
|
+
);
|
|
4650
|
+
EXCEPTION
|
|
4651
|
+
WHEN invalid_parameter_value OR invalid_text_representation THEN
|
|
4652
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_GET_INVALID' USING ERRCODE = '22023';
|
|
4653
|
+
END;
|
|
4654
|
+
$$;
|
|
4655
|
+
|
|
4656
|
+
CREATE OR REPLACE FUNCTION public.supacloud_workflow_events(request jsonb)
|
|
4657
|
+
RETURNS jsonb
|
|
4658
|
+
LANGUAGE plpgsql STABLE SECURITY DEFINER SET search_path = '' AS $$
|
|
4659
|
+
DECLARE
|
|
4660
|
+
after_event_id bigint;
|
|
4661
|
+
event_limit integer;
|
|
4662
|
+
BEGIN
|
|
4663
|
+
after_event_id := coalesce((request ->> 'afterEventId')::bigint, 0);
|
|
4664
|
+
event_limit := coalesce((request ->> 'limit')::integer, 100);
|
|
4665
|
+
IF after_event_id < 0 OR event_limit NOT BETWEEN 1 AND 500 THEN
|
|
4666
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
|
|
4667
|
+
END IF;
|
|
4668
|
+
RETURN supacloud_workflows.run_events(
|
|
4669
|
+
supacloud_workflows.request_uuid(request, 'runId'), after_event_id, event_limit
|
|
4670
|
+
);
|
|
4671
|
+
EXCEPTION
|
|
4672
|
+
WHEN invalid_parameter_value OR invalid_text_representation OR numeric_value_out_of_range THEN
|
|
4673
|
+
RAISE EXCEPTION 'SUPACLOUD_WORKFLOW_EVENTS_INVALID' USING ERRCODE = '22023';
|
|
4674
|
+
END;
|
|
4675
|
+
$$;
|
|
4676
|
+
|
|
4677
|
+
REVOKE ALL ON ALL TABLES IN SCHEMA supacloud_workflows
|
|
4678
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4679
|
+
REVOKE ALL ON ALL SEQUENCES IN SCHEMA supacloud_workflows
|
|
4680
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4681
|
+
REVOKE ALL ON ALL FUNCTIONS IN SCHEMA supacloud_workflows
|
|
4682
|
+
FROM PUBLIC, anon, authenticated, service_role;
|
|
4683
|
+
|
|
4684
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_start(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4685
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_claim(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4686
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_advance(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4687
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_complete(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4688
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_retry(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4689
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_fail(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4690
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_cancel(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4691
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_get(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4692
|
+
REVOKE ALL ON FUNCTION public.supacloud_workflow_events(jsonb) FROM PUBLIC, anon, authenticated;
|
|
4693
|
+
|
|
4694
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_start(jsonb) TO service_role;
|
|
4695
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_claim(jsonb) TO service_role;
|
|
4696
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_advance(jsonb) TO service_role;
|
|
4697
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_complete(jsonb) TO service_role;
|
|
4698
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_retry(jsonb) TO service_role;
|
|
4699
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_fail(jsonb) TO service_role;
|
|
4700
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_cancel(jsonb) TO service_role;
|
|
4701
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_get(jsonb) TO service_role;
|
|
4702
|
+
GRANT EXECUTE ON FUNCTION public.supacloud_workflow_events(jsonb) TO service_role;
|
|
4703
|
+
-- supacloud:sql-module:workflows-public:end
|
|
3583
4704
|
`;
|
|
3584
4705
|
var CRON_SQL = `
|
|
3585
4706
|
create schema if not exists cron;
|
|
@@ -3906,12 +5027,32 @@ function pickTag(text, base) {
|
|
|
3906
5027
|
return tag;
|
|
3907
5028
|
}
|
|
3908
5029
|
|
|
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
5030
|
// src/runtime/db/data-dir-lock.ts
|
|
3914
|
-
import { readFile, unlink } from "fs/promises";
|
|
5031
|
+
import { mkdir, open, readFile, unlink } from "fs/promises";
|
|
5032
|
+
import { dirname, resolve } from "path";
|
|
5033
|
+
async function acquireDataDirLock(dataDir, engineName = "database") {
|
|
5034
|
+
if (!dataDir || dataDir.includes("://"))
|
|
5035
|
+
return async () => {};
|
|
5036
|
+
const absoluteDataDir = resolve(dataDir);
|
|
5037
|
+
const lockPath = `${absoluteDataDir}.supacloud-lite.lock`;
|
|
5038
|
+
await mkdir(dirname(absoluteDataDir), { recursive: true, mode: 448 });
|
|
5039
|
+
const nonce = crypto.randomUUID();
|
|
5040
|
+
const handle = await createDataDirLock(absoluteDataDir, lockPath, nonce, engineName);
|
|
5041
|
+
let released = false;
|
|
5042
|
+
return async () => {
|
|
5043
|
+
if (released)
|
|
5044
|
+
return;
|
|
5045
|
+
released = true;
|
|
5046
|
+
await handle.close();
|
|
5047
|
+
const owner = await readDataDirLockOwner(lockPath);
|
|
5048
|
+
if (owner?.nonce !== nonce)
|
|
5049
|
+
return;
|
|
5050
|
+
await unlink(lockPath).catch((error) => {
|
|
5051
|
+
if (error.code !== "ENOENT")
|
|
5052
|
+
throw error;
|
|
5053
|
+
});
|
|
5054
|
+
};
|
|
5055
|
+
}
|
|
3915
5056
|
async function recoverStaleDataDirLock(lockPath) {
|
|
3916
5057
|
const lockState = await inspectDataDirLock(lockPath);
|
|
3917
5058
|
if (lockState.kind !== "stale")
|
|
@@ -3929,6 +5070,50 @@ async function readDataDirLockOwner(lockPath) {
|
|
|
3929
5070
|
const lockState = await inspectDataDirLock(lockPath);
|
|
3930
5071
|
return lockState.kind === "active" || lockState.kind === "stale" ? lockState.owner : null;
|
|
3931
5072
|
}
|
|
5073
|
+
async function assertDataDirUnlocked(dataDir) {
|
|
5074
|
+
if (!dataDir)
|
|
5075
|
+
return;
|
|
5076
|
+
const lockPath = `${resolve(dataDir)}.supacloud-lite.lock`;
|
|
5077
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
5078
|
+
if (lockState.kind === "missing")
|
|
5079
|
+
return;
|
|
5080
|
+
if (lockState.kind === "active") {
|
|
5081
|
+
throw new Error(`database data directory is already in use: ${resolve(dataDir)} (pid ${lockState.owner.pid})`);
|
|
5082
|
+
}
|
|
5083
|
+
throw unreadableLockError(lockPath, "database");
|
|
5084
|
+
}
|
|
5085
|
+
async function createDataDirLock(absoluteDataDir, lockPath, nonce, engineName) {
|
|
5086
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
5087
|
+
try {
|
|
5088
|
+
return await writeDataDirLock(lockPath, nonce);
|
|
5089
|
+
} catch (error) {
|
|
5090
|
+
if (error.code !== "EEXIST")
|
|
5091
|
+
throw error;
|
|
5092
|
+
const lockState = await recoverStaleDataDirLock(lockPath);
|
|
5093
|
+
if (lockState.kind === "active") {
|
|
5094
|
+
throw new Error(`${engineName} data directory is already in use: ${absoluteDataDir} (pid ${lockState.owner.pid})`);
|
|
5095
|
+
}
|
|
5096
|
+
if (lockState.kind === "unreadable")
|
|
5097
|
+
throw unreadableLockError(lockPath, engineName);
|
|
5098
|
+
}
|
|
5099
|
+
}
|
|
5100
|
+
throw unreadableLockError(lockPath, engineName);
|
|
5101
|
+
}
|
|
5102
|
+
async function writeDataDirLock(lockPath, nonce) {
|
|
5103
|
+
const handle = await open(lockPath, "wx", 384);
|
|
5104
|
+
try {
|
|
5105
|
+
await handle.writeFile(`${JSON.stringify({ pid: process.pid, nonce, createdAt: new Date().toISOString() })}
|
|
5106
|
+
`);
|
|
5107
|
+
return handle;
|
|
5108
|
+
} catch (error) {
|
|
5109
|
+
await handle.close().catch(() => {});
|
|
5110
|
+
await unlink(lockPath).catch(() => {});
|
|
5111
|
+
throw error;
|
|
5112
|
+
}
|
|
5113
|
+
}
|
|
5114
|
+
function unreadableLockError(lockPath, engineName) {
|
|
5115
|
+
return new Error(`${engineName} data directory has an unreadable lock: ${lockPath}. ` + "Confirm no SupaCloud Lite process is using it, then remove the lock manually.");
|
|
5116
|
+
}
|
|
3932
5117
|
async function inspectDataDirLock(lockPath) {
|
|
3933
5118
|
let contents;
|
|
3934
5119
|
try {
|
|
@@ -3982,7 +5167,7 @@ begin
|
|
|
3982
5167
|
end $$;
|
|
3983
5168
|
`;
|
|
3984
5169
|
async function createPgliteEngine(dataDir) {
|
|
3985
|
-
const releaseLock = await acquireDataDirLock(dataDir);
|
|
5170
|
+
const releaseLock = await acquireDataDirLock(dataDir, "PGlite");
|
|
3986
5171
|
let PGlite, extensions;
|
|
3987
5172
|
const standaloneAssets = getStandaloneAssets();
|
|
3988
5173
|
let cleanupStandaloneBundles = async () => {};
|
|
@@ -4106,63 +5291,6 @@ async function removePreparedBundles(cleanup) {
|
|
|
4106
5291
|
console.error("Unable to remove temporary PGlite extension bundles:", error);
|
|
4107
5292
|
}
|
|
4108
5293
|
}
|
|
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
5294
|
|
|
4167
5295
|
// src/runtime/db/database.ts
|
|
4168
5296
|
var DEFAULT_SEARCH_PATH_SQL = `set search_path to "$user", public, extensions`;
|
|
@@ -4180,20 +5308,30 @@ class Database {
|
|
|
4180
5308
|
}
|
|
4181
5309
|
static async create(dataDirOrEngine, opts) {
|
|
4182
5310
|
const engine = dataDirOrEngine && typeof dataDirOrEngine === "object" ? dataDirOrEngine : await createPgliteEngine(dataDirOrEngine);
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
await engine.
|
|
5311
|
+
try {
|
|
5312
|
+
if (engine.minimalBootstrap) {
|
|
5313
|
+
await engine.exec(MINIMAL_BOOTSTRAP_SQL);
|
|
5314
|
+
} else {
|
|
5315
|
+
await engine.exec(BOOTSTRAP_SQL);
|
|
5316
|
+
await engine.exec(PGMQ_SQL);
|
|
5317
|
+
await engine.exec(WORKFLOWS_SQL);
|
|
5318
|
+
await engine.exec(CRON_SQL);
|
|
5319
|
+
await engine.exec(NET_SQL);
|
|
5320
|
+
await engine.exec(EXT_COMPAT_SQL);
|
|
5321
|
+
await engine.exec(VAULT_SQL);
|
|
5322
|
+
if (opts?.vaultKey) {
|
|
5323
|
+
await engine.query(`select set_config('app.settings.vault_key', $1, false)`, [opts.vaultKey]);
|
|
5324
|
+
}
|
|
5325
|
+
}
|
|
5326
|
+
return new Database(engine);
|
|
5327
|
+
} catch (error) {
|
|
5328
|
+
try {
|
|
5329
|
+
await engine.close();
|
|
5330
|
+
} catch (cleanupError) {
|
|
5331
|
+
throw new AggregateError([error, cleanupError], "database bootstrap and cleanup failed");
|
|
4194
5332
|
}
|
|
5333
|
+
throw error;
|
|
4195
5334
|
}
|
|
4196
|
-
return new Database(engine);
|
|
4197
5335
|
}
|
|
4198
5336
|
query(sql, params) {
|
|
4199
5337
|
return this.engine.query(sql, params);
|
|
@@ -5932,6 +7070,25 @@ function parsePrefer(header) {
|
|
|
5932
7070
|
}
|
|
5933
7071
|
return prefer;
|
|
5934
7072
|
}
|
|
7073
|
+
function applyRequestRange(query, request) {
|
|
7074
|
+
const range = request.headers.get("range");
|
|
7075
|
+
if (!range || query.limits.has("") || query.offsets.has(""))
|
|
7076
|
+
return;
|
|
7077
|
+
const unit = request.headers.get("range-unit");
|
|
7078
|
+
if (unit !== null && unit.toLowerCase() !== "items")
|
|
7079
|
+
throw new ParseError(`unsupported range unit: ${unit}`);
|
|
7080
|
+
const match = range.match(/^(\d+)-(\d*)$/);
|
|
7081
|
+
if (!match)
|
|
7082
|
+
throw new ParseError(`invalid range: ${range}`);
|
|
7083
|
+
const start = Number(match[1]);
|
|
7084
|
+
const end = match[2] ? Number(match[2]) : undefined;
|
|
7085
|
+
if (!Number.isSafeInteger(start) || end !== undefined && (!Number.isSafeInteger(end) || end < start)) {
|
|
7086
|
+
throw new ParseError(`invalid range: ${range}`);
|
|
7087
|
+
}
|
|
7088
|
+
query.offsets.set("", start);
|
|
7089
|
+
if (end !== undefined)
|
|
7090
|
+
query.limits.set("", end - start + 1);
|
|
7091
|
+
}
|
|
5935
7092
|
var OBJECT_MEDIA = "application/vnd.pgrst.object+json";
|
|
5936
7093
|
var CSV_MEDIA = "text/csv";
|
|
5937
7094
|
var PLAN_MEDIA = "application/vnd.pgrst.plan";
|
|
@@ -6008,6 +7165,8 @@ class RestHandler {
|
|
|
6008
7165
|
const wantsObject = accept.includes(OBJECT_MEDIA);
|
|
6009
7166
|
const wantsCsv = accept.includes(CSV_MEDIA);
|
|
6010
7167
|
const q = parseQuery(url.searchParams);
|
|
7168
|
+
if (method === "GET" || method === "HEAD")
|
|
7169
|
+
applyRequestRange(q, req);
|
|
6011
7170
|
if (this.maxRows !== undefined && (method === "GET" || method === "HEAD")) {
|
|
6012
7171
|
const requested = q.limits.get("");
|
|
6013
7172
|
q.limits.set("", requested === undefined ? this.maxRows : Math.min(requested, this.maxRows));
|
|
@@ -6050,10 +7209,11 @@ class RestHandler {
|
|
|
6050
7209
|
}
|
|
6051
7210
|
return { rows: res.rows[0].body, count: count2 };
|
|
6052
7211
|
});
|
|
7212
|
+
const offset = q.offsets.get("") ?? 0;
|
|
6053
7213
|
return this.dataResponse(rows, {
|
|
6054
|
-
status: 200,
|
|
7214
|
+
status: count !== null && (offset > 0 || rows.length < count) ? 206 : 200,
|
|
6055
7215
|
count,
|
|
6056
|
-
offset
|
|
7216
|
+
offset,
|
|
6057
7217
|
wantsObject,
|
|
6058
7218
|
wantsCsv,
|
|
6059
7219
|
head: method === "HEAD"
|
|
@@ -6484,8 +7644,10 @@ function applyResize(image, metadata, options) {
|
|
|
6484
7644
|
const proportionalWidth = Math.max(1, Math.round(metadata.width * options.height / metadata.height));
|
|
6485
7645
|
image.resize(proportionalWidth);
|
|
6486
7646
|
}
|
|
6487
|
-
async function transformImage(
|
|
6488
|
-
|
|
7647
|
+
async function transformImage(source, options, knownSourceSize) {
|
|
7648
|
+
const actualSourceSize = source instanceof Uint8Array ? source.byteLength : await Promise.resolve(source.size);
|
|
7649
|
+
const sourceSize = Math.max(knownSourceSize ?? 0, actualSourceSize);
|
|
7650
|
+
if (sourceSize > MAX_TRANSFORM_BYTES) {
|
|
6489
7651
|
return {
|
|
6490
7652
|
ok: false,
|
|
6491
7653
|
status: 413,
|
|
@@ -6493,9 +7655,10 @@ async function transformImage(bytes, options) {
|
|
|
6493
7655
|
message: "The source image exceeds the 25MB transformation limit"
|
|
6494
7656
|
};
|
|
6495
7657
|
}
|
|
7658
|
+
const image = new Bun.Image(source, { maxPixels: MAX_TRANSFORM_PIXELS });
|
|
6496
7659
|
let metadata;
|
|
6497
7660
|
try {
|
|
6498
|
-
metadata = await
|
|
7661
|
+
metadata = await image.metadata();
|
|
6499
7662
|
} catch (error) {
|
|
6500
7663
|
return mapImageError(error);
|
|
6501
7664
|
}
|
|
@@ -6512,7 +7675,6 @@ async function transformImage(bytes, options) {
|
|
|
6512
7675
|
message: `The source format ${outputFormat} is not supported by this runtime`
|
|
6513
7676
|
};
|
|
6514
7677
|
}
|
|
6515
|
-
const image = new Bun.Image(bytes, { maxPixels: MAX_TRANSFORM_PIXELS });
|
|
6516
7678
|
applyResize(image, metadata, options);
|
|
6517
7679
|
if (options.format === "jpeg" || options.format === "origin" && outputFormat === "jpeg" && options.quality !== undefined) {
|
|
6518
7680
|
image.jpeg(options.quality === undefined ? undefined : { quality: options.quality });
|
|
@@ -6522,12 +7684,127 @@ async function transformImage(bytes, options) {
|
|
|
6522
7684
|
image.webp(options.quality === undefined ? undefined : { quality: options.quality });
|
|
6523
7685
|
}
|
|
6524
7686
|
try {
|
|
6525
|
-
|
|
7687
|
+
const bytes = await image.bytes();
|
|
7688
|
+
if (bytes.byteLength > MAX_TRANSFORM_BYTES) {
|
|
7689
|
+
return {
|
|
7690
|
+
ok: false,
|
|
7691
|
+
status: 413,
|
|
7692
|
+
error: "ImageTooLarge",
|
|
7693
|
+
message: "The transformed image exceeds the 25MB transformation limit"
|
|
7694
|
+
};
|
|
7695
|
+
}
|
|
7696
|
+
return { ok: true, bytes, contentType };
|
|
6526
7697
|
} catch (error) {
|
|
6527
7698
|
return mapImageError(error);
|
|
6528
7699
|
}
|
|
6529
7700
|
}
|
|
6530
7701
|
|
|
7702
|
+
// src/runtime/storage/image-transform-cache.ts
|
|
7703
|
+
var DEFAULT_MAX_CACHE_BYTES = 64 * 1024 * 1024;
|
|
7704
|
+
var DEFAULT_MAX_CACHE_ENTRIES = 128;
|
|
7705
|
+
var OBJECT_VERSION_PREFIX = "v2-";
|
|
7706
|
+
|
|
7707
|
+
class Semaphore {
|
|
7708
|
+
limit;
|
|
7709
|
+
active = 0;
|
|
7710
|
+
waiters = [];
|
|
7711
|
+
constructor(limit) {
|
|
7712
|
+
this.limit = limit;
|
|
7713
|
+
}
|
|
7714
|
+
async run(operation) {
|
|
7715
|
+
await this.acquire();
|
|
7716
|
+
try {
|
|
7717
|
+
return await operation();
|
|
7718
|
+
} finally {
|
|
7719
|
+
this.release();
|
|
7720
|
+
}
|
|
7721
|
+
}
|
|
7722
|
+
async acquire() {
|
|
7723
|
+
if (this.active < this.limit) {
|
|
7724
|
+
this.active += 1;
|
|
7725
|
+
return;
|
|
7726
|
+
}
|
|
7727
|
+
await new Promise((resolve2) => this.waiters.push(resolve2));
|
|
7728
|
+
}
|
|
7729
|
+
release() {
|
|
7730
|
+
const next = this.waiters.shift();
|
|
7731
|
+
if (next) {
|
|
7732
|
+
next();
|
|
7733
|
+
return;
|
|
7734
|
+
}
|
|
7735
|
+
this.active -= 1;
|
|
7736
|
+
}
|
|
7737
|
+
}
|
|
7738
|
+
var globalImageTransformSemaphore = new Semaphore(1);
|
|
7739
|
+
function imageTransformCacheKey(version, options) {
|
|
7740
|
+
if (!version?.startsWith(OBJECT_VERSION_PREFIX))
|
|
7741
|
+
return null;
|
|
7742
|
+
return [
|
|
7743
|
+
version,
|
|
7744
|
+
options.width ?? "",
|
|
7745
|
+
options.height ?? "",
|
|
7746
|
+
options.resize,
|
|
7747
|
+
options.quality ?? "",
|
|
7748
|
+
options.format
|
|
7749
|
+
].join("\x00");
|
|
7750
|
+
}
|
|
7751
|
+
|
|
7752
|
+
class ImageTransformCache {
|
|
7753
|
+
maxBytes;
|
|
7754
|
+
maxEntries;
|
|
7755
|
+
entries = new Map;
|
|
7756
|
+
inFlight = new Map;
|
|
7757
|
+
cachedBytes = 0;
|
|
7758
|
+
constructor(maxBytes = DEFAULT_MAX_CACHE_BYTES, maxEntries = DEFAULT_MAX_CACHE_ENTRIES) {
|
|
7759
|
+
this.maxBytes = maxBytes;
|
|
7760
|
+
this.maxEntries = maxEntries;
|
|
7761
|
+
}
|
|
7762
|
+
async getOrTransform(key, operation) {
|
|
7763
|
+
if (key === null)
|
|
7764
|
+
return globalImageTransformSemaphore.run(operation);
|
|
7765
|
+
const cached = this.entries.get(key);
|
|
7766
|
+
if (cached) {
|
|
7767
|
+
this.entries.delete(key);
|
|
7768
|
+
this.entries.set(key, cached);
|
|
7769
|
+
return cached.transform;
|
|
7770
|
+
}
|
|
7771
|
+
const pending = this.inFlight.get(key);
|
|
7772
|
+
if (pending)
|
|
7773
|
+
return pending;
|
|
7774
|
+
const transform = globalImageTransformSemaphore.run(operation);
|
|
7775
|
+
this.inFlight.set(key, transform);
|
|
7776
|
+
try {
|
|
7777
|
+
const transformResult = await transform;
|
|
7778
|
+
if (transformResult.ok)
|
|
7779
|
+
this.store(key, transformResult);
|
|
7780
|
+
return transformResult;
|
|
7781
|
+
} finally {
|
|
7782
|
+
if (this.inFlight.get(key) === transform)
|
|
7783
|
+
this.inFlight.delete(key);
|
|
7784
|
+
}
|
|
7785
|
+
}
|
|
7786
|
+
store(key, transform) {
|
|
7787
|
+
const size = transform.bytes.byteLength;
|
|
7788
|
+
if (size > this.maxBytes || this.maxEntries === 0)
|
|
7789
|
+
return;
|
|
7790
|
+
const previous = this.entries.get(key);
|
|
7791
|
+
if (previous) {
|
|
7792
|
+
this.cachedBytes -= previous.size;
|
|
7793
|
+
this.entries.delete(key);
|
|
7794
|
+
}
|
|
7795
|
+
this.entries.set(key, { transform, size });
|
|
7796
|
+
this.cachedBytes += size;
|
|
7797
|
+
while (this.entries.size > this.maxEntries || this.cachedBytes > this.maxBytes) {
|
|
7798
|
+
const oldestKey = this.entries.keys().next().value;
|
|
7799
|
+
if (oldestKey === undefined)
|
|
7800
|
+
break;
|
|
7801
|
+
const oldest = this.entries.get(oldestKey);
|
|
7802
|
+
this.entries.delete(oldestKey);
|
|
7803
|
+
this.cachedBytes -= oldest.size;
|
|
7804
|
+
}
|
|
7805
|
+
}
|
|
7806
|
+
}
|
|
7807
|
+
|
|
6531
7808
|
// src/runtime/storage/handler.ts
|
|
6532
7809
|
var MAX_SIGNED_URL_EXPIRY = 7 * 24 * 60 * 60;
|
|
6533
7810
|
function clampExpiry(expiresIn) {
|
|
@@ -6606,7 +7883,7 @@ var MAX_COMPLETED_TUS_UPLOADS = 64;
|
|
|
6606
7883
|
var COMPLETED_TUS_RETENTION_MS = 60 * 60 * 1000;
|
|
6607
7884
|
var PREFLIGHT_ROLLBACK = Symbol("storage-preflight-rollback");
|
|
6608
7885
|
var INTERNAL_STORAGE_BUCKET = ".supacloud-lite";
|
|
6609
|
-
var
|
|
7886
|
+
var OBJECT_VERSION_PREFIX2 = "v2-";
|
|
6610
7887
|
|
|
6611
7888
|
class StorageHandler {
|
|
6612
7889
|
db;
|
|
@@ -6614,6 +7891,7 @@ class StorageHandler {
|
|
|
6614
7891
|
config;
|
|
6615
7892
|
tusUploads = new Map;
|
|
6616
7893
|
mutationTail = Promise.resolve();
|
|
7894
|
+
imageTransforms = new ImageTransformCache;
|
|
6617
7895
|
constructor(db, driver, config) {
|
|
6618
7896
|
this.db = db;
|
|
6619
7897
|
this.driver = driver;
|
|
@@ -6656,15 +7934,21 @@ class StorageHandler {
|
|
|
6656
7934
|
const bucket2 = parts[3];
|
|
6657
7935
|
const key2 = parts.slice(4).join("/");
|
|
6658
7936
|
if (kind === "public" && (method === "GET" || method === "HEAD")) {
|
|
6659
|
-
const source = await this.
|
|
7937
|
+
const source = await this.loadPublicObject(bucket2, key2);
|
|
7938
|
+
if (source instanceof Response)
|
|
7939
|
+
return source;
|
|
6660
7940
|
return await this.transformImageResponse(source, url, method === "HEAD");
|
|
6661
7941
|
}
|
|
6662
7942
|
if (kind === "authenticated" && (method === "GET" || method === "HEAD")) {
|
|
6663
|
-
const source = await this.
|
|
7943
|
+
const source = await this.loadAuthenticatedObject(ctx, bucket2, key2);
|
|
7944
|
+
if (source instanceof Response)
|
|
7945
|
+
return source;
|
|
6664
7946
|
return await this.transformImageResponse(source, url, method === "HEAD");
|
|
6665
7947
|
}
|
|
6666
7948
|
if (kind === "sign" && method === "GET") {
|
|
6667
|
-
const source = await this.
|
|
7949
|
+
const source = await this.loadSignedObject(url, bucket2, key2);
|
|
7950
|
+
if (source instanceof Response)
|
|
7951
|
+
return source;
|
|
6668
7952
|
return await this.transformImageResponse(source, url, false);
|
|
6669
7953
|
}
|
|
6670
7954
|
return storageError(404, "not_found", `unknown render endpoint: ${rest}`);
|
|
@@ -6877,21 +8161,31 @@ class StorageHandler {
|
|
|
6877
8161
|
throw e;
|
|
6878
8162
|
}
|
|
6879
8163
|
}
|
|
6880
|
-
async transformImageResponse(
|
|
6881
|
-
if (!source.ok)
|
|
6882
|
-
return source;
|
|
8164
|
+
async transformImageResponse(row, url, head) {
|
|
6883
8165
|
const parsed = parseImageTransform(url.searchParams);
|
|
6884
8166
|
if (!parsed.ok)
|
|
6885
8167
|
return storageError(parsed.status, parsed.error, parsed.message);
|
|
6886
|
-
|
|
8168
|
+
let result;
|
|
8169
|
+
try {
|
|
8170
|
+
result = await this.imageTransforms.getOrTransform(imageTransformCacheKey(row.version, parsed.value), async () => {
|
|
8171
|
+
const source = await this.readObjectSource(row);
|
|
8172
|
+
if (source === null)
|
|
8173
|
+
throw new StorageObjectMissingError;
|
|
8174
|
+
return transformImage(source, parsed.value, objectSize(row));
|
|
8175
|
+
});
|
|
8176
|
+
} catch (error) {
|
|
8177
|
+
if (error instanceof StorageObjectMissingError) {
|
|
8178
|
+
return storageError(404, "not_found", "Object not found");
|
|
8179
|
+
}
|
|
8180
|
+
throw error;
|
|
8181
|
+
}
|
|
6887
8182
|
if (!result.ok)
|
|
6888
8183
|
return storageError(result.status, result.error, result.message);
|
|
6889
|
-
const headers =
|
|
8184
|
+
const headers = objectHeaders(row, result.bytes.length);
|
|
6890
8185
|
headers.delete("content-disposition");
|
|
6891
8186
|
headers.delete("etag");
|
|
6892
8187
|
headers.set("content-type", result.contentType);
|
|
6893
|
-
|
|
6894
|
-
return new Response(head ? null : result.bytes, { status: source.status, headers });
|
|
8188
|
+
return new Response(head ? null : result.bytes, { status: 200, headers });
|
|
6895
8189
|
}
|
|
6896
8190
|
async persistObject(ctx, bucketId, key, bytes, contentType, cacheControl, upsert) {
|
|
6897
8191
|
const metadata = objectMetadata(bytes.length, contentType, cacheControl);
|
|
@@ -7161,13 +8455,25 @@ class StorageHandler {
|
|
|
7161
8455
|
return null;
|
|
7162
8456
|
}
|
|
7163
8457
|
async download(ctx, bucketId, key, head) {
|
|
8458
|
+
const row = await this.loadAuthenticatedObject(ctx, bucketId, key);
|
|
8459
|
+
if (row instanceof Response)
|
|
8460
|
+
return row;
|
|
8461
|
+
return this.serveObject(row, head);
|
|
8462
|
+
}
|
|
8463
|
+
async loadAuthenticatedObject(ctx, bucketId, key) {
|
|
7164
8464
|
const res = await this.db.withContext(ctx, (q) => q(`select * from storage.objects where bucket_id = $1 and name = $2`, [bucketId, key]));
|
|
7165
8465
|
const row = res.rows[0];
|
|
7166
8466
|
if (!row)
|
|
7167
8467
|
return storageError(404, "not_found", "Object not found");
|
|
7168
|
-
return
|
|
8468
|
+
return row;
|
|
7169
8469
|
}
|
|
7170
8470
|
async downloadPublic(bucketId, key, head) {
|
|
8471
|
+
const row = await this.loadPublicObject(bucketId, key);
|
|
8472
|
+
if (row instanceof Response)
|
|
8473
|
+
return row;
|
|
8474
|
+
return this.serveObject(row, head);
|
|
8475
|
+
}
|
|
8476
|
+
async loadPublicObject(bucketId, key) {
|
|
7171
8477
|
const bucket = await this.loadBucket(bucketId);
|
|
7172
8478
|
if (!bucket?.public)
|
|
7173
8479
|
return storageError(400, "not_found", "Bucket is not public");
|
|
@@ -7178,24 +8484,16 @@ class StorageHandler {
|
|
|
7178
8484
|
const row = res.rows[0];
|
|
7179
8485
|
if (!row)
|
|
7180
8486
|
return storageError(404, "not_found", "Object not found");
|
|
7181
|
-
return
|
|
8487
|
+
return row;
|
|
7182
8488
|
}
|
|
7183
8489
|
async serveObject(row, head) {
|
|
7184
8490
|
const bytes = await this.readObjectBytes(row);
|
|
7185
8491
|
if (bytes === null)
|
|
7186
8492
|
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
|
-
};
|
|
8493
|
+
const contentType = String(row.metadata?.mimetype ?? "application/octet-stream");
|
|
8494
|
+
const headers = objectHeaders(row, bytes.length);
|
|
7197
8495
|
if (isRenderableActiveType(contentType))
|
|
7198
|
-
headers
|
|
8496
|
+
headers.set("content-disposition", "attachment");
|
|
7199
8497
|
return new Response(head ? null : bytes, { status: 200, headers });
|
|
7200
8498
|
}
|
|
7201
8499
|
async removeObjects(req, ctx, bucketId) {
|
|
@@ -7306,9 +8604,11 @@ class StorageHandler {
|
|
|
7306
8604
|
throw error;
|
|
7307
8605
|
}
|
|
7308
8606
|
async readObjectBytes(row) {
|
|
7309
|
-
|
|
7310
|
-
|
|
7311
|
-
|
|
8607
|
+
return this.driver.get(storageKey(row));
|
|
8608
|
+
}
|
|
8609
|
+
async readObjectSource(row) {
|
|
8610
|
+
const key = storageKey(row);
|
|
8611
|
+
return this.driver.getBlob ? this.driver.getBlob(key) : this.driver.get(key);
|
|
7312
8612
|
}
|
|
7313
8613
|
async cleanupObjectRows(rows) {
|
|
7314
8614
|
const keys = rows.flatMap((row) => [
|
|
@@ -7411,6 +8711,12 @@ class StorageHandler {
|
|
|
7411
8711
|
return json3(200, out);
|
|
7412
8712
|
}
|
|
7413
8713
|
async redeemSignedUrl(url, bucketId, key) {
|
|
8714
|
+
const row = await this.loadSignedObject(url, bucketId, key);
|
|
8715
|
+
if (row instanceof Response)
|
|
8716
|
+
return row;
|
|
8717
|
+
return this.serveObject(row, false);
|
|
8718
|
+
}
|
|
8719
|
+
async loadSignedObject(url, bucketId, key) {
|
|
7414
8720
|
const token = url.searchParams.get("token") ?? "";
|
|
7415
8721
|
const claims = await verifyJwt(token, this.config.jwtSecret);
|
|
7416
8722
|
if (!claims || claims.url !== `${bucketId}/${key}` || claims.type !== "download") {
|
|
@@ -7423,7 +8729,7 @@ class StorageHandler {
|
|
|
7423
8729
|
const row = res.rows[0];
|
|
7424
8730
|
if (!row)
|
|
7425
8731
|
return storageError(404, "not_found", "Object not found");
|
|
7426
|
-
return
|
|
8732
|
+
return row;
|
|
7427
8733
|
}
|
|
7428
8734
|
async signUploadUrl(ctx, bucketId, key) {
|
|
7429
8735
|
const keyErr = invalidObjectKey(key);
|
|
@@ -7481,6 +8787,9 @@ function objectJson(r) {
|
|
|
7481
8787
|
|
|
7482
8788
|
class StorageValidationError extends Error {
|
|
7483
8789
|
}
|
|
8790
|
+
|
|
8791
|
+
class StorageObjectMissingError extends Error {
|
|
8792
|
+
}
|
|
7484
8793
|
function parseSizeLimit(v) {
|
|
7485
8794
|
if (v === null || v === undefined || v === "")
|
|
7486
8795
|
return null;
|
|
@@ -7503,14 +8812,32 @@ function objectMetadata(size, contentType, cacheControl) {
|
|
|
7503
8812
|
httpStatusCode: 200
|
|
7504
8813
|
};
|
|
7505
8814
|
}
|
|
8815
|
+
function objectHeaders(row, contentLength) {
|
|
8816
|
+
const metadata = row.metadata ?? {};
|
|
8817
|
+
return new Headers({
|
|
8818
|
+
"content-type": String(metadata.mimetype ?? "application/octet-stream"),
|
|
8819
|
+
"content-length": String(contentLength),
|
|
8820
|
+
"cache-control": String(metadata.cacheControl ?? "no-cache"),
|
|
8821
|
+
etag: String(metadata.eTag ?? '""'),
|
|
8822
|
+
"last-modified": new Date(String(metadata.lastModified ?? Date.now())).toUTCString(),
|
|
8823
|
+
"x-content-type-options": "nosniff"
|
|
8824
|
+
});
|
|
8825
|
+
}
|
|
8826
|
+
function objectSize(row) {
|
|
8827
|
+
const size = Number(row.metadata?.size);
|
|
8828
|
+
return Number.isFinite(size) && size >= 0 ? size : undefined;
|
|
8829
|
+
}
|
|
7506
8830
|
function objectVersionKey(version) {
|
|
7507
8831
|
return `.supacloud-lite/objects/${version}`;
|
|
7508
8832
|
}
|
|
8833
|
+
function storageKey(row) {
|
|
8834
|
+
return isVersionedObjectVersion(row.version) ? objectVersionKey(row.version) : legacyObjectKey(row);
|
|
8835
|
+
}
|
|
7509
8836
|
function createObjectVersion() {
|
|
7510
|
-
return `${
|
|
8837
|
+
return `${OBJECT_VERSION_PREFIX2}${crypto.randomUUID()}`;
|
|
7511
8838
|
}
|
|
7512
8839
|
function isVersionedObjectVersion(version) {
|
|
7513
|
-
return version?.startsWith(
|
|
8840
|
+
return version?.startsWith(OBJECT_VERSION_PREFIX2) ?? false;
|
|
7514
8841
|
}
|
|
7515
8842
|
function isInternalStorageBucket(bucketId) {
|
|
7516
8843
|
return bucketId === INTERNAL_STORAGE_BUCKET || bucketId.startsWith(`${INTERNAL_STORAGE_BUCKET}/`);
|
|
@@ -8794,7 +10121,7 @@ ${msg.text}` : `[mail] to=${msg.to} subject="${msg.subject}"`));
|
|
|
8794
10121
|
const res = await safeHandle(req);
|
|
8795
10122
|
try {
|
|
8796
10123
|
const p = new URL(req.url).pathname;
|
|
8797
|
-
if (p !== "/health" && p !== "/") {
|
|
10124
|
+
if (p !== "/health" && p !== "/" && p !== "/auth/v1/health") {
|
|
8798
10125
|
const level = res.status >= 500 ? "error" : res.status >= 400 ? "warn" : "info";
|
|
8799
10126
|
logs.push(`${req.method} ${p} \u2192 ${res.status}`, level);
|
|
8800
10127
|
}
|
|
@@ -8859,21 +10186,797 @@ function withCors(res) {
|
|
|
8859
10186
|
}
|
|
8860
10187
|
|
|
8861
10188
|
// src/runtime/node/db-diff.ts
|
|
8862
|
-
import {
|
|
10189
|
+
import { mkdtempSync as mkdtempSync2 } from "fs";
|
|
10190
|
+
import { mkdir as mkdir2, rm, writeFile as writeFile2 } from "fs/promises";
|
|
10191
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
10192
|
+
import { dirname as dirname2, join as join2 } from "path";
|
|
10193
|
+
|
|
10194
|
+
// src/runtime/node/native/engine.ts
|
|
10195
|
+
import { execFileSync, spawn } from "child_process";
|
|
10196
|
+
import { createHash as createHash2, randomBytes as randomBytes2 } from "crypto";
|
|
10197
|
+
import { appendFileSync, chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, renameSync, rmSync } from "fs";
|
|
10198
|
+
import { writeFile } from "fs/promises";
|
|
10199
|
+
import { homedir, tmpdir } from "os";
|
|
8863
10200
|
import { join } from "path";
|
|
10201
|
+
import { extract as extractTar } from "tar";
|
|
10202
|
+
|
|
10203
|
+
// src/runtime/node/native/wire.ts
|
|
10204
|
+
import { createConnection } from "net";
|
|
10205
|
+
import { createHash, createHmac, pbkdf2Sync, randomBytes } from "crypto";
|
|
10206
|
+
|
|
10207
|
+
class PgWireError extends Error {
|
|
10208
|
+
code;
|
|
10209
|
+
detail;
|
|
10210
|
+
hint;
|
|
10211
|
+
severity;
|
|
10212
|
+
constructor(fields) {
|
|
10213
|
+
super(fields.get("M") ?? "postgres error");
|
|
10214
|
+
this.code = fields.get("C");
|
|
10215
|
+
this.detail = fields.get("D");
|
|
10216
|
+
this.hint = fields.get("H");
|
|
10217
|
+
this.severity = fields.get("S");
|
|
10218
|
+
}
|
|
10219
|
+
}
|
|
10220
|
+
|
|
10221
|
+
class PgWireClient {
|
|
10222
|
+
socket;
|
|
10223
|
+
buffer = Buffer.alloc(0);
|
|
10224
|
+
pending = null;
|
|
10225
|
+
queue = Promise.resolve();
|
|
10226
|
+
closed = false;
|
|
10227
|
+
onNotification = null;
|
|
10228
|
+
static async connect(opts) {
|
|
10229
|
+
const client = new PgWireClient;
|
|
10230
|
+
await client.open(opts);
|
|
10231
|
+
return client;
|
|
10232
|
+
}
|
|
10233
|
+
open(opts) {
|
|
10234
|
+
return new Promise((resolve2, reject) => {
|
|
10235
|
+
this.socket = opts.socketPath ? createConnection(opts.socketPath) : createConnection(opts.port ?? 5432, opts.host ?? "127.0.0.1");
|
|
10236
|
+
this.socket.on("error", (e) => {
|
|
10237
|
+
if (this.pending)
|
|
10238
|
+
this.pending.reject(e);
|
|
10239
|
+
reject(e);
|
|
10240
|
+
});
|
|
10241
|
+
this.socket.on("close", () => {
|
|
10242
|
+
this.closed = true;
|
|
10243
|
+
this.pending?.reject(new Error("connection closed"));
|
|
10244
|
+
});
|
|
10245
|
+
this.socket.on("connect", () => {
|
|
10246
|
+
const params = `user\x00${opts.user}\x00database\x00${opts.database}\x00client_encoding\x00UTF8\x00\x00`;
|
|
10247
|
+
const body = Buffer.from(params, "utf8");
|
|
10248
|
+
const msg = Buffer.alloc(8 + body.length);
|
|
10249
|
+
msg.writeInt32BE(8 + body.length, 0);
|
|
10250
|
+
msg.writeInt32BE(196608, 4);
|
|
10251
|
+
body.copy(msg, 8);
|
|
10252
|
+
this.socket.write(msg);
|
|
10253
|
+
});
|
|
10254
|
+
let clientNonce = "";
|
|
10255
|
+
let clientFirstBare = "";
|
|
10256
|
+
let serverSignature = "";
|
|
10257
|
+
const needPassword = () => {
|
|
10258
|
+
if (opts.password == null) {
|
|
10259
|
+
reject(new Error("the server requested a password but none was provided"));
|
|
10260
|
+
return false;
|
|
10261
|
+
}
|
|
10262
|
+
return true;
|
|
10263
|
+
};
|
|
10264
|
+
const startupHandler = (chunk) => {
|
|
10265
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
10266
|
+
let msg;
|
|
10267
|
+
while ((msg = this.nextMessage()) !== null) {
|
|
10268
|
+
const [type, payload] = msg;
|
|
10269
|
+
if (type === 82) {
|
|
10270
|
+
const code = payload.readInt32BE(0);
|
|
10271
|
+
if (code === 0) {} else if (code === 3) {
|
|
10272
|
+
if (!needPassword())
|
|
10273
|
+
return;
|
|
10274
|
+
this.socket.write(message(112, cstring(opts.password)));
|
|
10275
|
+
} else if (code === 5) {
|
|
10276
|
+
if (!needPassword())
|
|
10277
|
+
return;
|
|
10278
|
+
const salt = payload.subarray(4, 8);
|
|
10279
|
+
const inner = md5Hex(Buffer.from(opts.password + opts.user, "utf8"));
|
|
10280
|
+
const token = "md5" + md5Hex(Buffer.concat([Buffer.from(inner, "utf8"), salt]));
|
|
10281
|
+
this.socket.write(message(112, cstring(token)));
|
|
10282
|
+
} else if (code === 10) {
|
|
10283
|
+
if (!needPassword())
|
|
10284
|
+
return;
|
|
10285
|
+
const mechs = payload.subarray(4).toString("utf8").split("\x00").filter(Boolean);
|
|
10286
|
+
if (!mechs.includes("SCRAM-SHA-256")) {
|
|
10287
|
+
reject(new Error(`no supported SASL mechanism (server offered: ${mechs.join(", ")})`));
|
|
10288
|
+
return;
|
|
10289
|
+
}
|
|
10290
|
+
clientNonce = randomBytes(18).toString("base64");
|
|
10291
|
+
clientFirstBare = `n=,r=${clientNonce}`;
|
|
10292
|
+
const initial = Buffer.from(`n,,${clientFirstBare}`, "utf8");
|
|
10293
|
+
this.socket.write(message(112, Buffer.concat([cstring("SCRAM-SHA-256"), int32(initial.length), initial])));
|
|
10294
|
+
} else if (code === 11) {
|
|
10295
|
+
const serverFirst = payload.subarray(4).toString("utf8");
|
|
10296
|
+
const attrs = scramAttrs(serverFirst);
|
|
10297
|
+
if (!attrs.r?.startsWith(clientNonce)) {
|
|
10298
|
+
reject(new Error("SCRAM: server nonce does not extend client nonce"));
|
|
10299
|
+
return;
|
|
10300
|
+
}
|
|
10301
|
+
const salt = Buffer.from(attrs.s, "base64");
|
|
10302
|
+
const iterations = parseInt(attrs.i, 10);
|
|
10303
|
+
const saltedPassword = pbkdf2Sync(opts.password, salt, iterations, 32, "sha256");
|
|
10304
|
+
const clientKey = hmac(saltedPassword, "Client Key");
|
|
10305
|
+
const storedKey = sha256(clientKey);
|
|
10306
|
+
const finalNoProof = `c=biws,r=${attrs.r}`;
|
|
10307
|
+
const authMessage = `${clientFirstBare},${serverFirst},${finalNoProof}`;
|
|
10308
|
+
const clientSignature = hmac(storedKey, authMessage);
|
|
10309
|
+
const proof = xorBuffers(clientKey, clientSignature);
|
|
10310
|
+
serverSignature = hmac(hmac(saltedPassword, "Server Key"), authMessage).toString("base64");
|
|
10311
|
+
const clientFinal = `${finalNoProof},p=${proof.toString("base64")}`;
|
|
10312
|
+
this.socket.write(message(112, Buffer.from(clientFinal, "utf8")));
|
|
10313
|
+
} else if (code === 12) {
|
|
10314
|
+
const v = scramAttrs(payload.subarray(4).toString("utf8")).v;
|
|
10315
|
+
if (v && serverSignature && v !== serverSignature) {
|
|
10316
|
+
reject(new Error("SCRAM: server signature verification failed"));
|
|
10317
|
+
return;
|
|
10318
|
+
}
|
|
10319
|
+
} else {
|
|
10320
|
+
reject(new Error(`unsupported auth method ${code}`));
|
|
10321
|
+
return;
|
|
10322
|
+
}
|
|
10323
|
+
} else if (type === 69) {
|
|
10324
|
+
reject(new PgWireError(parseErrorFields(payload)));
|
|
10325
|
+
return;
|
|
10326
|
+
} else if (type === 90) {
|
|
10327
|
+
this.socket.off("data", startupHandler);
|
|
10328
|
+
this.socket.on("data", (c) => {
|
|
10329
|
+
this.buffer = Buffer.concat([this.buffer, c]);
|
|
10330
|
+
this.processMessages();
|
|
10331
|
+
});
|
|
10332
|
+
resolve2();
|
|
10333
|
+
return;
|
|
10334
|
+
}
|
|
10335
|
+
}
|
|
10336
|
+
};
|
|
10337
|
+
this.socket.on("data", startupHandler);
|
|
10338
|
+
});
|
|
10339
|
+
}
|
|
10340
|
+
run(send) {
|
|
10341
|
+
const op = this.queue.then(() => new Promise((resolve2, reject) => {
|
|
10342
|
+
if (this.closed)
|
|
10343
|
+
return reject(new Error("connection closed"));
|
|
10344
|
+
this.pending = { resolve: resolve2, reject, results: [], columns: [], error: null };
|
|
10345
|
+
send();
|
|
10346
|
+
}));
|
|
10347
|
+
this.queue = op.catch(() => {});
|
|
10348
|
+
return op;
|
|
10349
|
+
}
|
|
10350
|
+
async exec(sql) {
|
|
10351
|
+
return this.run(() => this.socket.write(message(81, cstring(sql))));
|
|
10352
|
+
}
|
|
10353
|
+
async query(sql, params = []) {
|
|
10354
|
+
const results = await this.run(() => {
|
|
10355
|
+
const parse = message(80, Buffer.concat([cstring(""), cstring(sql), int16(0)]));
|
|
10356
|
+
const paramBufs = [int16(0), int16(params.length)];
|
|
10357
|
+
for (const p of params) {
|
|
10358
|
+
if (p === null || p === undefined) {
|
|
10359
|
+
paramBufs.push(int32(-1));
|
|
10360
|
+
} else {
|
|
10361
|
+
const b = Buffer.from(String(p), "utf8");
|
|
10362
|
+
paramBufs.push(int32(b.length), b);
|
|
10363
|
+
}
|
|
10364
|
+
}
|
|
10365
|
+
paramBufs.push(int16(0));
|
|
10366
|
+
const bind = message(66, Buffer.concat([cstring(""), cstring(""), ...paramBufs]));
|
|
10367
|
+
const describe = message(68, Buffer.concat([Buffer.from("P"), cstring("")]));
|
|
10368
|
+
const execute = message(69, Buffer.concat([cstring(""), int32(0)]));
|
|
10369
|
+
const sync = message(83, Buffer.alloc(0));
|
|
10370
|
+
this.socket.write(Buffer.concat([parse, bind, describe, execute, sync]));
|
|
10371
|
+
});
|
|
10372
|
+
return results[0] ?? { rows: [] };
|
|
10373
|
+
}
|
|
10374
|
+
close() {
|
|
10375
|
+
return new Promise((resolve2) => {
|
|
10376
|
+
if (this.closed)
|
|
10377
|
+
return resolve2();
|
|
10378
|
+
this.socket.write(message(88, Buffer.alloc(0)));
|
|
10379
|
+
this.socket.end(() => resolve2());
|
|
10380
|
+
});
|
|
10381
|
+
}
|
|
10382
|
+
nextMessage() {
|
|
10383
|
+
if (this.buffer.length < 5)
|
|
10384
|
+
return null;
|
|
10385
|
+
const type = this.buffer[0];
|
|
10386
|
+
const length = this.buffer.readInt32BE(1);
|
|
10387
|
+
if (this.buffer.length < 1 + length)
|
|
10388
|
+
return null;
|
|
10389
|
+
const payload = this.buffer.subarray(5, 1 + length);
|
|
10390
|
+
this.buffer = this.buffer.subarray(1 + length);
|
|
10391
|
+
return [type, Buffer.from(payload)];
|
|
10392
|
+
}
|
|
10393
|
+
processMessages() {
|
|
10394
|
+
let msg;
|
|
10395
|
+
while ((msg = this.nextMessage()) !== null) {
|
|
10396
|
+
const [type, payload] = msg;
|
|
10397
|
+
const p = this.pending;
|
|
10398
|
+
switch (type) {
|
|
10399
|
+
case 84: {
|
|
10400
|
+
if (!p)
|
|
10401
|
+
break;
|
|
10402
|
+
const count = payload.readInt16BE(0);
|
|
10403
|
+
let off = 2;
|
|
10404
|
+
const columns = [];
|
|
10405
|
+
for (let i = 0;i < count; i++) {
|
|
10406
|
+
const end = payload.indexOf(0, off);
|
|
10407
|
+
const name = payload.toString("utf8", off, end);
|
|
10408
|
+
off = end + 1;
|
|
10409
|
+
const typeOid = payload.readInt32BE(off + 6);
|
|
10410
|
+
off += 18;
|
|
10411
|
+
columns.push({ name, typeOid });
|
|
10412
|
+
}
|
|
10413
|
+
p.columns = columns;
|
|
10414
|
+
break;
|
|
10415
|
+
}
|
|
10416
|
+
case 68: {
|
|
10417
|
+
if (!p)
|
|
10418
|
+
break;
|
|
10419
|
+
const count = payload.readInt16BE(0);
|
|
10420
|
+
let off = 2;
|
|
10421
|
+
const row = {};
|
|
10422
|
+
for (let i = 0;i < count; i++) {
|
|
10423
|
+
const len = payload.readInt32BE(off);
|
|
10424
|
+
off += 4;
|
|
10425
|
+
let value = null;
|
|
10426
|
+
if (len >= 0) {
|
|
10427
|
+
value = decodeValue(payload.toString("utf8", off, off + len), p.columns[i]?.typeOid ?? 25);
|
|
10428
|
+
off += len;
|
|
10429
|
+
}
|
|
10430
|
+
row[p.columns[i]?.name ?? `col${i}`] = value;
|
|
10431
|
+
}
|
|
10432
|
+
if (p.results.length === 0)
|
|
10433
|
+
p.results.push({ rows: [] });
|
|
10434
|
+
p.results[p.results.length - 1].rows.push(row);
|
|
10435
|
+
break;
|
|
10436
|
+
}
|
|
10437
|
+
case 67: {
|
|
10438
|
+
if (!p)
|
|
10439
|
+
break;
|
|
10440
|
+
const tag = payload.toString("utf8", 0, payload.length - 1);
|
|
10441
|
+
const parts = tag.split(" ");
|
|
10442
|
+
const affected = parseInt(parts[parts.length - 1], 10);
|
|
10443
|
+
if (p.results.length === 0)
|
|
10444
|
+
p.results.push({ rows: [] });
|
|
10445
|
+
const current = p.results[p.results.length - 1];
|
|
10446
|
+
if (!Number.isNaN(affected))
|
|
10447
|
+
current.affectedRows = affected;
|
|
10448
|
+
p.results.push({ rows: [] });
|
|
10449
|
+
p.columns = [];
|
|
10450
|
+
break;
|
|
10451
|
+
}
|
|
10452
|
+
case 69: {
|
|
10453
|
+
if (p)
|
|
10454
|
+
p.error = new PgWireError(parseErrorFields(payload));
|
|
10455
|
+
break;
|
|
10456
|
+
}
|
|
10457
|
+
case 65: {
|
|
10458
|
+
payload.readInt32BE(0);
|
|
10459
|
+
const channelEnd = payload.indexOf(0, 4);
|
|
10460
|
+
const channel = payload.toString("utf8", 4, channelEnd);
|
|
10461
|
+
const payloadEnd = payload.indexOf(0, channelEnd + 1);
|
|
10462
|
+
const body = payload.toString("utf8", channelEnd + 1, payloadEnd);
|
|
10463
|
+
this.onNotification?.(channel, body);
|
|
10464
|
+
break;
|
|
10465
|
+
}
|
|
10466
|
+
case 90: {
|
|
10467
|
+
if (!p)
|
|
10468
|
+
break;
|
|
10469
|
+
this.pending = null;
|
|
10470
|
+
if (p.error)
|
|
10471
|
+
p.reject(p.error);
|
|
10472
|
+
else {
|
|
10473
|
+
const results = p.results.filter((r, i) => i < p.results.length - 1 || r.rows.length > 0 || r.affectedRows !== undefined);
|
|
10474
|
+
p.resolve(results.length > 0 ? results : [{ rows: [] }]);
|
|
10475
|
+
}
|
|
10476
|
+
break;
|
|
10477
|
+
}
|
|
10478
|
+
}
|
|
10479
|
+
}
|
|
10480
|
+
}
|
|
10481
|
+
}
|
|
10482
|
+
var hmac = (key, data) => createHmac("sha256", key).update(data, "utf8").digest();
|
|
10483
|
+
var sha256 = (b) => createHash("sha256").update(b).digest();
|
|
10484
|
+
var md5Hex = (b) => createHash("md5").update(b).digest("hex");
|
|
10485
|
+
function xorBuffers(a, b) {
|
|
10486
|
+
const out = Buffer.alloc(a.length);
|
|
10487
|
+
for (let i = 0;i < a.length; i++)
|
|
10488
|
+
out[i] = a[i] ^ b[i];
|
|
10489
|
+
return out;
|
|
10490
|
+
}
|
|
10491
|
+
function scramAttrs(s) {
|
|
10492
|
+
const out = {};
|
|
10493
|
+
for (const part of s.split(",")) {
|
|
10494
|
+
const eq = part.indexOf("=");
|
|
10495
|
+
if (eq > 0)
|
|
10496
|
+
out[part.slice(0, eq)] = part.slice(eq + 1);
|
|
10497
|
+
}
|
|
10498
|
+
return out;
|
|
10499
|
+
}
|
|
10500
|
+
function message(type, body) {
|
|
10501
|
+
const out = Buffer.alloc(5 + body.length);
|
|
10502
|
+
out[0] = type;
|
|
10503
|
+
out.writeInt32BE(4 + body.length, 1);
|
|
10504
|
+
body.copy(out, 5);
|
|
10505
|
+
return out;
|
|
10506
|
+
}
|
|
10507
|
+
var cstring = (s) => Buffer.from(s + "\x00", "utf8");
|
|
10508
|
+
var int16 = (n) => {
|
|
10509
|
+
const b = Buffer.alloc(2);
|
|
10510
|
+
b.writeInt16BE(n);
|
|
10511
|
+
return b;
|
|
10512
|
+
};
|
|
10513
|
+
var int32 = (n) => {
|
|
10514
|
+
const b = Buffer.alloc(4);
|
|
10515
|
+
b.writeInt32BE(n);
|
|
10516
|
+
return b;
|
|
10517
|
+
};
|
|
10518
|
+
function parseErrorFields(payload) {
|
|
10519
|
+
const fields = new Map;
|
|
10520
|
+
let off = 0;
|
|
10521
|
+
while (off < payload.length && payload[off] !== 0) {
|
|
10522
|
+
const key = String.fromCharCode(payload[off]);
|
|
10523
|
+
const end = payload.indexOf(0, off + 1);
|
|
10524
|
+
fields.set(key, payload.toString("utf8", off + 1, end));
|
|
10525
|
+
off = end + 1;
|
|
10526
|
+
}
|
|
10527
|
+
return fields;
|
|
10528
|
+
}
|
|
10529
|
+
function decodeValue(text, oid) {
|
|
10530
|
+
switch (oid) {
|
|
10531
|
+
case 16:
|
|
10532
|
+
return text === "t";
|
|
10533
|
+
case 20: {
|
|
10534
|
+
const n = Number(text);
|
|
10535
|
+
return Number.isSafeInteger(n) ? n : text;
|
|
10536
|
+
}
|
|
10537
|
+
case 21:
|
|
10538
|
+
case 23:
|
|
10539
|
+
case 26:
|
|
10540
|
+
return Number(text);
|
|
10541
|
+
case 700:
|
|
10542
|
+
case 701:
|
|
10543
|
+
return Number(text);
|
|
10544
|
+
case 114:
|
|
10545
|
+
case 3802:
|
|
10546
|
+
return JSON.parse(text);
|
|
10547
|
+
case 1114:
|
|
10548
|
+
return new Date(text.replace(" ", "T") + "Z");
|
|
10549
|
+
case 1184: {
|
|
10550
|
+
let iso3 = text.replace(" ", "T");
|
|
10551
|
+
if (/[+-]\d\d$/.test(iso3))
|
|
10552
|
+
iso3 += ":00";
|
|
10553
|
+
return new Date(iso3);
|
|
10554
|
+
}
|
|
10555
|
+
case 1000:
|
|
10556
|
+
return parsePgArray(text).map((v) => v === "t");
|
|
10557
|
+
case 1007:
|
|
10558
|
+
return parsePgArray(text).map((v) => v === null ? null : Number(v));
|
|
10559
|
+
case 1016:
|
|
10560
|
+
return parsePgArray(text).map((v) => {
|
|
10561
|
+
if (v === null)
|
|
10562
|
+
return null;
|
|
10563
|
+
const n = Number(v);
|
|
10564
|
+
return Number.isSafeInteger(n) ? n : v;
|
|
10565
|
+
});
|
|
10566
|
+
case 1003:
|
|
10567
|
+
case 1009:
|
|
10568
|
+
case 1015:
|
|
10569
|
+
return parsePgArray(text);
|
|
10570
|
+
default:
|
|
10571
|
+
return text;
|
|
10572
|
+
}
|
|
10573
|
+
}
|
|
10574
|
+
function parsePgArray(text) {
|
|
10575
|
+
const out = [];
|
|
10576
|
+
if (text.length < 2)
|
|
10577
|
+
return out;
|
|
10578
|
+
let i = 1;
|
|
10579
|
+
while (i < text.length - 1) {
|
|
10580
|
+
if (text[i] === ",") {
|
|
10581
|
+
i++;
|
|
10582
|
+
continue;
|
|
10583
|
+
}
|
|
10584
|
+
if (text[i] === '"') {
|
|
10585
|
+
let value = "";
|
|
10586
|
+
i++;
|
|
10587
|
+
while (text[i] !== '"') {
|
|
10588
|
+
if (text[i] === "\\")
|
|
10589
|
+
i++;
|
|
10590
|
+
value += text[i++];
|
|
10591
|
+
}
|
|
10592
|
+
i++;
|
|
10593
|
+
out.push(value);
|
|
10594
|
+
} else {
|
|
10595
|
+
let value = "";
|
|
10596
|
+
while (i < text.length - 1 && text[i] !== ",")
|
|
10597
|
+
value += text[i++];
|
|
10598
|
+
out.push(value === "NULL" ? null : value);
|
|
10599
|
+
}
|
|
10600
|
+
}
|
|
10601
|
+
return out;
|
|
10602
|
+
}
|
|
10603
|
+
|
|
10604
|
+
// src/runtime/db/engine.ts
|
|
10605
|
+
class Mutex {
|
|
10606
|
+
tail = Promise.resolve();
|
|
10607
|
+
async lock() {
|
|
10608
|
+
let release;
|
|
10609
|
+
const next = new Promise((r) => release = r);
|
|
10610
|
+
const prev = this.tail;
|
|
10611
|
+
this.tail = this.tail.then(() => next);
|
|
10612
|
+
await prev;
|
|
10613
|
+
return release;
|
|
10614
|
+
}
|
|
10615
|
+
async run(fn) {
|
|
10616
|
+
const release = await this.lock();
|
|
10617
|
+
try {
|
|
10618
|
+
return await fn();
|
|
10619
|
+
} finally {
|
|
10620
|
+
release();
|
|
10621
|
+
}
|
|
10622
|
+
}
|
|
10623
|
+
}
|
|
10624
|
+
|
|
10625
|
+
// src/runtime/node/native/wire-engine.ts
|
|
10626
|
+
async function buildWireEngine(options) {
|
|
10627
|
+
const queryClient = await options.connect();
|
|
10628
|
+
let listenerClient;
|
|
10629
|
+
try {
|
|
10630
|
+
listenerClient = await options.connect();
|
|
10631
|
+
} catch (error) {
|
|
10632
|
+
await queryClient.close().catch(() => {});
|
|
10633
|
+
throw error;
|
|
10634
|
+
}
|
|
10635
|
+
const queryMutex = new Mutex;
|
|
10636
|
+
const listenerMutex = new Mutex;
|
|
10637
|
+
const listeners = new Map;
|
|
10638
|
+
listenerClient.onNotification = (channel, payload) => {
|
|
10639
|
+
for (const listener of listeners.get(channel) ?? [])
|
|
10640
|
+
listener(payload);
|
|
10641
|
+
};
|
|
10642
|
+
const transactionClient = {
|
|
10643
|
+
async query(sql, params) {
|
|
10644
|
+
const queryResult = await queryClient.query(sql, normalizeParams(params));
|
|
10645
|
+
return { rows: queryResult.rows, affectedRows: queryResult.affectedRows };
|
|
10646
|
+
},
|
|
10647
|
+
async exec(sql) {
|
|
10648
|
+
await queryClient.exec(sql);
|
|
10649
|
+
}
|
|
10650
|
+
};
|
|
10651
|
+
let closePromise = null;
|
|
10652
|
+
return {
|
|
10653
|
+
query(sql, params) {
|
|
10654
|
+
return queryMutex.run(() => transactionClient.query(sql, params));
|
|
10655
|
+
},
|
|
10656
|
+
exec(sql) {
|
|
10657
|
+
return queryMutex.run(() => transactionClient.exec(sql));
|
|
10658
|
+
},
|
|
10659
|
+
transaction(callback) {
|
|
10660
|
+
return queryMutex.run(async () => {
|
|
10661
|
+
await queryClient.exec("begin");
|
|
10662
|
+
try {
|
|
10663
|
+
const response = await callback(transactionClient);
|
|
10664
|
+
await queryClient.exec("commit");
|
|
10665
|
+
return response;
|
|
10666
|
+
} catch (error) {
|
|
10667
|
+
await queryClient.exec("rollback").catch(() => {});
|
|
10668
|
+
throw error;
|
|
10669
|
+
}
|
|
10670
|
+
});
|
|
10671
|
+
},
|
|
10672
|
+
async listen(channel, listener) {
|
|
10673
|
+
return listenerMutex.run(async () => {
|
|
10674
|
+
let channelListeners = listeners.get(channel);
|
|
10675
|
+
if (!channelListeners) {
|
|
10676
|
+
channelListeners = new Set;
|
|
10677
|
+
await listenerClient.exec(`listen "${channel.replaceAll('"', '""')}"`);
|
|
10678
|
+
listeners.set(channel, channelListeners);
|
|
10679
|
+
}
|
|
10680
|
+
channelListeners.add(listener);
|
|
10681
|
+
return () => {
|
|
10682
|
+
channelListeners.delete(listener);
|
|
10683
|
+
};
|
|
10684
|
+
});
|
|
10685
|
+
},
|
|
10686
|
+
close() {
|
|
10687
|
+
closePromise ??= closeWireEngine(queryClient, listenerClient, options.onClose);
|
|
10688
|
+
return closePromise;
|
|
10689
|
+
}
|
|
10690
|
+
};
|
|
10691
|
+
}
|
|
10692
|
+
async function closeWireEngine(queryClient, listenerClient, onClose) {
|
|
10693
|
+
const closeResults = await Promise.allSettled([queryClient.close(), listenerClient.close()]);
|
|
10694
|
+
let engineCleanupError;
|
|
10695
|
+
try {
|
|
10696
|
+
await onClose?.();
|
|
10697
|
+
} catch (error) {
|
|
10698
|
+
engineCleanupError = error;
|
|
10699
|
+
}
|
|
10700
|
+
const connectionErrors = closeResults.flatMap((closeResult) => closeResult.status === "rejected" ? [closeResult.reason] : []);
|
|
10701
|
+
if (engineCleanupError !== undefined)
|
|
10702
|
+
connectionErrors.push(engineCleanupError);
|
|
10703
|
+
if (connectionErrors.length > 0)
|
|
10704
|
+
throw new AggregateError(connectionErrors, "native database cleanup failed");
|
|
10705
|
+
}
|
|
10706
|
+
function normalizeParams(params) {
|
|
10707
|
+
return params?.map((parameter) => {
|
|
10708
|
+
if (parameter === null || parameter === undefined)
|
|
10709
|
+
return null;
|
|
10710
|
+
if (Array.isArray(parameter))
|
|
10711
|
+
return toPgArrayLiteral(parameter);
|
|
10712
|
+
if (parameter instanceof Date)
|
|
10713
|
+
return parameter.toISOString();
|
|
10714
|
+
if (typeof parameter === "object")
|
|
10715
|
+
return JSON.stringify(parameter);
|
|
10716
|
+
return parameter;
|
|
10717
|
+
});
|
|
10718
|
+
}
|
|
10719
|
+
function toPgArrayLiteral(array) {
|
|
10720
|
+
const encoded = array.map((element) => {
|
|
10721
|
+
if (element === null || element === undefined)
|
|
10722
|
+
return "NULL";
|
|
10723
|
+
if (Array.isArray(element))
|
|
10724
|
+
return toPgArrayLiteral(element);
|
|
10725
|
+
if (typeof element === "number" || typeof element === "boolean")
|
|
10726
|
+
return String(element);
|
|
10727
|
+
const text = typeof element === "object" ? JSON.stringify(element) : String(element);
|
|
10728
|
+
return `"${text.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
10729
|
+
});
|
|
10730
|
+
return `{${encoded.join(",")}}`;
|
|
10731
|
+
}
|
|
10732
|
+
|
|
10733
|
+
// src/runtime/node/native/engine.ts
|
|
10734
|
+
var DEFAULT_PG_VERSION = "17.7.0";
|
|
10735
|
+
var NATIVE_POSTGRES_MAJOR = DEFAULT_PG_VERSION.split(".")[0];
|
|
10736
|
+
function isNativeEngineSupported() {
|
|
10737
|
+
return (process.platform === "darwin" || process.platform === "linux") && (process.arch === "arm64" || process.arch === "x64") && (process.platform !== "linux" || isGlibcLinux());
|
|
10738
|
+
}
|
|
10739
|
+
function isGlibcLinux() {
|
|
10740
|
+
try {
|
|
10741
|
+
const version = execFileSync("ldd", ["--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
10742
|
+
return /glibc|gnu libc/i.test(version);
|
|
10743
|
+
} catch {
|
|
10744
|
+
return false;
|
|
10745
|
+
}
|
|
10746
|
+
}
|
|
10747
|
+
function target() {
|
|
10748
|
+
const arch = process.arch === "arm64" ? "aarch64" : process.arch === "x64" ? "x86_64" : null;
|
|
10749
|
+
if (!arch)
|
|
10750
|
+
throw new Error(`unsupported architecture for native engine: ${process.arch}`);
|
|
10751
|
+
if (process.platform === "darwin")
|
|
10752
|
+
return `${arch}-apple-darwin`;
|
|
10753
|
+
if (process.platform === "linux")
|
|
10754
|
+
return `${arch}-unknown-linux-gnu`;
|
|
10755
|
+
throw new Error(`unsupported platform for native engine: ${process.platform} (use the default PGlite engine)`);
|
|
10756
|
+
}
|
|
10757
|
+
function isCompleteInstall(dir) {
|
|
10758
|
+
return existsSync(join(dir, "bin", "postgres")) && existsSync(join(dir, "share", "postgres.bki"));
|
|
10759
|
+
}
|
|
10760
|
+
var PINNED_SHA256 = {
|
|
10761
|
+
"postgresql-17.7.0-x86_64-unknown-linux-gnu": "66ad03281a43624f955c8e16ac975cb0ab751e7edf8ba35308e3b08dd7d065c3",
|
|
10762
|
+
"postgresql-17.7.0-aarch64-unknown-linux-gnu": "89cc2f089880cc8e5e6b7a29387829ec4e4779427855bc0b9fa187c8fce33c8b",
|
|
10763
|
+
"postgresql-17.7.0-x86_64-apple-darwin": "0dd8c25173524bad4ae8ef6b970da1ac40f4c1f231150c416ccb8cd06feff8f2",
|
|
10764
|
+
"postgresql-17.7.0-aarch64-apple-darwin": "727ac08d20a704014a0d51eb3300aa0c8e292c1cf0a1c99d4f4b1002e1420220"
|
|
10765
|
+
};
|
|
10766
|
+
async function verifyTarball(tarball, key, url) {
|
|
10767
|
+
const actual = createHash2("sha256").update(readFileSync(tarball)).digest("hex");
|
|
10768
|
+
const pinned = PINNED_SHA256[key];
|
|
10769
|
+
if (pinned) {
|
|
10770
|
+
if (actual !== pinned) {
|
|
10771
|
+
throw new Error(`postgres binary checksum mismatch for ${key}: expected ${pinned}, got ${actual}`);
|
|
10772
|
+
}
|
|
10773
|
+
return;
|
|
10774
|
+
}
|
|
10775
|
+
const res = await fetchRelease(`${url}.sha256`);
|
|
10776
|
+
if (!res.ok)
|
|
10777
|
+
throw new Error(`could not fetch checksum for ${key}: HTTP ${res.status}`);
|
|
10778
|
+
const expected = (await res.text()).trim().split(/\s+/)[0].toLowerCase();
|
|
10779
|
+
if (!/^[0-9a-f]{64}$/.test(expected))
|
|
10780
|
+
throw new Error(`malformed published checksum for ${key}`);
|
|
10781
|
+
if (actual !== expected) {
|
|
10782
|
+
throw new Error(`postgres binary checksum mismatch for ${key}: expected ${expected}, got ${actual}`);
|
|
10783
|
+
}
|
|
10784
|
+
}
|
|
10785
|
+
async function ensurePostgres(version = DEFAULT_PG_VERSION, cacheDir, log) {
|
|
10786
|
+
const t = target();
|
|
10787
|
+
const root = cacheDir ?? join(homedir(), ".cache", "supacloud-lite");
|
|
10788
|
+
const dir = join(root, `postgresql-${version}-${t}`);
|
|
10789
|
+
if (isCompleteInstall(dir))
|
|
10790
|
+
return dir;
|
|
10791
|
+
const url = `https://github.com/theseus-rs/postgresql-binaries/releases/download/${version}/postgresql-${version}-${t}.tar.gz`;
|
|
10792
|
+
mkdirSync(root, { recursive: true });
|
|
10793
|
+
const uniq = `${process.pid}-${randomBytes2(6).toString("hex")}`;
|
|
10794
|
+
const tarball = join(root, `pg-${version}-${uniq}.tar.gz`);
|
|
10795
|
+
const tmpDir = join(root, `.tmp-${version}-${t}-${uniq}`);
|
|
10796
|
+
try {
|
|
10797
|
+
if (isCompleteInstall(dir))
|
|
10798
|
+
return dir;
|
|
10799
|
+
log?.(`downloading postgres ${version} (${t})\u2026`);
|
|
10800
|
+
const res = await fetchRelease(url);
|
|
10801
|
+
if (!res.ok)
|
|
10802
|
+
throw new Error(`failed to download ${url}: HTTP ${res.status}`);
|
|
10803
|
+
await writeFile(tarball, Buffer.from(await res.arrayBuffer()));
|
|
10804
|
+
await verifyTarball(tarball, `postgresql-${version}-${t}`, url);
|
|
10805
|
+
mkdirSync(tmpDir, { recursive: true });
|
|
10806
|
+
await extractTar({ cwd: tmpDir, file: tarball, gzip: true, preserveOwner: false, strict: true, strip: 1 });
|
|
10807
|
+
if (!isCompleteInstall(tmpDir))
|
|
10808
|
+
throw new Error("postgres archive extracted incompletely");
|
|
10809
|
+
try {
|
|
10810
|
+
renameSync(tmpDir, dir);
|
|
10811
|
+
} catch {
|
|
10812
|
+
if (!isCompleteInstall(dir)) {
|
|
10813
|
+
rmSync(dir, { recursive: true, force: true });
|
|
10814
|
+
renameSync(tmpDir, dir);
|
|
10815
|
+
}
|
|
10816
|
+
}
|
|
10817
|
+
log?.(`postgres installed to ${dir}`);
|
|
10818
|
+
return dir;
|
|
10819
|
+
} finally {
|
|
10820
|
+
rmSync(tarball, { force: true });
|
|
10821
|
+
rmSync(tmpDir, { recursive: true, force: true });
|
|
10822
|
+
}
|
|
10823
|
+
}
|
|
10824
|
+
async function fetchRelease(url) {
|
|
10825
|
+
let lastError;
|
|
10826
|
+
for (let attempt = 1;attempt <= 3; attempt++) {
|
|
10827
|
+
try {
|
|
10828
|
+
const response = await fetch(url);
|
|
10829
|
+
if (response.ok || response.status < 500)
|
|
10830
|
+
return response;
|
|
10831
|
+
lastError = new Error(`failed to download ${url}: HTTP ${response.status}`);
|
|
10832
|
+
} catch (error) {
|
|
10833
|
+
lastError = error;
|
|
10834
|
+
}
|
|
10835
|
+
if (attempt < 3)
|
|
10836
|
+
await new Promise((resolve2) => setTimeout(resolve2, attempt * 500));
|
|
10837
|
+
}
|
|
10838
|
+
throw lastError instanceof Error ? lastError : new Error(`failed to download ${url}`);
|
|
10839
|
+
}
|
|
10840
|
+
var TUNED_CONF = `
|
|
10841
|
+
# supacloud-lite: memory-lean settings for an embedded, single-app Postgres
|
|
10842
|
+
listen_addresses = ''
|
|
10843
|
+
shared_buffers = 16MB
|
|
10844
|
+
dynamic_shared_memory_type = posix
|
|
10845
|
+
max_connections = 10
|
|
10846
|
+
wal_level = minimal
|
|
10847
|
+
max_wal_senders = 0
|
|
10848
|
+
logging_collector = off
|
|
10849
|
+
`;
|
|
10850
|
+
async function createNativeEngine(opts) {
|
|
10851
|
+
const releaseLock = await acquireDataDirLock(opts.dataDir, "native PostgreSQL");
|
|
10852
|
+
let socketDirectory;
|
|
10853
|
+
let postgres;
|
|
10854
|
+
let removeExitHandler;
|
|
10855
|
+
try {
|
|
10856
|
+
const installDir = await ensurePostgres(opts.version, opts.cacheDir, opts.log);
|
|
10857
|
+
const bin = (name) => join(installDir, "bin", name);
|
|
10858
|
+
if (!existsSync(join(opts.dataDir, "PG_VERSION"))) {
|
|
10859
|
+
mkdirSync(opts.dataDir, { recursive: true });
|
|
10860
|
+
try {
|
|
10861
|
+
execFileSync(bin("initdb"), ["-U", "postgres", "-A", "trust", "-E", "UTF8", "-D", opts.dataDir], {
|
|
10862
|
+
stdio: "pipe"
|
|
10863
|
+
});
|
|
10864
|
+
} catch (error) {
|
|
10865
|
+
const stderr = error.stderr?.toString() ?? "";
|
|
10866
|
+
throw new Error(`initdb failed:
|
|
10867
|
+
${stderr || error.message}`);
|
|
10868
|
+
}
|
|
10869
|
+
appendFileSync(join(opts.dataDir, "postgresql.conf"), TUNED_CONF);
|
|
10870
|
+
}
|
|
10871
|
+
removeStalePidFile(join(opts.dataDir, "postmaster.pid"));
|
|
10872
|
+
socketDirectory = mkdtempSync(join(tmpdir(), "scl-"));
|
|
10873
|
+
chmodSync(socketDirectory, 448);
|
|
10874
|
+
postgres = spawn(bin("postgres"), ["-D", opts.dataDir, "-k", socketDirectory, "-c", "timezone=UTC"], {
|
|
10875
|
+
stdio: ["ignore", "ignore", "pipe"],
|
|
10876
|
+
detached: false
|
|
10877
|
+
});
|
|
10878
|
+
let postgresExited = false;
|
|
10879
|
+
let postgresStderr = "";
|
|
10880
|
+
postgres.stderr?.on("data", (chunk) => {
|
|
10881
|
+
postgresStderr = (postgresStderr + chunk.toString()).slice(-4000);
|
|
10882
|
+
});
|
|
10883
|
+
postgres.on("exit", () => postgresExited = true);
|
|
10884
|
+
const killPostgres = () => {
|
|
10885
|
+
if (!postgresExited)
|
|
10886
|
+
postgres?.kill("SIGTERM");
|
|
10887
|
+
};
|
|
10888
|
+
process.once("exit", killPostgres);
|
|
10889
|
+
removeExitHandler = () => process.off("exit", killPostgres);
|
|
10890
|
+
const socketPath = join(socketDirectory, ".s.PGSQL.5432");
|
|
10891
|
+
const connect = async () => {
|
|
10892
|
+
const deadline = Date.now() + 20000;
|
|
10893
|
+
while (Date.now() <= deadline) {
|
|
10894
|
+
try {
|
|
10895
|
+
return await PgWireClient.connect({ socketPath, user: "postgres", database: "postgres" });
|
|
10896
|
+
} catch (error) {
|
|
10897
|
+
if (postgresExited) {
|
|
10898
|
+
const detail = postgresStderr.trim();
|
|
10899
|
+
throw new Error(`embedded postgres failed to start${detail ? `:
|
|
10900
|
+
${detail}` : " (no output)"}
|
|
10901
|
+
|
|
10902
|
+
` + `data dir: ${opts.dataDir}
|
|
10903
|
+
` + "If a previous run is still holding it, stop it; or delete the data dir to start fresh.");
|
|
10904
|
+
}
|
|
10905
|
+
await new Promise((resolve2) => setTimeout(resolve2, 150));
|
|
10906
|
+
}
|
|
10907
|
+
}
|
|
10908
|
+
throw new Error(`timed out waiting for embedded postgres at ${socketPath}`);
|
|
10909
|
+
};
|
|
10910
|
+
return await buildWireEngine({
|
|
10911
|
+
connect,
|
|
10912
|
+
onClose: async () => {
|
|
10913
|
+
removeExitHandler?.();
|
|
10914
|
+
await stopPostgres(postgres, () => postgresExited);
|
|
10915
|
+
rmSync(socketDirectory, { recursive: true, force: true });
|
|
10916
|
+
await releaseLock();
|
|
10917
|
+
}
|
|
10918
|
+
});
|
|
10919
|
+
} catch (error) {
|
|
10920
|
+
removeExitHandler?.();
|
|
10921
|
+
if (postgres)
|
|
10922
|
+
await stopPostgres(postgres, () => postgres.exitCode !== null);
|
|
10923
|
+
if (socketDirectory)
|
|
10924
|
+
rmSync(socketDirectory, { recursive: true, force: true });
|
|
10925
|
+
await releaseLock();
|
|
10926
|
+
throw error;
|
|
10927
|
+
}
|
|
10928
|
+
}
|
|
10929
|
+
async function stopPostgres(postgres, hasExited) {
|
|
10930
|
+
if (hasExited())
|
|
10931
|
+
return;
|
|
10932
|
+
postgres.kill("SIGINT");
|
|
10933
|
+
await new Promise((resolve2) => {
|
|
10934
|
+
const killTimeout = setTimeout(() => {
|
|
10935
|
+
postgres.kill("SIGKILL");
|
|
10936
|
+
resolve2();
|
|
10937
|
+
}, 5000);
|
|
10938
|
+
postgres.once("exit", () => {
|
|
10939
|
+
clearTimeout(killTimeout);
|
|
10940
|
+
resolve2();
|
|
10941
|
+
});
|
|
10942
|
+
});
|
|
10943
|
+
}
|
|
10944
|
+
function removeStalePidFile(pidPath) {
|
|
10945
|
+
if (!existsSync(pidPath))
|
|
10946
|
+
return;
|
|
10947
|
+
try {
|
|
10948
|
+
const pid = Number.parseInt(readFileSync(pidPath, "utf8").split(`
|
|
10949
|
+
`)[0]?.trim() ?? "", 10);
|
|
10950
|
+
if (!pid) {
|
|
10951
|
+
rmSync(pidPath, { force: true });
|
|
10952
|
+
return;
|
|
10953
|
+
}
|
|
10954
|
+
try {
|
|
10955
|
+
process.kill(pid, 0);
|
|
10956
|
+
} catch {
|
|
10957
|
+
rmSync(pidPath, { force: true });
|
|
10958
|
+
}
|
|
10959
|
+
} catch {}
|
|
10960
|
+
}
|
|
10961
|
+
|
|
10962
|
+
// src/runtime/node/db-diff.ts
|
|
8864
10963
|
async function computeDbDiff(opts) {
|
|
8865
10964
|
const schema = opts.schema ?? "public";
|
|
8866
|
-
|
|
8867
|
-
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
8868
|
-
migrations: opts.migrations,
|
|
8869
|
-
startRuntimeServices: false
|
|
8870
|
-
});
|
|
10965
|
+
let shadow;
|
|
8871
10966
|
let live;
|
|
10967
|
+
let unclaimedLiveEngine = opts.liveEngine;
|
|
8872
10968
|
let operationFailed = false;
|
|
8873
10969
|
try {
|
|
10970
|
+
shadow = await createBackend({
|
|
10971
|
+
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
10972
|
+
migrations: opts.migrations,
|
|
10973
|
+
startRuntimeServices: false
|
|
10974
|
+
});
|
|
10975
|
+
const liveEngine = unclaimedLiveEngine;
|
|
10976
|
+
unclaimedLiveEngine = undefined;
|
|
8874
10977
|
live = await createBackend({
|
|
8875
|
-
engine:
|
|
8876
|
-
dataDir:
|
|
10978
|
+
engine: liveEngine,
|
|
10979
|
+
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
8877
10980
|
migrations: opts.migrations,
|
|
8878
10981
|
startRuntimeServices: false
|
|
8879
10982
|
});
|
|
@@ -8885,26 +10988,57 @@ async function computeDbDiff(opts) {
|
|
|
8885
10988
|
throw error;
|
|
8886
10989
|
} finally {
|
|
8887
10990
|
try {
|
|
8888
|
-
await
|
|
10991
|
+
await closeResources(unclaimedLiveEngine, live, shadow);
|
|
8889
10992
|
} catch (error) {
|
|
8890
10993
|
if (!operationFailed)
|
|
8891
10994
|
throw error;
|
|
8892
10995
|
}
|
|
8893
10996
|
}
|
|
8894
10997
|
}
|
|
10998
|
+
function shadowNativeDataDir() {
|
|
10999
|
+
return join2(mkdtempSync2(join2(tmpdir2(), "supacloud-lite-shadow-")), "pg");
|
|
11000
|
+
}
|
|
11001
|
+
async function createTemporaryNativeEngine() {
|
|
11002
|
+
const dataDir = shadowNativeDataDir();
|
|
11003
|
+
let engine;
|
|
11004
|
+
try {
|
|
11005
|
+
engine = await createNativeEngine({ dataDir });
|
|
11006
|
+
} catch (error) {
|
|
11007
|
+
try {
|
|
11008
|
+
await rm(dirname2(dataDir), { recursive: true, force: true });
|
|
11009
|
+
} catch (cleanupError) {
|
|
11010
|
+
throw new AggregateError([error, cleanupError], "temporary native database initialization cleanup failed");
|
|
11011
|
+
}
|
|
11012
|
+
throw error;
|
|
11013
|
+
}
|
|
11014
|
+
return {
|
|
11015
|
+
...engine,
|
|
11016
|
+
async close() {
|
|
11017
|
+
try {
|
|
11018
|
+
await engine.close();
|
|
11019
|
+
} finally {
|
|
11020
|
+
await rm(dirname2(dataDir), { recursive: true, force: true });
|
|
11021
|
+
}
|
|
11022
|
+
}
|
|
11023
|
+
};
|
|
11024
|
+
}
|
|
8895
11025
|
async function pullSchema(opts) {
|
|
8896
11026
|
const schema = opts.schema ?? "public";
|
|
8897
|
-
|
|
8898
|
-
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
8899
|
-
migrations: opts.migrations,
|
|
8900
|
-
startRuntimeServices: false
|
|
8901
|
-
});
|
|
11027
|
+
let shadow;
|
|
8902
11028
|
let live;
|
|
11029
|
+
let unclaimedLiveEngine = opts.liveEngine;
|
|
8903
11030
|
let operationFailed = false;
|
|
8904
11031
|
try {
|
|
11032
|
+
shadow = await createBackend({
|
|
11033
|
+
engine: opts.makeShadowEngine ? await opts.makeShadowEngine() : undefined,
|
|
11034
|
+
migrations: opts.migrations,
|
|
11035
|
+
startRuntimeServices: false
|
|
11036
|
+
});
|
|
11037
|
+
const liveEngine = unclaimedLiveEngine;
|
|
11038
|
+
unclaimedLiveEngine = undefined;
|
|
8905
11039
|
live = await createBackend({
|
|
8906
|
-
engine:
|
|
8907
|
-
dataDir:
|
|
11040
|
+
engine: liveEngine,
|
|
11041
|
+
dataDir: liveEngine ? undefined : opts.liveDataDir,
|
|
8908
11042
|
migrations: opts.migrations,
|
|
8909
11043
|
startRuntimeServices: false
|
|
8910
11044
|
});
|
|
@@ -8920,8 +11054,8 @@ async function pullSchema(opts) {
|
|
|
8920
11054
|
let path = null;
|
|
8921
11055
|
if (opts.migrationsDir) {
|
|
8922
11056
|
await mkdir2(opts.migrationsDir, { recursive: true });
|
|
8923
|
-
path =
|
|
8924
|
-
await
|
|
11057
|
+
path = join2(opts.migrationsDir, `${stamp}_${name}.sql`);
|
|
11058
|
+
await writeFile2(path, body);
|
|
8925
11059
|
}
|
|
8926
11060
|
await live.db.query(`insert into supabase_migrations.schema_migrations (version, name, statements)
|
|
8927
11061
|
values ($1, $2, $3) on conflict (version) do nothing`, [stamp, `${stamp}_${name}`, [body]]);
|
|
@@ -8931,15 +11065,15 @@ async function pullSchema(opts) {
|
|
|
8931
11065
|
throw error;
|
|
8932
11066
|
} finally {
|
|
8933
11067
|
try {
|
|
8934
|
-
await
|
|
11068
|
+
await closeResources(unclaimedLiveEngine, live, shadow);
|
|
8935
11069
|
} catch (error) {
|
|
8936
11070
|
if (!operationFailed)
|
|
8937
11071
|
throw error;
|
|
8938
11072
|
}
|
|
8939
11073
|
}
|
|
8940
11074
|
}
|
|
8941
|
-
async function
|
|
8942
|
-
const results = await Promise.allSettled(
|
|
11075
|
+
async function closeResources(...resources) {
|
|
11076
|
+
const results = await Promise.allSettled(resources.filter((resource) => resource !== undefined).map(async (resource) => await resource.close()));
|
|
8943
11077
|
const failed = results.find((result) => result.status === "rejected");
|
|
8944
11078
|
if (failed?.status === "rejected")
|
|
8945
11079
|
throw failed.reason;
|
|
@@ -8947,9 +11081,9 @@ async function closeBackends(...backends) {
|
|
|
8947
11081
|
|
|
8948
11082
|
// src/runtime/node/project.ts
|
|
8949
11083
|
import { readdir, readFile as readFile2 } from "fs/promises";
|
|
8950
|
-
import { join as
|
|
11084
|
+
import { join as join3 } from "path";
|
|
8951
11085
|
async function loadSupabaseProject(projectDir, seed = {}) {
|
|
8952
|
-
const migrationsDir =
|
|
11086
|
+
const migrationsDir = join3(projectDir, "supabase", "migrations");
|
|
8953
11087
|
const migrations = [];
|
|
8954
11088
|
let entries = [];
|
|
8955
11089
|
try {
|
|
@@ -8961,19 +11095,19 @@ async function loadSupabaseProject(projectDir, seed = {}) {
|
|
|
8961
11095
|
for (const entry of entries.sort()) {
|
|
8962
11096
|
if (!entry.endsWith(".sql"))
|
|
8963
11097
|
continue;
|
|
8964
|
-
const sql = await readFile2(
|
|
11098
|
+
const sql = await readFile2(join3(migrationsDir, entry), "utf8");
|
|
8965
11099
|
migrations.push({ name: entry.replace(/\.sql$/, ""), sql });
|
|
8966
11100
|
}
|
|
8967
11101
|
let seedSql;
|
|
8968
11102
|
if (seed.enabled !== false) {
|
|
8969
11103
|
const parts = [];
|
|
8970
|
-
const supabaseDir =
|
|
11104
|
+
const supabaseDir = join3(projectDir, "supabase");
|
|
8971
11105
|
for (const configuredPath of seed.paths ?? ["seed.sql"]) {
|
|
8972
11106
|
const pattern = configuredPath.replace(/^\.\//, "");
|
|
8973
11107
|
const matches = /[*?[\]{}]/.test(pattern) ? [...new Bun.Glob(pattern).scanSync({ cwd: supabaseDir, onlyFiles: true })].sort() : [pattern];
|
|
8974
11108
|
for (const relativePath of matches) {
|
|
8975
11109
|
try {
|
|
8976
|
-
parts.push(await readFile2(
|
|
11110
|
+
parts.push(await readFile2(join3(supabaseDir, relativePath), "utf8"));
|
|
8977
11111
|
} catch (error) {
|
|
8978
11112
|
if (!isNotFound(error))
|
|
8979
11113
|
throw error;
|
|
@@ -8991,8 +11125,8 @@ function isNotFound(error) {
|
|
|
8991
11125
|
}
|
|
8992
11126
|
|
|
8993
11127
|
// src/project-runtime.ts
|
|
8994
|
-
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as
|
|
8995
|
-
import { dirname as
|
|
11128
|
+
import { chmod, link, lstat, mkdir as mkdir5, readFile as readFile6, realpath as realpath2, unlink as unlink2, writeFile as writeFile5 } from "fs/promises";
|
|
11129
|
+
import { dirname as dirname5, isAbsolute, join as join8, parse, relative, resolve as resolve2 } from "path";
|
|
8996
11130
|
|
|
8997
11131
|
// src/runtime/node/bun-server.ts
|
|
8998
11132
|
async function serveBun(backend, opts = {}) {
|
|
@@ -9023,8 +11157,8 @@ async function serveBun(backend, opts = {}) {
|
|
|
9023
11157
|
}, { vsn: ws.data.vsn });
|
|
9024
11158
|
ws.data.session = session;
|
|
9025
11159
|
},
|
|
9026
|
-
message(ws,
|
|
9027
|
-
ws.data.session?.onMessage(typeof
|
|
11160
|
+
message(ws, message2) {
|
|
11161
|
+
ws.data.session?.onMessage(typeof message2 === "string" ? message2 : new Uint8Array(message2));
|
|
9028
11162
|
},
|
|
9029
11163
|
close(ws) {
|
|
9030
11164
|
ws.data.session?.onClose();
|
|
@@ -9042,8 +11176,8 @@ async function serveBun(backend, opts = {}) {
|
|
|
9042
11176
|
}
|
|
9043
11177
|
|
|
9044
11178
|
// src/runtime/node/fs-driver.ts
|
|
9045
|
-
import { mkdir as mkdir3, readFile as readFile3, rename, rm, writeFile as
|
|
9046
|
-
import { dirname as
|
|
11179
|
+
import { mkdir as mkdir3, readFile as readFile3, rename, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
11180
|
+
import { dirname as dirname3, join as join4, normalize, sep } from "path";
|
|
9047
11181
|
|
|
9048
11182
|
class FsStorageDriver {
|
|
9049
11183
|
root;
|
|
@@ -9051,7 +11185,7 @@ class FsStorageDriver {
|
|
|
9051
11185
|
this.root = root;
|
|
9052
11186
|
}
|
|
9053
11187
|
resolve(key) {
|
|
9054
|
-
const path = normalize(
|
|
11188
|
+
const path = normalize(join4(this.root, key));
|
|
9055
11189
|
if (!path.startsWith(normalize(this.root) + sep)) {
|
|
9056
11190
|
throw new Error(`invalid storage key: ${key}`);
|
|
9057
11191
|
}
|
|
@@ -9059,13 +11193,13 @@ class FsStorageDriver {
|
|
|
9059
11193
|
}
|
|
9060
11194
|
async put(key, data) {
|
|
9061
11195
|
const path = this.resolve(key);
|
|
9062
|
-
await mkdir3(
|
|
11196
|
+
await mkdir3(dirname3(path), { recursive: true });
|
|
9063
11197
|
const temporaryPath = `${path}.${crypto.randomUUID()}.tmp`;
|
|
9064
11198
|
try {
|
|
9065
|
-
await
|
|
11199
|
+
await writeFile3(temporaryPath, data);
|
|
9066
11200
|
await rename(temporaryPath, path);
|
|
9067
11201
|
} catch (error) {
|
|
9068
|
-
await
|
|
11202
|
+
await rm2(temporaryPath, { force: true }).catch(() => {});
|
|
9069
11203
|
throw error;
|
|
9070
11204
|
}
|
|
9071
11205
|
}
|
|
@@ -9078,8 +11212,12 @@ class FsStorageDriver {
|
|
|
9078
11212
|
throw e;
|
|
9079
11213
|
}
|
|
9080
11214
|
}
|
|
11215
|
+
async getBlob(key) {
|
|
11216
|
+
const file = Bun.file(this.resolve(key));
|
|
11217
|
+
return await file.exists() ? file : null;
|
|
11218
|
+
}
|
|
9081
11219
|
async delete(key) {
|
|
9082
|
-
await
|
|
11220
|
+
await rm2(this.resolve(key), { force: true });
|
|
9083
11221
|
}
|
|
9084
11222
|
async deleteMany(keys) {
|
|
9085
11223
|
for (const k of keys)
|
|
@@ -9088,15 +11226,15 @@ class FsStorageDriver {
|
|
|
9088
11226
|
}
|
|
9089
11227
|
|
|
9090
11228
|
// src/runtime/node/config-toml.ts
|
|
9091
|
-
import { readFileSync } from "fs";
|
|
9092
|
-
import { join as
|
|
11229
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
11230
|
+
import { join as join5 } from "path";
|
|
9093
11231
|
function emptyTable() {
|
|
9094
11232
|
return { values: new Map, children: new Map };
|
|
9095
11233
|
}
|
|
9096
11234
|
function loadConfigToml(projectDir, env = process.env) {
|
|
9097
11235
|
let text;
|
|
9098
11236
|
try {
|
|
9099
|
-
text =
|
|
11237
|
+
text = readFileSync2(join5(projectDir, "supabase", "config.toml"), "utf8");
|
|
9100
11238
|
} catch {
|
|
9101
11239
|
return emptyTable();
|
|
9102
11240
|
}
|
|
@@ -9461,16 +11599,16 @@ function readFunctions(root) {
|
|
|
9461
11599
|
}
|
|
9462
11600
|
|
|
9463
11601
|
// src/runtime/node/load-functions.ts
|
|
9464
|
-
import { readdir as readdir2, readFile as readFile5, realpath, rm as
|
|
9465
|
-
import { join as
|
|
11602
|
+
import { readdir as readdir2, readFile as readFile5, realpath, rm as rm4, stat } from "fs/promises";
|
|
11603
|
+
import { dirname as dirname4, join as join7 } from "path";
|
|
9466
11604
|
import { pathToFileURL } from "url";
|
|
9467
11605
|
|
|
9468
11606
|
// src/runtime/node/bundle-function.ts
|
|
9469
|
-
import { createHash } from "crypto";
|
|
9470
|
-
import { mkdir as mkdir4, readFile as readFile4, writeFile as
|
|
9471
|
-
import { existsSync } from "fs";
|
|
9472
|
-
import { tmpdir } from "os";
|
|
9473
|
-
import { join as
|
|
11607
|
+
import { createHash as createHash3 } from "crypto";
|
|
11608
|
+
import { mkdir as mkdir4, readFile as readFile4, rm as rm3, writeFile as writeFile4 } from "fs/promises";
|
|
11609
|
+
import { existsSync as existsSync2 } from "fs";
|
|
11610
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
11611
|
+
import { join as join6 } from "path";
|
|
9474
11612
|
function rewriteRemoteSpecifier(spec) {
|
|
9475
11613
|
if (spec.startsWith("npm:"))
|
|
9476
11614
|
return `https://esm.sh/${spec.slice(4)}`;
|
|
@@ -9478,18 +11616,18 @@ function rewriteRemoteSpecifier(spec) {
|
|
|
9478
11616
|
return `https://esm.sh/jsr/${spec.slice(4)}`;
|
|
9479
11617
|
return spec;
|
|
9480
11618
|
}
|
|
9481
|
-
var HTTP_CACHE =
|
|
11619
|
+
var HTTP_CACHE = join6(tmpdir3(), "supacloud-lite-fn-http");
|
|
9482
11620
|
async function fetchModule(url) {
|
|
9483
|
-
const key =
|
|
9484
|
-
const cached =
|
|
9485
|
-
if (
|
|
11621
|
+
const key = createHash3("sha256").update(url).digest("hex");
|
|
11622
|
+
const cached = join6(HTTP_CACHE, key);
|
|
11623
|
+
if (existsSync2(cached))
|
|
9486
11624
|
return readFile4(cached, "utf8");
|
|
9487
11625
|
const res = await fetch(url, { redirect: "follow" });
|
|
9488
11626
|
if (!res.ok)
|
|
9489
11627
|
throw new Error(`failed to fetch ${url}: HTTP ${res.status}`);
|
|
9490
11628
|
const body = await res.text();
|
|
9491
11629
|
await mkdir4(HTTP_CACHE, { recursive: true });
|
|
9492
|
-
await
|
|
11630
|
+
await writeFile4(cached, body);
|
|
9493
11631
|
return body;
|
|
9494
11632
|
}
|
|
9495
11633
|
function remotePlugin() {
|
|
@@ -9512,28 +11650,37 @@ function remotePlugin() {
|
|
|
9512
11650
|
};
|
|
9513
11651
|
}
|
|
9514
11652
|
async function bundleFunction(entryPath, name) {
|
|
9515
|
-
const outDir =
|
|
11653
|
+
const outDir = join6(tmpdir3(), "supacloud-lite-fn-bundle", name);
|
|
9516
11654
|
await mkdir4(outDir, { recursive: true });
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
11655
|
+
try {
|
|
11656
|
+
const buildOutput = await Bun.build({
|
|
11657
|
+
entrypoints: [entryPath],
|
|
11658
|
+
format: "esm",
|
|
11659
|
+
target: "bun",
|
|
11660
|
+
outdir: outDir,
|
|
11661
|
+
naming: `${name}-[hash].[ext]`,
|
|
11662
|
+
plugins: [remotePlugin()]
|
|
11663
|
+
});
|
|
11664
|
+
if (!buildOutput.success || buildOutput.outputs.length === 0) {
|
|
11665
|
+
throw new Error(buildOutput.logs.map((item) => item.message).join(`
|
|
9527
11666
|
`) || `failed to bundle ${entryPath}`);
|
|
11667
|
+
}
|
|
11668
|
+
return buildOutput.outputs[0].path;
|
|
11669
|
+
} catch (buildError) {
|
|
11670
|
+
try {
|
|
11671
|
+
await rm3(outDir, { recursive: true, force: true });
|
|
11672
|
+
} catch (cleanupError) {
|
|
11673
|
+
throw new AggregateError([buildError, cleanupError], `failed to clean function bundle directory ${outDir}`);
|
|
11674
|
+
}
|
|
11675
|
+
throw buildError;
|
|
9528
11676
|
}
|
|
9529
|
-
return result.outputs[0].path;
|
|
9530
11677
|
}
|
|
9531
11678
|
|
|
9532
11679
|
// src/runtime/node/load-functions.ts
|
|
9533
11680
|
async function loadFunctionEnv(projectDir) {
|
|
9534
11681
|
let text;
|
|
9535
11682
|
try {
|
|
9536
|
-
text = await readFile5(
|
|
11683
|
+
text = await readFile5(join7(projectDir, "supabase", "functions", ".env"), "utf8");
|
|
9537
11684
|
} catch {
|
|
9538
11685
|
return {};
|
|
9539
11686
|
}
|
|
@@ -9572,7 +11719,7 @@ async function loadFunctions2(projectDir, options = {}) {
|
|
|
9572
11719
|
}
|
|
9573
11720
|
async function loadFunctionsUnlocked(projectDir, options) {
|
|
9574
11721
|
const functions = new Map;
|
|
9575
|
-
const root =
|
|
11722
|
+
const root = join7(projectDir, "supabase", "functions");
|
|
9576
11723
|
let entries = [];
|
|
9577
11724
|
try {
|
|
9578
11725
|
entries = await readdir2(root);
|
|
@@ -9585,10 +11732,10 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
9585
11732
|
continue;
|
|
9586
11733
|
if (options[name]?.enabled === false)
|
|
9587
11734
|
continue;
|
|
9588
|
-
const dir =
|
|
11735
|
+
const dir = join7(root, name);
|
|
9589
11736
|
if (!(await stat(dir)).isDirectory())
|
|
9590
11737
|
continue;
|
|
9591
|
-
const candidates = options[name]?.entrypoint ? [
|
|
11738
|
+
const candidates = options[name]?.entrypoint ? [join7(projectDir, options[name].entrypoint)] : ["index.ts", "index.tsx", "index.js", "index.mjs"].map((f) => join7(dir, f));
|
|
9592
11739
|
for (const path of candidates) {
|
|
9593
11740
|
try {
|
|
9594
11741
|
await stat(path);
|
|
@@ -9609,11 +11756,12 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
9609
11756
|
resetCapturedHandler();
|
|
9610
11757
|
const mod = await import(importUrl);
|
|
9611
11758
|
const denoHandler = takeCapturedHandler();
|
|
9612
|
-
const
|
|
11759
|
+
const defaultExport = mod.default;
|
|
11760
|
+
const handler = typeof defaultExport === "function" ? defaultExport : defaultExport && typeof defaultExport.fetch === "function" ? defaultExport.fetch.bind(defaultExport) : denoHandler ? (req) => denoHandler(req) : undefined;
|
|
9613
11761
|
if (handler) {
|
|
9614
11762
|
functions.set(name, handler);
|
|
9615
11763
|
} else {
|
|
9616
|
-
console.warn(` warning: function "${name}" has no default
|
|
11764
|
+
console.warn(` warning: function "${name}" has no default function, fetch object, or Deno.serve() handler, skipped`);
|
|
9617
11765
|
}
|
|
9618
11766
|
} catch (e) {
|
|
9619
11767
|
const msg = e instanceof Error ? e.message : String(e);
|
|
@@ -9624,7 +11772,7 @@ async function loadFunctionsUnlocked(projectDir, options) {
|
|
|
9624
11772
|
}
|
|
9625
11773
|
} finally {
|
|
9626
11774
|
if (bundledPath)
|
|
9627
|
-
await
|
|
11775
|
+
await rm4(dirname4(bundledPath), { recursive: true, force: true }).catch(() => {});
|
|
9628
11776
|
}
|
|
9629
11777
|
break;
|
|
9630
11778
|
}
|
|
@@ -9698,6 +11846,21 @@ class S3StorageDriver {
|
|
|
9698
11846
|
throw error;
|
|
9699
11847
|
}
|
|
9700
11848
|
}
|
|
11849
|
+
async getBlob(key) {
|
|
11850
|
+
const file = this.client.file(this.objectKey(key));
|
|
11851
|
+
if (!await file.exists())
|
|
11852
|
+
return null;
|
|
11853
|
+
if (typeof file.arrayBuffer === "function")
|
|
11854
|
+
return file;
|
|
11855
|
+
try {
|
|
11856
|
+
const bytes = Uint8Array.from(await file.bytes());
|
|
11857
|
+
return new Blob([bytes.buffer]);
|
|
11858
|
+
} catch (error) {
|
|
11859
|
+
if (isNotFoundError(error))
|
|
11860
|
+
return null;
|
|
11861
|
+
throw error;
|
|
11862
|
+
}
|
|
11863
|
+
}
|
|
9701
11864
|
async delete(key) {
|
|
9702
11865
|
await this.client.file(this.objectKey(key)).delete();
|
|
9703
11866
|
}
|
|
@@ -9717,14 +11880,16 @@ var RESET_INVALID_SECRETS_ERROR = "db reset requires a valid project secrets mar
|
|
|
9717
11880
|
function resolveProjectPaths(options = {}) {
|
|
9718
11881
|
const projectDir = resolve2(options.projectDir ?? process.cwd());
|
|
9719
11882
|
const stateDir = resolvePath(projectDir, options.stateDir ?? process.env.SUPACLOUD_LITE_STATE_DIR ?? ".supacloud-lite");
|
|
9720
|
-
const
|
|
9721
|
-
const
|
|
11883
|
+
const databaseEngine = resolveDatabaseEngine(options.engine, options.memory);
|
|
11884
|
+
const dataDir = options.memory ? undefined : resolvePath(projectDir, options.dataDir ?? process.env.SUPACLOUD_LITE_DATA_DIR ?? join8(stateDir, databaseEngine === "native" ? "pgdata" : "db"));
|
|
11885
|
+
const storageDir = resolvePath(projectDir, options.storageDir ?? process.env.SUPACLOUD_LITE_STORAGE_DIR ?? join8(stateDir, "storage"));
|
|
9722
11886
|
return {
|
|
9723
11887
|
projectDir,
|
|
9724
11888
|
stateDir,
|
|
9725
11889
|
dataDir,
|
|
9726
11890
|
storageDir,
|
|
9727
|
-
secretsFile:
|
|
11891
|
+
secretsFile: join8(stateDir, "secrets.json"),
|
|
11892
|
+
databaseEngine
|
|
9728
11893
|
};
|
|
9729
11894
|
}
|
|
9730
11895
|
async function assertResetPathsSafe(paths) {
|
|
@@ -9737,7 +11902,7 @@ async function assertResetPathsSafe(paths) {
|
|
|
9737
11902
|
}
|
|
9738
11903
|
const canonicalStateDir = await realpath2(stateDir);
|
|
9739
11904
|
const secretsFile = resolve2(paths.secretsFile);
|
|
9740
|
-
if (secretsFile !==
|
|
11905
|
+
if (secretsFile !== join8(stateDir, "secrets.json")) {
|
|
9741
11906
|
throw new Error(`refusing to reset a state directory with an invalid secrets marker path: ${secretsFile}`);
|
|
9742
11907
|
}
|
|
9743
11908
|
const markerInfo = await requiredResetEntry(secretsFile);
|
|
@@ -9750,12 +11915,12 @@ async function assertResetPathsSafe(paths) {
|
|
|
9750
11915
|
["storage", paths.storageDir]
|
|
9751
11916
|
];
|
|
9752
11917
|
for (const [label, targetPath] of targets) {
|
|
9753
|
-
const
|
|
9754
|
-
const relativePath = relative(stateDir,
|
|
11918
|
+
const target2 = resolve2(targetPath);
|
|
11919
|
+
const relativePath = relative(stateDir, target2);
|
|
9755
11920
|
if (!relativePath || relativePath.startsWith("..") || isAbsolute(relativePath)) {
|
|
9756
|
-
throw new Error(`refusing to reset ${label} path outside the state directory: ${
|
|
11921
|
+
throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
|
|
9757
11922
|
}
|
|
9758
|
-
await assertResetTargetCanonical(stateDir, canonicalStateDir,
|
|
11923
|
+
await assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label);
|
|
9759
11924
|
}
|
|
9760
11925
|
}
|
|
9761
11926
|
async function requiredResetEntry(path) {
|
|
@@ -9782,8 +11947,8 @@ async function assertResetSecretsValid(path) {
|
|
|
9782
11947
|
throw new Error(RESET_INVALID_SECRETS_ERROR);
|
|
9783
11948
|
}
|
|
9784
11949
|
}
|
|
9785
|
-
async function assertResetTargetCanonical(stateDir, canonicalStateDir,
|
|
9786
|
-
let current =
|
|
11950
|
+
async function assertResetTargetCanonical(stateDir, canonicalStateDir, target2, label) {
|
|
11951
|
+
let current = target2;
|
|
9787
11952
|
while (current !== stateDir) {
|
|
9788
11953
|
try {
|
|
9789
11954
|
if ((await lstat(current)).isSymbolicLink()) {
|
|
@@ -9793,20 +11958,20 @@ async function assertResetTargetCanonical(stateDir, canonicalStateDir, target, l
|
|
|
9793
11958
|
if (error.code !== "ENOENT")
|
|
9794
11959
|
throw error;
|
|
9795
11960
|
}
|
|
9796
|
-
const parent =
|
|
11961
|
+
const parent = dirname5(current);
|
|
9797
11962
|
if (parent === current)
|
|
9798
|
-
throw new Error(`refusing to reset ${label} path outside the state directory: ${
|
|
11963
|
+
throw new Error(`refusing to reset ${label} path outside the state directory: ${target2}`);
|
|
9799
11964
|
current = parent;
|
|
9800
11965
|
}
|
|
9801
|
-
const existingAncestor = await nearestExistingAncestor(
|
|
9802
|
-
const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor,
|
|
11966
|
+
const existingAncestor = await nearestExistingAncestor(target2);
|
|
11967
|
+
const canonicalTarget = resolve2(await realpath2(existingAncestor), relative(existingAncestor, target2));
|
|
9803
11968
|
const canonicalRelative = relative(canonicalStateDir, canonicalTarget);
|
|
9804
11969
|
if (!canonicalRelative || canonicalRelative.startsWith("..") || isAbsolute(canonicalRelative)) {
|
|
9805
|
-
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${
|
|
11970
|
+
throw new Error(`refusing to reset ${label} path outside the canonical state directory: ${target2}`);
|
|
9806
11971
|
}
|
|
9807
11972
|
}
|
|
9808
|
-
async function nearestExistingAncestor(
|
|
9809
|
-
let current =
|
|
11973
|
+
async function nearestExistingAncestor(target2) {
|
|
11974
|
+
let current = target2;
|
|
9810
11975
|
while (true) {
|
|
9811
11976
|
try {
|
|
9812
11977
|
await lstat(current);
|
|
@@ -9815,9 +11980,9 @@ async function nearestExistingAncestor(target) {
|
|
|
9815
11980
|
if (error.code !== "ENOENT")
|
|
9816
11981
|
throw error;
|
|
9817
11982
|
}
|
|
9818
|
-
const parent =
|
|
11983
|
+
const parent = dirname5(current);
|
|
9819
11984
|
if (parent === current)
|
|
9820
|
-
throw new Error(`unable to resolve an existing ancestor for ${
|
|
11985
|
+
throw new Error(`unable to resolve an existing ancestor for ${target2}`);
|
|
9821
11986
|
current = parent;
|
|
9822
11987
|
}
|
|
9823
11988
|
}
|
|
@@ -9841,7 +12006,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
9841
12006
|
};
|
|
9842
12007
|
const temporaryFile = `${paths.secretsFile}.${crypto.randomUUID()}.tmp`;
|
|
9843
12008
|
try {
|
|
9844
|
-
await
|
|
12009
|
+
await writeFile5(temporaryFile, `${JSON.stringify(candidate, null, 2)}
|
|
9845
12010
|
`, { mode: 384, flag: "wx" });
|
|
9846
12011
|
await link(temporaryFile, paths.secretsFile);
|
|
9847
12012
|
stored = candidate;
|
|
@@ -9850,7 +12015,7 @@ async function ensureProjectSecrets(paths) {
|
|
|
9850
12015
|
throw error2;
|
|
9851
12016
|
stored = validateSecrets(JSON.parse(await readFile6(paths.secretsFile, "utf8")));
|
|
9852
12017
|
} finally {
|
|
9853
|
-
await
|
|
12018
|
+
await unlink2(temporaryFile).catch((error2) => {
|
|
9854
12019
|
if (error2.code !== "ENOENT")
|
|
9855
12020
|
throw error2;
|
|
9856
12021
|
});
|
|
@@ -9882,42 +12047,58 @@ async function createProjectBackend(options = {}) {
|
|
|
9882
12047
|
const webhooks = options.includeWebhooks === false ? [] : await loadWebhooks(paths.projectDir);
|
|
9883
12048
|
const configuredStorageBackend = options.storageDriver ? "fs" : resolveStorageBackend(options.storageBackend);
|
|
9884
12049
|
const storageBackend = options.storageDriver ? "custom" : configuredStorageBackend;
|
|
12050
|
+
const databaseEngine = paths.databaseEngine;
|
|
9885
12051
|
if (paths.dataDir) {
|
|
9886
12052
|
await mkdir5(paths.dataDir, { recursive: true, mode: 448 });
|
|
9887
12053
|
await chmod(paths.dataDir, 448);
|
|
9888
12054
|
}
|
|
9889
12055
|
await mkdir5(paths.storageDir, { recursive: true, mode: 448 });
|
|
9890
12056
|
await chmod(paths.storageDir, 448);
|
|
9891
|
-
const
|
|
9892
|
-
|
|
9893
|
-
|
|
9894
|
-
|
|
9895
|
-
|
|
9896
|
-
|
|
9897
|
-
|
|
9898
|
-
|
|
9899
|
-
|
|
9900
|
-
|
|
9901
|
-
|
|
9902
|
-
|
|
9903
|
-
|
|
9904
|
-
|
|
9905
|
-
|
|
9906
|
-
|
|
9907
|
-
|
|
9908
|
-
|
|
9909
|
-
|
|
9910
|
-
|
|
9911
|
-
|
|
9912
|
-
|
|
9913
|
-
|
|
9914
|
-
|
|
9915
|
-
|
|
9916
|
-
|
|
9917
|
-
|
|
9918
|
-
|
|
9919
|
-
|
|
9920
|
-
|
|
12057
|
+
const storageDriver = options.storageDriver ?? createStorageDriver(configuredStorageBackend, paths.storageDir, options.s3);
|
|
12058
|
+
const engine = databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: options.log }) : undefined;
|
|
12059
|
+
let backend;
|
|
12060
|
+
try {
|
|
12061
|
+
backend = await createBackend({
|
|
12062
|
+
engine,
|
|
12063
|
+
dataDir: databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12064
|
+
jwtSecret: secrets.jwtSecret,
|
|
12065
|
+
vaultKey: secrets.vaultKey,
|
|
12066
|
+
apiUrl: url,
|
|
12067
|
+
siteUrl: options.siteUrl ?? process.env.SUPACLOUD_LITE_SITE_URL ?? config.auth.siteUrl ?? url,
|
|
12068
|
+
host,
|
|
12069
|
+
jwtExpiry: config.auth.jwtExpiry,
|
|
12070
|
+
uriAllowList: config.auth.uriAllowList,
|
|
12071
|
+
authEnabled: config.auth.enabled,
|
|
12072
|
+
authSettings: config.auth.settings,
|
|
12073
|
+
authRateLimits: config.auth.rateLimits,
|
|
12074
|
+
sessionTimeboxSeconds: config.auth.sessionTimeboxSeconds,
|
|
12075
|
+
sessionInactivitySeconds: config.auth.sessionInactivitySeconds,
|
|
12076
|
+
oauthProviders: config.auth.oauthProviders,
|
|
12077
|
+
smsSender: options.smsSender,
|
|
12078
|
+
dbSchemas: config.api.schemas,
|
|
12079
|
+
maxRows: config.api.maxRows,
|
|
12080
|
+
storageFileSizeLimit: config.storage.fileSizeLimit,
|
|
12081
|
+
buckets: config.storage.buckets,
|
|
12082
|
+
migrations: options.applyMigrations === false ? [] : project.migrations,
|
|
12083
|
+
seedSql: options.applyMigrations === false || options.includeSeed === false ? undefined : project.seedSql,
|
|
12084
|
+
functions,
|
|
12085
|
+
functionVerifyJwt: Object.fromEntries(Object.entries(config.functions).map(([name, functionOptions]) => [name, functionOptions.verifyJwt !== false])),
|
|
12086
|
+
functionEnv,
|
|
12087
|
+
webhooks,
|
|
12088
|
+
startRuntimeServices: options.startRuntimeServices,
|
|
12089
|
+
storageDriver,
|
|
12090
|
+
log: options.log
|
|
12091
|
+
});
|
|
12092
|
+
} catch (error) {
|
|
12093
|
+
if (engine) {
|
|
12094
|
+
try {
|
|
12095
|
+
await engine.close();
|
|
12096
|
+
} catch (cleanupError) {
|
|
12097
|
+
throw new AggregateError([error, cleanupError], "project database startup cleanup failed");
|
|
12098
|
+
}
|
|
12099
|
+
}
|
|
12100
|
+
throw error;
|
|
12101
|
+
}
|
|
9921
12102
|
return {
|
|
9922
12103
|
backend,
|
|
9923
12104
|
config,
|
|
@@ -9928,9 +12109,22 @@ async function createProjectBackend(options = {}) {
|
|
|
9928
12109
|
migrationCount: project.migrations.length,
|
|
9929
12110
|
functionNames: [...functions.keys()],
|
|
9930
12111
|
webhookCount: webhooks.length,
|
|
9931
|
-
storageBackend
|
|
12112
|
+
storageBackend,
|
|
12113
|
+
databaseEngine
|
|
9932
12114
|
};
|
|
9933
12115
|
}
|
|
12116
|
+
function resolveDatabaseEngine(value, memory = false) {
|
|
12117
|
+
const configured = value ?? process.env.SUPACLOUD_LITE_ENGINE ?? "pglite";
|
|
12118
|
+
if (configured !== "pglite" && configured !== "native") {
|
|
12119
|
+
throw new Error(`unsupported SUPACLOUD_LITE_ENGINE: ${configured}`);
|
|
12120
|
+
}
|
|
12121
|
+
if (configured === "native" && memory)
|
|
12122
|
+
throw new Error("--memory is only supported by the pglite engine");
|
|
12123
|
+
if (configured === "native" && !isNativeEngineSupported()) {
|
|
12124
|
+
throw new Error(`native PostgreSQL requires macOS or glibc Linux on x64/arm64; ` + `${process.platform}/${process.arch} must use --engine pglite`);
|
|
12125
|
+
}
|
|
12126
|
+
return configured;
|
|
12127
|
+
}
|
|
9934
12128
|
function resolveStorageBackend(value) {
|
|
9935
12129
|
const configured = value ?? process.env.SUPACLOUD_LITE_STORAGE_BACKEND ?? "fs";
|
|
9936
12130
|
if (configured === "fs" || configured === "memory" || configured === "s3")
|
|
@@ -9985,7 +12179,7 @@ async function startProjectServer(options = {}) {
|
|
|
9985
12179
|
}
|
|
9986
12180
|
async function loadWebhooks(projectDir) {
|
|
9987
12181
|
try {
|
|
9988
|
-
const parsed = JSON.parse(await readFile6(
|
|
12182
|
+
const parsed = JSON.parse(await readFile6(join8(projectDir, "supabase", "webhooks.json"), "utf8"));
|
|
9989
12183
|
return Array.isArray(parsed) ? parsed : [];
|
|
9990
12184
|
} catch (error) {
|
|
9991
12185
|
if (error.code === "ENOENT")
|
|
@@ -10036,9 +12230,9 @@ async function findEphemeralPort(host = "127.0.0.1") {
|
|
|
10036
12230
|
}
|
|
10037
12231
|
|
|
10038
12232
|
// src/snapshot.ts
|
|
10039
|
-
import { chmod as chmod2, copyFile, lstat as lstat2, mkdir as mkdir6, mkdtemp, readdir as readdir3, readFile as readFile7, rename as rename2, rm as
|
|
10040
|
-
import { dirname as
|
|
10041
|
-
import { create as createTar, extract as
|
|
12233
|
+
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";
|
|
12234
|
+
import { dirname as dirname6, join as join9, parse as parse2, relative as relative2, resolve as resolve3, sep as sep2 } from "path";
|
|
12235
|
+
import { create as createTar, extract as extractTar2 } from "tar";
|
|
10042
12236
|
var SNAPSHOT_FORMAT = "supacloud-lite-snapshot";
|
|
10043
12237
|
var SNAPSHOT_VERSION = 1;
|
|
10044
12238
|
async function createSnapshot(options) {
|
|
@@ -10053,21 +12247,27 @@ async function createSnapshot(options) {
|
|
|
10053
12247
|
storageBackend: options.storageBackend,
|
|
10054
12248
|
includesDatabase: Boolean(paths.dataDir),
|
|
10055
12249
|
includesLocalStorage: options.storageBackend === "fs",
|
|
10056
|
-
includesSecrets: true
|
|
12250
|
+
includesSecrets: true,
|
|
12251
|
+
databaseEngine: paths.databaseEngine,
|
|
12252
|
+
...paths.databaseEngine === "native" ? {
|
|
12253
|
+
platform: process.platform,
|
|
12254
|
+
architecture: process.arch,
|
|
12255
|
+
postgresMajor: await readPostgresMajor(paths.dataDir)
|
|
12256
|
+
} : {}
|
|
10057
12257
|
};
|
|
10058
12258
|
const output = resolve3(options.output);
|
|
10059
12259
|
if (await existingInfo(output))
|
|
10060
12260
|
throw new Error(`snapshot output already exists: ${output}`);
|
|
10061
|
-
await mkdir6(
|
|
10062
|
-
const stagingRoot = await mkdtemp(
|
|
12261
|
+
await mkdir6(dirname6(output), { recursive: true });
|
|
12262
|
+
const stagingRoot = await mkdtemp(join9(dirname6(output), ".supacloud-lite-snapshot-"));
|
|
10063
12263
|
try {
|
|
10064
|
-
await
|
|
12264
|
+
await writeFile6(join9(stagingRoot, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
10065
12265
|
`);
|
|
10066
|
-
await stageFile(paths.secretsFile,
|
|
12266
|
+
await stageFile(paths.secretsFile, join9(stagingRoot, "secrets.json"));
|
|
10067
12267
|
if (paths.dataDir)
|
|
10068
|
-
await stageDirectory(paths.dataDir,
|
|
12268
|
+
await stageDirectory(paths.dataDir, join9(stagingRoot, "database"));
|
|
10069
12269
|
if (options.storageBackend === "fs")
|
|
10070
|
-
await stageDirectory(paths.storageDir,
|
|
12270
|
+
await stageDirectory(paths.storageDir, join9(stagingRoot, "storage"));
|
|
10071
12271
|
const entries = ["manifest.json", "secrets.json"];
|
|
10072
12272
|
if (paths.dataDir)
|
|
10073
12273
|
entries.push("database");
|
|
@@ -10078,23 +12278,23 @@ async function createSnapshot(options) {
|
|
|
10078
12278
|
await chmod2(output, 384);
|
|
10079
12279
|
return manifest;
|
|
10080
12280
|
} catch (error) {
|
|
10081
|
-
await
|
|
12281
|
+
await rm5(output, { force: true });
|
|
10082
12282
|
throw error;
|
|
10083
12283
|
} finally {
|
|
10084
|
-
await
|
|
12284
|
+
await rm5(stagingRoot, { recursive: true, force: true });
|
|
10085
12285
|
}
|
|
10086
12286
|
}
|
|
10087
12287
|
async function restoreSnapshot(options) {
|
|
10088
12288
|
const paths = normalizePaths(options.paths);
|
|
10089
12289
|
await assertSnapshotPaths(paths, { requireSecrets: false, allowMissingState: true });
|
|
10090
12290
|
await assertNoDataDirectoryLock(paths);
|
|
10091
|
-
const stagingRoot = await mkdtemp(
|
|
10092
|
-
const payloadRoot =
|
|
12291
|
+
const stagingRoot = await mkdtemp(join9(dirname6(paths.stateDir), ".supacloud-lite-restore-"));
|
|
12292
|
+
const payloadRoot = join9(stagingRoot, "payload");
|
|
10093
12293
|
const rollbackId = crypto.randomUUID();
|
|
10094
12294
|
const rollbackPaths = [];
|
|
10095
12295
|
try {
|
|
10096
12296
|
await mkdir6(payloadRoot, { recursive: true });
|
|
10097
|
-
await
|
|
12297
|
+
await extractTar2({
|
|
10098
12298
|
cwd: payloadRoot,
|
|
10099
12299
|
file: resolve3(options.input),
|
|
10100
12300
|
preserveOwner: false,
|
|
@@ -10119,6 +12319,7 @@ async function restoreSnapshot(options) {
|
|
|
10119
12319
|
if (manifest.storageBackend !== options.storageBackend) {
|
|
10120
12320
|
throw new Error(`snapshot storage backend is ${manifest.storageBackend}, but the target uses ${options.storageBackend}; ` + "restore with the matching --storage-backend value");
|
|
10121
12321
|
}
|
|
12322
|
+
assertDatabaseSnapshotCompatible(manifest, paths);
|
|
10122
12323
|
if (manifest.includesDatabase !== Boolean(paths.dataDir)) {
|
|
10123
12324
|
throw new Error("snapshot database mode does not match the target; do not restore a persistent snapshot into --memory");
|
|
10124
12325
|
}
|
|
@@ -10127,27 +12328,27 @@ async function restoreSnapshot(options) {
|
|
|
10127
12328
|
}
|
|
10128
12329
|
await assertSnapshotPayload(payloadRoot, manifest);
|
|
10129
12330
|
if (manifest.includesDatabase)
|
|
10130
|
-
await mkdir6(
|
|
12331
|
+
await mkdir6(join9(payloadRoot, "database"), { recursive: true });
|
|
10131
12332
|
if (manifest.includesLocalStorage)
|
|
10132
|
-
await mkdir6(
|
|
12333
|
+
await mkdir6(join9(payloadRoot, "storage"), { recursive: true });
|
|
10133
12334
|
await assertRestoreTargets(paths, manifest, options.force === true);
|
|
10134
|
-
const stateStage =
|
|
12335
|
+
const stateStage = join9(stagingRoot, "state");
|
|
10135
12336
|
await mkdir6(stateStage, { recursive: true });
|
|
10136
|
-
await copyEntry(
|
|
12337
|
+
await copyEntry(join9(payloadRoot, "secrets.json"), join9(stateStage, "secrets.json"));
|
|
10137
12338
|
if (paths.dataDir && isWithin(paths.stateDir, paths.dataDir)) {
|
|
10138
|
-
await copyEntry(
|
|
12339
|
+
await copyEntry(join9(payloadRoot, "database"), join9(stateStage, relative2(paths.stateDir, paths.dataDir)));
|
|
10139
12340
|
}
|
|
10140
12341
|
if (options.storageBackend === "fs" && isWithin(paths.stateDir, paths.storageDir)) {
|
|
10141
|
-
await copyEntry(
|
|
12342
|
+
await copyEntry(join9(payloadRoot, "storage"), join9(stateStage, relative2(paths.stateDir, paths.storageDir)));
|
|
10142
12343
|
}
|
|
10143
12344
|
const swaps = [];
|
|
10144
12345
|
try {
|
|
10145
12346
|
await applyDirectorySwap(stateStage, paths.stateDir, options.force === true, rollbackId, swaps);
|
|
10146
12347
|
if (paths.dataDir && !isWithin(paths.stateDir, paths.dataDir)) {
|
|
10147
|
-
await applyDirectorySwap(
|
|
12348
|
+
await applyDirectorySwap(join9(payloadRoot, "database"), paths.dataDir, options.force === true, rollbackId, swaps);
|
|
10148
12349
|
}
|
|
10149
12350
|
if (options.storageBackend === "fs" && !isWithin(paths.stateDir, paths.storageDir)) {
|
|
10150
|
-
await applyDirectorySwap(
|
|
12351
|
+
await applyDirectorySwap(join9(payloadRoot, "storage"), paths.storageDir, options.force === true, rollbackId, swaps);
|
|
10151
12352
|
}
|
|
10152
12353
|
} catch (error) {
|
|
10153
12354
|
await rollbackDirectorySwaps(swaps);
|
|
@@ -10166,7 +12367,7 @@ async function restoreSnapshot(options) {
|
|
|
10166
12367
|
} catch (error) {
|
|
10167
12368
|
throw error instanceof Error ? error : new Error(String(error));
|
|
10168
12369
|
} finally {
|
|
10169
|
-
await
|
|
12370
|
+
await rm5(stagingRoot, { recursive: true, force: true });
|
|
10170
12371
|
}
|
|
10171
12372
|
}
|
|
10172
12373
|
function normalizePaths(paths) {
|
|
@@ -10183,7 +12384,7 @@ async function assertSnapshotPaths(paths, options = {}) {
|
|
|
10183
12384
|
try {
|
|
10184
12385
|
if (paths.stateDir === parse2(paths.stateDir).root)
|
|
10185
12386
|
throw new Error("snapshot state directory must not be the filesystem root");
|
|
10186
|
-
if (paths.secretsFile !==
|
|
12387
|
+
if (paths.secretsFile !== join9(paths.stateDir, "secrets.json"))
|
|
10187
12388
|
throw new Error("snapshot secrets path must be inside the state directory");
|
|
10188
12389
|
const stateInfo = await lstat2(paths.stateDir);
|
|
10189
12390
|
if (!stateInfo.isDirectory() || stateInfo.isSymbolicLink())
|
|
@@ -10241,10 +12442,10 @@ async function stageDirectory(root, destination) {
|
|
|
10241
12442
|
throw error;
|
|
10242
12443
|
}
|
|
10243
12444
|
await mkdir6(destination, { recursive: true });
|
|
10244
|
-
const walk = async (current,
|
|
12445
|
+
const walk = async (current, target2) => {
|
|
10245
12446
|
for (const entry of await readdir3(current, { withFileTypes: true })) {
|
|
10246
|
-
const fullPath =
|
|
10247
|
-
const targetPath =
|
|
12447
|
+
const fullPath = join9(current, entry.name);
|
|
12448
|
+
const targetPath = join9(target2, entry.name);
|
|
10248
12449
|
if (entry.isSymbolicLink())
|
|
10249
12450
|
throw new Error(`snapshot refuses symbolic link: ${fullPath}`);
|
|
10250
12451
|
if (entry.isDirectory()) {
|
|
@@ -10259,14 +12460,14 @@ async function stageDirectory(root, destination) {
|
|
|
10259
12460
|
};
|
|
10260
12461
|
await walk(root, destination);
|
|
10261
12462
|
}
|
|
10262
|
-
async function stageFile(source,
|
|
10263
|
-
await mkdir6(
|
|
10264
|
-
await copyFile(source,
|
|
12463
|
+
async function stageFile(source, target2) {
|
|
12464
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12465
|
+
await copyFile(source, target2);
|
|
10265
12466
|
}
|
|
10266
12467
|
async function readManifest(payloadRoot) {
|
|
10267
12468
|
let parsed;
|
|
10268
12469
|
try {
|
|
10269
|
-
parsed = JSON.parse(await readFile7(
|
|
12470
|
+
parsed = JSON.parse(await readFile7(join9(payloadRoot, "manifest.json"), "utf8"));
|
|
10270
12471
|
} catch (error) {
|
|
10271
12472
|
throw new Error(`invalid snapshot manifest: ${error instanceof Error ? error.message : String(error)}`);
|
|
10272
12473
|
}
|
|
@@ -10278,13 +12479,38 @@ function isSnapshotManifest(value) {
|
|
|
10278
12479
|
if (!value || typeof value !== "object")
|
|
10279
12480
|
return false;
|
|
10280
12481
|
const candidate = value;
|
|
10281
|
-
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;
|
|
12482
|
+
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");
|
|
12483
|
+
}
|
|
12484
|
+
function assertDatabaseSnapshotCompatible(manifest, paths) {
|
|
12485
|
+
const sourceEngine = manifest.databaseEngine ?? "pglite";
|
|
12486
|
+
if (sourceEngine !== paths.databaseEngine) {
|
|
12487
|
+
throw new Error(`snapshot database engine is ${sourceEngine}, but the target uses ${paths.databaseEngine}`);
|
|
12488
|
+
}
|
|
12489
|
+
if (sourceEngine !== "native")
|
|
12490
|
+
return;
|
|
12491
|
+
if (manifest.platform !== process.platform || manifest.architecture !== process.arch) {
|
|
12492
|
+
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}`);
|
|
12493
|
+
}
|
|
12494
|
+
if (manifest.postgresMajor !== NATIVE_POSTGRES_MAJOR) {
|
|
12495
|
+
throw new Error(`native PostgreSQL snapshot major is ${manifest.postgresMajor ?? "unknown"}, ` + `but this Lite build uses ${NATIVE_POSTGRES_MAJOR}`);
|
|
12496
|
+
}
|
|
12497
|
+
}
|
|
12498
|
+
async function readPostgresMajor(dataDir) {
|
|
12499
|
+
if (!dataDir)
|
|
12500
|
+
return;
|
|
12501
|
+
try {
|
|
12502
|
+
return (await readFile7(join9(dataDir, "PG_VERSION"), "utf8")).trim();
|
|
12503
|
+
} catch (error) {
|
|
12504
|
+
if (error.code === "ENOENT")
|
|
12505
|
+
return;
|
|
12506
|
+
throw error;
|
|
12507
|
+
}
|
|
10282
12508
|
}
|
|
10283
12509
|
async function assertSnapshotPayload(payloadRoot, manifest) {
|
|
10284
12510
|
const required = ["manifest.json", "secrets.json"];
|
|
10285
12511
|
for (const path of required) {
|
|
10286
12512
|
try {
|
|
10287
|
-
await lstat2(
|
|
12513
|
+
await lstat2(join9(payloadRoot, path));
|
|
10288
12514
|
} catch {
|
|
10289
12515
|
throw new Error(`snapshot is missing required payload: ${path}`);
|
|
10290
12516
|
}
|
|
@@ -10307,9 +12533,9 @@ async function assertRestoreTargets(paths, manifest, force) {
|
|
|
10307
12533
|
if (manifest.includesLocalStorage && !isWithin(paths.stateDir, paths.storageDir))
|
|
10308
12534
|
targets.push(paths.storageDir);
|
|
10309
12535
|
if (!force) {
|
|
10310
|
-
for (const
|
|
10311
|
-
if (await directoryHasEntries(
|
|
10312
|
-
throw new Error(`restore target is not empty: ${
|
|
12536
|
+
for (const target2 of targets) {
|
|
12537
|
+
if (await directoryHasEntries(target2))
|
|
12538
|
+
throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
|
|
10313
12539
|
}
|
|
10314
12540
|
}
|
|
10315
12541
|
}
|
|
@@ -10322,34 +12548,34 @@ async function directoryHasEntries(path) {
|
|
|
10322
12548
|
throw error;
|
|
10323
12549
|
}
|
|
10324
12550
|
}
|
|
10325
|
-
async function applyDirectorySwap(source,
|
|
10326
|
-
const targetInfo = await existingInfo(
|
|
12551
|
+
async function applyDirectorySwap(source, target2, force, rollbackId, swaps) {
|
|
12552
|
+
const targetInfo = await existingInfo(target2);
|
|
10327
12553
|
if (targetInfo && !targetInfo.isDirectory())
|
|
10328
|
-
throw new Error(`restore target is not a directory: ${
|
|
10329
|
-
const swap = { target };
|
|
12554
|
+
throw new Error(`restore target is not a directory: ${target2}`);
|
|
12555
|
+
const swap = { target: target2 };
|
|
10330
12556
|
if (targetInfo) {
|
|
10331
12557
|
if (!force) {
|
|
10332
|
-
if (await directoryHasEntries(
|
|
10333
|
-
throw new Error(`restore target is not empty: ${
|
|
10334
|
-
await
|
|
12558
|
+
if (await directoryHasEntries(target2))
|
|
12559
|
+
throw new Error(`restore target is not empty: ${target2}; pass --force to replace it`);
|
|
12560
|
+
await rm5(target2, { recursive: true, force: true });
|
|
10335
12561
|
} else {
|
|
10336
|
-
swap.rollbackPath =
|
|
10337
|
-
await rename2(
|
|
12562
|
+
swap.rollbackPath = join9(dirname6(target2), `.${target2.split(sep2).pop() ?? "state"}.restore-${rollbackId}`);
|
|
12563
|
+
await rename2(target2, swap.rollbackPath);
|
|
10338
12564
|
}
|
|
10339
12565
|
}
|
|
10340
12566
|
try {
|
|
10341
|
-
await mkdir6(
|
|
10342
|
-
await rename2(source,
|
|
12567
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12568
|
+
await rename2(source, target2);
|
|
10343
12569
|
swaps.push(swap);
|
|
10344
12570
|
} catch (error) {
|
|
10345
12571
|
if (swap.rollbackPath)
|
|
10346
|
-
await rename2(swap.rollbackPath,
|
|
12572
|
+
await rename2(swap.rollbackPath, target2).catch(() => {});
|
|
10347
12573
|
throw error;
|
|
10348
12574
|
}
|
|
10349
12575
|
}
|
|
10350
12576
|
async function rollbackDirectorySwaps(swaps) {
|
|
10351
12577
|
for (const swap of [...swaps].reverse()) {
|
|
10352
|
-
await
|
|
12578
|
+
await rm5(swap.target, { recursive: true, force: true });
|
|
10353
12579
|
if (swap.rollbackPath)
|
|
10354
12580
|
await rename2(swap.rollbackPath, swap.target);
|
|
10355
12581
|
}
|
|
@@ -10363,17 +12589,17 @@ async function existingInfo(path) {
|
|
|
10363
12589
|
throw error;
|
|
10364
12590
|
}
|
|
10365
12591
|
}
|
|
10366
|
-
async function copyEntry(source,
|
|
12592
|
+
async function copyEntry(source, target2) {
|
|
10367
12593
|
const info = await lstat2(source);
|
|
10368
12594
|
if (info.isSymbolicLink())
|
|
10369
12595
|
throw new Error(`snapshot refuses symbolic link: ${source}`);
|
|
10370
12596
|
if (info.isDirectory()) {
|
|
10371
|
-
await mkdir6(
|
|
12597
|
+
await mkdir6(target2, { recursive: true });
|
|
10372
12598
|
for (const entry of await readdir3(source))
|
|
10373
|
-
await copyEntry(
|
|
12599
|
+
await copyEntry(join9(source, entry), join9(target2, entry));
|
|
10374
12600
|
} else if (info.isFile()) {
|
|
10375
|
-
await mkdir6(
|
|
10376
|
-
await Bun.write(
|
|
12601
|
+
await mkdir6(dirname6(target2), { recursive: true });
|
|
12602
|
+
await Bun.write(target2, Bun.file(source));
|
|
10377
12603
|
} else
|
|
10378
12604
|
throw new Error(`snapshot refuses unsupported filesystem entry: ${source}`);
|
|
10379
12605
|
}
|
|
@@ -10384,7 +12610,7 @@ async function hardenRestoredTree(root) {
|
|
|
10384
12610
|
if (info.isDirectory()) {
|
|
10385
12611
|
await chmod2(root, 448);
|
|
10386
12612
|
for (const entry of await readdir3(root))
|
|
10387
|
-
await hardenRestoredTree(
|
|
12613
|
+
await hardenRestoredTree(join9(root, entry));
|
|
10388
12614
|
return;
|
|
10389
12615
|
}
|
|
10390
12616
|
if (info.isFile()) {
|
|
@@ -10395,7 +12621,7 @@ async function hardenRestoredTree(root) {
|
|
|
10395
12621
|
}
|
|
10396
12622
|
async function assertNoSymlinks(root) {
|
|
10397
12623
|
for (const entry of await readdir3(root, { withFileTypes: true })) {
|
|
10398
|
-
const fullPath =
|
|
12624
|
+
const fullPath = join9(root, entry.name);
|
|
10399
12625
|
if (entry.isSymbolicLink())
|
|
10400
12626
|
throw new Error(`snapshot refuses symbolic link in archive: ${fullPath}`);
|
|
10401
12627
|
if (entry.isDirectory())
|
|
@@ -10468,6 +12694,8 @@ function parseArgs(argv) {
|
|
|
10468
12694
|
options.storageBackend = next();
|
|
10469
12695
|
else if (argument === "--s3-prefix")
|
|
10470
12696
|
options.s3 = { ...options.s3, prefix: next() };
|
|
12697
|
+
else if (argument === "--engine")
|
|
12698
|
+
options.engine = next();
|
|
10471
12699
|
else if (argument === "--memory")
|
|
10472
12700
|
options.memory = true;
|
|
10473
12701
|
else if (argument === "--output" || argument === "-o")
|
|
@@ -10530,6 +12758,7 @@ ${privilegedKey}
|
|
|
10530
12758
|
}
|
|
10531
12759
|
const project2 = await createProjectBackend({
|
|
10532
12760
|
...options,
|
|
12761
|
+
applyMigrations: false,
|
|
10533
12762
|
includeFunctions: false,
|
|
10534
12763
|
includeWebhooks: false,
|
|
10535
12764
|
startRuntimeServices: false,
|
|
@@ -10538,8 +12767,8 @@ ${privilegedKey}
|
|
|
10538
12767
|
try {
|
|
10539
12768
|
const source = await generateTypes(project2.backend.db, "public");
|
|
10540
12769
|
if (options.output) {
|
|
10541
|
-
await mkdir7(
|
|
10542
|
-
await
|
|
12770
|
+
await mkdir7(dirname7(options.output), { recursive: true });
|
|
12771
|
+
await writeFile7(options.output, source);
|
|
10543
12772
|
await writeStandardOutput(`Wrote ${options.output}
|
|
10544
12773
|
`);
|
|
10545
12774
|
} else
|
|
@@ -10552,6 +12781,7 @@ ${privilegedKey}
|
|
|
10552
12781
|
if (options.command === "inspect") {
|
|
10553
12782
|
const project2 = await createProjectBackend({
|
|
10554
12783
|
...options,
|
|
12784
|
+
applyMigrations: false,
|
|
10555
12785
|
includeFunctions: false,
|
|
10556
12786
|
includeWebhooks: false,
|
|
10557
12787
|
startRuntimeServices: false,
|
|
@@ -10587,13 +12817,13 @@ ${privilegedKey}
|
|
|
10587
12817
|
}
|
|
10588
12818
|
if (options.command !== "start")
|
|
10589
12819
|
throw new Error(`unknown command: ${options.command}`);
|
|
10590
|
-
const project = await startProjectServer({ ...options, log: (
|
|
12820
|
+
const project = await startProjectServer({ ...options, log: (message2) => console.log(` ${message2}`) });
|
|
10591
12821
|
const shutdown = waitForShutdown(() => project.close());
|
|
10592
12822
|
await writeStandardOutput(`
|
|
10593
12823
|
SupaCloud Lite running
|
|
10594
12824
|
|
|
10595
12825
|
API URL: ${project.url}
|
|
10596
|
-
Engine:
|
|
12826
|
+
Engine: ${formatDatabaseEngine(project.databaseEngine, paths.dataDir)}
|
|
10597
12827
|
Storage: ${formatStorage(project.storageBackend, paths.storageDir)}
|
|
10598
12828
|
Migrations: ${project.migrationCount} file(s)
|
|
10599
12829
|
Functions: ${project.functionNames.length ? project.functionNames.join(", ") : "none"}
|
|
@@ -10615,9 +12845,10 @@ async function runDbCommand(options) {
|
|
|
10615
12845
|
throw new Error("db reset refuses the s3 storage backend because remote objects cannot be deleted atomically");
|
|
10616
12846
|
}
|
|
10617
12847
|
await assertResetPathsSafe(paths);
|
|
12848
|
+
await assertDataDirUnlocked(paths.dataDir);
|
|
10618
12849
|
if (paths.dataDir)
|
|
10619
|
-
await
|
|
10620
|
-
await
|
|
12850
|
+
await rm6(paths.dataDir, { recursive: true, force: true });
|
|
12851
|
+
await rm6(paths.storageDir, { recursive: true, force: true });
|
|
10621
12852
|
const project2 = await createProjectBackend({
|
|
10622
12853
|
...options,
|
|
10623
12854
|
includeFunctions: false,
|
|
@@ -10636,7 +12867,13 @@ async function runDbCommand(options) {
|
|
|
10636
12867
|
}
|
|
10637
12868
|
const project = await loadSupabaseProject(resolve4(options.projectDir ?? process.cwd()));
|
|
10638
12869
|
if (subcommand === "diff") {
|
|
10639
|
-
const
|
|
12870
|
+
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
|
|
12871
|
+
const ddl = await computeDbDiff({
|
|
12872
|
+
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12873
|
+
liveEngine,
|
|
12874
|
+
migrations: project.migrations,
|
|
12875
|
+
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined
|
|
12876
|
+
});
|
|
10640
12877
|
if (ddl.length === 0) {
|
|
10641
12878
|
await writeStandardError(`No schema changes found.
|
|
10642
12879
|
`);
|
|
@@ -10648,9 +12885,9 @@ async function runDbCommand(options) {
|
|
|
10648
12885
|
`;
|
|
10649
12886
|
if (options.diffFile) {
|
|
10650
12887
|
const stamp = timestamp();
|
|
10651
|
-
const output =
|
|
10652
|
-
await mkdir7(
|
|
10653
|
-
await
|
|
12888
|
+
const output = join10(paths.projectDir, "supabase", "migrations", `${stamp}_${options.diffFile}.sql`);
|
|
12889
|
+
await mkdir7(join10(paths.projectDir, "supabase", "migrations"), { recursive: true });
|
|
12890
|
+
await writeFile7(output, source);
|
|
10654
12891
|
await writeStandardOutput(`Wrote ${output}
|
|
10655
12892
|
`);
|
|
10656
12893
|
} else
|
|
@@ -10658,10 +12895,13 @@ async function runDbCommand(options) {
|
|
|
10658
12895
|
return;
|
|
10659
12896
|
}
|
|
10660
12897
|
if (subcommand === "pull") {
|
|
12898
|
+
const liveEngine = paths.databaseEngine === "native" ? await createNativeEngine({ dataDir: paths.dataDir, log: quietLog }) : undefined;
|
|
10661
12899
|
const result = await pullSchema({
|
|
10662
|
-
liveDataDir: paths.dataDir,
|
|
12900
|
+
liveDataDir: paths.databaseEngine === "pglite" ? paths.dataDir : undefined,
|
|
12901
|
+
liveEngine,
|
|
10663
12902
|
migrations: project.migrations,
|
|
10664
|
-
|
|
12903
|
+
makeShadowEngine: paths.databaseEngine === "native" ? createTemporaryNativeEngine : undefined,
|
|
12904
|
+
migrationsDir: join10(paths.projectDir, "supabase", "migrations"),
|
|
10665
12905
|
name: options.positionals[1] ?? "remote_schema"
|
|
10666
12906
|
});
|
|
10667
12907
|
if (!result.path)
|
|
@@ -10682,7 +12922,7 @@ async function runSnapshotCommand(options) {
|
|
|
10682
12922
|
throw new Error("snapshot does not support --memory because the database is not durable");
|
|
10683
12923
|
if (subcommand === "create") {
|
|
10684
12924
|
await ensureProjectSecrets(paths);
|
|
10685
|
-
const output = options.output ??
|
|
12925
|
+
const output = options.output ?? join10(paths.stateDir, "backups", `snapshot-${timestamp()}.tar.gz`);
|
|
10686
12926
|
const manifest = await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
|
|
10687
12927
|
await writeStandardOutput(`Snapshot created: ${output}
|
|
10688
12928
|
`);
|
|
@@ -10716,7 +12956,7 @@ async function runUpgradeCommand(options) {
|
|
|
10716
12956
|
const paths = resolveProjectPaths(options);
|
|
10717
12957
|
const storageBackend = resolveStorageBackend(options.storageBackend);
|
|
10718
12958
|
await ensureProjectSecrets(paths);
|
|
10719
|
-
const output = options.output ??
|
|
12959
|
+
const output = options.output ?? join10(paths.stateDir, "backups", `pre-upgrade-${timestamp()}.tar.gz`);
|
|
10720
12960
|
await createSnapshot({ paths, packageVersion: package_default.version, storageBackend, output });
|
|
10721
12961
|
await writeStandardOutput(`Pre-upgrade snapshot: ${output}
|
|
10722
12962
|
`);
|
|
@@ -10800,6 +13040,7 @@ Options:
|
|
|
10800
13040
|
--storage-dir <p> object storage directory
|
|
10801
13041
|
--storage-backend <b> fs, memory, or s3 (default fs)
|
|
10802
13042
|
--s3-prefix <p> optional key prefix for the s3 backend
|
|
13043
|
+
--engine <e> pglite (default) or native (macOS/glibc Linux x64/arm64)
|
|
10803
13044
|
--memory use an in-memory PGlite database
|
|
10804
13045
|
-o, --output <p> output file for gen types
|
|
10805
13046
|
-f, --file <name> migration suffix for db diff
|
|
@@ -10815,6 +13056,10 @@ function formatStorage(backend, storageDir) {
|
|
|
10815
13056
|
return "custom driver";
|
|
10816
13057
|
return storageDir;
|
|
10817
13058
|
}
|
|
13059
|
+
function formatDatabaseEngine(engine, dataDir) {
|
|
13060
|
+
const label = engine === "native" ? "Native PostgreSQL" : "PGlite";
|
|
13061
|
+
return dataDir ? `${label} (${dataDir})` : `${label} (memory)`;
|
|
13062
|
+
}
|
|
10818
13063
|
var windowsLifecycleRef = process.platform === "win32" ? setInterval(() => {}, 1000) : null;
|
|
10819
13064
|
var exitCode = 0;
|
|
10820
13065
|
try {
|