diff --git a/apps/docs/public/cli/config.schema.json b/apps/docs/public/cli/config.schema.json index 5a7d25463b..07538197f1 100644 --- a/apps/docs/public/cli/config.schema.json +++ b/apps/docs/public/cli/config.schema.json @@ -20,12 +20,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -44,12 +39,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -82,12 +72,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -103,12 +88,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -148,12 +128,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -176,12 +151,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -206,25 +176,12 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], - "description": "Password character requirements.", - "default": "" + "$ref": "#/$defs/Union_1" }, "publishable_key": { "type": "string", @@ -291,12 +248,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -306,12 +258,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -326,12 +273,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -382,12 +324,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -397,12 +334,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -436,12 +368,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -451,12 +378,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -466,12 +388,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -509,12 +426,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -540,12 +452,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -583,12 +490,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -607,8 +509,64 @@ }, "additionalProperties": false }, + "workers": { + "anyOf": [ + { + "$ref": "#/$defs/Objects_27" + }, + { + "type": "null" + } + ] + }, "experimental": { - "$ref": "#/$defs/Objects_27" + "type": "object", + "properties": { + "orioledb_version": { + "type": "string", + "description": "Postgres storage engine version for OrioleDB." + }, + "s3_host": { + "type": "string", + "description": "S3 bucket URL.", + "examples": [ + ".s3-.amazonaws.com", + "env(S3_HOST)" + ] + }, + "s3_region": { + "type": "string", + "description": "S3 bucket region.", + "examples": [ + "us-east-1", + "env(S3_REGION)" + ] + }, + "s3_access_key": { + "type": "string", + "description": "S3 access key.", + "examples": [ + "env(S3_ACCESS_KEY)" + ] + }, + "s3_secret_key": { + "type": "string", + "description": "S3 secret key.", + "examples": [ + "env(S3_SECRET_KEY)" + ] + }, + "webhooks": { + "$ref": "#/$defs/Objects_28" + }, + "pgdelta": { + "$ref": "#/$defs/Objects_29" + }, + "inspect": { + "$ref": "#/$defs/Objects_30" + } + }, + "additionalProperties": false }, "remotes": { "anyOf": [ @@ -638,12 +596,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -662,12 +615,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -700,12 +648,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -721,12 +664,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -766,12 +704,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -794,12 +727,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -824,25 +752,12 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, "password_requirements": { - "type": "string", - "enum": [ - "", - "letters_digits", - "lower_upper_letters_digits", - "lower_upper_letters_digits_symbols" - ], - "description": "Password character requirements.", - "default": "" + "$ref": "#/$defs/Union_1" }, "publishable_key": { "type": "string", @@ -909,12 +824,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -924,12 +834,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -944,12 +849,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1000,12 +900,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1015,12 +910,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1054,12 +944,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1069,12 +954,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1084,12 +964,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1127,12 +1002,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -1158,12 +1028,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -1201,12 +1066,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1225,8 +1085,64 @@ }, "additionalProperties": false }, + "workers": { + "anyOf": [ + { + "$ref": "#/$defs/Objects_27" + }, + { + "type": "null" + } + ] + }, "experimental": { - "$ref": "#/$defs/Objects_27" + "type": "object", + "properties": { + "orioledb_version": { + "type": "string", + "description": "Postgres storage engine version for OrioleDB." + }, + "s3_host": { + "type": "string", + "description": "S3 bucket URL.", + "examples": [ + ".s3-.amazonaws.com", + "env(S3_HOST)" + ] + }, + "s3_region": { + "type": "string", + "description": "S3 bucket region.", + "examples": [ + "us-east-1", + "env(S3_REGION)" + ] + }, + "s3_access_key": { + "type": "string", + "description": "S3 access key.", + "examples": [ + "env(S3_ACCESS_KEY)" + ] + }, + "s3_secret_key": { + "type": "string", + "description": "S3 secret key.", + "examples": [ + "env(S3_SECRET_KEY)" + ] + }, + "webhooks": { + "$ref": "#/$defs/Objects_28" + }, + "pgdelta": { + "$ref": "#/$defs/Objects_29" + }, + "inspect": { + "$ref": "#/$defs/Objects_30" + } + }, + "additionalProperties": false } }, "additionalProperties": false @@ -1247,6 +1163,14 @@ }, "additionalProperties": false, "$defs": { + "Union_": { + "type": "string", + "enum": [ + "Infinity", + "-Infinity", + "NaN" + ] + }, "Arrays_": { "type": "array", "items": { @@ -1299,6 +1223,17 @@ "https://127.0.0.1:3000" ] }, + "Union_1": { + "type": "string", + "enum": [ + "", + "letters_digits", + "lower_upper_letters_digits", + "lower_upper_letters_digits_symbols" + ], + "description": "Password character requirements.", + "default": "" + }, "Objects_1": { "type": "object", "properties": { @@ -1308,12 +1243,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1323,12 +1253,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1338,12 +1263,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1353,12 +1273,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1368,12 +1283,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1383,12 +1293,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1398,12 +1303,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -1591,12 +1491,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1635,12 +1530,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -1696,12 +1586,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1711,12 +1596,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -1738,12 +1618,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -2962,12 +2837,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -2986,12 +2856,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3001,12 +2866,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -3055,6 +2915,16 @@ }, "additionalProperties": false }, + "Union_2": { + "anyOf": [ + { + "type": "number" + }, + { + "$ref": "#/$defs/Union_" + } + ] + }, "Objects_16": { "type": "object", "properties": { @@ -3068,94 +2938,22 @@ "type": "string" }, "max_connections": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_locks_per_transaction": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_parallel_maintenance_workers": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_parallel_workers": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_parallel_workers_per_gather": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_replication_slots": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_slot_wal_keep_size": { "type": "string" @@ -3170,34 +2968,10 @@ "type": "string" }, "max_wal_senders": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "max_worker_processes": { - "anyOf": [ - { - "type": "number" - }, - { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] - } - ] + "$ref": "#/$defs/Union_2" }, "session_replication_role": { "type": "string", @@ -3389,12 +3163,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] } @@ -3449,12 +3218,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3464,12 +3228,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3479,12 +3238,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3534,12 +3288,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3549,12 +3298,7 @@ "type": "number" }, { - "type": "string", - "enum": [ - "Infinity", - "-Infinity", - "NaN" - ] + "$ref": "#/$defs/Union_" } ] }, @@ -3592,109 +3336,115 @@ }, "Objects_27": { "type": "object", - "properties": { - "orioledb_version": { - "type": "string", - "description": "Postgres storage engine version for OrioleDB." - }, - "s3_host": { - "type": "string", - "description": "S3 bucket URL.", - "examples": [ - ".s3-.amazonaws.com", - "env(S3_HOST)" - ] - }, - "s3_region": { - "type": "string", - "description": "S3 bucket region.", - "examples": [ - "us-east-1", - "env(S3_REGION)" - ] - }, - "s3_access_key": { - "type": "string", - "description": "S3 access key.", - "examples": [ - "env(S3_ACCESS_KEY)" - ] - }, - "s3_secret_key": { - "type": "string", - "description": "S3 secret key.", - "examples": [ - "env(S3_SECRET_KEY)" - ] - }, - "webhooks": { - "type": "object", - "properties": { - "enabled": { - "type": "boolean", - "description": "Enable experimental webhooks.", - "default": false - } - }, - "additionalProperties": false - }, - "pgdelta": { + "patternProperties": { + "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$": { "type": "object", "properties": { - "enabled": { - "type": "boolean", - "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", - "default": false + "runtime": { + "type": "string", + "description": "Runtime the worker is built on: `dockerfile` to build the directory's own\nDockerfile, or one of the catalog runtimes (`node`, `deno`). Guessed from\nmarker files when unset.", + "examples": [ + "node" + ] }, - "declarative_schema_path": { + "size": { "type": "string", - "description": "Directory under supabase/ where declarative schema files are written.", + "description": "Instance size, denominated by memory. Each size implies its own vCPU count,\nso it is the one dial rather than two.", "examples": [ - "./schemas" + "2gb" ] }, - "format_options": { + "instances": { + "type": "integer", + "allOf": [ + { + "minimum": 0, + "description": "Number of instances to run. Every deploy sends a complete spec, so a count\nrecorded here is what keeps a scaled worker scaled; `--instances` overrides\nit for one deploy. Defaults to 1.", + "examples": [ + 3 + ] + } + ] + }, + "source": { "type": "string", - "description": "JSON string passed through to pg-delta SQL formatting.", + "description": "Directory holding the worker's code, relative to the project root, when it\ndoes not live at `supabase/workers//`.", "examples": [ - "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" + "packages/api" ] } }, "additionalProperties": false + } + }, + "description": "Worker-specific configuration keyed by worker name.", + "default": {} + }, + "Objects_28": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Enable experimental webhooks.", + "default": false + } + }, + "additionalProperties": false + }, + "Objects_29": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Use pg-delta as the schema diff engine for db diff / db pull / db remote commit. Set false to fall back to the legacy migra engine.", + "default": false }, - "inspect": { - "type": "object", - "properties": { - "rules": { - "type": "array", - "items": { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "Inspection query." - }, - "name": { - "type": "string", - "description": "Inspection rule name." - }, - "pass": { - "type": "string", - "description": "Success message." - }, - "fail": { - "type": "string", - "description": "Failure message." - } - }, - "additionalProperties": false + "declarative_schema_path": { + "type": "string", + "description": "Directory under supabase/ where declarative schema files are written.", + "examples": [ + "./schemas" + ] + }, + "format_options": { + "type": "string", + "description": "JSON string passed through to pg-delta SQL formatting.", + "examples": [ + "{\"keywordCase\":\"upper\",\"indent\":2,\"maxWidth\":80,\"commaStyle\":\"trailing\"}" + ] + } + }, + "additionalProperties": false + }, + "Objects_30": { + "type": "object", + "properties": { + "rules": { + "type": "array", + "items": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Inspection query." }, - "description": "Inspection rules.", - "default": [] - } + "name": { + "type": "string", + "description": "Inspection rule name." + }, + "pass": { + "type": "string", + "description": "Success message." + }, + "fail": { + "type": "string", + "description": "Failure message." + } + }, + "additionalProperties": false }, - "additionalProperties": false + "description": "Inspection rules.", + "default": [] } }, "additionalProperties": false diff --git a/packages/config/src/base.ts b/packages/config/src/base.ts index d84ba7c2c4..b4504d92f6 100644 --- a/packages/config/src/base.ts +++ b/packages/config/src/base.ts @@ -10,6 +10,7 @@ import { inbucket } from "./inbucket.ts"; import { realtime } from "./realtime.ts"; import { storage } from "./storage.ts"; import { studio } from "./studio.ts"; +import { workers } from "./workers.ts"; const projectId = Schema.optionalKey( Schema.String.annotate({ @@ -37,6 +38,7 @@ const baseProjectConfigFields = { realtime, storage, studio, + workers, experimental, }; @@ -52,6 +54,7 @@ const remoteProjectConfig = Schema.Struct({ realtime, storage, studio, + workers, experimental, }).pipe(Schema.withDecodingDefault(Effect.succeed({}))); diff --git a/packages/config/src/io.unit.test.ts b/packages/config/src/io.unit.test.ts index 0159b251d4..248c29679f 100644 --- a/packages/config/src/io.unit.test.ts +++ b/packages/config/src/io.unit.test.ts @@ -1246,6 +1246,34 @@ project_id = "dupref" } }); + test("loads a [remotes.*.workers] section alongside the project's own", async () => { + const cwd = makeTempProject(); + + try { + await mkdir(join(cwd, "supabase"), { recursive: true }); + await writeFile( + join(cwd, "supabase", "config.toml"), + `project_id = "baseref" + +[workers.api] +runtime = "node" + +[remotes.staging] +project_id = "abcdefghijklmnopqrst" + +[remotes.staging.workers.api] +runtime = "deno" +`, + ); + + const loaded = await runConfigEffect(loadProjectConfig(cwd)); + expect(loaded).not.toBeNull(); + expect(loaded!.config.workers).toEqual({ api: { runtime: "node" } }); + } finally { + await rm(cwd, { recursive: true, force: true }); + } + }); + test("loads successfully with an invalid [remotes.*] project_id format when goViperCompat is omitted", async () => { const cwd = makeTempProject(); diff --git a/packages/config/src/workers.ts b/packages/config/src/workers.ts new file mode 100644 index 0000000000..5932416494 --- /dev/null +++ b/packages/config/src/workers.ts @@ -0,0 +1,89 @@ +import dedent from "dedent"; +import { Effect, Schema } from "effect"; + +const tags = ["workers"]; + +const links = [ + { + name: "`supabase workers` CLI subcommands", + link: "https://supabase.com/docs/reference/cli/supabase-workers", + }, +]; + +/** + * Worker names end up in hostnames, so they are DNS labels — the same pattern + * the Management API validates `:name` against + * (`v2/projects/{ref}/workers/{name}`). + */ +const workerName = Schema.String.check(Schema.isPattern(/^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/)); + +const worker = Schema.Struct({ + runtime: Schema.optionalKey( + Schema.String.annotate({ + description: dedent` + Runtime the worker is built on: \`dockerfile\` to build the directory's own + Dockerfile, or one of the catalog runtimes (\`node\`, \`deno\`). Guessed from + marker files when unset. + `, + examples: ["node"], + tags, + links, + }), + ), + size: Schema.optionalKey( + Schema.String.annotate({ + description: dedent` + Instance size, denominated by memory. Each size implies its own vCPU count, + so it is the one dial rather than two. + `, + examples: ["2gb"], + tags, + links, + }), + ), + instances: Schema.optionalKey( + // Bounded to match `spec.instances` in the Management API's input schema. A + // value that gets past here is dropped rather than sent, so leaving it + // unbounded deploys a different count than the config asked for. + Schema.Number.check( + Schema.isInt().annotate({ expected: "a whole number of instances" }), + Schema.isGreaterThanOrEqualTo(0).annotate({ expected: "zero or more instances" }), + ).annotate({ + description: dedent` + Number of instances to run. Every deploy sends a complete spec, so a count + recorded here is what keeps a scaled worker scaled; \`--instances\` overrides + it for one deploy. Defaults to 1. + `, + examples: [3], + tags, + links, + }), + ), + source: Schema.optionalKey( + Schema.String.annotate({ + description: dedent` + Directory holding the worker's code, relative to the project root, when it + does not live at \`supabase/workers//\`. + `, + examples: ["packages/api"], + tags, + links, + }), + ), +}); + +/** + * `[workers]` — one `[workers.]` table per worker, mirroring the + * `[functions.]` convention in the same file. + * + * Workers live at `supabase/workers//`; one whose code lives somewhere + * else entirely uses its own `source`, which is anchored to the project root and + * so can leave `supabase/`. + */ +export const workers = Schema.Record(workerName, worker) + .annotate({ + default: {}, + description: "Worker-specific configuration keyed by worker name.", + tags, + }) + .pipe(Schema.withDecodingDefault(Effect.succeed({}))); diff --git a/packages/config/src/workers.unit.test.ts b/packages/config/src/workers.unit.test.ts new file mode 100644 index 0000000000..2a6e0c7b33 --- /dev/null +++ b/packages/config/src/workers.unit.test.ts @@ -0,0 +1,83 @@ +import { Schema } from "effect"; +import { describe, expect, test } from "vitest"; +import { workers } from "./workers.ts"; + +const decode = Schema.decodeUnknownSync(workers); + +const workerNamePattern = "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$"; + +describe("workers schema", () => { + test("decodes a worker table with every dial set", () => { + expect( + decode({ api: { runtime: "node", size: "4gb", instances: 3, source: "packages/api" } }), + ).toEqual({ api: { runtime: "node", size: "4gb", instances: 3, source: "packages/api" } }); + }); + + test("defaults to an empty section when the key is absent", () => { + expect(Schema.decodeUnknownSync(Schema.Struct({ workers }))({})).toEqual({ workers: {} }); + }); + + // Keys outside the DNS-label pattern fall outside the record's index + // signature and are dropped, the same way `[functions.]` treats a slug + // its own pattern does not match. `supabase workers new` validates the name + // up front so the CLI never writes one that would vanish here. + test("drops worker names that are not DNS labels", () => { + expect(decode({ Not_A_Label: {}, api: { runtime: "node" } })).toEqual({ + api: { runtime: "node" }, + }); + }); + + // Every dial is optional: a worker scaffolded by `supabase workers new` records + // only what it prompted for, and `push` resolves the rest from its own defaults. + test("decodes a worker table with no dials set", () => { + expect(decode({ api: {} })).toEqual({ api: {} }); + }); + + test("rejects a non-numeric instance count", () => { + expect(() => decode({ api: { instances: "three" } })).toThrow(); + }); + + // `spec.instances` is an integer in the Management API's input schema, and a + // value that slips through here is dropped downstream and silently rescales + // the worker to 1 rather than failing. Named at load time instead. + test.each([ + ["a fraction", 1.5], + ["a negative count", -1], + ])("rejects %s as an instance count", (_label, instances) => { + expect(() => decode({ api: { instances } })).toThrow(); + }); + + test("accepts zero instances", () => { + expect(decode({ api: { instances: 0 } })).toEqual({ api: { instances: 0 } }); + }); + + test("rejects a bare value where a worker table belongs", () => { + expect(() => decode({ api: "node" })).toThrow(); + }); + + // The published asset at `PROJECT_CONFIG_SCHEMA_URL` is what editors read, so + // the worker dials have to stay described and completable — and a worker value + // has to be a plain table, or an editor would accept a bare scalar the CLI + // refuses to load. + test("includes worker properties in the generated JSON schema", () => { + const json = JSON.parse(JSON.stringify(Schema.toJsonSchemaDocument(workers).schema)); + const objectSchema = json.anyOf?.find((entry: { type?: string }) => entry?.type === "object"); + const workerSchema = objectSchema?.patternProperties?.[workerNamePattern]; + + expect(workerSchema?.properties?.runtime).toBeDefined(); + expect(workerSchema?.properties?.size).toBeDefined(); + expect(workerSchema?.properties?.instances).toBeDefined(); + expect(workerSchema?.properties?.source).toBeDefined(); + }); + + // An integer bound the published schema carries, so an editor flags `1.5` + // before the CLI ever reads it. + test("bounds instances as a non-negative integer in the generated JSON schema", () => { + const json = JSON.parse(JSON.stringify(Schema.toJsonSchemaDocument(workers).schema)); + const objectSchema = json.anyOf?.find((entry: { type?: string }) => entry?.type === "object"); + const workerSchema = objectSchema?.patternProperties?.[workerNamePattern]; + + expect(workerSchema?.properties?.instances?.type).toBe("integer"); + expect(JSON.stringify(workerSchema?.properties?.instances)).toContain('"minimum":0'); + }); +});