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
4 changes: 2 additions & 2 deletions apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,8 @@ Legend:
| `gen bearer-jwt` | `wrapped` | [`../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts`](../src/legacy/commands/gen/bearer-jwt/bearer-jwt.command.ts) |
| `gen keys` | `wrapped` | [`../src/legacy/commands/gen/keys/keys.command.ts`](../src/legacy/commands/gen/keys/keys.command.ts) |
| `functions list` | `wrapped` | [`../src/legacy/commands/functions/list/list.command.ts`](../src/legacy/commands/functions/list/list.command.ts) |
| `functions delete` | `wrapped` | [`../src/legacy/commands/functions/delete/delete.command.ts`](../src/legacy/commands/functions/delete/delete.command.ts) |
| `functions download` | `wrapped` | [`../src/legacy/commands/functions/download/download.command.ts`](../src/legacy/commands/functions/download/download.command.ts) |
| `functions delete` | `ported` | [`../src/legacy/commands/functions/delete/delete.command.ts`](../src/legacy/commands/functions/delete/delete.command.ts) |
| `functions download` | `ported` | [`../src/legacy/commands/functions/download/download.command.ts`](../src/legacy/commands/functions/download/download.command.ts) |
| `functions deploy` | `wrapped` | [`../src/legacy/commands/functions/deploy/deploy.command.ts`](../src/legacy/commands/functions/deploy/deploy.command.ts) |
| `functions new` | `wrapped` | [`../src/legacy/commands/functions/new/new.command.ts`](../src/legacy/commands/functions/new/new.command.ts) |
| `functions serve` | `wrapped` | [`../src/legacy/commands/functions/serve/serve.command.ts`](../src/legacy/commands/functions/serve/serve.command.ts) |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,15 +42,16 @@ Prints a success message after the function is deleted.

### `--output-format json`

Not applicable (proxied to Go binary).
Prints a structured success result with the function slug and project ref.

### `--output-format stream-json`

Not applicable (proxied to Go binary).
Prints a structured success result with the function slug and project ref.

## Notes

- Requires exactly one argument: the function slug/name.
- Does NOT remove the function from the local filesystem.
- Requires a linked project (`--project-ref` or linked project config).
- Phase 0 proxy: all invocations are forwarded to the bundled Go binary.
- Runs natively in TypeScript through the Management API.
- Refreshes the linked-project telemetry cache and flushes telemetry state after resolving a project ref.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Argument, Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyFunctionsDelete } from "./delete.handler.ts";

const config = {
Expand All @@ -19,5 +22,21 @@ export const legacyFunctionsDeleteCommand = Command.make("delete", config).pipe(
"Delete a Function from the linked Supabase project. This does NOT remove the Function locally.",
),
Command.withShortDescription("Delete a Function from Supabase"),
Command.withHandler((flags) => legacyFunctionsDelete(flags)),
Command.withExamples([
{
command: "supabase functions delete hello-world",
description: "Delete a deployed function from the linked project",
},
{
command: "supabase functions delete hello-world --project-ref abcdefghijklmnopqrst",
description: "Delete a deployed function from a specific project",
},
]),
Command.withHandler((flags) =>
legacyFunctionsDelete(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["functions", "delete"])),
);
40 changes: 35 additions & 5 deletions apps/cli/src/legacy/commands/functions/delete/delete.handler.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,42 @@
import { Effect, Option } from "effect";
import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts";
import { deleteFunction } from "../../../../shared/functions/delete.ts";
import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import type { LegacyFunctionsDeleteFlags } from "./delete.command.ts";

export const legacyFunctionsDelete = Effect.fn("legacy.functions.delete")(function* (
flags: LegacyFunctionsDeleteFlags,
) {
const proxy = yield* LegacyGoProxy;
const args: string[] = ["functions", "delete", flags.functionName];
if (Option.isSome(flags.projectRef)) args.push("--project-ref", flags.projectRef.value);
yield* proxy.exec(args);
const api = yield* LegacyPlatformApi;
const resolver = yield* LegacyProjectRefResolver;
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;
let resolvedProjectRef = Option.none<string>();

yield* deleteFunction(
{ slug: flags.functionName, projectRef: flags.projectRef },
{
api,
resolveProjectRef: (projectRef) =>
resolver.resolve(projectRef).pipe(
Effect.tap((ref) =>
Effect.sync(() => {
resolvedProjectRef = Option.some(ref);
}),
),
),
},
).pipe(
Effect.ensuring(
Effect.suspend(() =>
Option.match(resolvedProjectRef, {
onNone: () => Effect.void,
onSome: (ref) => linkedProjectCache.cache(ref),
}),
),
),
Effect.ensuring(telemetryState.flush),
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { describe, expect, it } from "@effect/vitest";
import { Effect, Option } from "effect";

import {
buildLegacyTestRuntime,
mockLegacyCliConfig,
mockLegacyLinkedProjectCacheTracked,
mockLegacyPlatformApi,
mockLegacyTelemetryStateTracked,
useLegacyTempWorkdir,
} from "../../../../../tests/helpers/legacy-mocks.ts";
import { mockOutput } from "../../../../../tests/helpers/mocks.ts";
import { legacyFunctionsDelete } from "./delete.handler.ts";

const tempRoot = useLegacyTempWorkdir("supabase-functions-delete-legacy-");

describe("legacy functions delete", () => {
it.live("deletes a function natively through the Management API", () => {
const out = mockOutput({ format: "text" });
const api = mockLegacyPlatformApi({ response: { status: 200, body: null } });
const linkedProjectCache = mockLegacyLinkedProjectCacheTracked();
const telemetry = mockLegacyTelemetryStateTracked();
const layer = buildLegacyTestRuntime({
out,
api,
cliConfig: mockLegacyCliConfig({ workdir: tempRoot.current }),
linkedProjectCache: linkedProjectCache.layer,
telemetry: telemetry.layer,
});

return Effect.gen(function* () {
yield* legacyFunctionsDelete({
functionName: "hello-world",
projectRef: Option.none(),
});

expect(api.requests).toHaveLength(1);
expect(api.requests[0]?.method).toBe("DELETE");
expect(api.requests[0]?.url).toBe(
"https://api.supabase.com/v1/projects/abcdefghijklmnopqrst/functions/hello-world",
);
expect(out.stdoutText).toBe(
"Deleted Function hello-world from project abcdefghijklmnopqrst.\n",
);
expect(linkedProjectCache.cached).toBe(true);
expect(telemetry.flushed).toBe(true);
}).pipe(Effect.provide(layer));
});

it.live("uses an explicit project ref", () => {
const out = mockOutput({ format: "text" });
const api = mockLegacyPlatformApi({ response: { status: 200, body: null } });
const layer = buildLegacyTestRuntime({
out,
api,
cliConfig: mockLegacyCliConfig({
workdir: tempRoot.current,
projectId: Option.none(),
}),
});

return Effect.gen(function* () {
yield* legacyFunctionsDelete({
functionName: "hello-world",
projectRef: Option.some("qrstuvwxyzabcdefghij"),
});

expect(api.requests[0]?.url).toContain("/projects/qrstuvwxyzabcdefghij/functions/");
}).pipe(Effect.provide(layer));
});
});
27 changes: 18 additions & 9 deletions apps/cli/src/legacy/commands/functions/download/SIDE_EFFECTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,23 @@

## Files Written

| Path | Format | When |
| ---------------------------------------------- | ---------- | ---------------------------------- |
| `<workdir>/supabase/functions/<slug>/index.ts` | TypeScript | always (downloads function source) |
| Path | Format | When |
| --------------------------------------------------- | ------ | ---------------------------------------- |
| `<workdir>/supabase/functions/<slug>/<remote path>` | bytes | for each source file returned by the API |

## API Routes

| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------ | ------------ | ------------ | ---------------------- |
| `GET` | `/v1/projects/{ref}/functions/{slug}/body` | Bearer token | none | function source code |
| Method | Path | Auth | Request body | Response (used fields) |
| ------ | ------------------------------------------ | ------------ | ------------ | ------------------------------------------ |
| `GET` | `/v1/projects/{ref}/functions` | Bearer token | none | function slugs, when downloading all |
| `GET` | `/v1/projects/{ref}/functions/{slug}` | Bearer token | none | entrypoint path, when absent from metadata |
| `GET` | `/v1/projects/{ref}/functions/{slug}/body` | Bearer token | none | multipart function source |

## Subprocesses

| Command | When | Purpose |
| ------------------------------------ | ----------------------------------- | ----------------------------------- |
| `supabase-go functions download ...` | `--use-docker` or `--legacy-bundle` | preserve hidden compatibility modes |

## Environment Variables

Expand All @@ -42,15 +50,16 @@ Prints progress and success messages as functions are downloaded.

### `--output-format json`

Not applicable (proxied to Go binary).
Prints a structured success result with the downloaded function slugs and project ref.

### `--output-format stream-json`

Not applicable (proxied to Go binary).
Prints a structured success result with the downloaded function slugs and project ref.

## Notes

- If no function name is provided, downloads all functions.
- Requires a linked project (`--project-ref` or linked project config).
- Native downloads reject path traversal and symlink escapes before writing source files.
- `--use-docker` and `--legacy-bundle` are hidden flags forwarded to the Go binary for backward compatibility; they are mutually exclusive with `--use-api`.
- Phase 0 proxy: all invocations are forwarded to the bundled Go binary.
- Refreshes the linked-project telemetry cache and flushes telemetry state after resolving a project ref.
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { Argument, Command, Flag } from "effect/unstable/cli";
import type * as CliCommand from "effect/unstable/cli/Command";
import { withJsonErrorHandling } from "../../../../shared/output/json-error-handling.ts";
import { legacyManagementApiRuntimeLayer } from "../../../shared/legacy-management-api-runtime.layer.ts";
import { withLegacyCommandInstrumentation } from "../../../telemetry/legacy-command-instrumentation.ts";
import { legacyFunctionsDownload } from "./download.handler.ts";

const config = {
Expand Down Expand Up @@ -31,5 +34,21 @@ export const legacyFunctionsDownloadCommand = Command.make("download", config).p
"Download the source code for a Function from the linked Supabase project. If no function name is provided, downloads all functions.",
),
Command.withShortDescription("Download a Function from Supabase"),
Command.withHandler((flags) => legacyFunctionsDownload(flags)),
Command.withExamples([
{
command: "supabase functions download hello-world",
description: "Download a single function from the linked project",
},
{
command: "supabase functions download --project-ref abcdefghijklmnopqrst",
description: "Download all functions from a specific project",
},
]),
Command.withHandler((flags) =>
legacyFunctionsDownload(flags).pipe(
withLegacyCommandInstrumentation({ flags }),
withJsonErrorHandling,
),
),
Command.provide(legacyManagementApiRuntimeLayer(["functions", "download"])),
);
Original file line number Diff line number Diff line change
@@ -1,16 +1,49 @@
import { Effect, Option } from "effect";
import {
downloadFunctions,
makeGoProxyDownloadArgs,
} from "../../../../shared/functions/download.ts";
import { LegacyGoProxy } from "../../../../shared/legacy/go-proxy.service.ts";
import { LegacyPlatformApi } from "../../../auth/legacy-platform-api.service.ts";
import { LegacyCliConfig } from "../../../config/legacy-cli-config.service.ts";
import { LegacyProjectRefResolver } from "../../../config/legacy-project-ref.service.ts";
import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-project-cache.service.ts";
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import type { LegacyFunctionsDownloadFlags } from "./download.command.ts";

export const legacyFunctionsDownload = Effect.fn("legacy.functions.download")(function* (
flags: LegacyFunctionsDownloadFlags,
) {
const api = yield* LegacyPlatformApi;
const cliConfig = yield* LegacyCliConfig;
const resolver = yield* LegacyProjectRefResolver;
const linkedProjectCache = yield* LegacyLinkedProjectCache;
const telemetryState = yield* LegacyTelemetryState;
const proxy = yield* LegacyGoProxy;
const args: string[] = ["functions", "download"];
if (Option.isSome(flags.functionName)) args.push(flags.functionName.value);
if (Option.isSome(flags.projectRef)) args.push("--project-ref", flags.projectRef.value);
if (flags.useApi) args.push("--use-api");
if (flags.useDocker) args.push("--use-docker");
if (flags.legacyBundle) args.push("--legacy-bundle");
yield* proxy.exec(args);
let resolvedProjectRef = Option.none<string>();

yield* downloadFunctions(flags, {
api,
projectRoot: cliConfig.workdir,
Comment thread
7ttp marked this conversation as resolved.
resolveProjectRef: (projectRef) =>
resolver.resolve(projectRef).pipe(
Effect.tap((ref) =>
Effect.sync(() => {
resolvedProjectRef = Option.some(ref);
}),
),
),
proxyDownload: (proxyFlags, projectRef) =>
proxy.exec(makeGoProxyDownloadArgs(proxyFlags, projectRef)),
}).pipe(
Effect.ensuring(
Effect.suspend(() =>
Option.match(resolvedProjectRef, {
onNone: () => Effect.void,
onSome: (ref) => linkedProjectCache.cache(ref),
}),
),
),
Effect.ensuring(telemetryState.flush),
);
});
Loading
Loading