Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/stack-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ const applyStackMigrateAndSeed = (
export const projectCatalogOverlay = (
toml: Pick<DbConfigValues, "webhooksEnabled" | "baseline" | "vault">,
workdir: string,
): StackCatalogSetupInput["overlay"] => ({
): NonNullable<StackCatalogSetupInput["overlay"]> => ({
webhooks: "config",
webhooksEnabled: toml.webhooksEnabled,
apiAutoExposeNewTables: toml.baseline.apiAutoExposeNewTables,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe("stack catalog setup", { timeout: 180_000 }, () => {
args: ["--dbname", databaseUrl, "-At"],
stdin: Stream.make(
new TextEncoder().encode(
"select coalesce(to_regclass('auth.users')::text,'missing'), coalesce(to_regclass('auth.sessions')::text,'missing'), coalesce(to_regclass('storage.objects')::text,'missing'), coalesce(to_regclass('storage.s3_multipart_uploads')::text,'missing'), coalesce(to_regclass('realtime.messages')::text,'missing'), coalesce(to_regclass('realtime.subscription')::text,'missing'), coalesce(to_regclass('public.catalog_overlay')::text,'missing'), coalesce(to_regprocedure('supabase_functions.http_request()')::text,'missing');",
"select coalesce(to_regclass('auth.users')::text,'missing'), coalesce(to_regclass('auth.sessions')::text,'missing'), coalesce(to_regclass('storage.objects')::text,'missing'), coalesce(to_regclass('storage.s3_multipart_uploads')::text,'missing'), coalesce(to_regclass('realtime.messages')::text,'missing'), coalesce(to_regclass('realtime.subscription')::text,'missing'), coalesce(to_regclass('public.catalog_overlay')::text,'missing'), coalesce(to_regprocedure('supabase_functions.http_request()')::text,'missing'), (select tableowner from pg_tables where tablename = 'catalog_overlay');",
),
),
stdout: (bytes) =>
Expand All @@ -139,7 +139,7 @@ describe("stack catalog setup", { timeout: 180_000 }, () => {
);
expect(query.exitCode, errors.join("")).toBe(0);
expect(rows.join("").trim()).toBe(
"users|sessions|storage.objects|storage.s3_multipart_uploads|realtime.messages|realtime.subscription|catalog_overlay|supabase_functions.http_request()",
"users|sessions|storage.objects|storage.s3_multipart_uploads|realtime.messages|realtime.subscription|catalog_overlay|supabase_functions.http_request()|postgres",
);
// pg_net queues webhook requests under the image's function policy.
const pgNet: Array<string> = [];
Expand Down
21 changes: 14 additions & 7 deletions apps/cli/src/command-internal/stack-catalog-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,8 @@ export interface StackCatalogSetupInput {
readonly databaseServices: ReadonlyArray<DatabaseService>;
readonly selection: ArtifactSelection;
};
readonly overlay: StackCatalogOverlay;
/** Project SQL for a first setup; omitted when only the catalog changes. */
readonly overlay?: StackCatalogOverlay;
}

const catalogError = (cause: unknown): StackCatalogSetupError =>
Expand Down Expand Up @@ -185,12 +186,18 @@ const applyCatalog = Effect.fn("StackCatalogSetup.apply")(function* (
.makeTempDirectoryScoped({ prefix: "supabase-stack-catalog-sql-" })
.pipe(Effect.mapError(catalogError));
yield* ensureStackWebhookSchema(session, fs, path, sqlPath);
yield* applyDatabaseOverlay(session, fs, path, input.overlay.workdir, {
webhooksEnabled: input.overlay.webhooksEnabled,
apiAutoExposeNewTables: input.overlay.apiAutoExposeNewTables,
vault: input.overlay.vault,
webhooks: input.overlay.webhooks,
announceRoles: input.overlay.announceRoles,
const { overlay } = input;
if (overlay === undefined) return;
const projectSession = yield* dbConn.connect(
{ ...connection, user: "postgres" },
{ isLocal: true, dnsResolver: "native" },
);
yield* applyDatabaseOverlay(projectSession, fs, path, overlay.workdir, {
webhooksEnabled: overlay.webhooksEnabled,
apiAutoExposeNewTables: overlay.apiAutoExposeNewTables,
vault: overlay.vault,
webhooks: overlay.webhooks,
announceRoles: overlay.announceRoles,
}).pipe(Effect.withSpan("StackCatalogSetup.applyOverlay"));
}),
);
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/src/command-internal/stack-shadow-cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ export const stackShadowCacheEntry = Effect.fn("StackShadowCache.entry")(functio
enabled ? pinKey(pin) : Effect.succeed("");
const keyInputs: ShadowCacheKeyInputs = {
postgresImage: yield* encodeKeyParts([
"stack-v3",
"stack-v4",
runtime,
platform,
arch,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -400,8 +400,9 @@ reset of database data removes the record. A service on an external database has

The first configured startup prepares the database catalog, temporarily runs configured schema-owning
services, creates the `supabase_functions` schema, which migrations must not recreate, applies the
database overlay, and runs project migrations and seeds. Membership changes
apply needed catalog and webhook setup without replaying project migrations or seeds. An unchanged
database overlay (`roles.sql`, vault, API grants) as `postgres`, like legacy `start`, and runs project
migrations and seeds. Membership changes apply needed catalog and webhook setup without replaying
the overlay, project migrations or seeds. An unchanged
composition reapplies the webhook setting before activation, first creating the
`supabase_functions` schema when the database lacks it.

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1106,8 +1106,9 @@ export const stackStart = Effect.fn("experimental.stack.start")(function* (flags
databaseServices: catalogDatabaseServices(requested),
selection: config.selection,
},
overlay: projectCatalogOverlay(toml, target.projectRoot),
...(migrations === undefined ? {} : { migrations }),
...(migrations === undefined
? {}
: { overlay: projectCatalogOverlay(toml, target.projectRoot), migrations }),
}).pipe(
Effect.tapError((error) => starting.fail(error.message)),
Effect.mapError(stackError),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,7 @@ const fakeStack = (compositionStart?: Stack["composition"]["start"]) => {
let hostDestroyed = 0;
let composed = 0;
let catalogApplied = 0;
const catalogOverlays: Array<boolean> = [];
let compositionStarts = 0;
let lifecycle: "stopped" | "running" = "stopped";
let activations = new Map<string, "eager" | "lazy">();
Expand Down Expand Up @@ -498,8 +499,12 @@ const fakeStack = (compositionStart?: Stack["composition"]["start"]) => {
get compositionStarts() {
return compositionStarts;
},
applyCatalog() {
get catalogOverlays() {
return catalogOverlays;
},
applyCatalog(overlay: boolean) {
catalogApplied += 1;
catalogOverlays.push(overlay);
},
get savedCredentials() {
return savedCredentials;
Expand Down Expand Up @@ -635,7 +640,7 @@ const layers = (
ciUnsetLayer,
Layer.succeed(CliArgs, { args: ["stack", "start"] }),
Layer.succeed(StackCatalogSetup, {
apply: () => Effect.sync(() => fixture.applyCatalog()),
apply: (input) => Effect.sync(() => fixture.applyCatalog(input.overlay !== undefined)),
}),
Layer.succeed(DbConnection, { connect: () => Effect.scoped(Effect.succeed(session)) }),
Layer.succeed(YesFlag, false),
Expand Down Expand Up @@ -1404,6 +1409,23 @@ describe("experimental stack start", () => {
}).pipe(Effect.provide([BunServices.layer, BunCrypto.layer])),
);

it.effect("applies the project overlay once when a later start adds a service", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const root = yield* fs.makeTempDirectoryScoped({ prefix: "stack-start-overlay-once-" });
yield* fs.makeDirectory(`${root}/supabase`, { recursive: true });
yield* fs.writeFileString(`${root}/supabase/config.toml`, 'project_id = "overlay-once"\n');
const fixture = fakeStack();

yield* stackStart(flags(["storage"])).pipe(Effect.provide(layers(root, fixture)));
yield* fixture.stack.composition.stop;
yield* stackStart(flags()).pipe(Effect.provide(layers(root, fixture)));

expect(fixture.members.some(({ service }) => service === "storage")).toBe(true);
expect(fixture.catalogOverlays).toEqual([true, false]);
}).pipe(Effect.provide([BunServices.layer, BunCrypto.layer])),
);

it.effect("creates the supabase_functions schema once when restarting a stack without it", () =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
Expand Down
Loading