diff --git a/smithy/.gitignore b/smithy/.gitignore new file mode 100644 index 0000000..1baffe2 --- /dev/null +++ b/smithy/.gitignore @@ -0,0 +1,3 @@ +build/ +extensions/.gradle/ +extensions/build/ 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/extensions/.gradle/9.6.0/checksums/checksums.lock b/smithy/extensions/.gradle/9.6.0/checksums/checksums.lock new file mode 100644 index 0000000..d389752 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/checksums/checksums.lock differ diff --git a/smithy/extensions/.gradle/9.6.0/checksums/md5-checksums.bin b/smithy/extensions/.gradle/9.6.0/checksums/md5-checksums.bin new file mode 100644 index 0000000..f81520f Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/checksums/md5-checksums.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/checksums/sha1-checksums.bin b/smithy/extensions/.gradle/9.6.0/checksums/sha1-checksums.bin new file mode 100644 index 0000000..d9370db Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/checksums/sha1-checksums.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/checksums/sha256-checksums.bin b/smithy/extensions/.gradle/9.6.0/checksums/sha256-checksums.bin new file mode 100644 index 0000000..e725402 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/checksums/sha256-checksums.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/checksums/sha512-checksums.bin b/smithy/extensions/.gradle/9.6.0/checksums/sha512-checksums.bin new file mode 100644 index 0000000..e4cc0eb Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/checksums/sha512-checksums.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.bin b/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.bin new file mode 100644 index 0000000..c75e348 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.lock b/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.lock new file mode 100644 index 0000000..0bf1f71 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/executionHistory/executionHistory.lock differ diff --git a/smithy/extensions/.gradle/9.6.0/fileChanges/last-build.bin b/smithy/extensions/.gradle/9.6.0/fileChanges/last-build.bin new file mode 100644 index 0000000..f76dd23 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/fileChanges/last-build.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.bin b/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.bin new file mode 100644 index 0000000..af6d04b Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.lock b/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.lock new file mode 100644 index 0000000..76229da Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/fileHashes/fileHashes.lock differ diff --git a/smithy/extensions/.gradle/9.6.0/fileHashes/resourceHashesCache.bin b/smithy/extensions/.gradle/9.6.0/fileHashes/resourceHashesCache.bin new file mode 100644 index 0000000..bc53db0 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/fileHashes/resourceHashesCache.bin differ diff --git a/smithy/extensions/.gradle/9.6.0/gc.properties b/smithy/extensions/.gradle/9.6.0/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/smithy/extensions/.gradle/9.6.0/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock b/smithy/extensions/.gradle/9.6.0/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock new file mode 100644 index 0000000..aa3cd41 Binary files /dev/null and b/smithy/extensions/.gradle/9.6.0/kotlin-dsl-plugin-entries/kotlin-dsl-plugin-entries.lock differ diff --git a/smithy/extensions/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/smithy/extensions/.gradle/buildOutputCleanup/buildOutputCleanup.lock new file mode 100644 index 0000000..51fbe89 Binary files /dev/null and b/smithy/extensions/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/smithy/extensions/.gradle/buildOutputCleanup/cache.properties b/smithy/extensions/.gradle/buildOutputCleanup/cache.properties new file mode 100644 index 0000000..603268f --- /dev/null +++ b/smithy/extensions/.gradle/buildOutputCleanup/cache.properties @@ -0,0 +1,2 @@ +#Wed Jul 01 07:27:00 GMT-03:00 2026 +gradle.version=9.6.0 diff --git a/smithy/extensions/.gradle/buildOutputCleanup/outputFiles.bin b/smithy/extensions/.gradle/buildOutputCleanup/outputFiles.bin new file mode 100644 index 0000000..e2a37c6 Binary files /dev/null and b/smithy/extensions/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/smithy/extensions/.gradle/file-system.probe b/smithy/extensions/.gradle/file-system.probe new file mode 100644 index 0000000..5927026 Binary files /dev/null and b/smithy/extensions/.gradle/file-system.probe differ diff --git a/smithy/extensions/.gradle/kotlin/errors/errors-1782901640577.log b/smithy/extensions/.gradle/kotlin/errors/errors-1782901640577.log new file mode 100644 index 0000000..0d26950 --- /dev/null +++ b/smithy/extensions/.gradle/kotlin/errors/errors-1782901640577.log @@ -0,0 +1,47 @@ +kotlin version: 1.9.22 +error message: java.lang.IllegalArgumentException: 26.0.1 + at org.jetbrains.kotlin.com.intellij.util.lang.JavaVersion.parse(JavaVersion.java:305) + at org.jetbrains.kotlin.com.intellij.util.lang.JavaVersion.current(JavaVersion.java:174) + at org.jetbrains.kotlin.cli.jvm.modules.JavaVersionUtilsKt.isAtLeastJava9(javaVersionUtils.kt:11) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem$Companion$globalJrtFsCache$1.invoke(CoreJrtFileSystem.kt:83) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem$Companion$globalJrtFsCache$1.invoke(CoreJrtFileSystem.kt:74) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem.globalJrtFsCache$lambda$1(CoreJrtFileSystem.kt:74) + at org.jetbrains.kotlin.com.intellij.util.containers.ConcurrentFactoryMap$2.create(ConcurrentFactoryMap.java:174) + at org.jetbrains.kotlin.com.intellij.util.containers.ConcurrentFactoryMap.get(ConcurrentFactoryMap.java:40) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem$roots$1.invoke(CoreJrtFileSystem.kt:34) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem$roots$1.invoke(CoreJrtFileSystem.kt:33) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem.roots$lambda$0(CoreJrtFileSystem.kt:33) + at org.jetbrains.kotlin.com.intellij.util.containers.ConcurrentFactoryMap$2.create(ConcurrentFactoryMap.java:174) + at org.jetbrains.kotlin.com.intellij.util.containers.ConcurrentFactoryMap.get(ConcurrentFactoryMap.java:40) + at org.jetbrains.kotlin.cli.jvm.modules.CoreJrtFileSystem.findFileByPath(CoreJrtFileSystem.kt:42) + at org.jetbrains.kotlin.cli.jvm.modules.CliJavaModuleFinder.(CliJavaModuleFinder.kt:44) + at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.(KotlinCoreEnvironment.kt:210) + at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment.(KotlinCoreEnvironment.kt) + at org.jetbrains.kotlin.cli.jvm.compiler.KotlinCoreEnvironment$Companion.createForProduction(KotlinCoreEnvironment.kt:446) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.createCoreEnvironment(K2JVMCompiler.kt:199) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:150) + at org.jetbrains.kotlin.cli.jvm.K2JVMCompiler.doExecute(K2JVMCompiler.kt:50) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:104) + at org.jetbrains.kotlin.cli.common.CLICompiler.execImpl(CLICompiler.kt:48) + at org.jetbrains.kotlin.cli.common.CLITool.exec(CLITool.kt:101) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:463) + at org.jetbrains.kotlin.incremental.IncrementalJvmCompilerRunner.runCompiler(IncrementalJvmCompilerRunner.kt:62) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.doCompile(IncrementalCompilerRunner.kt:477) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileImpl(IncrementalCompilerRunner.kt:400) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compileNonIncrementally(IncrementalCompilerRunner.kt:281) + at org.jetbrains.kotlin.incremental.IncrementalCompilerRunner.compile(IncrementalCompilerRunner.kt:125) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.execIncrementalCompiler(CompileServiceImpl.kt:657) + at org.jetbrains.kotlin.daemon.CompileServiceImplBase.access$execIncrementalCompiler(CompileServiceImpl.kt:105) + at org.jetbrains.kotlin.daemon.CompileServiceImpl.compile(CompileServiceImpl.kt:1624) + at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104) + at java.base/java.lang.reflect.Method.invoke(Method.java:565) + at java.rmi/sun.rmi.server.UnicastServerRef.dispatch(UnicastServerRef.java:351) + at java.rmi/sun.rmi.transport.Transport.serviceCall(Transport.java:166) + at java.rmi/sun.rmi.transport.tcp.TCPTransport.handleMessages(TCPTransport.java:543) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run0(TCPTransport.java:744) + at java.rmi/sun.rmi.transport.tcp.TCPTransport$ConnectionHandler.run(TCPTransport.java:623) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1090) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614) + at java.base/java.lang.Thread.run(Thread.java:1516) + + diff --git a/smithy/extensions/.gradle/vcs-1/gc.properties b/smithy/extensions/.gradle/vcs-1/gc.properties new file mode 100644 index 0000000..e69de29 diff --git a/smithy/extensions/build.gradle.kts b/smithy/extensions/build.gradle.kts new file mode 100644 index 0000000..3fb7bc8 --- /dev/null +++ b/smithy/extensions/build.gradle.kts @@ -0,0 +1,36 @@ +plugins { + `java-library` + `maven-publish` +} + +group = "io.supabase" +version = "1.0.0-SNAPSHOT" + +repositories { + mavenCentral() +} + +dependencies { + // Provided at runtime by the Smithy CLI — compile-only here + compileOnly("software.amazon.smithy:smithy-openapi:1.52.1") + compileOnly("software.amazon.smithy:smithy-jsonschema:1.52.1") + compileOnly("software.amazon.smithy:smithy-model:1.52.1") +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +publishing { + publications { + create("mavenJava") { + from(components["java"]) + artifactId = "smithy-supabase-extensions" + } + } + repositories { + mavenLocal() + } +} diff --git a/smithy/extensions/build/classes/java/main/io/supabase/smithy/MultipartFormOpenApiMapper.class b/smithy/extensions/build/classes/java/main/io/supabase/smithy/MultipartFormOpenApiMapper.class new file mode 100644 index 0000000..321979f Binary files /dev/null and b/smithy/extensions/build/classes/java/main/io/supabase/smithy/MultipartFormOpenApiMapper.class differ diff --git a/smithy/extensions/build/kotlin/compileKotlin/cacheable/dirty-sources.txt b/smithy/extensions/build/kotlin/compileKotlin/cacheable/dirty-sources.txt new file mode 100644 index 0000000..3de0968 --- /dev/null +++ b/smithy/extensions/build/kotlin/compileKotlin/cacheable/dirty-sources.txt @@ -0,0 +1 @@ +/Users/guilherme/src/github.com/supabase/sdk/.claude/worktrees/happy-brattain-ebac12/smithy/extensions/src/main/kotlin/io/supabase/smithy/MultipartFormOpenApiMapper.kt \ No newline at end of file diff --git a/smithy/extensions/build/kotlin/compileKotlin/local-state/build-history.bin b/smithy/extensions/build/kotlin/compileKotlin/local-state/build-history.bin new file mode 100644 index 0000000..eb5a38c Binary files /dev/null and b/smithy/extensions/build/kotlin/compileKotlin/local-state/build-history.bin differ diff --git a/smithy/extensions/build/libs/smithy-supabase-extensions-1.0.0-SNAPSHOT.jar b/smithy/extensions/build/libs/smithy-supabase-extensions-1.0.0-SNAPSHOT.jar new file mode 100644 index 0000000..e705a9a Binary files /dev/null and b/smithy/extensions/build/libs/smithy-supabase-extensions-1.0.0-SNAPSHOT.jar differ diff --git a/smithy/extensions/build/publications/mavenJava/module.json b/smithy/extensions/build/publications/mavenJava/module.json new file mode 100644 index 0000000..3419ec2 --- /dev/null +++ b/smithy/extensions/build/publications/mavenJava/module.json @@ -0,0 +1,60 @@ +{ + "formatVersion": "1.1", + "component": { + "group": "io.supabase", + "module": "smithy-supabase-extensions", + "version": "1.0.0-SNAPSHOT", + "attributes": { + "org.gradle.status": "integration" + } + }, + "createdBy": { + "gradle": { + "version": "9.6.0" + } + }, + "variants": [ + { + "name": "apiElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 17, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-api" + }, + "files": [ + { + "name": "smithy-supabase-extensions-1.0.0-SNAPSHOT.jar", + "url": "smithy-supabase-extensions-1.0.0-SNAPSHOT.jar", + "size": 3979, + "sha512": "a94423a090a86f781718f959dbef6f1a86ba40a68d30efcc5c09e82a21db76dc9ab0a4c164332862e8c5f7cfed1d8d83b09420869702f186dfc85ec9e0d1b42d", + "sha256": "b5e2c4ef19054d3f5eea74a34e8f6d5770daed2b0781bb2dfe5937f379ef8361", + "sha1": "cf19c98118c8a4a7253ac9a1541a30f71dce36bc", + "md5": "63a0f25ca00576403cf83af7bcff285e" + } + ] + }, + { + "name": "runtimeElements", + "attributes": { + "org.gradle.category": "library", + "org.gradle.dependency.bundling": "external", + "org.gradle.jvm.version": 17, + "org.gradle.libraryelements": "jar", + "org.gradle.usage": "java-runtime" + }, + "files": [ + { + "name": "smithy-supabase-extensions-1.0.0-SNAPSHOT.jar", + "url": "smithy-supabase-extensions-1.0.0-SNAPSHOT.jar", + "size": 3979, + "sha512": "a94423a090a86f781718f959dbef6f1a86ba40a68d30efcc5c09e82a21db76dc9ab0a4c164332862e8c5f7cfed1d8d83b09420869702f186dfc85ec9e0d1b42d", + "sha256": "b5e2c4ef19054d3f5eea74a34e8f6d5770daed2b0781bb2dfe5937f379ef8361", + "sha1": "cf19c98118c8a4a7253ac9a1541a30f71dce36bc", + "md5": "63a0f25ca00576403cf83af7bcff285e" + } + ] + } + ] +} diff --git a/smithy/extensions/build/publications/mavenJava/pom-default.xml b/smithy/extensions/build/publications/mavenJava/pom-default.xml new file mode 100644 index 0000000..e0fabe2 --- /dev/null +++ b/smithy/extensions/build/publications/mavenJava/pom-default.xml @@ -0,0 +1,13 @@ + + + + + + + + 4.0.0 + io.supabase + smithy-supabase-extensions + 1.0.0-SNAPSHOT + diff --git a/smithy/extensions/build/reports/problems/problems-report.html b/smithy/extensions/build/reports/problems/problems-report.html new file mode 100644 index 0000000..217c21a --- /dev/null +++ b/smithy/extensions/build/reports/problems/problems-report.html @@ -0,0 +1,666 @@ + + + + + + + + + + + + + Gradle Configuration Cache + + + +
+ +
+ Loading... +
+ + + + + + diff --git a/smithy/extensions/build/resources/main/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper b/smithy/extensions/build/resources/main/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper new file mode 100644 index 0000000..5e1db66 --- /dev/null +++ b/smithy/extensions/build/resources/main/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper @@ -0,0 +1 @@ +io.supabase.smithy.MultipartFormOpenApiMapper diff --git a/smithy/extensions/build/tmp/compileJava/compileTransaction/stash-dir/MultipartFormOpenApiMapper.class.uniqueId0 b/smithy/extensions/build/tmp/compileJava/compileTransaction/stash-dir/MultipartFormOpenApiMapper.class.uniqueId0 new file mode 100644 index 0000000..5677ddc Binary files /dev/null and b/smithy/extensions/build/tmp/compileJava/compileTransaction/stash-dir/MultipartFormOpenApiMapper.class.uniqueId0 differ diff --git a/smithy/extensions/build/tmp/compileJava/previous-compilation-data.bin b/smithy/extensions/build/tmp/compileJava/previous-compilation-data.bin new file mode 100644 index 0000000..62ebfc4 Binary files /dev/null and b/smithy/extensions/build/tmp/compileJava/previous-compilation-data.bin differ diff --git a/smithy/extensions/build/tmp/jar/MANIFEST.MF b/smithy/extensions/build/tmp/jar/MANIFEST.MF new file mode 100644 index 0000000..58630c0 --- /dev/null +++ b/smithy/extensions/build/tmp/jar/MANIFEST.MF @@ -0,0 +1,2 @@ +Manifest-Version: 1.0 + diff --git a/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/module-maven-metadata.xml b/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/module-maven-metadata.xml new file mode 100644 index 0000000..61365a8 --- /dev/null +++ b/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/module-maven-metadata.xml @@ -0,0 +1,12 @@ + + + io.supabase + smithy-supabase-extensions + + 1.0.0-SNAPSHOT + + 1.0.0-SNAPSHOT + + 20260701103251 + + diff --git a/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/snapshot-maven-metadata.xml b/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/snapshot-maven-metadata.xml new file mode 100644 index 0000000..43627ee --- /dev/null +++ b/smithy/extensions/build/tmp/publishMavenJavaPublicationToMavenLocal/snapshot-maven-metadata.xml @@ -0,0 +1,29 @@ + + + io.supabase + smithy-supabase-extensions + + 20260701103251 + + true + + + + module + 1.0.0-SNAPSHOT + 20260701103251 + + + jar + 1.0.0-SNAPSHOT + 20260701103251 + + + pom + 1.0.0-SNAPSHOT + 20260701103251 + + + + 1.0.0-SNAPSHOT + diff --git a/smithy/extensions/settings.gradle.kts b/smithy/extensions/settings.gradle.kts new file mode 100644 index 0000000..aa533fc --- /dev/null +++ b/smithy/extensions/settings.gradle.kts @@ -0,0 +1 @@ +rootProject.name = "smithy-supabase-extensions" diff --git a/smithy/extensions/src/main/java/io/supabase/smithy/MultipartFormOpenApiMapper.java b/smithy/extensions/src/main/java/io/supabase/smithy/MultipartFormOpenApiMapper.java new file mode 100644 index 0000000..fe10081 --- /dev/null +++ b/smithy/extensions/src/main/java/io/supabase/smithy/MultipartFormOpenApiMapper.java @@ -0,0 +1,102 @@ +package io.supabase.smithy; + +import java.util.ArrayList; +import java.util.List; + +import software.amazon.smithy.model.node.ArrayNode; +import software.amazon.smithy.model.node.Node; +import software.amazon.smithy.model.node.ObjectNode; +import software.amazon.smithy.model.shapes.OperationShape; +import software.amazon.smithy.model.shapes.ShapeId; +import software.amazon.smithy.model.shapes.StructureShape; +import software.amazon.smithy.model.traits.Trait; +import software.amazon.smithy.openapi.fromsmithy.Context; +import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper; +import software.amazon.smithy.jsonschema.Schema; +import software.amazon.smithy.openapi.model.MediaTypeObject; +import software.amazon.smithy.openapi.model.OperationObject; +import software.amazon.smithy.openapi.model.RequestBodyObject; + +/** + * Smithy OpenAPI mapper intended to handle the {@code io.supabase.traits#httpMultipartForm} + * trait. Currently not active: the trait is stripped as a DynamicTrait by the OpenAPI + * converter before {@link #updateOperation} is called. Multipart injection is handled by + * {@code patch-openapi.py} instead. This class is kept for future reference. + */ +public final class MultipartFormOpenApiMapper implements OpenApiMapper { + + static { + System.err.println("[MultipartFormOpenApiMapper] class loaded via SPI"); + } + + private static final ShapeId TRAIT_ID = ShapeId.from("io.supabase.traits#httpMultipartForm"); + + @Override + public OperationObject updateOperation( + Context context, + OperationShape shape, + OperationObject operation, + String httpMethodName, + String path) { + + System.err.println("[MultipartFormOpenApiMapper] updateOperation: " + shape.getId() + " " + httpMethodName + " " + path); + + ShapeId inputId = shape.getInput().orElse(null); + if (inputId == null) { + return operation; + } + + StructureShape input = context.getModel().expectShape(inputId, StructureShape.class); + Trait rawTrait = input.findTrait(TRAIT_ID).orElse(null); + if (rawTrait == null) { + return operation; + } + + ObjectNode traitNode = rawTrait.toNode().expectObjectNode(); + ArrayNode fieldsArray = traitNode.getArrayMember("fields").orElse(Node.arrayNode()); + + Schema.Builder bodySchemaBuilder = Schema.builder().type("object"); + List required = new ArrayList<>(); + + for (Node fieldNode : fieldsArray.getElements()) { + ObjectNode field = fieldNode.expectObjectNode(); + String name = field.expectStringMember("name").getValue(); + String fieldType = field.expectStringMember("fieldType").getValue(); + boolean isRequired = field.getBooleanMemberOrDefault("required", false); + + Schema fieldSchema; + switch (fieldType) { + case "binary": + fieldSchema = Schema.builder().type("string").format("binary").build(); + break; + case "object": + fieldSchema = Schema.builder().type("object").build(); + break; + default: + fieldSchema = Schema.builder().type("string").build(); + } + + bodySchemaBuilder.putProperty(name, fieldSchema); + if (isRequired) { + required.add(name); + } + } + + if (!required.isEmpty()) { + bodySchemaBuilder.required(required); + } + + RequestBodyObject requestBody = RequestBodyObject.builder() + .putContent( + "multipart/form-data", + MediaTypeObject.builder() + .schema(bodySchemaBuilder.build()) + .build()) + .required(true) + .build(); + + return operation.toBuilder() + .requestBody(requestBody) + .build(); + } +} diff --git a/smithy/extensions/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper b/smithy/extensions/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper new file mode 100644 index 0000000..5e1db66 --- /dev/null +++ b/smithy/extensions/src/main/resources/META-INF/services/software.amazon.smithy.openapi.fromsmithy.OpenApiMapper @@ -0,0 +1 @@ +io.supabase.smithy.MultipartFormOpenApiMapper 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..9b66239 --- /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..a85ce9c --- /dev/null +++ b/smithy/model/storage.smithy @@ -0,0 +1,498 @@ +$version: "2" + +namespace io.supabase.storage + +use aws.protocols#restJson1 +use io.supabase#StringList +use io.supabase.traits#httpMultipartForm + +@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 + UploadObject + UpdateObject + 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 +} + +/// Upload a new object. Body is multipart/form-data with a required file part. +/// Smithy has no native multipart/form-data support; the @httpMultipartForm trait +/// documents intent and patch-openapi.py injects the correct requestBody schema. +@http(method: "POST", uri: "/object/{bucketId}/{wildcardPath+}", code: 200) +operation UploadObject { + input: UploadObjectInput + output: FileUploadedResponse + errors: [StorageError] +} + +@httpMultipartForm(fields: [ + {name: "file", fieldType: "binary", required: true}, + {name: "cacheControl", fieldType: "string", required: false}, + {name: "metadata", fieldType: "object", required: false} +]) +structure UploadObjectInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String + @httpHeader("x-upsert") upsert: String +} + +/// Replace an existing object. Body is multipart/form-data (see UploadObject). +@http(method: "PUT", uri: "/object/{bucketId}/{wildcardPath+}", code: 200) +@idempotent +operation UpdateObject { + input: UpdateObjectInput + output: FileUploadedResponse + errors: [StorageError] +} + +@httpMultipartForm(fields: [ + {name: "file", fieldType: "binary", required: true}, + {name: "cacheControl", fieldType: "string", required: false}, + {name: "metadata", fieldType: "object", required: false} +]) +structure UpdateObjectInput { + @required @httpLabel bucketId: String + @required @httpLabel wildcardPath: String +} + +structure FileUploadedResponse { + @required @jsonName("Key") key: String + @required @jsonName("Id") id: 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/model/traits.smithy b/smithy/model/traits.smithy new file mode 100644 index 0000000..46635eb --- /dev/null +++ b/smithy/model/traits.smithy @@ -0,0 +1,47 @@ +$version: "2" + +namespace io.supabase.traits + +/// Marks an operation input structure as using multipart/form-data encoding. +/// +/// The Smithy OpenAPI converter has no native multipart/form-data support; +/// this trait signals the MultipartFormOpenApiMapper plugin (smithy/extensions/) +/// to emit the correct "multipart/form-data" requestBody schema for the operation. +/// Without the plugin the operation is generated with no request body — the plugin +/// is a required build-time dependency for correct OpenAPI output. +/// +/// Usage: +/// @httpMultipartForm(fields: [ +/// {name: "file", fieldType: "binary", required: true}, +/// {name: "cacheControl", fieldType: "string", required: false}, +/// {name: "metadata", fieldType: "object", required: false} +/// ]) +/// structure MyOperationInput { ... } +@trait(selector: "structure") +structure httpMultipartForm { + fields: MultipartFieldList +} + +list MultipartFieldList { + member: MultipartField +} + +structure MultipartField { + /// Name of the form field as it appears on the wire. + @required + name: String + + /// JSON Schema type for this field. One of: "string", "binary", "object". + /// "binary" emits format: binary (file upload). "object" emits type: object. + @required + fieldType: MultipartFieldType + + /// Whether this field is required in the multipart body. + required: Boolean +} + +enum MultipartFieldType { + STRING = "string" + BINARY = "binary" + OBJECT = "object" +} diff --git a/smithy/openapi/DatabaseService.openapi.json b/smithy/openapi/DatabaseService.openapi.json new file mode 100644 index 0000000..51263cd --- /dev/null +++ b/smithy/openapi/DatabaseService.openapi.json @@ -0,0 +1,741 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Database API", + "version": "1.0", + "description": "PostgREST-backed database API.\n\nBase URL: https://{project-ref}.supabase.co/rest/v1\n\nKnown limitations:\n 1. Write operations return 204 (no body) by default and 200 with a body when\n Prefer: return=representation \u2014 the model uses 200 throughout so generators\n always produce body-parsing code; clients must tolerate empty bodies.\n 2. RPC GET arguments are function-specific; they are expressed via the same\n @httpQueryParams map as row filters, with function-defined keys." + }, + "paths": { + "/rpc/{functionName}": { + "get": { + "operationId": "CallRpcGet", + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "args", + "in": "query", + "description": "Function arguments \u2014 each entry becomes a query parameter.\nKeys and value formats are defined by the PostgreSQL function signature.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Accept-Profile", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "CallRpcGet 200 response", + "headers": { + "Content-Range": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcGetOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CallRpcPost", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcPostInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "description": "e.g. \"params=single-object\" \u2014 treat the entire body as a single parameter.", + "schema": { + "type": "string", + "description": "e.g. \"params=single-object\" \u2014 treat the entire body as a single parameter." + } + } + ], + "responses": { + "200": { + "description": "CallRpcPost 200 response", + "headers": { + "Content-Range": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcPostOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + } + }, + "/{table}": { + "delete": { + "operationId": "DeleteRows", + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be deleted.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "DeleteRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/DeleteRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "SelectRows", + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "description": "Column selection \u2014 comma-separated, supports aliasing, casting, embedded\nresources, and JSON operators. e.g. \"id,name,orders(total,status)\".", + "schema": { + "type": "string", + "description": "Column selection \u2014 comma-separated, supports aliasing, casting, embedded\nresources, and JSON operators. e.g. \"id,name,orders(total,status)\"." + } + }, + { + "name": "order", + "in": "query", + "description": "Ordering \u2014 e.g. \"name.asc,age.desc.nullslast\"", + "schema": { + "type": "string", + "description": "Ordering \u2014 e.g. \"name.asc,age.desc.nullslast\"" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of rows to return.", + "schema": { + "type": "number", + "description": "Maximum number of rows to return." + } + }, + { + "name": "offset", + "in": "query", + "description": "Row offset for pagination.", + "schema": { + "type": "number", + "description": "Row offset for pagination." + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 each entry becomes a query parameter.\nKey: column name (or \"or\"/\"and\" for logical groups).\nValue: \"{operator}.{value}\" e.g. {\"id\": \"eq.5\", \"name\": \"like.foo*\"}.\nSee FilterOperator for the full list of operators.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Accept", + "in": "header", + "description": "Response format. e.g. \"application/json\" (default), \"text/csv\",\n\"application/vnd.pgrst.object+json\" (singular-row mode).", + "schema": { + "type": "string", + "description": "Response format. e.g. \"application/json\" (default), \"text/csv\",\n\"application/vnd.pgrst.object+json\" (singular-row mode)." + } + }, + { + "name": "Accept-Profile", + "in": "header", + "description": "Target a non-default schema exposed by PostgREST.", + "schema": { + "type": "string", + "description": "Target a non-default schema exposed by PostgREST." + } + }, + { + "name": "Prefer", + "in": "header", + "description": "Counting mode. e.g. \"count=exact\", \"count=planned\", \"count=estimated\".", + "schema": { + "type": "string", + "description": "Counting mode. e.g. \"count=exact\", \"count=planned\", \"count=estimated\"." + } + }, + { + "name": "Range", + "in": "header", + "description": "Range-based pagination \u2014 e.g. \"0-9\" (ten rows starting at 0).", + "schema": { + "type": "string", + "description": "Range-based pagination \u2014 e.g. \"0-9\" (ten rows starting at 0)." + } + }, + { + "name": "Range-Unit", + "in": "header", + "description": "Unit for the Range header. Defaults to \"items\".", + "schema": { + "type": "string", + "description": "Unit for the Range header. Defaults to \"items\"." + } + } + ], + "responses": { + "200": { + "description": "SelectRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/SelectRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "UpdateRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpdateRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be updated.\nKey: column name. Value: \"{operator}.{value}\" e.g. {\"id\": \"eq.5\"}.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "UpdateRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpdateRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "InsertRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InsertRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "description": "Columns to select in the returned representation (requires return=representation).", + "schema": { + "type": "string", + "description": "Columns to select in the returned representation (requires return=representation)." + } + }, + { + "name": "columns", + "in": "query", + "description": "Restrict which columns may be populated (useful with CSV uploads).", + "schema": { + "type": "string", + "description": "Restrict which columns may be populated (useful with CSV uploads)." + } + }, + { + "name": "Content-Profile", + "in": "header", + "description": "Target a non-default schema for the write.", + "schema": { + "type": "string", + "description": "Target a non-default schema for the write." + } + }, + { + "name": "Prefer", + "in": "header", + "description": "Return behavior and conflict handling.\ne.g. \"return=representation\", \"return=minimal\" (default),\n \"return=headers-only\", \"resolution=merge-duplicates\".", + "schema": { + "type": "string", + "description": "Return behavior and conflict handling.\ne.g. \"return=representation\", \"return=minimal\" (default),\n \"return=headers-only\", \"resolution=merge-duplicates\"." + } + } + ], + "responses": { + "201": { + "description": "InsertRows 201 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InsertRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpsertRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpsertRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "on_conflict", + "in": "query", + "description": "Columns to match for conflict detection (if not the primary key).", + "schema": { + "type": "string", + "description": "Columns to match for conflict detection (if not the primary key)." + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be upserted.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "description": "e.g. \"return=representation\", \"resolution=merge-duplicates\",\n \"resolution=ignore-duplicates\".", + "schema": { + "type": "string", + "description": "e.g. \"return=representation\", \"resolution=merge-duplicates\",\n \"resolution=ignore-duplicates\"." + } + } + ], + "responses": { + "200": { + "description": "UpsertRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpsertRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CallRpcGetOutputPayload": { + "type": "string", + "format": "byte" + }, + "CallRpcPostInputPayload": { + "type": "string", + "description": "Named parameters as a JSON object, or a single argument when combined\nwith Prefer: params=single-object.", + "format": "byte" + }, + "CallRpcPostOutputPayload": { + "type": "string", + "format": "byte" + }, + "DatabaseErrorResponseContent": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "PostgreSQL error code (e.g. \"23505\") or PostgREST error code (e.g. \"PGRST301\")." + }, + "message": { + "type": "string", + "description": "Human-readable error message." + }, + "details": { + "type": "string", + "description": "Extra context \u2014 constraint name, offending column, etc." + }, + "hint": { + "type": "string", + "description": "Hint from PostgreSQL." + } + } + }, + "DeleteRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "InsertRowsInputPayload": { + "type": "string", + "description": "JSON object or array of objects to insert.", + "format": "byte" + }, + "InsertRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "SelectRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "StringMap": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Generic string-to-string map \u2014 used for arbitrary query parameter collections\n(e.g. PostgREST filter params, RPC GET arguments)." + }, + "UpdateRowsInputPayload": { + "type": "string", + "description": "Partial JSON object with fields to update.", + "format": "byte" + }, + "UpdateRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "UpsertRowsInputPayload": { + "type": "string", + "description": "JSON object or array of objects to upsert.", + "format": "byte" + }, + "UpsertRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "FilterOperator": { + "type": "string", + "description": "PostgREST column filter operators. Format a filter value as \"{operator}.{value}\", e.g. \"eq.5\". Prefix with \"not.\" to negate: \"not.eq.5\". For logical grouping use keys \"or\" / \"and\" in the filters map.", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "like", + "ilike", + "match", + "imatch", + "is", + "isdistinct", + "in", + "cs", + "cd", + "ov", + "sl", + "sr", + "nxl", + "nxr", + "adj", + "fts", + "plfts", + "phfts", + "wfts" + ] + } + } + } +} \ No newline at end of file diff --git a/smithy/openapi/FunctionsService.openapi.json b/smithy/openapi/FunctionsService.openapi.json new file mode 100644 index 0000000..9b115da --- /dev/null +++ b/smithy/openapi/FunctionsService.openapi.json @@ -0,0 +1,357 @@ +{ + "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": "query", + "in": "query", + "description": "Arbitrary query parameters appended to the function URL.\nCorresponds to FunctionInvokeOptions.query.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "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": "query", + "in": "query", + "description": "Arbitrary query parameters appended to the function URL.\nCorresponds to FunctionInvokeOptions.query.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "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": "query", + "in": "query", + "description": "Arbitrary query parameters appended to the function URL.\nCorresponds to FunctionInvokeOptions.query.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "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": "query", + "in": "query", + "description": "Arbitrary query parameters appended to the function URL.\nCorresponds to FunctionInvokeOptions.query.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "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": "query", + "in": "query", + "description": "Arbitrary query parameters appended to the function URL.\nCorresponds to FunctionInvokeOptions.query.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "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" + }, + "StringMap": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Generic string-to-string map — used for arbitrary query parameter collections\n(e.g. PostgREST filter params, RPC GET arguments)." + } + } + } +} diff --git a/smithy/openapi/StorageService.openapi.json b/smithy/openapi/StorageService.openapi.json new file mode 100644 index 0000000..f9a096e --- /dev/null +++ b/smithy/openapi/StorageService.openapi.json @@ -0,0 +1,1401 @@ +{ + "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": { + "description": "Upload a new object. Body is multipart/form-data with a required file part.\nSmithy has no native multipart/form-data support; the @httpMultipartForm trait\ndocuments intent and patch-openapi.py injects the correct requestBody schema.", + "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" + } + } + ], + "responses": { + "200": { + "description": "UploadObject 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadObjectResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + } + } + } + } + }, + "put": { + "description": "Replace an existing object. Body is multipart/form-data (see UploadObject).", + "operationId": "UpdateObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateObject 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateObjectResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + }, + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object" + } + } + } + } + } + } + } + }, + "/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" + ] + }, + "UpdateObjectResponseContent": { + "type": "object", + "properties": { + "Key": { + "type": "string" + }, + "Id": { + "type": "string" + } + }, + "required": [ + "Id", + "Key" + ] + }, + "UploadChunkInputPayload": { + "type": "string", + "description": "Raw chunk bytes, streamed directly \u2014 never buffered.", + "format": "binary" + }, + "UploadObjectResponseContent": { + "type": "object", + "properties": { + "Key": { + "type": "string" + }, + "Id": { + "type": "string" + } + }, + "required": [ + "Id", + "Key" + ] + } + } + } +} \ No newline at end of file diff --git a/smithy/patch-openapi.py b/smithy/patch-openapi.py new file mode 100644 index 0000000..5449fc4 --- /dev/null +++ b/smithy/patch-openapi.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +""" +Post-process the Smithy-generated OpenAPI JSON with patches that Smithy +cannot express natively. + +Storage patches: + 1. UploadObject / UpdateObject requestBody: inject multipart/form-data schema. + Smithy has no native multipart/form-data support. The @httpMultipartForm + trait in the model documents intent; this script performs the actual injection. + 2. 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) + +Database patches: + 3. FilterOperator enum — defined in Smithy but not referenced as a member + type (filter map values are raw strings), so it is absent from the + generated output. Injected here so OpenAPI-based generators emit it. +""" +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) + +service_title = d.get("info", {}).get("title", "") + +# ── Patch 3: FilterOperator enum (DatabaseService only) ─────────────────── +if service_title == "Supabase Database API": + d["components"]["schemas"]["FilterOperator"] = { + "type": "string", + "description": ( + "PostgREST column filter operators. " + "Format a filter value as \"{operator}.{value}\", e.g. \"eq.5\". " + "Prefix with \"not.\" to negate: \"not.eq.5\". " + "For logical grouping use keys \"or\" / \"and\" in the filters map." + ), + "enum": [ + "eq", "neq", "lt", "lte", "gt", "gte", + "like", "ilike", "match", "imatch", + "is", "isdistinct", "in", + "cs", "cd", "ov", + "sl", "sr", "nxl", "nxr", "adj", + "fts", "plfts", "phfts", "wfts", + ], + } + with open(path, "w") as f: + json.dump(d, f, indent=4) + print(f"Patched (database): {path}") + sys.exit(0) + +# ── Patch 1: multipart/form-data for UploadObject and UpdateObject ──────── +MULTIPART_BODY = { + "required": True, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["file"], + "properties": { + "file": {"type": "string", "format": "binary"}, + "cacheControl": {"type": "string"}, + "metadata": {"type": "object"}, + }, + } + } + }, +} + +upload_path = "/object/{bucketId}/{wildcardPath+}" +if upload_path in d.get("paths", {}): + for method in ("post", "put"): + if method in d["paths"][upload_path]: + d["paths"][upload_path][method]["requestBody"] = MULTIPART_BODY + +# ── Patch 2: streaming blob → binary ────────────────────────────────────── +schema = d["components"]["schemas"].get("UploadChunkInputPayload", {}) +if schema.get("format") == "byte": + schema["format"] = "binary" + +with open(path, "w") as f: + json.dump(d, f, indent=2) + +print(f"Patched (storage): {path}") diff --git a/smithy/smithy-build.json b/smithy/smithy-build.json new file mode 100644 index 0000000..facd36d --- /dev/null +++ b/smithy/smithy-build.json @@ -0,0 +1,64 @@ +{ + "version": "1.0", + "maven": { + "dependencies": [ + "software.amazon.smithy:smithy-openapi:1.52.1", + "software.amazon.smithy:smithy-aws-traits:1.52.1", + "io.supabase:smithy-supabase-extensions:1.0.0-SNAPSHOT" + ], + "repositories": [ + {"url": "file://${HOME}/.m2/repository"} + ] + }, + "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" + } + } + } + } +}