diff --git a/smithy/model/storage.smithy b/smithy/model/storage.smithy index a85ce9c..cb72233 100644 --- a/smithy/model/storage.smithy +++ b/smithy/model/storage.smithy @@ -44,6 +44,11 @@ operation ListBuckets { errors: [StorageError] } +/// NOTE: the API returns a bare JSON array — there is no {items:[]} envelope. +/// Verified against a live platform (supabase start). restJson1 cannot bind a +/// list to the HTTP payload (@httpPayload supports string/blob/structure/union/ +/// document only), so patch-openapi.py unwraps this envelope in the generated +/// OpenAPI. Applies to all *Output structures holding a single `items` list. structure ListBucketsOutput { @required items: BucketList diff --git a/smithy/openapi/DatabaseService.openapi.json b/smithy/openapi/DatabaseService.openapi.json index 51263cd..470204f 100644 --- a/smithy/openapi/DatabaseService.openapi.json +++ b/smithy/openapi/DatabaseService.openapi.json @@ -55,7 +55,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/CallRpcGetOutputPayload" + "type": "string", + "format": "binary" } } } @@ -78,7 +79,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/CallRpcPostInputPayload" + "type": "string", + "format": "binary" } } } @@ -129,7 +131,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/CallRpcPostOutputPayload" + "type": "string", + "format": "binary" } } } @@ -205,7 +208,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/DeleteRowsOutputPayload" + "type": "string", + "format": "binary" } } } @@ -339,7 +343,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/SelectRowsOutputPayload" + "type": "string", + "format": "binary" } } } @@ -362,7 +367,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/UpdateRowsInputPayload" + "type": "string", + "format": "binary" } } }, @@ -423,7 +429,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/UpdateRowsOutputPayload" + "type": "string", + "format": "binary" } } } @@ -446,7 +453,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/InsertRowsInputPayload" + "type": "string", + "format": "binary" } } }, @@ -513,7 +521,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/InsertRowsOutputPayload" + "type": "string", + "format": "binary" } } } @@ -536,7 +545,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/UpsertRowsInputPayload" + "type": "string", + "format": "binary" } } }, @@ -608,7 +618,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/UpsertRowsOutputPayload" + "type": "string", + "format": "binary" } } } @@ -629,19 +640,6 @@ }, "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": { @@ -663,23 +661,6 @@ } } }, - "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": { @@ -687,24 +668,6 @@ }, "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.", diff --git a/smithy/openapi/FunctionsService.openapi.json b/smithy/openapi/FunctionsService.openapi.json index 9b115da..57295bb 100644 --- a/smithy/openapi/FunctionsService.openapi.json +++ b/smithy/openapi/FunctionsService.openapi.json @@ -1,357 +1,330 @@ { - "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" - } - } - } - } - } + "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": { + "type": "string", + "format": "binary" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" }, - "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" - } - } - } - } + "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": { + "type": "string", + "format": "binary" } - }, - "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" - } - } - } - } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" } + } + } + } + } + }, + "get": { + "operationId": "InvokeFunctionGet", + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" }, - "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" - } - } - } - } + "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": { + "type": "string", + "format": "binary" } - }, - "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" - } - } - } - } + } + } + }, + "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": { + }, + "patch": { + "operationId": "InvokeFunctionPatch", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { "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" + "format": "binary" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" }, - "InvokeFunctionPutInputPayload": { + "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": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "InvokeFunctionPost", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { "type": "string", - "format": "byte" + "format": "binary" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" }, - "InvokeFunctionPutOutputPayload": { + "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": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "InvokeFunctionPut", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { "type": "string", - "format": "byte" + "format": "binary" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" }, - "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)." + "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": { + "type": "string", + "format": "binary" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FunctionsErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } } + }, + "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)." + } } -} + } +} \ No newline at end of file diff --git a/smithy/openapi/StorageService.openapi.json b/smithy/openapi/StorageService.openapi.json index f9a096e..49f2dec 100644 --- a/smithy/openapi/StorageService.openapi.json +++ b/smithy/openapi/StorageService.openapi.json @@ -14,7 +14,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListBucketsResponseContent" + "type": "array", + "items": { + "$ref": "#/components/schemas/Bucket" + } } } } @@ -230,7 +233,7 @@ } } }, - "/object/info/{bucketId}/{wildcardPath+}": { + "/object/info/{bucketId}/{wildcardPath}": { "get": { "operationId": "GetObjectInfo", "parameters": [ @@ -243,7 +246,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -304,7 +307,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ListObjectsResponseContent" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } } } } @@ -381,7 +387,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateSignedUrlsResponseContent" + "type": "array", + "items": { + "$ref": "#/components/schemas/SignedUrlResult" + } } } } @@ -399,7 +408,7 @@ } } }, - "/object/sign/{bucketId}/{wildcardPath+}": { + "/object/sign/{bucketId}/{wildcardPath}": { "post": { "operationId": "CreateSignedUrl", "requestBody": { @@ -422,7 +431,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -454,7 +463,7 @@ } } }, - "/object/upload/sign/{bucketId}/{wildcardPath+}": { + "/object/upload/sign/{bucketId}/{wildcardPath}": { "post": { "operationId": "CreateSignedUploadUrl", "parameters": [ @@ -467,7 +476,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -535,7 +544,10 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/DeleteObjectsResponseContent" + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } } } } @@ -553,7 +565,7 @@ } } }, - "/object/{bucketId}/{wildcardPath+}": { + "/object/{bucketId}/{wildcardPath}": { "head": { "operationId": "HeadObject", "parameters": [ @@ -566,7 +578,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -603,7 +615,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -679,7 +691,7 @@ "required": true }, { - "name": "wildcardPath+", + "name": "wildcardPath", "in": "path", "schema": { "type": "string" @@ -859,7 +871,8 @@ "content": { "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/UploadChunkInputPayload" + "type": "string", + "format": "binary" } } }, @@ -936,7 +949,8 @@ "type": "boolean" }, "file_size_limit": { - "type": "number" + "type": "number", + "nullable": true }, "allowed_mime_types": { "type": "array", @@ -1004,7 +1018,8 @@ "type": "boolean" }, "file_size_limit": { - "type": "number" + "type": "number", + "nullable": true }, "allowed_mime_types": { "type": "array", @@ -1072,20 +1087,6 @@ "paths" ] }, - "CreateSignedUrlsResponseContent": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/SignedUrlResult" - } - } - }, - "required": [ - "items" - ] - }, "DeleteObjectsRequestContent": { "type": "object", "properties": { @@ -1101,20 +1102,6 @@ "prefixes" ] }, - "DeleteObjectsResponseContent": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileObject" - } - } - }, - "required": [ - "items" - ] - }, "FileMetadata": { "type": "object", "properties": { @@ -1122,7 +1109,8 @@ "type": "string" }, "size": { - "type": "number" + "type": "number", + "nullable": true }, "mimetype": { "type": "string" @@ -1134,10 +1122,12 @@ "type": "string" }, "contentLength": { - "type": "number" + "type": "number", + "nullable": true }, "httpStatusCode": { - "type": "number" + "type": "number", + "nullable": true } } }, @@ -1180,7 +1170,8 @@ "type": "boolean" }, "file_size_limit": { - "type": "number" + "type": "number", + "nullable": true }, "allowed_mime_types": { "type": "array", @@ -1209,7 +1200,8 @@ "type": "string" }, "size": { - "type": "number" + "type": "number", + "nullable": true }, "mimetype": { "type": "string" @@ -1221,27 +1213,15 @@ "type": "string" }, "contentLength": { - "type": "number" + "type": "number", + "nullable": true }, "httpStatusCode": { - "type": "number" + "type": "number", + "nullable": true } } }, - "ListBucketsResponseContent": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/Bucket" - } - } - }, - "required": [ - "items" - ] - }, "ListObjectsRequestContent": { "type": "object", "properties": { @@ -1249,10 +1229,12 @@ "type": "string" }, "limit": { - "type": "number" + "type": "number", + "nullable": true }, "offset": { - "type": "number" + "type": "number", + "nullable": true }, "sortBy": { "$ref": "#/components/schemas/SortBy" @@ -1262,20 +1244,6 @@ "prefix" ] }, - "ListObjectsResponseContent": { - "type": "object", - "properties": { - "items": { - "type": "array", - "items": { - "$ref": "#/components/schemas/FileObject" - } - } - }, - "required": [ - "items" - ] - }, "MoveObjectRequestContent": { "type": "object", "properties": { @@ -1347,7 +1315,8 @@ "type": "boolean" }, "file_size_limit": { - "type": "number" + "type": "number", + "nullable": true }, "allowed_mime_types": { "type": "array", @@ -1376,11 +1345,6 @@ "Key" ] }, - "UploadChunkInputPayload": { - "type": "string", - "description": "Raw chunk bytes, streamed directly \u2014 never buffered.", - "format": "binary" - }, "UploadObjectResponseContent": { "type": "object", "properties": { diff --git a/smithy/patch-openapi.py b/smithy/patch-openapi.py index 5449fc4..9c5c704 100644 --- a/smithy/patch-openapi.py +++ b/smithy/patch-openapi.py @@ -3,17 +3,40 @@ 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 +Patches applied to every service: + A. List-response envelope unwrap. The APIs return bare JSON arrays for list + operations, but Smithy operation outputs must be structures, and restJson1 + forbids @httpPayload on list members ("AWS Protocols only support binding + the following shape types to the payload: string, blob, structure, union, + and document"). The model therefore wraps lists in an {items:[]} structure + that does not exist on the wire; this patch unwraps it so response schemas + are top-level arrays, matching production. Verified against a live + platform: with the envelope, generated clients either fail to deserialize + (NSwag: throws) or silently return empty results (Kiota: .Items == null). + B. Optional scalar fields become nullable. Smithy marks non-@required members + optional, but the OpenAPI conversion emits plain non-nullable schemas for + them. Generators in languages with non-nullable value types (C#, Kotlin) + then map them to types whose default is a real value (0/false) and + serialize it — e.g. CreateBucket with file_size_limit:0 creates a bucket + that rejects every upload with 413, and ListObjects with limit:0 is a 400. + nullable:true lets generators produce optional value types (long?, int?). + C. Greedy path label cleanup. Smithy's {wildcardPath+} greedy label leaks the + RFC-6570 '+' into OpenAPI path templates and parameter names, producing + invalid identifiers in generated code (24 C# compile errors observed). + D. Streaming bodies become inline binary. @streaming blob / blob payloads + convert to $ref'd schemas with format:byte (base64). Generators then + buffer and base64-encode instead of streaming (NSwag emits byte[]). + Replacing octet-stream content with inline {type:string, format:binary} + makes generators emit true streams (NSwag: Stream/FileResponse; also what + swift-openapi-generator needs to emit HTTPBody). + +Service-specific patches: + E. Storage — 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. Non-file parts are optional. + F. Database — 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 @@ -25,10 +48,83 @@ d = json.load(f) service_title = d.get("info", {}).get("title", "") +schemas = d.get("components", {}).get("schemas", {}) +removed = [] + + +def iter_operations(doc): + for p, methods in doc.get("paths", {}).items(): + for method, op in methods.items(): + if isinstance(op, dict): + yield op + + +def iter_contents(op): + body = op.get("requestBody", {}) + if isinstance(body, dict): + yield body.get("content", {}) + for resp in op.get("responses", {}).values(): + if isinstance(resp, dict): + yield resp.get("content", {}) + + +# ── Patch A: unwrap {items:[]} list-response envelopes ───────────────────── +def is_items_envelope(schema): + return ( + isinstance(schema, dict) + and schema.get("type") == "object" + and set(schema.get("properties", {}).keys()) == {"items"} + and schema["properties"]["items"].get("type") == "array" + ) -# ── Patch 3: FilterOperator enum (DatabaseService only) ─────────────────── + +envelopes = {name for name, s in schemas.items() if is_items_envelope(s)} +for op in iter_operations(d): + for resp in op.get("responses", {}).values(): + for media in resp.get("content", {}).values(): + ref = media.get("schema", {}).get("$ref", "") + name = ref.rsplit("/", 1)[-1] + if name in envelopes: + media["schema"] = schemas[name]["properties"]["items"] + +# ── Patch B: optional scalar properties → nullable ───────────────────────── +SCALARS = ("number", "integer", "boolean") +for s in schemas.values(): + if not isinstance(s, dict) or s.get("type") != "object": + continue + required = set(s.get("required", [])) + for prop_name, prop in s.get("properties", {}).items(): + if prop_name in required or not isinstance(prop, dict): + continue + if prop.get("type") in SCALARS: + prop["nullable"] = True + +# ── Patch C: strip greedy-label '+' from paths and parameter names ───────── +d["paths"] = {p.replace("+}", "}"): v for p, v in d.get("paths", {}).items()} +for op in iter_operations(d): + for param in op.get("parameters", []): + if isinstance(param, dict) and param.get("name", "").endswith("+"): + param["name"] = param["name"].rstrip("+") + +# ── Patch D: octet-stream $ref bodies → inline binary ────────────────────── +inlined_refs = set() +for op in iter_operations(d): + for content in iter_contents(op): + media = content.get("application/octet-stream") + if media and "$ref" in media.get("schema", {}): + inlined_refs.add(media["schema"]["$ref"].rsplit("/", 1)[-1]) + media["schema"] = {"type": "string", "format": "binary"} + +# ── Cleanup: drop components orphaned by patches A and D ─────────────────── +doc_json = json.dumps(d) +for name in envelopes | inlined_refs: + if f'"#/components/schemas/{name}"' not in doc_json: + schemas.pop(name, None) + removed.append(name) + +# ── Patch F: FilterOperator enum (DatabaseService only) ──────────────────── if service_title == "Supabase Database API": - d["components"]["schemas"]["FilterOperator"] = { + schemas["FilterOperator"] = { "type": "string", "description": ( "PostgREST column filter operators. " @@ -47,10 +143,10 @@ } with open(path, "w") as f: json.dump(d, f, indent=4) - print(f"Patched (database): {path}") + print(f"Patched (database): {path}" + (f" — removed {removed}" if removed else "")) sys.exit(0) -# ── Patch 1: multipart/form-data for UploadObject and UpdateObject ──────── +# ── Patch E: multipart/form-data for UploadObject and UpdateObject ───────── MULTIPART_BODY = { "required": True, "content": { @@ -68,18 +164,14 @@ }, } -upload_path = "/object/{bucketId}/{wildcardPath+}" +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}") +label = "storage" if "Storage" in service_title else "functions" +print(f"Patched ({label}): {path}" + (f" — removed {removed}" if removed else "")) diff --git a/smithy/smithy-build.json b/smithy/smithy-build.json index facd36d..47b1932 100644 --- a/smithy/smithy-build.json +++ b/smithy/smithy-build.json @@ -7,7 +7,8 @@ "io.supabase:smithy-supabase-extensions:1.0.0-SNAPSHOT" ], "repositories": [ - {"url": "file://${HOME}/.m2/repository"} + {"url": "file://${HOME}/.m2/repository"}, + {"url": "https://repo.maven.apache.org/maven2"} ] }, "sources": ["model"],