Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/config/src/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -37,6 +38,7 @@ const baseProjectConfigFields = {
realtime,
storage,
studio,
workers,
experimental,
};

Expand All @@ -52,6 +54,7 @@ const remoteProjectConfig = Schema.Struct({
realtime,
storage,
studio,
workers,
experimental,
}).pipe(Schema.withDecodingDefault(Effect.succeed({})));

Expand Down
92 changes: 92 additions & 0 deletions packages/config/src/workers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
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}`). `root` is excluded from the key pattern
* because `[workers]` carries both the project-wide `root` scalar and one
* sub-table per worker; without the exclusion the record's index signature also
* claims `root` and rejects its string value.
*/
const workerName = Schema.String.check(
Schema.isPattern(/^(?!root$)[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,
}),
),
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 root>/<name>/\`.
`,
examples: ["packages/api"],
tags,
links,
}),
),
});

/**
* `[workers]` — a project-wide `root` plus one `[workers.<name>]` table per
* worker, mirroring the `[functions.<slug>]` convention in the same file.
*
* `root` names the directory workers are grouped in, relative to `supabase/`;
* a single worker whose code lives somewhere else entirely uses its own
* `source` instead, which is anchored to the project root and so can leave
* `supabase/`.
*/
export const workers = Schema.StructWithRest(
Schema.Struct({
root: Schema.optionalKey(
Schema.String.annotate({
description: dedent`
Directory workers are grouped in, relative to \`supabase/\`. Defaults to
\`workers\`.
`,
examples: ["services"],
tags,
links,
}),
),
}),
[Schema.Record(workerName, worker)],
)
.annotate({
default: {},
description: "Worker-specific configuration keyed by worker name.",
tags,
})
.pipe(Schema.withDecodingDefault(Effect.succeed({})));
46 changes: 46 additions & 0 deletions packages/config/src/workers.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Schema } from "effect";
import { describe, expect, test } from "vitest";
import { workers } from "./workers.ts";

const decode = Schema.decodeUnknownSync(workers);

const workerNamePattern = "^(?!root$)[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$";

describe("workers schema", () => {
test("decodes the project-wide root alongside per-worker tables", () => {
expect(
decode({
root: "services",
api: { runtime: "node", size: "4gb", source: "packages/api" },
}),
).toEqual({
root: "services",
api: { runtime: "node", size: "4gb", 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.<slug>]` 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" },
});
});

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(objectSchema?.properties?.root).toBeDefined();
expect(workerSchema?.properties?.runtime).toBeDefined();
expect(workerSchema?.properties?.size).toBeDefined();
expect(workerSchema?.properties?.source).toBeDefined();
});
});