diff --git a/smithy/README.md b/smithy/README.md new file mode 100644 index 0000000..6dc30d8 --- /dev/null +++ b/smithy/README.md @@ -0,0 +1,115 @@ +# Supabase Smithy Models + +Canonical [Smithy IDL](https://smithy.io/) definitions for the Supabase HTTP APIs. These models are the shared source-of-truth for SDK codegen spikes — each SDK team runs their own generator toolchain against the same models. + +## Structure + +``` +smithy/ + model/ + common.smithy # Shared shapes (StringList, etc.) + storage.smithy # Supabase Storage API (buckets, objects, TUS resumable uploads) + functions.smithy # Supabase Edge Functions API (invoke: GET/POST/PUT/PATCH/DELETE) + database.smithy # Supabase Database API (PostgREST row + RPC operations) + openapi/ + StorageService.openapi.json # Generated OpenAPI 3.0 — committed for SDK consumers + FunctionsService.openapi.json # Generated OpenAPI 3.0 — committed for SDK consumers + DatabaseService.openapi.json # Generated OpenAPI 3.0 — committed for SDK consumers + smithy-build.json # Smithy build config (Smithy CLI / Gradle) + patch-openapi.py # Post-generation patches (see Known Limitations) + README.md +``` + +## Generating the OpenAPI artifacts + +**Requirements:** [Smithy CLI](https://smithy.io/2.0/guides/smithy-cli/cli-installation.html) or Gradle with the Smithy Gradle plugin. + +```bash +cd smithy +smithy build # emits openapi/ into smithy/build/smithy/*/openapi/ +python patch-openapi.py build/smithy/storage-openapi/openapi/StorageService.openapi.json +python patch-openapi.py build/smithy/functions-openapi/openapi/FunctionsService.openapi.json +python patch-openapi.py build/smithy/database-openapi/openapi/DatabaseService.openapi.json +``` + +The committed files in `openapi/` are the patched outputs — SDK teams can consume them directly without installing Smithy. + +## Services modelled + +### Storage (`model/storage.smithy`) + +Covers the full Supabase Storage HTTP API: + +| Group | Operations | +|-------|-----------| +| Buckets | `ListBuckets`, `GetBucket`, `CreateBucket`, `UpdateBucket`, `EmptyBucket`, `DeleteBucket` | +| Objects | `MoveObject`, `CopyObject`, `DeleteObjects`, `ListObjects`, `GetObjectInfo`, `HeadObject` | +| Signed URLs | `CreateSignedUrl`, `CreateSignedUrls`, `CreateSignedUploadUrl` | +| Direct upload | `UploadObject` (POST multipart), `UpdateObject` (PUT multipart) — OpenAPI-only; see Known Limitations | +| TUS resumable | `CreateTusUpload` (POST), `UploadChunk` (PATCH), `GetUploadOffset` (HEAD) | + +### Functions (`model/functions.smithy`) + +Models all five HTTP methods on `/functions/v1/{functionName}`: + +`InvokeFunctionGet`, `InvokeFunctionPost`, `InvokeFunctionPut`, `InvokeFunctionPatch`, `InvokeFunctionDelete` + +Smithy requires one operation per HTTP method — a dispatch switch in the client maps `FunctionInvokeOptions.method` to the right operation at runtime. + +### Database (`model/database.smithy`) + +Covers the PostgREST HTTP API (base URL: `/rest/v1`): + +| Group | Operations | +|-------|-----------| +| Row CRUD | `SelectRows` (GET), `InsertRows` (POST), `UpdateRows` (PATCH), `UpsertRows` (PUT), `DeleteRows` (DELETE) | +| RPC | `CallRpcPost` (POST), `CallRpcGet` (GET) | + +All row operations target `/{table}`. The model captures fixed query params (`select`, `order`, `limit`, `offset`, `on_conflict`, `columns`) and well-known request/response headers (`Prefer`, `Range`, `Range-Unit`, `Accept`, `Accept-Profile`, `Content-Profile`, `Content-Range`). + +Request and response bodies are typed as `Blob` because the row shape is fully dynamic (depends on the table schema). + +Horizontal filter parameters (`?column=op.value`, e.g. `?id=eq.5`) are expressed via an `@httpQueryParams StringMap` member (`filters`) on each read/write input — each map entry becomes a separate query parameter. A `FilterOperator` enum documents all supported operators so they are generated as typed constants in every SDK. RPC GET uses the same pattern (map named `args`) for function-specific query parameters. + +## Known Limitations + +These are gaps found during the Swift spike (see [SDK-1103](https://linear.app/supabase/issue/SDK-1103)) that require workarounds or are out of scope for codegen: + +| # | Gap | Workaround | +|---|-----|-----------| +| 1 | `@streaming blob` emits `format: byte` (base64) in OpenAPI; generators need `format: binary` | `patch-openapi.py` rewrites the format after generation | +| 2 | No native `multipart/form-data` trait in Smithy | `patch-openapi.py` injects `UploadObject`/`UpdateObject` multipart operations directly into the OpenAPI JSON | +| 3 | Smithy requires a fixed HTTP method per operation; Functions supports any method at runtime | Model 5 separate operations; client dispatches at runtime | +| 4 | `GET` + `@httpPayload` is illegal in Smithy | Separate `InvokeFunctionGetInput` shape without a body | +| 5 | Realtime (WebSocket / event-emitter) is incompatible with REST codegen | Out of scope; Realtime stays hand-written in all SDKs | +| 6 | Write operations return 204 (no body) by default and 200 with a body for `return=representation` — Smithy requires a single fixed success code | Model uses 200 throughout; SDK clients must tolerate empty response bodies | + +## Scope for SDK spikes + +The models here cover **Storage**, **Functions**, and **Database (PostgREST)**. Each SDK spike (see Linear issues SDK-1103 through SDK-1109) must also verify **Auth**: + +- **Auth** — sign-in/up, token refresh, OTP, OAuth redirects, session management + +Auth model does not exist yet. It may need to be added here, or the teams may determine that it is unsuitable for codegen (OAuth redirects and cookie-based session management are difficult to express in Smithy). + +For PostgREST the key open question for each SDK spike is whether the transport-layer codegen is useful. The `database.smithy` model covers the full HTTP surface: fixed params (`select`, `order`, `limit`, `offset`), filter params via `@httpQueryParams` + `FilterOperator` enum, and all relevant headers. The only part that stays hand-written is the query-builder API (`.eq()`, `.like()`, etc.) that constructs the filter map — that is by design, not a model gap. + +## Generator toolchains by SDK + +Each SDK team runs their own generator against the OpenAPI artifacts: + +| SDK | Candidate toolchain | +|-----|-------------------| +| Swift | `swift-openapi-generator` (spike done — see [PR #1047](https://github.com/supabase/supabase-swift/pull/1047)) | +| JavaScript/TypeScript | TypeSpec → `@hey-api/openapi-ts` or `@typespec/http-client-js` | +| Python | TypeSpec → `openapi-python-client` or `@typespec/http-client-python` | +| Dart/Flutter | OpenAPI Generator `dart-dio` (no official TypeSpec/Smithy Dart emitter) | +| C# | Kiota or `@typespec/http-client-csharp` | +| Go | `oapi-codegen` or `ogen` | +| Kotlin | `smithy-kotlin` (KMP-compatible) or custom TypeSpec emitter | + +## Reference + +- RFC: [Auto-generating parts of the Supabase SDKs](https://linear.app/supabase/project/rfc-auto-generating-parts-of-the-supabase-sdks-581579f2a632) +- Swift spike PR: [supabase/supabase-swift#1047](https://github.com/supabase/supabase-swift/pull/1047) +- Linear spike issues: SDK-1103 (Swift) · SDK-1104 (JS) · SDK-1105 (Python) · SDK-1106 (Dart) · SDK-1107 (C#) · SDK-1108 (Go) · SDK-1109 (Kotlin) diff --git a/smithy/model/common.smithy b/smithy/model/common.smithy new file mode 100644 index 0000000..93d2ab9 --- /dev/null +++ b/smithy/model/common.smithy @@ -0,0 +1,15 @@ +$version: "2" + +namespace io.supabase + +/// Common string list shape reused across services. +list StringList { + member: String +} + +/// Generic string-to-string map — used for arbitrary query parameter collections +/// (e.g. PostgREST filter params, RPC GET arguments). +map StringMap { + key: String + value: String +} diff --git a/smithy/model/database.smithy b/smithy/model/database.smithy new file mode 100644 index 0000000..b9227d1 --- /dev/null +++ b/smithy/model/database.smithy @@ -0,0 +1,395 @@ +$version: "2" + +namespace io.supabase.database + +use aws.protocols#restJson1 +use io.supabase#StringMap + +/// PostgREST-backed database API. +/// +/// Base URL: https://{project-ref}.supabase.co/rest/v1 +/// +/// Known limitations: +/// 1. Write operations return 204 (no body) by default and 200 with a body when +/// Prefer: return=representation — the model uses 200 throughout so generators +/// always produce body-parsing code; clients must tolerate empty bodies. +/// 2. RPC GET arguments are function-specific; they are expressed via the same +/// @httpQueryParams map as row filters, with function-defined keys. +@restJson1 +@title("Supabase Database API") +service DatabaseService { + version: "1.0" + operations: [ + SelectRows + InsertRows + UpdateRows + UpsertRows + DeleteRows + CallRpcPost + CallRpcGet + ] + errors: [DatabaseError] +} + +// ─── Filter Operators ──────────────────────────────────────────────────────── +// +// PostgREST horizontal filters are expressed as query parameters: +// ?{column}={operator}.{value} e.g. ?id=eq.5&name=like.foo* +// +// Use @httpQueryParams on input structures to pass a StringMap where each entry +// is a column=operator.value pair. Construct values using FilterOperator: +// filters["id"] = FilterOperator.EQ + "." + "5" → ?id=eq.5 +// filters["name"] = FilterOperator.LIKE + "." + "foo*" → ?name=like.foo* +// +// Negation: prefix the operator with "not.": +// filters["id"] = "not.eq.5" → ?id=not.eq.5 +// +// Logical grouping: use the special keys "or" / "and" with a bracketed list: +// filters["or"] = "(id.eq.1,id.eq.2)" → ?or=(id.eq.1,id.eq.2) +// +// Named @httpQuery members (select, order, limit, offset, on_conflict) take +// precedence; do not put those keys in the filters map. + +/// All filter operators supported by PostgREST. +/// Format a filter value as "{operator}.{value}", e.g. FilterOperator.EQ + ".5". +/// Prefix with "not." to negate: "not." + FilterOperator.EQ + ".5". +enum FilterOperator { + /// = (equal) + EQ = "eq" + /// <> (not equal) + NEQ = "neq" + /// < (less than) + LT = "lt" + /// <= (less than or equal) + LTE = "lte" + /// > (greater than) + GT = "gt" + /// >= (greater than or equal) + GTE = "gte" + /// LIKE — case-sensitive pattern match, use * as wildcard + LIKE = "like" + /// ILIKE — case-insensitive pattern match, use * as wildcard + ILIKE = "ilike" + /// ~ — case-sensitive regex match + MATCH = "match" + /// ~* — case-insensitive regex match + IMATCH = "imatch" + /// IS — for null, true, false, unknown + IS = "is" + /// IS DISTINCT FROM + IS_DISTINCT = "isdistinct" + /// IN — value is a parenthesised list: "in.(1,2,3)" + IN = "in" + /// @> (contains — arrays, ranges, jsonb) + CS = "cs" + /// <@ (contained by — arrays, ranges, jsonb) + CD = "cd" + /// && (overlap — arrays, ranges) + OV = "ov" + /// << (strictly left of — ranges) + SL = "sl" + /// >> (strictly right of — ranges) + SR = "sr" + /// &< (does not extend to the left of — ranges) + NXL = "nxl" + /// &> (does not extend to the right of — ranges) + NXR = "nxr" + /// -|- (adjacent — ranges) + ADJ = "adj" + /// @@ to_tsquery (full-text search) + FTS = "fts" + /// @@ plainto_tsquery + PLFTS = "plfts" + /// @@ phraseto_tsquery + PHFTS = "phfts" + /// @@ websearch_to_tsquery + WFTS = "wfts" +} + +// ─── Row Operations ────────────────────────────────────────────────────────── + +@http(method: "GET", uri: "/{table}", code: 200) +@readonly +operation SelectRows { + input: SelectRowsInput + output: RowsOutput + errors: [DatabaseError] +} + +structure SelectRowsInput { + @required + @httpLabel + table: String + + /// Column selection — comma-separated, supports aliasing, casting, embedded + /// resources, and JSON operators. e.g. "id,name,orders(total,status)". + @httpQuery("select") + select: String + + /// Ordering — e.g. "name.asc,age.desc.nullslast" + @httpQuery("order") + order: String + + /// Maximum number of rows to return. + @httpQuery("limit") + limit: Integer + + /// Row offset for pagination. + @httpQuery("offset") + offset: Integer + + /// Counting mode. e.g. "count=exact", "count=planned", "count=estimated". + @httpHeader("Prefer") + prefer: String + + /// Range-based pagination — e.g. "0-9" (ten rows starting at 0). + @httpHeader("Range") + range: String + + /// Unit for the Range header. Defaults to "items". + @httpHeader("Range-Unit") + rangeUnit: String + + /// Response format. e.g. "application/json" (default), "text/csv", + /// "application/vnd.pgrst.object+json" (singular-row mode). + @httpHeader("Accept") + accept: String + + /// Target a non-default schema exposed by PostgREST. + @httpHeader("Accept-Profile") + acceptProfile: String + + /// Horizontal filters — each entry becomes a query parameter. + /// Key: column name (or "or"/"and" for logical groups). + /// Value: "{operator}.{value}" e.g. {"id": "eq.5", "name": "like.foo*"}. + /// See FilterOperator for the full list of operators. + @httpQueryParams + filters: StringMap +} + +structure RowsOutput { + @httpPayload + body: Blob + + /// Pagination info — e.g. "0-9/200" (range/total) or "0-9/*" (unknown count). + @httpHeader("Content-Range") + contentRange: String +} + +@http(method: "POST", uri: "/{table}", code: 201) +operation InsertRows { + input: InsertRowsInput + output: RowsOutput + errors: [DatabaseError] +} + +structure InsertRowsInput { + @required + @httpLabel + table: String + + /// JSON object or array of objects to insert. + @httpPayload + @required + body: Blob + + /// Return behavior and conflict handling. + /// e.g. "return=representation", "return=minimal" (default), + /// "return=headers-only", "resolution=merge-duplicates". + @httpHeader("Prefer") + prefer: String + + /// Columns to select in the returned representation (requires return=representation). + @httpQuery("select") + select: String + + /// Restrict which columns may be populated (useful with CSV uploads). + @httpQuery("columns") + columns: String + + /// Target a non-default schema for the write. + @httpHeader("Content-Profile") + contentProfile: String +} + +@http(method: "PATCH", uri: "/{table}", code: 200) +operation UpdateRows { + input: UpdateRowsInput + output: RowsOutput + errors: [DatabaseError] +} + +structure UpdateRowsInput { + @required + @httpLabel + table: String + + /// Partial JSON object with fields to update. + @httpPayload + @required + body: Blob + + @httpHeader("Prefer") + prefer: String + + @httpQuery("select") + select: String + + @httpHeader("Content-Profile") + contentProfile: String + + /// Horizontal filters — rows matching these filters will be updated. + /// Key: column name. Value: "{operator}.{value}" e.g. {"id": "eq.5"}. + @httpQueryParams + filters: StringMap +} + +@http(method: "PUT", uri: "/{table}", code: 200) +@idempotent +operation UpsertRows { + input: UpsertRowsInput + output: RowsOutput + errors: [DatabaseError] +} + +structure UpsertRowsInput { + @required + @httpLabel + table: String + + /// JSON object or array of objects to upsert. + @httpPayload + @required + body: Blob + + /// e.g. "return=representation", "resolution=merge-duplicates", + /// "resolution=ignore-duplicates". + @httpHeader("Prefer") + prefer: String + + @httpQuery("select") + select: String + + /// Columns to match for conflict detection (if not the primary key). + @httpQuery("on_conflict") + onConflict: String + + @httpHeader("Content-Profile") + contentProfile: String + + /// Horizontal filters — rows matching these filters will be upserted. + @httpQueryParams + filters: StringMap +} + +@http(method: "DELETE", uri: "/{table}", code: 200) +@idempotent +operation DeleteRows { + input: DeleteRowsInput + output: RowsOutput + errors: [DatabaseError] +} + +structure DeleteRowsInput { + @required + @httpLabel + table: String + + @httpHeader("Prefer") + prefer: String + + @httpQuery("select") + select: String + + @httpHeader("Content-Profile") + contentProfile: String + + /// Horizontal filters — rows matching these filters will be deleted. + @httpQueryParams + filters: StringMap +} + +// ─── RPC Operations ────────────────────────────────────────────────────────── +// +// Calls a PostgreSQL function via /rpc/{functionName}. +// POST: function receives named parameters as a JSON object in the body. +// GET: read-only (STABLE/IMMUTABLE) functions only; parameters are query params. +// Argument names are function-specific — pass them via the args map. + +@http(method: "POST", uri: "/rpc/{functionName}", code: 200) +operation CallRpcPost { + input: CallRpcPostInput + output: RpcOutput + errors: [DatabaseError] +} + +structure CallRpcPostInput { + @required + @httpLabel + functionName: String + + /// Named parameters as a JSON object, or a single argument when combined + /// with Prefer: params=single-object. + @httpPayload + body: Blob + + /// e.g. "params=single-object" — treat the entire body as a single parameter. + @httpHeader("Prefer") + prefer: String + + @httpQuery("select") + select: String + + @httpHeader("Content-Profile") + contentProfile: String +} + +@http(method: "GET", uri: "/rpc/{functionName}", code: 200) +@readonly +operation CallRpcGet { + input: CallRpcGetInput + output: RpcOutput + errors: [DatabaseError] +} + +structure CallRpcGetInput { + @required + @httpLabel + functionName: String + + @httpQuery("select") + select: String + + @httpHeader("Accept-Profile") + acceptProfile: String + + /// Function arguments — each entry becomes a query parameter. + /// Keys and value formats are defined by the PostgreSQL function signature. + @httpQueryParams + args: StringMap +} + +structure RpcOutput { + @httpPayload + body: Blob + + @httpHeader("Content-Range") + contentRange: String +} + +// ─── Errors ────────────────────────────────────────────────────────────────── + +@error("client") +structure DatabaseError { + /// PostgreSQL error code (e.g. "23505") or PostgREST error code (e.g. "PGRST301"). + code: String + + /// Human-readable error message. + message: String + + /// Extra context — constraint name, offending column, etc. + details: String + + /// Hint from PostgreSQL. + hint: String +} diff --git a/smithy/model/functions.smithy b/smithy/model/functions.smithy new file mode 100644 index 0000000..bc7a056 --- /dev/null +++ b/smithy/model/functions.smithy @@ -0,0 +1,110 @@ +$version: "2" + +namespace io.supabase.functions + +use aws.protocols#restJson1 +use io.supabase#StringMap + +@restJson1 +@title("Supabase Functions API") +service FunctionsService { + version: "1.0" + operations: [ + InvokeFunctionGet + InvokeFunctionPost + InvokeFunctionPut + InvokeFunctionPatch + InvokeFunctionDelete + ] + errors: [FunctionsError] +} + +// ─── Shared Shapes ───────────────────────────────────────────────────────── + +/// Input for methods that carry a request body (POST, PUT, PATCH, DELETE). +structure InvokeFunctionInput { + @required + @httpLabel + functionName: String + + @httpHeader("x-region") + region: String + + @httpPayload + body: Blob + + /// Arbitrary query parameters appended to the function URL. + /// Corresponds to FunctionInvokeOptions.query. + @httpQueryParams + query: StringMap +} + +/// Input for GET — no body, which GET does not support. +structure InvokeFunctionGetInput { + @required + @httpLabel + functionName: String + + @httpHeader("x-region") + region: String + + /// Arbitrary query parameters appended to the function URL. + /// Corresponds to FunctionInvokeOptions.query. + @httpQueryParams + query: StringMap +} + +structure InvokeFunctionOutput { + @httpPayload + body: Blob +} + +// ─── Operations (one per HTTP method) ────────────────────────────────────── +// +// Smithy requires a fixed HTTP method per operation. We model all five +// methods Supabase Edge Functions accept; FunctionsClient.invoke() dispatches +// to the appropriate generated method based on FunctionInvokeOptions.method. + +@http(method: "GET", uri: "/functions/v1/{functionName}", code: 200) +@readonly +operation InvokeFunctionGet { + input: InvokeFunctionGetInput + output: InvokeFunctionOutput + errors: [FunctionsError] +} + +@http(method: "POST", uri: "/functions/v1/{functionName}", code: 200) +operation InvokeFunctionPost { + input: InvokeFunctionInput + output: InvokeFunctionOutput + errors: [FunctionsError] +} + +@http(method: "PUT", uri: "/functions/v1/{functionName}", code: 200) +@idempotent +operation InvokeFunctionPut { + input: InvokeFunctionInput + output: InvokeFunctionOutput + errors: [FunctionsError] +} + +@http(method: "PATCH", uri: "/functions/v1/{functionName}", code: 200) +operation InvokeFunctionPatch { + input: InvokeFunctionInput + output: InvokeFunctionOutput + errors: [FunctionsError] +} + +@http(method: "DELETE", uri: "/functions/v1/{functionName}", code: 200) +@idempotent +@suppress(["HttpMethodSemantics.UnexpectedPayload"]) +operation InvokeFunctionDelete { + input: InvokeFunctionInput + output: InvokeFunctionOutput + errors: [FunctionsError] +} + +@error("client") +structure FunctionsError { + message: String +} diff --git a/smithy/model/storage.smithy b/smithy/model/storage.smithy new file mode 100644 index 0000000..13fd406 --- /dev/null +++ b/smithy/model/storage.smithy @@ -0,0 +1,450 @@ +$version: "2" + +namespace io.supabase.storage + +use aws.protocols#restJson1 +use io.supabase#StringList + +@restJson1 +@title("Supabase Storage API") +service StorageService { + version: "1.0" + operations: [ + ListBuckets + GetBucket + CreateBucket + UpdateBucket + EmptyBucket + DeleteBucket + MoveObject + CopyObject + DeleteObjects + ListObjects + GetObjectInfo + HeadObject + CreateSignedUrl + CreateSignedUrls + CreateSignedUploadUrl + CreateTusUpload + UploadChunk + GetUploadOffset + ] + errors: [StorageError] +} + +// ─── Bucket Operations ───────────────────────────────────────────────────── + +@http(method: "GET", uri: "/bucket", code: 200) +@readonly +operation ListBuckets { + output: ListBucketsOutput + errors: [StorageError] +} + +structure ListBucketsOutput { + @required + items: BucketList +} + +list BucketList { + member: Bucket +} + +@http(method: "GET", uri: "/bucket/{id}", code: 200) +@readonly +operation GetBucket { + input: GetBucketInput + output: Bucket + errors: [StorageError] +} + +structure GetBucketInput { + @required + @httpLabel + id: String +} + +@http(method: "POST", uri: "/bucket", code: 200) +operation CreateBucket { + input: CreateBucketInput + errors: [StorageError] +} + +structure CreateBucketInput { + @required id: String + @required name: String + @required @jsonName("public") isPublic: Boolean + file_size_limit: Long + allowed_mime_types: StringList +} + +@http(method: "PUT", uri: "/bucket/{id}", code: 200) +@idempotent +operation UpdateBucket { + input: UpdateBucketInput + errors: [StorageError] +} + +structure UpdateBucketInput { + @required + @httpLabel + id: String + + @required @jsonName("public") isPublic: Boolean + file_size_limit: Long + allowed_mime_types: StringList +} + +@http(method: "POST", uri: "/bucket/{id}/empty", code: 200) +operation EmptyBucket { + input: EmptyBucketInput + errors: [StorageError] +} + +structure EmptyBucketInput { + @required + @httpLabel + id: String +} + +@http(method: "DELETE", uri: "/bucket/{id}", code: 200) +@idempotent +operation DeleteBucket { + input: DeleteBucketInput + errors: [StorageError] +} + +structure DeleteBucketInput { + @required + @httpLabel + id: String +} + +// ─── Object Operations ───────────────────────────────────────────────────── + +@http(method: "POST", uri: "/object/move", code: 200) +operation MoveObject { + input: MoveObjectInput + errors: [StorageError] +} + +structure MoveObjectInput { + @required bucketId: String + @required sourceKey: String + @required destinationKey: String + destinationBucket: String +} + +@http(method: "POST", uri: "/object/copy", code: 200) +operation CopyObject { + input: CopyObjectInput + output: CopyObjectOutput + errors: [StorageError] +} + +structure CopyObjectInput { + @required bucketId: String + @required sourceKey: String + @required destinationKey: String + destinationBucket: String +} + +structure CopyObjectOutput { + @required Key: String +} + +@http(method: "DELETE", uri: "/object/{bucketId}", code: 200) +@idempotent +@suppress(["HttpMethodSemantics.UnexpectedPayload"]) +operation DeleteObjects { + input: DeleteObjectsInput + output: DeleteObjectsOutput + errors: [StorageError] +} + +structure DeleteObjectsInput { + @required + @httpLabel + bucketId: String + + @required prefixes: StringList +} + +structure DeleteObjectsOutput { + @required + items: FileObjectList +} + +list FileObjectList { + member: FileObject +} + +@http(method: "POST", uri: "/object/list/{bucketId}", code: 200) +operation ListObjects { + input: ListObjectsInput + output: ListObjectsOutput + errors: [StorageError] +} + +structure ListObjectsInput { + @required + @httpLabel + bucketId: String + + @required prefix: String + limit: Integer + offset: Integer + sortBy: SortBy +} + +structure SortBy { + column: String + order: String +} + +structure ListObjectsOutput { + @required + items: FileObjectList +} + +@http(method: "GET", uri: "/object/info/{bucketId}/{wildcardPath+}", code: 200) +@readonly +operation GetObjectInfo { + input: GetObjectInfoInput + output: FileInfo + errors: [StorageError] +} + +structure GetObjectInfoInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String +} + +@http(method: "HEAD", uri: "/object/{bucketId}/{wildcardPath+}", code: 200) +@readonly +operation HeadObject { + input: HeadObjectInput + errors: [StorageError] +} + +structure HeadObjectInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String +} + +@http(method: "POST", uri: "/object/sign/{bucketId}/{wildcardPath+}", code: 200) +operation CreateSignedUrl { + input: CreateSignedUrlInput + output: CreateSignedUrlOutput + errors: [StorageError] +} + +structure CreateSignedUrlInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String + @required expiresIn: Integer +} + +structure CreateSignedUrlOutput { + @required signedURL: String +} + +@http(method: "POST", uri: "/object/sign/{bucketId}", code: 200) +operation CreateSignedUrls { + input: CreateSignedUrlsInput + output: CreateSignedUrlsOutput + errors: [StorageError] +} + +structure CreateSignedUrlsInput { + @required @httpLabel bucketId: String + @required expiresIn: Integer + @required paths: StringList +} + +structure CreateSignedUrlsOutput { + @required + items: SignedUrlResultList +} + +list SignedUrlResultList { + member: SignedUrlResult +} + +structure SignedUrlResult { + signedURL: String + @required path: String + error: String +} + +@http(method: "POST", uri: "/object/upload/sign/{bucketId}/{wildcardPath+}", code: 200) +operation CreateSignedUploadUrl { + input: CreateSignedUploadUrlInput + output: CreateSignedUploadUrlOutput + errors: [StorageError] +} + +structure CreateSignedUploadUrlInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String + @httpHeader("x-upsert") upsert: String +} + +structure CreateSignedUploadUrlOutput { + @required url: String +} + +// ─── TUS Resumable Upload Operations ─────────────────────────────────────── +// +// Models the three HTTP operations of the TUS 1.0.0 protocol. The application- +// level state machine (chunk sequencing, 409 retry, pause/resume) is NOT +// generated — it lives in TUSUploadEngine, which calls these operations. + +/// Step 1: Create a new TUS upload session. +/// The server responds with a Location header containing the upload URL. +@http(method: "POST", uri: "/upload/resumable", code: 201) +operation CreateTusUpload { + input: CreateTusUploadInput + output: CreateTusUploadOutput + errors: [StorageError] +} + +structure CreateTusUploadInput { + /// Total size of the file in bytes. + @httpHeader("Upload-Length") + @required + uploadLength: Long + + /// Base64-encoded TUS metadata (bucketName, objectName, contentType, cacheControl). + @httpHeader("Upload-Metadata") + @required + uploadMetadata: String + + @httpHeader("Tus-Resumable") + @required + tusResumable: String + + /// Set to "true" to overwrite an existing object at the same path. + @httpHeader("x-upsert") + upsert: String +} + +structure CreateTusUploadOutput { + /// Full URL of the created upload session. Used in subsequent PATCH/HEAD requests. + @httpHeader("Location") + @required + location: String +} + +/// Step 2: Upload a chunk of data to an existing TUS session. +/// Repeat with increasing Upload-Offset until all bytes are sent. +@http(method: "PATCH", uri: "/upload/resumable/{uploadId}", code: 204) +@suppress(["HttpMethodSemantics.UnexpectedPayload"]) +operation UploadChunk { + input: UploadChunkInput + output: UploadChunkOutput + errors: [StorageError] +} + +@streaming +blob ChunkBody + +structure UploadChunkInput { + @httpLabel + @required + uploadId: String + + /// Byte offset at which this chunk begins. + @httpHeader("Upload-Offset") + @required + uploadOffset: Long + + @httpHeader("Tus-Resumable") + @required + tusResumable: String + + /// Raw chunk bytes, streamed directly — never buffered. + @httpPayload + @required + body: ChunkBody +} + +structure UploadChunkOutput { + /// New server-side offset after the chunk was accepted. + @httpHeader("Upload-Offset") + @required + uploadOffset: Long +} + +/// Step 3: Query the server-side offset of a TUS session (used when resuming). +@http(method: "HEAD", uri: "/upload/resumable/{uploadId}", code: 200) +@readonly +operation GetUploadOffset { + input: GetUploadOffsetInput + output: GetUploadOffsetOutput + errors: [StorageError] +} + +structure GetUploadOffsetInput { + @httpLabel + @required + uploadId: String + + @httpHeader("Tus-Resumable") + @required + tusResumable: String +} + +structure GetUploadOffsetOutput { + @httpHeader("Upload-Offset") + @required + uploadOffset: Long +} + +// ─── Shared Shapes ───────────────────────────────────────────────────────── + +structure Bucket { + @required id: String + @required name: String + @required @jsonName("public") isPublic: Boolean + file_size_limit: Long + allowed_mime_types: StringList + created_at: String + updated_at: String +} + +structure FileObject { + @required name: String + id: String + updated_at: String + created_at: String + last_accessed_at: String + metadata: FileMetadata +} + +structure FileMetadata { + eTag: String + size: Long + mimetype: String + cacheControl: String + lastModified: String + contentLength: Long + httpStatusCode: Integer +} + +structure FileInfo { + eTag: String + size: Long + mimetype: String + cacheControl: String + lastModified: String + contentLength: Long + httpStatusCode: Integer +} + +@error("client") +structure StorageError { + message: String + error: String + statusCode: String +} diff --git a/smithy/openapi/FunctionsService.openapi.json b/smithy/openapi/FunctionsService.openapi.json new file mode 100644 index 0000000..9f4f9db --- /dev/null +++ b/smithy/openapi/FunctionsService.openapi.json @@ -0,0 +1,305 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Functions API", + "version": "1.0" + }, + "paths": { + "/functions/v1/{functionName}": { + "delete": { + "operationId": "InvokeFunctionDelete", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionDeleteInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionDelete 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionDeleteOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "InvokeFunctionGet", + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionGet 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionGetOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "InvokeFunctionPatch", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPatchInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPatch 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPatchOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "InvokeFunctionPost", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPostInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPost 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPostOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "InvokeFunctionPut", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPutInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPut 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPutOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FunctionsErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "InvokeFunctionDeleteInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionDeleteOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionGetOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPatchInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPatchOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPostInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPostOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPutInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPutOutputPayload": { + "type": "string", + "format": "byte" + } + } + } +} diff --git a/smithy/openapi/StorageService.openapi.json b/smithy/openapi/StorageService.openapi.json new file mode 100644 index 0000000..0e40c00 --- /dev/null +++ b/smithy/openapi/StorageService.openapi.json @@ -0,0 +1,1388 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Storage API", + "version": "1.0" + }, + "paths": { + "/bucket": { + "get": { + "operationId": "ListBuckets", + "responses": { + "200": { + "description": "ListBuckets 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBucketsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CreateBucket", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBucketRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/bucket/{id}": { + "delete": { + "operationId": "DeleteBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetBucket 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBucketResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpdateBucket", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBucketRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/bucket/{id}/empty": { + "post": { + "operationId": "EmptyBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "EmptyBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/copy": { + "post": { + "operationId": "CopyObject", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopyObjectRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CopyObject 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopyObjectResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/info/{bucketId}/{wildcardPath+}": { + "get": { + "operationId": "GetObjectInfo", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetObjectInfo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetObjectInfoResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/list/{bucketId}": { + "post": { + "operationId": "ListObjects", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListObjectsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListObjects 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListObjectsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/move": { + "post": { + "operationId": "MoveObject", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveObjectRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MoveObject 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/sign/{bucketId}": { + "post": { + "operationId": "CreateSignedUrls", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateSignedUrls 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/sign/{bucketId}/{wildcardPath+}": { + "post": { + "operationId": "CreateSignedUrl", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateSignedUrl 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/upload/sign/{bucketId}/{wildcardPath+}": { + "post": { + "operationId": "CreateSignedUploadUrl", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "CreateSignedUploadUrl 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUploadUrlResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/{bucketId}": { + "delete": { + "operationId": "DeleteObjects", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteObjectsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteObjects 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteObjectsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/{bucketId}/{wildcardPath+}": { + "head": { + "operationId": "HeadObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "HeadObject 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "UploadObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "schema": { + "type": "string" + }, + "required": false + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "file" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Upload successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadedResponse" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpdateObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "file" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Upload successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadedResponse" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/upload/resumable": { + "post": { + "description": "Step 1: Create a new TUS upload session.\nThe server responds with a Location header containing the upload URL.", + "operationId": "CreateTusUpload", + "parameters": [ + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Upload-Length", + "in": "header", + "description": "Total size of the file in bytes.", + "schema": { + "type": "number", + "description": "Total size of the file in bytes." + }, + "required": true + }, + { + "name": "Upload-Metadata", + "in": "header", + "description": "Base64-encoded TUS metadata (bucketName, objectName, contentType, cacheControl).", + "schema": { + "type": "string", + "description": "Base64-encoded TUS metadata (bucketName, objectName, contentType, cacheControl)." + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "description": "Set to \"true\" to overwrite an existing object at the same path.", + "schema": { + "type": "string", + "description": "Set to \"true\" to overwrite an existing object at the same path." + } + } + ], + "responses": { + "201": { + "description": "CreateTusUpload 201 response", + "headers": { + "Location": { + "description": "Full URL of the created upload session. Used in subsequent PATCH/HEAD requests.", + "schema": { + "type": "string", + "description": "Full URL of the created upload session. Used in subsequent PATCH/HEAD requests." + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/upload/resumable/{uploadId}": { + "head": { + "description": "Step 3: Query the server-side offset of a TUS session (used when resuming).", + "operationId": "GetUploadOffset", + "parameters": [ + { + "name": "uploadId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetUploadOffset 200 response", + "headers": { + "Upload-Offset": { + "schema": { + "type": "number" + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "description": "Step 2: Upload a chunk of data to an existing TUS session.\nRepeat with increasing Upload-Offset until all bytes are sent.", + "operationId": "UploadChunk", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UploadChunkInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "uploadId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Upload-Offset", + "in": "header", + "description": "Byte offset at which this chunk begins.", + "schema": { + "type": "number", + "description": "Byte offset at which this chunk begins." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "UploadChunk 204 response", + "headers": { + "Upload-Offset": { + "description": "New server-side offset after the chunk was accepted.", + "schema": { + "type": "number", + "description": "New server-side offset after the chunk was accepted." + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Bucket": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "CopyObjectRequestContent": { + "type": "object", + "properties": { + "bucketId": { + "type": "string" + }, + "sourceKey": { + "type": "string" + }, + "destinationKey": { + "type": "string" + }, + "destinationBucket": { + "type": "string" + } + }, + "required": [ + "bucketId", + "destinationKey", + "sourceKey" + ] + }, + "CopyObjectResponseContent": { + "type": "object", + "properties": { + "Key": { + "type": "string" + } + }, + "required": [ + "Key" + ] + }, + "CreateBucketRequestContent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "CreateSignedUploadUrlResponseContent": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "CreateSignedUrlRequestContent": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number" + } + }, + "required": [ + "expiresIn" + ] + }, + "CreateSignedUrlResponseContent": { + "type": "object", + "properties": { + "signedURL": { + "type": "string" + } + }, + "required": [ + "signedURL" + ] + }, + "CreateSignedUrlsRequestContent": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "expiresIn", + "paths" + ] + }, + "CreateSignedUrlsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SignedUrlResult" + } + } + }, + "required": [ + "items" + ] + }, + "DeleteObjectsRequestContent": { + "type": "object", + "properties": { + "prefixes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "prefixes" + ] + }, + "DeleteObjectsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } + } + }, + "required": [ + "items" + ] + }, + "FileMetadata": { + "type": "object", + "properties": { + "eTag": { + "type": "string" + }, + "size": { + "type": "number" + }, + "mimetype": { + "type": "string" + }, + "cacheControl": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "contentLength": { + "type": "number" + }, + "httpStatusCode": { + "type": "number" + } + } + }, + "FileObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "last_accessed_at": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/FileMetadata" + } + }, + "required": [ + "name" + ] + }, + "GetBucketResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "GetObjectInfoResponseContent": { + "type": "object", + "properties": { + "eTag": { + "type": "string" + }, + "size": { + "type": "number" + }, + "mimetype": { + "type": "string" + }, + "cacheControl": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "contentLength": { + "type": "number" + }, + "httpStatusCode": { + "type": "number" + } + } + }, + "ListBucketsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bucket" + } + } + }, + "required": [ + "items" + ] + }, + "ListObjectsRequestContent": { + "type": "object", + "properties": { + "prefix": { + "type": "string" + }, + "limit": { + "type": "number" + }, + "offset": { + "type": "number" + }, + "sortBy": { + "$ref": "#/components/schemas/SortBy" + } + }, + "required": [ + "prefix" + ] + }, + "ListObjectsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } + } + }, + "required": [ + "items" + ] + }, + "MoveObjectRequestContent": { + "type": "object", + "properties": { + "bucketId": { + "type": "string" + }, + "sourceKey": { + "type": "string" + }, + "destinationKey": { + "type": "string" + }, + "destinationBucket": { + "type": "string" + } + }, + "required": [ + "bucketId", + "destinationKey", + "sourceKey" + ] + }, + "SignedUrlResult": { + "type": "object", + "properties": { + "signedURL": { + "type": "string" + }, + "path": { + "type": "string" + }, + "error": { + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "SortBy": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "order": { + "type": "string" + } + } + }, + "StorageErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "error": { + "type": "string" + }, + "statusCode": { + "type": "string" + } + } + }, + "UpdateBucketRequestContent": { + "type": "object", + "properties": { + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "public" + ] + }, + "UploadChunkInputPayload": { + "type": "string", + "description": "Raw chunk bytes, streamed directly \u2014 never buffered.", + "format": "binary" + }, + "FileUploadedResponse": { + "type": "object", + "properties": { + "Key": { + "type": "string" + }, + "Id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "Key", + "Id" + ] + } + } + } +} \ No newline at end of file diff --git a/smithy/patch-openapi.py b/smithy/patch-openapi.py new file mode 100644 index 0000000..e75cda3 --- /dev/null +++ b/smithy/patch-openapi.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Post-process the Smithy-generated OpenAPI JSON with patches that Smithy +cannot express natively: + +1. UploadChunk body: format: byte → format: binary + (@streaming blob translates to format:byte but swift-openapi-generator + needs format:binary to emit HTTPBody instead of Base64EncodedData) + +2. UploadObject (POST) and UpdateObject (PUT) with multipart/form-data + (Smithy has no native multipart/form-data trait; these are authored here) +""" +import json +import sys + +path = sys.argv[1] if len(sys.argv) > 1 else "output/openapi/StorageService.openapi.json" + +with open(path) as f: + d = json.load(f) + +# ── Patch 1: streaming blob → binary ───────────────────────────────────── +schema = d["components"]["schemas"].get("UploadChunkInputPayload", {}) +if schema.get("format") == "byte": + schema["format"] = "binary" + +# ── Patch 2: multipart upload/update operations ─────────────────────────── +d["components"]["schemas"]["FileUploadedResponse"] = { + "type": "object", + "properties": { + "Key": {"type": "string"}, + "Id": {"type": "string", "format": "uuid"}, + }, + "required": ["Key", "Id"], +} + +upload_form_schema = { + "type": "object", + "properties": { + "cacheControl": {"type": "string"}, + "metadata": {"type": "object", "additionalProperties": True}, + "file": {"type": "string", "format": "binary"}, + }, + "required": ["file"], +} + +upload_responses = { + "200": { + "description": "Upload successful", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/FileUploadedResponse"} + } + }, + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": {"$ref": "#/components/schemas/StorageErrorResponseContent"} + } + }, + }, +} + +wildcard_path = "/object/{bucketId}/{wildcardPath+}" +d["paths"][wildcard_path]["post"] = { + "operationId": "UploadObject", + "parameters": [ + {"name": "bucketId", "in": "path", "schema": {"type": "string"}, "required": True}, + {"name": "wildcardPath+", "in": "path", "schema": {"type": "string"}, "required": True}, + {"name": "x-upsert", "in": "header", "schema": {"type": "string"}, "required": False}, + ], + "requestBody": { + "required": True, + "content": {"multipart/form-data": {"schema": upload_form_schema}}, + }, + "responses": upload_responses, +} + +d["paths"][wildcard_path]["put"] = { + "operationId": "UpdateObject", + "parameters": [ + {"name": "bucketId", "in": "path", "schema": {"type": "string"}, "required": True}, + {"name": "wildcardPath+", "in": "path", "schema": {"type": "string"}, "required": True}, + ], + "requestBody": { + "required": True, + "content": {"multipart/form-data": {"schema": upload_form_schema}}, + }, + "responses": upload_responses, +} + +with open(path, "w") as f: + json.dump(d, f, indent=2) diff --git a/smithy/smithy-build.json b/smithy/smithy-build.json new file mode 100644 index 0000000..539c0ad --- /dev/null +++ b/smithy/smithy-build.json @@ -0,0 +1,60 @@ +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy:smithy-openapi:1.52.1", + "software.amazon.smithy:smithy-aws-traits:1.52.1" + ] + }, + "sources": ["model"], + "projections": { + "storage-openapi": { + "transforms": [ + { + "name": "includeServices", + "args": { + "services": ["io.supabase.storage#StorageService"] + } + } + ], + "plugins": { + "openapi": { + "service": "io.supabase.storage#StorageService", + "protocol": "aws.protocols#restJson1" + } + } + }, + "functions-openapi": { + "transforms": [ + { + "name": "includeServices", + "args": { + "services": ["io.supabase.functions#FunctionsService"] + } + } + ], + "plugins": { + "openapi": { + "service": "io.supabase.functions#FunctionsService", + "protocol": "aws.protocols#restJson1" + } + } + }, + "database-openapi": { + "transforms": [ + { + "name": "includeServices", + "args": { + "services": ["io.supabase.database#DatabaseService"] + } + } + ], + "plugins": { + "openapi": { + "service": "io.supabase.database#DatabaseService", + "protocol": "aws.protocols#restJson1" + } + } + } + } +}