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
2 changes: 2 additions & 0 deletions apps/cli/src/cli/root.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { logoutCommand } from "../commands/logout/logout.command.ts";
import { migrationCommand } from "../commands/migration/migration.command.ts";
import { networkBansCommand } from "../commands/network-bans/network-bans.command.ts";
import { networkRestrictionsCommand } from "../commands/network-restrictions/network-restrictions.command.ts";
import { notebooksCommand } from "../commands/notebooks/notebooks.command.ts";
import { orgsCommand } from "../commands/orgs/orgs.command.ts";
import { postgresConfigCommand } from "../commands/postgres-config/postgres-config.command.ts";
import { projectsCommand } from "../commands/projects/projects.command.ts";
Expand Down Expand Up @@ -112,6 +113,7 @@ export const rootCommandForFeatures = (
migrationCommand,
networkBansCommand,
networkRestrictionsCommand,
notebooksCommand,
orgsCommand,
postgresConfigCommand,
projectsCommand,
Expand Down
10 changes: 10 additions & 0 deletions apps/cli/src/commands/notebooks/notebooks.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Command } from "effect/unstable/cli";
import { notebooksPullCommand } from "./pull/pull.command.ts";

export const notebooksCommand = Command.make("notebooks").pipe(
Command.withDescription(
"Manage Supabase notebooks: SQL and markdown cells stored with your project, kept in supabase/notebooks/<name>.json.",
),
Command.withShortDescription("Manage Supabase notebooks"),
Command.withSubcommands([notebooksPullCommand]),
);
89 changes: 89 additions & 0 deletions apps/cli/src/commands/notebooks/notebooks.errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Data } from "effect";
import {
actionability,
type CliErrorActionabilityDeclaration,
ErrorActionabilityId,
statusCodeActionability,
} from "../../shared/telemetry/error-actionability.ts";

/**
* One network / status pair covers every notebook route rather than one pair
* per call: the notebook commands all walk the same routes, and the failing one
* is already named by the message the caller templates in ("failed to list
* notebooks", "failed to update notebook <name>", …).
*/
export class NotebooksNetworkError extends Data.TaggedError("NotebooksNetworkError")<{
readonly message: string;
readonly decode?: boolean;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return this.decode === true
? { ...actionability.apiStatus, fingerprint_suffix: "api_response" }
: actionability.externalNetwork;
}
}

export class NotebooksUnexpectedStatusError extends Data.TaggedError(
"NotebooksUnexpectedStatusError",
)<{
readonly status: number;
readonly body: string;
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return statusCodeActionability(this.status);
}
}

/** A file under `supabase/notebooks/` is not readable, not JSON, or not a notebook. */
export class NotebookFileError extends Data.TaggedError("NotebookFileError")<{
readonly detail: string;
readonly suggestion: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidConfig;
}
}

/** The single-notebook pull argument is not a Management API notebook UUID. */
export class NotebookIdError extends Data.TaggedError("NotebookIdError")<{
readonly detail: string;
readonly suggestion: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidInput;
}
}

/**
* Two project notebooks share one name. The API does not require notebook names
* to be unique, but a directory of files does — so there is no way to say which
* of them a local file corresponds to, and guessing would write one user's
* notebook over another's.
*/
export class NotebookNameConflictError extends Data.TaggedError("NotebookNameConflictError")<{
readonly detail: string;
readonly suggestion: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidConfig;
}
}

export class NotebooksEnvNotSupportedError extends Data.TaggedError(
"NotebooksEnvNotSupportedError",
)<{
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return actionability.invalidInput;
}
}

export class NotebooksPaginationError extends Data.TaggedError("NotebooksPaginationError")<{
readonly message: string;
}> {
get [ErrorActionabilityId](): CliErrorActionabilityDeclaration {
return { ...actionability.apiStatus, fingerprint_suffix: "api_response" };
}
}
Loading
Loading