diff --git a/Package.resolved b/Package.resolved index dbe652486..9e59d50a1 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "3e0ec39db7462db772bff31e5dec74e206852e14497dcb8547439d644238bf93", + "originHash" : "790cefd577223868798720e4296e2574e4026302c4820c9810c806cd154b7456", "pins" : [ { "identity" : "mocker", @@ -64,6 +64,15 @@ "version" : "1.3.1" } }, + { + "identity" : "swift-openapi-runtime", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-runtime", + "state" : { + "revision" : "3d3a8457661daf7fb260ceeb9f0e24e5204ba5fb", + "version" : "1.12.0" + } + }, { "identity" : "swift-snapshot-testing", "kind" : "remoteSourceControl", diff --git a/Package.swift b/Package.swift index 8fcf767ea..801961a88 100644 --- a/Package.swift +++ b/Package.swift @@ -30,6 +30,7 @@ let package = Package( .package(url: "https://github.com/pointfreeco/swift-snapshot-testing", from: "1.17.0"), .package(url: "https://github.com/pointfreeco/xctest-dynamic-overlay", from: "1.2.2"), .package(url: "https://github.com/WeTransfer/Mocker", from: "3.0.0"), + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.6.0"), ], targets: [ .target( @@ -169,6 +170,7 @@ let package = Package( dependencies: [ .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), "Helpers", ] ), diff --git a/Sources/Storage/Generated/Client.swift b/Sources/Storage/Generated/Client.swift new file mode 100644 index 000000000..a599d5cd9 --- /dev/null +++ b/Sources/Storage/Generated/Client.swift @@ -0,0 +1,477 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +import HTTPTypes +/// API documentation for Supabase Storage +internal struct Client: APIProtocol { + /// The underlying HTTP client. + private let client: UniversalClient + /// Creates a new client. + /// - Parameters: + /// - serverURL: The server URL that the client connects to. Any server + /// URLs defined in the OpenAPI document are available as static methods + /// on the ``Servers`` type. + /// - configuration: A set of configuration values for the client. + /// - transport: A transport that performs HTTP operations. + /// - middlewares: A list of middlewares to call before the transport. + internal init( + serverURL: Foundation.URL, + configuration: Configuration = .init(), + transport: any ClientTransport, + middlewares: [any ClientMiddleware] = [] + ) { + self.client = .init( + serverURL: serverURL, + configuration: configuration, + transport: transport, + middlewares: middlewares + ) + } + private var converter: Converter { + client.converter + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/CreateVectorBucket`. + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)`. + internal func vectorBucketCreate(_ input: Operations.vectorBucketCreate.Input) async throws -> Operations.vectorBucketCreate.Output { + try await client.send( + input: input, + forOperation: Operations.vectorBucketCreate.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/vector/CreateVectorBucket", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + return .ok(.init()) + case 400 ... 499: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketCreate.Output.ClientError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .clientError( + statusCode: response.status.code, + .init(body: body) + ) + case 500 ... 599: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketCreate.Output.ServerError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .serverError( + statusCode: response.status.code, + .init(body: body) + ) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/DeleteVectorBucket`. + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)`. + internal func vectorBucketDelete(_ input: Operations.vectorBucketDelete.Input) async throws -> Operations.vectorBucketDelete.Output { + try await client.send( + input: input, + forOperation: Operations.vectorBucketDelete.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/vector/DeleteVectorBucket", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + return .ok(.init()) + case 400 ... 499: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketDelete.Output.ClientError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .clientError( + statusCode: response.status.code, + .init(body: body) + ) + case 500 ... 599: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketDelete.Output.ServerError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .serverError( + statusCode: response.status.code, + .init(body: body) + ) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// List vector buckets + /// + /// - Remark: HTTP `POST /vector/ListVectorBuckets`. + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)`. + internal func vectorBucketList(_ input: Operations.vectorBucketList.Input) async throws -> Operations.vectorBucketList.Output { + try await client.send( + input: input, + forOperation: Operations.vectorBucketList.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/vector/ListVectorBuckets", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketList.Output.Ok.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.listVectorBucketsResponse.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .ok(.init(body: body)) + case 400 ... 499: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketList.Output.ClientError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .clientError( + statusCode: response.status.code, + .init(body: body) + ) + case 500 ... 599: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketList.Output.ServerError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .serverError( + statusCode: response.status.code, + .init(body: body) + ) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/GetVectorBucket`. + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)`. + internal func vectorBucketGet(_ input: Operations.vectorBucketGet.Input) async throws -> Operations.vectorBucketGet.Output { + try await client.send( + input: input, + forOperation: Operations.vectorBucketGet.id, + serializer: { input in + let path = try converter.renderedPath( + template: "/vector/GetVectorBucket", + parameters: [] + ) + var request: HTTPTypes.HTTPRequest = .init( + soar_path: path, + method: .post + ) + suppressMutabilityWarning(&request) + converter.setAcceptHeader( + in: &request.headerFields, + contentTypes: input.headers.accept + ) + let body: OpenAPIRuntime.HTTPBody? + switch input.body { + case let .json(value): + body = try converter.setRequiredRequestBodyAsJSON( + value, + headerFields: &request.headerFields, + contentType: "application/json; charset=utf-8" + ) + } + return (request, body) + }, + deserializer: { response, responseBody in + switch response.status.code { + case 200: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketGet.Output.Ok.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.getVectorBucketResponse.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .ok(.init(body: body)) + case 400 ... 499: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketGet.Output.ClientError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .clientError( + statusCode: response.status.code, + .init(body: body) + ) + case 500 ... 599: + let contentType = converter.extractContentTypeIfPresent(in: response.headerFields) + let body: Operations.vectorBucketGet.Output.ServerError.Body + let chosenContentType = try converter.bestContentType( + received: contentType, + options: [ + "application/json" + ] + ) + switch chosenContentType { + case "application/json": + body = try await converter.getResponseBodyAsJSON( + Components.Schemas.errorSchema.self, + from: responseBody, + transforming: { value in + .json(value) + } + ) + default: + preconditionFailure("bestContentType chose an invalid content type.") + } + return .serverError( + statusCode: response.status.code, + .init(body: body) + ) + default: + return .undocumented( + statusCode: response.status.code, + .init( + headerFields: response.headerFields, + body: responseBody + ) + ) + } + } + ) + } +} diff --git a/Sources/Storage/Generated/Types.swift b/Sources/Storage/Generated/Types.swift new file mode 100644 index 000000000..3a126eaed --- /dev/null +++ b/Sources/Storage/Generated/Types.swift @@ -0,0 +1,1184 @@ +// Generated by swift-openapi-generator, do not modify. +@_spi(Generated) import OpenAPIRuntime +#if os(Linux) +@preconcurrency import struct Foundation.URL +@preconcurrency import struct Foundation.Data +@preconcurrency import struct Foundation.Date +#else +import struct Foundation.URL +import struct Foundation.Data +import struct Foundation.Date +#endif +/// A type that performs HTTP operations defined by the OpenAPI document. +internal protocol APIProtocol: Sendable { + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/CreateVectorBucket`. + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)`. + func vectorBucketCreate(_ input: Operations.vectorBucketCreate.Input) async throws -> Operations.vectorBucketCreate.Output + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/DeleteVectorBucket`. + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)`. + func vectorBucketDelete(_ input: Operations.vectorBucketDelete.Input) async throws -> Operations.vectorBucketDelete.Output + /// List vector buckets + /// + /// - Remark: HTTP `POST /vector/ListVectorBuckets`. + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)`. + func vectorBucketList(_ input: Operations.vectorBucketList.Input) async throws -> Operations.vectorBucketList.Output + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/GetVectorBucket`. + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)`. + func vectorBucketGet(_ input: Operations.vectorBucketGet.Input) async throws -> Operations.vectorBucketGet.Output +} + +/// Convenience overloads for operation inputs. +extension APIProtocol { + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/CreateVectorBucket`. + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)`. + internal func vectorBucketCreate( + headers: Operations.vectorBucketCreate.Input.Headers = .init(), + body: Operations.vectorBucketCreate.Input.Body + ) async throws -> Operations.vectorBucketCreate.Output { + try await vectorBucketCreate(Operations.vectorBucketCreate.Input( + headers: headers, + body: body + )) + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/DeleteVectorBucket`. + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)`. + internal func vectorBucketDelete( + headers: Operations.vectorBucketDelete.Input.Headers = .init(), + body: Operations.vectorBucketDelete.Input.Body + ) async throws -> Operations.vectorBucketDelete.Output { + try await vectorBucketDelete(Operations.vectorBucketDelete.Input( + headers: headers, + body: body + )) + } + /// List vector buckets + /// + /// - Remark: HTTP `POST /vector/ListVectorBuckets`. + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)`. + internal func vectorBucketList( + headers: Operations.vectorBucketList.Input.Headers = .init(), + body: Operations.vectorBucketList.Input.Body + ) async throws -> Operations.vectorBucketList.Output { + try await vectorBucketList(Operations.vectorBucketList.Input( + headers: headers, + body: body + )) + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/GetVectorBucket`. + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)`. + internal func vectorBucketGet( + headers: Operations.vectorBucketGet.Input.Headers = .init(), + body: Operations.vectorBucketGet.Input.Body + ) async throws -> Operations.vectorBucketGet.Output { + try await vectorBucketGet(Operations.vectorBucketGet.Input( + headers: headers, + body: body + )) + } +} + +/// Server URLs defined in the OpenAPI document. +internal enum Servers {} + +/// Types generated from the components section of the OpenAPI document. +internal enum Components { + /// Types generated from the `#/components/schemas` section of the OpenAPI document. + internal enum Schemas { + /// - Remark: Generated from `#/components/schemas/errorSchema`. + internal struct errorSchema: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/errorSchema/statusCode`. + internal var statusCode: Swift.String + /// - Remark: Generated from `#/components/schemas/errorSchema/error`. + internal var error: Swift.String + /// - Remark: Generated from `#/components/schemas/errorSchema/message`. + internal var message: Swift.String + /// - Remark: Generated from `#/components/schemas/errorSchema/code`. + internal var code: Swift.String + /// Creates a new `errorSchema`. + /// + /// - Parameters: + /// - statusCode: + /// - error: + /// - message: + /// - code: + internal init( + statusCode: Swift.String, + error: Swift.String, + message: Swift.String, + code: Swift.String + ) { + self.statusCode = statusCode + self.error = error + self.message = message + self.code = code + } + internal enum CodingKeys: String, CodingKey { + case statusCode + case error + case message + case code + } + } + /// - Remark: Generated from `#/components/schemas/vectorBucketSchema`. + internal struct vectorBucketSchema: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/vectorBucketSchema/vectorBucketName`. + internal var vectorBucketName: Swift.String + /// Unix timestamp (seconds) of when the bucket was created, if known. + /// + /// - Remark: Generated from `#/components/schemas/vectorBucketSchema/creationTime`. + internal var creationTime: Swift.Int? + /// Creates a new `vectorBucketSchema`. + /// + /// - Parameters: + /// - vectorBucketName: + /// - creationTime: Unix timestamp (seconds) of when the bucket was created, if known. + internal init( + vectorBucketName: Swift.String, + creationTime: Swift.Int? = nil + ) { + self.vectorBucketName = vectorBucketName + self.creationTime = creationTime + } + internal enum CodingKeys: String, CodingKey { + case vectorBucketName + case creationTime + } + internal init(from decoder: any Swift.Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.vectorBucketName = try container.decode( + Swift.String.self, + forKey: .vectorBucketName + ) + self.creationTime = try container.decodeIfPresent( + Swift.Int.self, + forKey: .creationTime + ) + try decoder.ensureNoAdditionalProperties(knownKeys: [ + "vectorBucketName", + "creationTime" + ]) + } + } + /// - Remark: Generated from `#/components/schemas/getVectorBucketResponse`. + internal struct getVectorBucketResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/getVectorBucketResponse/vectorBucket`. + internal var vectorBucket: Components.Schemas.vectorBucketSchema + /// Creates a new `getVectorBucketResponse`. + /// + /// - Parameters: + /// - vectorBucket: + internal init(vectorBucket: Components.Schemas.vectorBucketSchema) { + self.vectorBucket = vectorBucket + } + internal enum CodingKeys: String, CodingKey { + case vectorBucket + } + internal init(from decoder: any Swift.Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.vectorBucket = try container.decode( + Components.Schemas.vectorBucketSchema.self, + forKey: .vectorBucket + ) + try decoder.ensureNoAdditionalProperties(knownKeys: [ + "vectorBucket" + ]) + } + } + /// - Remark: Generated from `#/components/schemas/listVectorBucketsResponse`. + internal struct listVectorBucketsResponse: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/components/schemas/listVectorBucketsResponse/vectorBuckets`. + internal var vectorBuckets: [Components.Schemas.vectorBucketSchema] + /// - Remark: Generated from `#/components/schemas/listVectorBucketsResponse/nextToken`. + internal var nextToken: Swift.String? + /// Creates a new `listVectorBucketsResponse`. + /// + /// - Parameters: + /// - vectorBuckets: + /// - nextToken: + internal init( + vectorBuckets: [Components.Schemas.vectorBucketSchema], + nextToken: Swift.String? = nil + ) { + self.vectorBuckets = vectorBuckets + self.nextToken = nextToken + } + internal enum CodingKeys: String, CodingKey { + case vectorBuckets + case nextToken + } + internal init(from decoder: any Swift.Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.vectorBuckets = try container.decode( + [Components.Schemas.vectorBucketSchema].self, + forKey: .vectorBuckets + ) + self.nextToken = try container.decodeIfPresent( + Swift.String.self, + forKey: .nextToken + ) + try decoder.ensureNoAdditionalProperties(knownKeys: [ + "vectorBuckets", + "nextToken" + ]) + } + } + } + /// Types generated from the `#/components/parameters` section of the OpenAPI document. + internal enum Parameters {} + /// Types generated from the `#/components/requestBodies` section of the OpenAPI document. + internal enum RequestBodies {} + /// Types generated from the `#/components/responses` section of the OpenAPI document. + internal enum Responses {} + /// Types generated from the `#/components/headers` section of the OpenAPI document. + internal enum Headers {} +} + +/// API operations, with input and output types, generated from `#/paths` in the OpenAPI document. +internal enum Operations { + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/CreateVectorBucket`. + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)`. + internal enum vectorBucketCreate { + internal static let id: Swift.String = "vectorBucketCreate" + internal struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/header`. + internal struct Headers: Sendable, Hashable { + internal var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + internal init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + internal var headers: Operations.vectorBucketCreate.Input.Headers + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/requestBody`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/requestBody/json`. + internal struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/requestBody/json/vectorBucketName`. + internal var vectorBucketName: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - vectorBucketName: + internal init(vectorBucketName: Swift.String) { + self.vectorBucketName = vectorBucketName + } + internal enum CodingKeys: String, CodingKey { + case vectorBucketName + } + } + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/requestBody/content/application\/json`. + case json(Operations.vectorBucketCreate.Input.Body.jsonPayload) + } + internal var body: Operations.vectorBucketCreate.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + /// - body: + internal init( + headers: Operations.vectorBucketCreate.Input.Headers = .init(), + body: Operations.vectorBucketCreate.Input.Body + ) { + self.headers = headers + self.body = body + } + } + internal enum Output: Sendable, Hashable { + internal struct Ok: Sendable, Hashable { + /// Creates a new `Ok`. + internal init() {} + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.vectorBucketCreate.Output.Ok) + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)/responses/200`. + /// + /// HTTP response code: `200 ok`. + internal static var ok: Self { + .ok(.init()) + } + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + internal var ok: Operations.vectorBucketCreate.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + internal struct ClientError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/responses/4XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/responses/4XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketCreate.Output.ClientError.Body + /// Creates a new `ClientError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketCreate.Output.ClientError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)/responses/4XX`. + /// + /// HTTP response code: `400...499 clientError`. + case clientError(statusCode: Swift.Int, Operations.vectorBucketCreate.Output.ClientError) + /// The associated value of the enum case if `self` is `.clientError`. + /// + /// - Throws: An error if `self` is not `.clientError`. + /// - SeeAlso: `.clientError`. + internal var clientError: Operations.vectorBucketCreate.Output.ClientError { + get throws { + switch self { + case let .clientError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "clientError", + response: self + ) + } + } + } + internal struct ServerError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/responses/5XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/CreateVectorBucket/POST/responses/5XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketCreate.Output.ServerError.Body + /// Creates a new `ServerError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketCreate.Output.ServerError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/CreateVectorBucket/post(vectorBucketCreate)/responses/5XX`. + /// + /// HTTP response code: `500...599 serverError`. + case serverError(statusCode: Swift.Int, Operations.vectorBucketCreate.Output.ServerError) + /// The associated value of the enum case if `self` is `.serverError`. + /// + /// - Throws: An error if `self` is not `.serverError`. + /// - SeeAlso: `.serverError`. + internal var serverError: Operations.vectorBucketCreate.Output.ServerError { + get throws { + switch self { + case let .serverError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "serverError", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + internal enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + internal init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + internal var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + internal static var allCases: [Self] { + [ + .json + ] + } + } + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/DeleteVectorBucket`. + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)`. + internal enum vectorBucketDelete { + internal static let id: Swift.String = "vectorBucketDelete" + internal struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/header`. + internal struct Headers: Sendable, Hashable { + internal var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + internal init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + internal var headers: Operations.vectorBucketDelete.Input.Headers + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/requestBody`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/requestBody/json`. + internal struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/requestBody/json/vectorBucketName`. + internal var vectorBucketName: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - vectorBucketName: + internal init(vectorBucketName: Swift.String) { + self.vectorBucketName = vectorBucketName + } + internal enum CodingKeys: String, CodingKey { + case vectorBucketName + } + } + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/requestBody/content/application\/json`. + case json(Operations.vectorBucketDelete.Input.Body.jsonPayload) + } + internal var body: Operations.vectorBucketDelete.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + /// - body: + internal init( + headers: Operations.vectorBucketDelete.Input.Headers = .init(), + body: Operations.vectorBucketDelete.Input.Body + ) { + self.headers = headers + self.body = body + } + } + internal enum Output: Sendable, Hashable { + internal struct Ok: Sendable, Hashable { + /// Creates a new `Ok`. + internal init() {} + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.vectorBucketDelete.Output.Ok) + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)/responses/200`. + /// + /// HTTP response code: `200 ok`. + internal static var ok: Self { + .ok(.init()) + } + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + internal var ok: Operations.vectorBucketDelete.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + internal struct ClientError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/responses/4XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/responses/4XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketDelete.Output.ClientError.Body + /// Creates a new `ClientError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketDelete.Output.ClientError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)/responses/4XX`. + /// + /// HTTP response code: `400...499 clientError`. + case clientError(statusCode: Swift.Int, Operations.vectorBucketDelete.Output.ClientError) + /// The associated value of the enum case if `self` is `.clientError`. + /// + /// - Throws: An error if `self` is not `.clientError`. + /// - SeeAlso: `.clientError`. + internal var clientError: Operations.vectorBucketDelete.Output.ClientError { + get throws { + switch self { + case let .clientError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "clientError", + response: self + ) + } + } + } + internal struct ServerError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/responses/5XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/DeleteVectorBucket/POST/responses/5XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketDelete.Output.ServerError.Body + /// Creates a new `ServerError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketDelete.Output.ServerError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/DeleteVectorBucket/post(vectorBucketDelete)/responses/5XX`. + /// + /// HTTP response code: `500...599 serverError`. + case serverError(statusCode: Swift.Int, Operations.vectorBucketDelete.Output.ServerError) + /// The associated value of the enum case if `self` is `.serverError`. + /// + /// - Throws: An error if `self` is not `.serverError`. + /// - SeeAlso: `.serverError`. + internal var serverError: Operations.vectorBucketDelete.Output.ServerError { + get throws { + switch self { + case let .serverError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "serverError", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + internal enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + internal init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + internal var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + internal static var allCases: [Self] { + [ + .json + ] + } + } + } + /// List vector buckets + /// + /// - Remark: HTTP `POST /vector/ListVectorBuckets`. + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)`. + internal enum vectorBucketList { + internal static let id: Swift.String = "vectorBucketList" + internal struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/header`. + internal struct Headers: Sendable, Hashable { + internal var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + internal init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + internal var headers: Operations.vectorBucketList.Input.Headers + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody/json`. + internal struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody/json/maxResults`. + internal var maxResults: Swift.Double? + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody/json/nextToken`. + internal var nextToken: Swift.String? + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody/json/prefix`. + internal var prefix: Swift.String? + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - maxResults: + /// - nextToken: + /// - prefix: + internal init( + maxResults: Swift.Double? = nil, + nextToken: Swift.String? = nil, + prefix: Swift.String? = nil + ) { + self.maxResults = maxResults + self.nextToken = nextToken + self.prefix = prefix + } + internal enum CodingKeys: String, CodingKey { + case maxResults + case nextToken + case prefix + } + } + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/requestBody/content/application\/json`. + case json(Operations.vectorBucketList.Input.Body.jsonPayload) + } + internal var body: Operations.vectorBucketList.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + /// - body: + internal init( + headers: Operations.vectorBucketList.Input.Headers = .init(), + body: Operations.vectorBucketList.Input.Body + ) { + self.headers = headers + self.body = body + } + } + internal enum Output: Sendable, Hashable { + internal struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/200/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/200/content/application\/json`. + case json(Components.Schemas.listVectorBucketsResponse) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.listVectorBucketsResponse { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketList.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketList.Output.Ok.Body) { + self.body = body + } + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.vectorBucketList.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + internal var ok: Operations.vectorBucketList.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + internal struct ClientError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/4XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/4XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketList.Output.ClientError.Body + /// Creates a new `ClientError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketList.Output.ClientError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)/responses/4XX`. + /// + /// HTTP response code: `400...499 clientError`. + case clientError(statusCode: Swift.Int, Operations.vectorBucketList.Output.ClientError) + /// The associated value of the enum case if `self` is `.clientError`. + /// + /// - Throws: An error if `self` is not `.clientError`. + /// - SeeAlso: `.clientError`. + internal var clientError: Operations.vectorBucketList.Output.ClientError { + get throws { + switch self { + case let .clientError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "clientError", + response: self + ) + } + } + } + internal struct ServerError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/5XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/ListVectorBuckets/POST/responses/5XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketList.Output.ServerError.Body + /// Creates a new `ServerError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketList.Output.ServerError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/ListVectorBuckets/post(vectorBucketList)/responses/5XX`. + /// + /// HTTP response code: `500...599 serverError`. + case serverError(statusCode: Swift.Int, Operations.vectorBucketList.Output.ServerError) + /// The associated value of the enum case if `self` is `.serverError`. + /// + /// - Throws: An error if `self` is not `.serverError`. + /// - SeeAlso: `.serverError`. + internal var serverError: Operations.vectorBucketList.Output.ServerError { + get throws { + switch self { + case let .serverError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "serverError", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + internal enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + internal init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + internal var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + internal static var allCases: [Self] { + [ + .json + ] + } + } + } + /// Create a vector bucket + /// + /// - Remark: HTTP `POST /vector/GetVectorBucket`. + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)`. + internal enum vectorBucketGet { + internal static let id: Swift.String = "vectorBucketGet" + internal struct Input: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/header`. + internal struct Headers: Sendable, Hashable { + internal var accept: [OpenAPIRuntime.AcceptHeaderContentType] + /// Creates a new `Headers`. + /// + /// - Parameters: + /// - accept: + internal init(accept: [OpenAPIRuntime.AcceptHeaderContentType] = .defaultValues()) { + self.accept = accept + } + } + internal var headers: Operations.vectorBucketGet.Input.Headers + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/requestBody`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/requestBody/json`. + internal struct jsonPayload: Codable, Hashable, Sendable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/requestBody/json/vectorBucketName`. + internal var vectorBucketName: Swift.String + /// Creates a new `jsonPayload`. + /// + /// - Parameters: + /// - vectorBucketName: + internal init(vectorBucketName: Swift.String) { + self.vectorBucketName = vectorBucketName + } + internal enum CodingKeys: String, CodingKey { + case vectorBucketName + } + } + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/requestBody/content/application\/json`. + case json(Operations.vectorBucketGet.Input.Body.jsonPayload) + } + internal var body: Operations.vectorBucketGet.Input.Body + /// Creates a new `Input`. + /// + /// - Parameters: + /// - headers: + /// - body: + internal init( + headers: Operations.vectorBucketGet.Input.Headers = .init(), + body: Operations.vectorBucketGet.Input.Body + ) { + self.headers = headers + self.body = body + } + } + internal enum Output: Sendable, Hashable { + internal struct Ok: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/200/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/200/content/application\/json`. + case json(Components.Schemas.getVectorBucketResponse) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.getVectorBucketResponse { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketGet.Output.Ok.Body + /// Creates a new `Ok`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketGet.Output.Ok.Body) { + self.body = body + } + } + /// Successful response + /// + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)/responses/200`. + /// + /// HTTP response code: `200 ok`. + case ok(Operations.vectorBucketGet.Output.Ok) + /// The associated value of the enum case if `self` is `.ok`. + /// + /// - Throws: An error if `self` is not `.ok`. + /// - SeeAlso: `.ok`. + internal var ok: Operations.vectorBucketGet.Output.Ok { + get throws { + switch self { + case let .ok(response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "ok", + response: self + ) + } + } + } + internal struct ClientError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/4XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/4XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketGet.Output.ClientError.Body + /// Creates a new `ClientError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketGet.Output.ClientError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)/responses/4XX`. + /// + /// HTTP response code: `400...499 clientError`. + case clientError(statusCode: Swift.Int, Operations.vectorBucketGet.Output.ClientError) + /// The associated value of the enum case if `self` is `.clientError`. + /// + /// - Throws: An error if `self` is not `.clientError`. + /// - SeeAlso: `.clientError`. + internal var clientError: Operations.vectorBucketGet.Output.ClientError { + get throws { + switch self { + case let .clientError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "clientError", + response: self + ) + } + } + } + internal struct ServerError: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/5XX/content`. + internal enum Body: Sendable, Hashable { + /// - Remark: Generated from `#/paths/vector/GetVectorBucket/POST/responses/5XX/content/application\/json`. + case json(Components.Schemas.errorSchema) + /// The associated value of the enum case if `self` is `.json`. + /// + /// - Throws: An error if `self` is not `.json`. + /// - SeeAlso: `.json`. + internal var json: Components.Schemas.errorSchema { + get throws { + switch self { + case let .json(body): + return body + } + } + } + } + /// Received HTTP response body + internal var body: Operations.vectorBucketGet.Output.ServerError.Body + /// Creates a new `ServerError`. + /// + /// - Parameters: + /// - body: Received HTTP response body + internal init(body: Operations.vectorBucketGet.Output.ServerError.Body) { + self.body = body + } + } + /// Error response + /// + /// - Remark: Generated from `#/paths//vector/GetVectorBucket/post(vectorBucketGet)/responses/5XX`. + /// + /// HTTP response code: `500...599 serverError`. + case serverError(statusCode: Swift.Int, Operations.vectorBucketGet.Output.ServerError) + /// The associated value of the enum case if `self` is `.serverError`. + /// + /// - Throws: An error if `self` is not `.serverError`. + /// - SeeAlso: `.serverError`. + internal var serverError: Operations.vectorBucketGet.Output.ServerError { + get throws { + switch self { + case let .serverError(_, response): + return response + default: + try throwUnexpectedResponseStatus( + expectedStatus: "serverError", + response: self + ) + } + } + } + /// Undocumented response. + /// + /// A response with a code that is not documented in the OpenAPI document. + case undocumented(statusCode: Swift.Int, OpenAPIRuntime.UndocumentedPayload) + } + internal enum AcceptableContentType: AcceptableProtocol { + case json + case other(Swift.String) + internal init?(rawValue: Swift.String) { + switch rawValue.lowercased() { + case "application/json": + self = .json + default: + self = .other(rawValue) + } + } + internal var rawValue: Swift.String { + switch self { + case let .other(string): + return string + case .json: + return "application/json" + } + } + internal static var allCases: [Self] { + [ + .json + ] + } + } + } +} diff --git a/Sources/Storage/OpenAPI/StorageOpenAPITransport.swift b/Sources/Storage/OpenAPI/StorageOpenAPITransport.swift new file mode 100644 index 000000000..954124fe1 --- /dev/null +++ b/Sources/Storage/OpenAPI/StorageOpenAPITransport.swift @@ -0,0 +1,84 @@ +import Foundation +import HTTPTypes +import OpenAPIRuntime + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// Bridges the generated OpenAPI client's HTTP layer onto ``StorageApi``'s shared header-merging +/// and ``StorageHTTPSession`` pipeline. Unlike the hand-written `execute(_:)` path, this transport +/// never throws on non-2xx responses — it returns them as-is so the generated client's typed +/// `Output` cases carry the real status and error body, and callers decode ``StorageError`` from +/// those cases instead. +struct StorageOpenAPITransport: ClientTransport { + var execute: @Sendable (Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse + + func send( + _ request: HTTPTypes.HTTPRequest, + body: OpenAPIRuntime.HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { + let requestTarget = request.path ?? "" + let pathAndQuery = requestTarget.split(separator: "?", maxSplits: 1) + let path = String(pathAndQuery.first ?? "") + let query = pathAndQuery.count > 1 ? String(pathAndQuery[1]) : nil + + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { + throw StorageOpenAPITransportError.invalidBaseURL(baseURL) + } + components.percentEncodedPath += path + components.percentEncodedQuery = query + + guard let url = components.url else { + throw StorageOpenAPITransportError.invalidRequestURL(path: requestTarget, baseURL: baseURL) + } + + let requestBody: Data? + if let body { + requestBody = try await Data(collecting: body, upTo: .max) + } else { + requestBody = nil + } + + var headers = HTTPFields() + for field in request.headerFields { + // ponytail: the generated client always sets an `Accept` header via + // `converter.setAcceptHeader`; the hand-written implementations being migrated never sent + // one, so drop it here to keep wire behavior byte-identical during the migration. + if field.name == .accept { continue } + if field.name == .contentType, field.value == "application/json; charset=utf-8" { + // ponytail: the generated client always appends `; charset=utf-8` to JSON content types + // via `converter.setRequiredRequestBodyAsJSON`; the hand-written implementations being + // migrated always sent plain `application/json`, so strip the suffix here to keep wire + // behavior byte-identical during the migration. + headers[field.name] = "application/json" + continue + } + headers[field.name] = field.value + } + + let helpersRequest = Helpers.HTTPRequest( + url: url, + method: request.method, + headers: headers, + body: requestBody + ) + + let response = try await execute(helpersRequest) + + let responseBody: OpenAPIRuntime.HTTPBody? = + response.data.isEmpty ? nil : OpenAPIRuntime.HTTPBody(response.data) + + return ( + HTTPTypes.HTTPResponse(status: .init(code: response.statusCode)), + responseBody + ) + } +} + +enum StorageOpenAPITransportError: Error { + case invalidBaseURL(URL) + case invalidRequestURL(path: String, baseURL: URL) +} diff --git a/Sources/Storage/OpenAPI/openapi-generator-config.yaml b/Sources/Storage/OpenAPI/openapi-generator-config.yaml new file mode 100644 index 000000000..1a8ede969 --- /dev/null +++ b/Sources/Storage/OpenAPI/openapi-generator-config.yaml @@ -0,0 +1,10 @@ +generate: + - types + - client +accessModifier: internal +filter: + operations: + - vectorBucketCreate + - vectorBucketDelete + - vectorBucketGet + - vectorBucketList \ No newline at end of file diff --git a/Sources/Storage/OpenAPI/openapi.json b/Sources/Storage/OpenAPI/openapi.json new file mode 100644 index 000000000..e22b9a57e --- /dev/null +++ b/Sources/Storage/OpenAPI/openapi.json @@ -0,0 +1,6517 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Supabase Storage API", + "description": "API documentation for Supabase Storage", + "version": "0.0.0" + }, + "components": { + "securitySchemes": { + "bearerAuth": { + "type": "http", + "scheme": "bearer", + "bearerFormat": "jwt" + } + }, + "schemas": { + "authSchema": { + "type": "object", + "properties": { + "authorization": { + "type": "string", + "example": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZS1kZW1vIiwicm9sZSI6ImFub24ifQ.625_WdcF3KHqz5amU0x2X5WWHP-OEs_4qj0ssLNHzTs" + } + }, + "required": [ + "authorization" + ] + }, + "errorSchema": { + "type": "object", + "properties": { + "statusCode": { + "type": "string" + }, + "error": { + "type": "string" + }, + "message": { + "type": "string" + }, + "code": { + "type": "string" + } + }, + "required": [ + "statusCode", + "error", + "message", + "code" + ] + }, + "vectorBucketSchema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "creationTime": { + "type": "integer", + "nullable": true, + "description": "Unix timestamp (seconds) of when the bucket was created, if known." + } + }, + "required": [ + "vectorBucketName" + ], + "additionalProperties": false, + "example": { + "vectorBucketName": "embeddings-prod", + "creationTime": 1735689600 + } + }, + "getVectorBucketResponse": { + "type": "object", + "properties": { + "vectorBucket": { + "$ref": "#/components/schemas/vectorBucketSchema" + } + }, + "required": [ + "vectorBucket" + ], + "additionalProperties": false + }, + "listVectorBucketsResponse": { + "type": "object", + "properties": { + "vectorBuckets": { + "type": "array", + "items": { + "$ref": "#/components/schemas/vectorBucketSchema" + } + }, + "nextToken": { + "type": "string" + } + }, + "required": [ + "vectorBuckets" + ], + "additionalProperties": false + }, + "queryVectorBodyDoc": { + "type": "object", + "properties": { + "filter": { + "type": "object", + "description": "Boolean filter expression. Supports field operators ($eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $exists) and logical operators ($and, $or) with arbitrary nesting.", + "additionalProperties": true + }, + "indexArn": { + "type": "string" + }, + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$" + }, + "queryVector": { + "type": "object", + "properties": { + "float32": { + "type": "array", + "minItems": 1, + "items": { + "type": "number" + } + } + }, + "required": [ + "float32" + ], + "additionalProperties": false + }, + "returnDistance": { + "type": "boolean" + }, + "returnMetadata": { + "type": "boolean" + }, + "topK": { + "type": "integer", + "minimum": 1, + "maximum": 100, + "description": "Number of nearest-neighbor results to return, from 1 to 100." + }, + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "vectorBucketName", + "indexName", + "queryVector", + "topK" + ], + "additionalProperties": false + } + } + }, + "paths": { + "/metrics": { + "get": { + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/upload/resumable/": { + "post": { + "operationId": "tusUploadCreate", + "summary": "Handle POST request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "options": { + "operationId": "tusOptions", + "summary": "Handle OPTIONS request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "description": "Handle OPTIONS request for TUS Resumable uploads", + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/upload/resumable/{wildcard}": { + "post": { + "summary": "Handle POST request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "put": { + "operationId": "tusUploadPart", + "summary": "Handle PUT request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "patch": { + "summary": "Handle PATCH request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "tusUploadGetHead", + "summary": "Handle HEAD request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "delete": { + "operationId": "tusUploadDelete", + "summary": "Handle DELETE request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "options": { + "summary": "Handle OPTIONS request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "description": "Handle OPTIONS request for TUS Resumable uploads", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/upload/resumable/sign/": { + "post": { + "operationId": "tusUploadCreateSigned", + "summary": "Handle POST request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "options": { + "operationId": "tusOptionsSigned", + "summary": "Handle OPTIONS request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "description": "Handle OPTIONS request for TUS Resumable uploads", + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/upload/resumable/sign/{wildcard}": { + "post": { + "summary": "Handle POST request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "put": { + "operationId": "tusUploadPartSigned", + "summary": "Handle PUT request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "patch": { + "summary": "Handle PATCH request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "tusUploadGetSignedHead", + "summary": "Handle HEAD request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "delete": { + "operationId": "tusUploadDeleteSigned", + "summary": "Handle DELETE request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "options": { + "summary": "Handle OPTIONS request for TUS Resumable uploads", + "tags": [ + "resumable" + ], + "description": "Handle OPTIONS request for TUS Resumable uploads", + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/bucket/{bucketId}/empty": { + "post": { + "operationId": "bucketEmpty", + "summary": "Empty a bucket", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketId", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Empty bucket has been queued. Completion may take up to an hour." + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/bucket": { + "head": { + "operationId": "bucketListHead", + "summary": "Gets all buckets", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "example": 10, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 0, + "in": "query", + "name": "offset", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "id", + "name", + "created_at", + "updated_at" + ] + }, + "in": "query", + "name": "sortColumn", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "in": "query", + "name": "sortOrder", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "my-bucket", + "in": "query", + "name": "search", + "required": false + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + }, + "example": [ + { + "id": "avatars", + "type": "STANDARD", + "name": "avatars", + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + ] + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "post": { + "operationId": "bucketCreate", + "summary": "Create a bucket", + "tags": [ + "bucket" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "avatars" + }, + "id": { + "type": "string", + "example": "avatars" + }, + "public": { + "type": "boolean", + "example": false + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "anyOf": [ + { + "type": "integer", + "examples": [ + 1000 + ], + "nullable": true, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^[0-9]+(?:\\.[0-9]+)?(?:[gG][bB]|[mM][bB]|[kK][bB]|[bB])$", + "examples": [ + "100MB" + ], + "nullable": true + } + ] + }, + "allowed_mime_types": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + }, + "example": [ + "image/png", + "image/jpg" + ] + } + }, + "required": [ + "name" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "avatars" + } + }, + "required": [ + "name" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "get": { + "operationId": "bucketList", + "summary": "Gets all buckets", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 1 + }, + "example": 10, + "in": "query", + "name": "limit", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 0, + "in": "query", + "name": "offset", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "id", + "name", + "created_at", + "updated_at" + ] + }, + "in": "query", + "name": "sortColumn", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + }, + "in": "query", + "name": "sortOrder", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "my-bucket", + "in": "query", + "name": "search", + "required": false + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + }, + "example": [ + { + "id": "avatars", + "type": "STANDARD", + "name": "avatars", + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + ] + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/bucket/{bucketId}": { + "get": { + "operationId": "bucketGet", + "summary": "Get details of a bucket", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketId", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "bucketGetHead", + "summary": "Get details of a bucket", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketId", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false + }, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "put": { + "operationId": "bucketUpdate", + "summary": "Update properties of a bucket", + "tags": [ + "bucket" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "minProperties": 1, + "properties": { + "public": { + "type": "boolean", + "example": false + }, + "file_size_limit": { + "anyOf": [ + { + "type": "integer", + "examples": [ + 1000 + ], + "nullable": true, + "minimum": 0 + }, + { + "type": "string", + "pattern": "^[0-9]+(?:\\.[0-9]+)?(?:[gG][bB]|[mM][bB]|[kK][bB]|[bB])$", + "examples": [ + "100MB" + ], + "nullable": true + } + ] + }, + "allowed_mime_types": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "example": [ + "image/png", + "image/jpg" + ] + } + } + }, + "anyOf": [ + { + "required": [ + "public" + ] + }, + { + "required": [ + "file_size_limit" + ] + }, + { + "required": [ + "allowed_mime_types" + ] + } + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "bucketId", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully updated" + } + }, + "required": [ + "message" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "delete": { + "operationId": "bucketDelete", + "summary": "Delete a bucket", + "tags": [ + "bucket" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketId", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted" + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/{bucketName}/{wildcard}": { + "delete": { + "operationId": "objectDelete", + "summary": "Delete an object", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully deleted" + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "get": { + "summary": "Get object", + "tags": [ + "object" + ], + "description": "Serve objects", + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "put": { + "operationId": "objectUploadUpdate", + "summary": "Update the object at an existing key", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Key": { + "type": "string", + "example": "avatars/folder/cat.png" + } + }, + "required": [ + "Key" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "description": "Head object info", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "post": { + "operationId": "objectUpload", + "summary": "Upload a new object", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Key": { + "type": "string", + "example": "avatars/folder/cat.png" + } + }, + "required": [ + "Key" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/{bucketName}": { + "delete": { + "operationId": "objectDeleteMany", + "summary": "Delete multiple objects", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prefixes": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "description": "At most 1000 objects can be deleted per request.", + "example": [ + "folder/cat.png", + "folder/morecats.png" + ] + } + }, + "required": [ + "prefixes" + ] + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "bucket_id": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "user_metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "buckets": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "example": { + "name": "folder/cat.png", + "bucket_id": "avatars", + "owner": "317eadce-631a-4429-a0bb-f19a7a517b4a", + "id": "eaa8bdb5-2e00-4767-b5a9-d2502efe2196", + "updated_at": "2021-04-06T16:30:35.394674+00:00", + "created_at": "2021-04-06T16:30:35.394674+00:00", + "last_accessed_at": "2021-04-06T16:30:35.394674+00:00", + "metadata": { + "size": 1234 + } + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/authenticated/{bucketName}/{wildcard}": { + "get": { + "operationId": "objectGetAuthenticated", + "summary": "Retrieve an object", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "examples": { + "filename.jpg": { + "value": "filename.jpg" + }, + "example2": { + "value": null + } + }, + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "objectHeadAuthenticatedInfoHead", + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/upload/sign/{bucketName}/{wildcard}": { + "post": { + "operationId": "objectSignUploadUrl", + "summary": "Generate a presigned url to upload an object", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "url": { + "type": "string", + "example": "/object/sign/upload/avatars/folder/cat.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJhdmF0YXJzL2ZvbGRlci9jYXQucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.s7Gt8ME80iREVxPhH01ZNv8oUn4XtaWsmiQ5csiUHn4" + }, + "token": { + "type": "string" + } + }, + "required": [ + "url" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "put": { + "operationId": "objectUploadSigned", + "summary": "Uploads an object via a presigned URL", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJidWNrZXQyL3B1YmxpYy9zYWRjYXQtdXBsb2FkMjMucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.uBQcXzuvXxfw-9WgzWMBfE_nR3VOgpvfZe032sfLSSk", + "in": "query", + "name": "token", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "Key": { + "type": "string", + "example": "avatars/folder/cat.png" + } + }, + "required": [ + "Key" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/sign/{bucketName}/{wildcard}": { + "post": { + "operationId": "objectSign", + "summary": "Generate a presigned url to retrieve an object", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expiresIn": { + "type": "integer", + "minimum": 1, + "example": 60000 + }, + "transform": { + "type": "object", + "properties": { + "height": { + "type": "integer", + "minimum": 0, + "example": 100 + }, + "width": { + "type": "integer", + "minimum": 0, + "example": 100 + }, + "resize": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "format": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "quality": { + "type": "integer", + "minimum": 20, + "maximum": 100 + } + } + } + }, + "required": [ + "expiresIn" + ] + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "signedURL": { + "type": "string", + "example": "/object/sign/avatars/folder/cat.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJhdmF0YXJzL2ZvbGRlci9jYXQucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.s7Gt8ME80iREVxPhH01ZNv8oUn4XtaWsmiQ5csiUHn4" + } + }, + "required": [ + "signedURL" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "get": { + "operationId": "objectGetSigned", + "summary": "Retrieve an object via a presigned URL", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "examples": { + "filename.jpg": { + "value": "filename.jpg" + }, + "example2": { + "value": null + } + }, + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJidWNrZXQyL3B1YmxpYy9zYWRjYXQtdXBsb2FkMjMucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.uBQcXzuvXxfw-9WgzWMBfE_nR3VOgpvfZe032sfLSSk", + "in": "query", + "name": "token", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "objectGetSignedHead", + "summary": "Retrieve an object via a presigned URL", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "examples": { + "filename.jpg": { + "value": "filename.jpg" + }, + "example2": { + "value": null + } + }, + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJidWNrZXQyL3B1YmxpYy9zYWRjYXQtdXBsb2FkMjMucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.uBQcXzuvXxfw-9WgzWMBfE_nR3VOgpvfZe032sfLSSk", + "in": "query", + "name": "token", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/sign/{bucketName}": { + "post": { + "operationId": "objectSignMany", + "summary": "Generate presigned urls to retrieve objects", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "expiresIn": { + "type": "integer", + "minimum": 1, + "example": 60000 + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "minItems": 1, + "maxItems": 1000, + "example": [ + "folder/cat.png", + "folder/morecats.png" + ] + } + }, + "required": [ + "expiresIn", + "paths" + ] + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "array", + "items": { + "type": "object", + "properties": { + "error": { + "type": [ + "null", + "string" + ], + "example": "Either the object does not exist or you do not have access to it" + }, + "path": { + "type": "string", + "example": "folder/cat.png" + }, + "signedURL": { + "type": [ + "null", + "string" + ], + "example": "/object/sign/avatars/folder/cat.png?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJhdmF0YXJzL2ZvbGRlci9jYXQucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.s7Gt8ME80iREVxPhH01ZNv8oUn4XtaWsmiQ5csiUHn4" + } + }, + "required": [ + "error", + "path", + "signedURL" + ] + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/move": { + "post": { + "operationId": "objectMove", + "summary": "Moves an object", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bucketId": { + "type": "string", + "example": "avatars" + }, + "sourceKey": { + "type": "string", + "example": "folder/cat.png" + }, + "destinationBucket": { + "type": "string", + "example": "users" + }, + "destinationKey": { + "type": "string", + "example": "folder/newcat.png" + } + }, + "required": [ + "bucketId", + "sourceKey", + "destinationKey" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "Successfully moved" + } + }, + "required": [ + "message" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/list-v2/{bucketName}": { + "post": { + "operationId": "objectListV2", + "summary": "Search for objects under a prefix", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "example": "folder/subfolder" + }, + "limit": { + "type": "integer", + "minimum": 1, + "example": 10 + }, + "cursor": { + "type": "string" + }, + "with_delimiter": { + "type": "boolean" + }, + "sortBy": { + "type": "object", + "properties": { + "column": { + "type": "string", + "enum": [ + "name", + "updated_at", + "created_at" + ] + }, + "order": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "required": [ + "column" + ] + } + } + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "bucketName", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/list/{bucketName}": { + "post": { + "operationId": "objectList", + "summary": "Search for objects under a prefix", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "prefix": { + "type": "string", + "example": "folder/subfolder" + }, + "limit": { + "type": "integer", + "minimum": 1, + "example": 10 + }, + "offset": { + "type": "integer", + "minimum": 0, + "example": 0 + }, + "sortBy": { + "type": "object", + "properties": { + "column": { + "type": "string", + "enum": [ + "name", + "updated_at", + "created_at", + "last_accessed_at" + ] + }, + "order": { + "type": "string", + "enum": [ + "asc", + "desc" + ] + } + }, + "required": [ + "column" + ] + }, + "search": { + "type": "string" + } + }, + "required": [ + "prefix" + ] + } + } + } + }, + "parameters": [ + { + "schema": { + "type": "string" + }, + "in": "path", + "name": "bucketName", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "bucket_id": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "user_metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "buckets": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + }, + "required": [ + "name" + ], + "additionalProperties": false, + "example": { + "name": "folder/cat.png", + "bucket_id": "avatars", + "owner": "317eadce-631a-4429-a0bb-f19a7a517b4a", + "id": "eaa8bdb5-2e00-4767-b5a9-d2502efe2196", + "updated_at": "2021-04-06T16:30:35.394674+00:00", + "created_at": "2021-04-06T16:30:35.394674+00:00", + "last_accessed_at": "2021-04-06T16:30:35.394674+00:00", + "metadata": { + "size": 1234 + } + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/info/authenticated/{bucketName}/{wildcard}": { + "get": { + "operationId": "objectGetAuthenticatedInfo", + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "objectGetAuthenticatedInfoHead", + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/info/{bucketName}/{wildcard}": { + "get": { + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "description": "Object Info", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "summary": "Retrieve object info", + "tags": [ + "object" + ], + "description": "Object Info", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/copy": { + "post": { + "operationId": "objectCopy", + "summary": "Copies an object", + "tags": [ + "object" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "bucketId": { + "type": "string", + "example": "avatars" + }, + "sourceKey": { + "type": "string", + "example": "folder/source.png" + }, + "destinationBucket": { + "type": "string", + "example": "users" + }, + "destinationKey": { + "type": "string", + "example": "folder/destination.png" + }, + "metadata": { + "type": "object", + "properties": { + "cacheControl": { + "type": "string" + }, + "mimetype": { + "type": "string" + } + } + }, + "copyMetadata": { + "type": "boolean", + "example": true + } + }, + "required": [ + "sourceKey", + "bucketId", + "destinationKey" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "Id": { + "type": "string" + }, + "Key": { + "type": "string", + "example": "folder/destination.png" + }, + "name": { + "type": "string" + }, + "bucket_id": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "version": { + "type": "string" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "updated_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "created_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "last_accessed_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ] + }, + "metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "user_metadata": { + "anyOf": [ + { + "type": "object", + "additionalProperties": true + }, + { + "type": "null" + } + ] + }, + "buckets": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "owner": { + "type": "string" + }, + "owner_id": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "type": { + "type": "string", + "enum": [ + "STANDARD", + "ANALYTICS" + ] + }, + "file_size_limit": { + "type": [ + "null", + "integer" + ] + }, + "allowed_mime_types": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + } + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name" + ], + "additionalProperties": false, + "example": { + "id": "bucket2", + "name": "bucket2", + "public": false, + "file_size_limit": 1000000, + "allowed_mime_types": [ + "image/png", + "image/jpeg" + ], + "owner": "4d56e902-f0a0-4662-8448-a4d9e643c142", + "created_at": "2021-02-17T04:43:32.770206+00:00", + "updated_at": "2021-02-17T04:43:32.770206+00:00" + } + } + }, + "required": [ + "Key" + ] + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/public/{bucketName}/{wildcard}": { + "get": { + "operationId": "objectGetPublic", + "summary": "Retrieve an object from a public bucket", + "tags": [ + "object" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "examples": { + "filename.jpg": { + "value": "filename.jpg" + }, + "example2": { + "value": null + } + }, + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "objectInfoPublicHead", + "summary": "Get object info", + "tags": [ + "object" + ], + "description": "returns object info", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/object/info/public/{bucketName}/{wildcard}": { + "get": { + "operationId": "objectInfoPublic", + "summary": "Get object info", + "tags": [ + "object" + ], + "description": "returns object info", + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/render/image/authenticated/{bucketName}/{wildcard}": { + "get": { + "operationId": "renderImageAuthenticated", + "summary": "Render an authenticated image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "renderImageAuthenticatedHead", + "summary": "Render an authenticated image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/render/image/sign/{bucketName}/{wildcard}": { + "get": { + "operationId": "renderImageSign", + "summary": "Render an authenticated image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJidWNrZXQyL3B1YmxpYy9zYWRjYXQtdXBsb2FkMjMucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.uBQcXzuvXxfw-9WgzWMBfE_nR3VOgpvfZe032sfLSSk", + "in": "query", + "name": "token", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "renderImageSignHead", + "summary": "Render an authenticated image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "string" + }, + "example": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1cmwiOiJidWNrZXQyL3B1YmxpYy9zYWRjYXQtdXBsb2FkMjMucG5nIiwiaWF0IjoxNjE3NzI2MjczLCJleHAiOjE2MTc3MjcyNzN9.uBQcXzuvXxfw-9WgzWMBfE_nR3VOgpvfZe032sfLSSk", + "in": "query", + "name": "token", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/render/image/public/{bucketName}/{wildcard}": { + "get": { + "operationId": "renderImagePublic", + "summary": "Render a public image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "head": { + "operationId": "renderImagePublicHead", + "summary": "Render a public image with the given transformations", + "tags": [ + "transformation" + ], + "parameters": [ + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "height", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 0 + }, + "example": 100, + "in": "query", + "name": "width", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "cover", + "contain", + "fill" + ] + }, + "in": "query", + "name": "resize", + "required": false + }, + { + "schema": { + "type": "string", + "enum": [ + "origin", + "avif", + "webp" + ] + }, + "in": "query", + "name": "format", + "required": false + }, + { + "schema": { + "type": "integer", + "minimum": 20, + "maximum": 100 + }, + "in": "query", + "name": "quality", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "filename.png", + "in": "query", + "name": "download", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string" + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/cdn/": { + "delete": { + "operationId": "cdnPurgeTenantCache", + "summary": "Purge cache for entire tenant or tenant transformations", + "tags": [ + "cdn" + ], + "parameters": [ + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "transformations", + "required": false + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/cdn/{bucketName}": { + "delete": { + "operationId": "cdnPurgeBucketCache", + "summary": "Purge cache for an entire bucket or bucket transformations", + "tags": [ + "cdn" + ], + "parameters": [ + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "transformations", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/cdn/{bucketName}/{wildcard}": { + "delete": { + "operationId": "cdnPurgeObjectCache", + "summary": "Purge cache for an object or object transformations", + "tags": [ + "cdn" + ], + "parameters": [ + { + "schema": { + "type": "boolean" + }, + "in": "query", + "name": "transformations", + "required": false + }, + { + "schema": { + "type": "string" + }, + "example": "avatars", + "in": "path", + "name": "bucketName", + "required": true + }, + { + "schema": { + "type": "string", + "minLength": 1 + }, + "example": "folder/cat.png", + "in": "path", + "name": "wildcard", + "required": true + } + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "description": "Successful response", + "type": "object", + "properties": { + "message": { + "type": "string", + "example": "success" + } + } + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/health": { + "head": { + "summary": "healthcheck", + "tags": [ + "health" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + }, + "get": { + "summary": "healthcheck", + "tags": [ + "health" + ], + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Default Response", + "content": { + "application/json": { + "schema": { + "description": "Default Response" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/CreateIndex": { + "post": { + "operationId": "vectorIndexCreate", + "summary": "Create a vector index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "dataType": { + "type": "string", + "enum": [ + "float32" + ] + }, + "dimension": { + "type": "integer", + "minimum": 1, + "maximum": 4096, + "description": "Vector dimensionality. S3 Vectors supports up to 4096. The local pgvector backend supports up to 4000 and will reject larger values." + }, + "distanceMetric": { + "type": "string", + "enum": [ + "cosine", + "euclidean" + ] + }, + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$", + "description": "3-63 chars, lowercase letters, numbers, hyphens, dots; must start/end with letter or number. Must be unique within the vector bucket." + }, + "metadataConfiguration": { + "type": "object", + "required": [ + "nonFilterableMetadataKeys" + ], + "properties": { + "nonFilterableMetadataKeys": { + "type": "array", + "minItems": 1, + "maxItems": 10, + "uniqueItems": true, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 63 + } + } + } + }, + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "dataType", + "dimension", + "distanceMetric", + "indexName", + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/DeleteIndex": { + "post": { + "operationId": "vectorIndexDelete", + "summary": "Delete a vector index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$", + "description": "3-63 chars, lowercase letters, numbers, hyphens, dots; must start/end with letter or number. Must be unique within the vector bucket." + }, + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "indexName", + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/ListIndexes": { + "post": { + "operationId": "vectorIndexList", + "summary": "List indexes in a vector bucket", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "maxResults": { + "type": "number", + "minimum": 1, + "maximum": 500, + "default": 500 + }, + "nextToken": { + "type": "string" + }, + "prefix": { + "type": "string" + } + }, + "required": [ + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/GetIndex": { + "post": { + "operationId": "vectorIndexGet", + "summary": "Get a vector index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$", + "description": "3-63 chars, lowercase letters, numbers, hyphens, dots; must start/end with letter or number. Must be unique within the vector bucket." + } + }, + "required": [ + "vectorBucketName", + "indexName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/CreateVectorBucket": { + "post": { + "operationId": "vectorBucketCreate", + "summary": "Create a vector bucket", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response" + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/DeleteVectorBucket": { + "post": { + "operationId": "vectorBucketDelete", + "summary": "Create a vector bucket", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response" + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/ListVectorBuckets": { + "post": { + "operationId": "vectorBucketList", + "summary": "List vector buckets", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "maxResults": { + "type": "number", + "minimum": 1, + "maximum": 500, + "default": 500 + }, + "nextToken": { + "type": "string" + }, + "prefix": { + "type": "string" + } + } + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/listVectorBucketsResponse" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/GetVectorBucket": { + "post": { + "operationId": "vectorBucketGet", + "summary": "Create a vector bucket", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "200": { + "description": "Successful response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/getVectorBucketResponse" + } + } + } + }, + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/PutVectors": { + "post": { + "operationId": "vectorVectorsPut", + "summary": "Put vectors into an index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$", + "description": "3-63 chars, lowercase letters, numbers, hyphens, dots; must start/end with letter or number. Must be unique within the vector bucket." + }, + "vectors": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "items": { + "type": "object", + "properties": { + "data": { + "type": "object", + "properties": { + "float32": { + "type": "array", + "minItems": 1, + "items": { + "type": "number" + } + } + }, + "required": [ + "float32" + ] + }, + "metadata": { + "type": "object", + "additionalProperties": { + "anyOf": [ + { + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "number" + } + ] + }, + { + "type": "array", + "items": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "boolean" + }, + { + "type": "number" + } + ] + } + } + ] + } + }, + "key": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "required": [ + "data", + "key" + ] + } + } + }, + "required": [ + "vectorBucketName", + "indexName", + "vectors" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/QueryVectors": { + "post": { + "operationId": "vectorVectorsQuery", + "summary": "Query vectors", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/queryVectorBodyDoc" + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/DeleteVectors": { + "post": { + "operationId": "vectorVectorsDelete", + "summary": "Delete vectors from an index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "indexName": { + "type": "string" + }, + "keys": { + "type": "array", + "minItems": 1, + "maxItems": 500, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + } + }, + "required": [ + "vectorBucketName", + "indexName", + "keys" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/ListVectors": { + "post": { + "operationId": "vectorVectorsList", + "summary": "List vectors in a vector index", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "vectorBucketName": { + "type": "string" + }, + "indexArn": { + "type": "string" + }, + "indexName": { + "type": "string", + "minLength": 3, + "maxLength": 45, + "pattern": "^[a-z0-9](?:[a-z0-9.-]{1,61})?[a-z0-9]$", + "description": "3-63 chars, lowercase letters, numbers, hyphens, dots; must start/end with letter or number. Must be unique within the vector bucket." + }, + "maxResults": { + "type": "integer", + "minimum": 1, + "maximum": 1000 + }, + "nextToken": { + "type": "string" + }, + "returnData": { + "type": "boolean" + }, + "returnMetadata": { + "type": "boolean" + }, + "segmentCount": { + "type": "integer", + "minimum": 1, + "maximum": 16 + }, + "segmentIndex": { + "type": "integer", + "minimum": 0, + "maximum": 15 + } + }, + "required": [ + "vectorBucketName", + "indexName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + }, + "/vector/GetVectors": { + "post": { + "operationId": "vectorVectorsGet", + "summary": "Returns vector attributes", + "tags": [ + "vector" + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "indexName": { + "type": "string" + }, + "keys": { + "type": "array", + "minItems": 1, + "maxItems": 100, + "items": { + "type": "string", + "minLength": 1, + "maxLength": 1024 + } + }, + "returnData": { + "type": "boolean", + "default": false + }, + "returnMetadata": { + "type": "boolean", + "default": false + }, + "vectorBucketName": { + "type": "string" + } + }, + "required": [ + "indexName", + "keys", + "vectorBucketName" + ] + } + } + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "responses": { + "4XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + }, + "5XX": { + "description": "Error response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/errorSchema" + } + } + } + } + } + } + } + }, + "tags": [ + { + "name": "object", + "description": "Object end-points" + }, + { + "name": "bucket", + "description": "Bucket end-points" + }, + { + "name": "s3", + "description": "S3 end-points" + }, + { + "name": "transformation", + "description": "Image transformation" + }, + { + "name": "resumable", + "description": "Resumable Upload end-points" + }, + { + "name": "cdn", + "description": "CDN cache management" + }, + { + "name": "health", + "description": "Health check end-points" + }, + { + "name": "iceberg", + "description": "Apache Iceberg REST catalog" + }, + { + "name": "vector", + "description": "Vector storage and search" + } + ] +} \ No newline at end of file diff --git a/Sources/Storage/StorageApi.swift b/Sources/Storage/StorageApi.swift index 9c4c7aecf..0136d9bf7 100644 --- a/Sources/Storage/StorageApi.swift +++ b/Sources/Storage/StorageApi.swift @@ -1,6 +1,7 @@ import ConcurrencyExtras import Foundation import HTTPTypes +import OpenAPIRuntime #if canImport(FoundationNetworking) import FoundationNetworking @@ -29,6 +30,10 @@ public class StorageApi: @unchecked Sendable { /// The configuration used to initialize this client instance. public let configuration: StorageClientConfiguration + /// The generated OpenAPI client for the Storage HTTP API. Internal implementation detail — + /// ``StorageBucketApi``/``StorageFileApi`` use this instead of hand-building requests. + let openAPIClient: Client + private struct MutableState { var headers: [String: String] } @@ -83,6 +88,17 @@ public class StorageApi: @unchecked Sendable { fetch: configuration.session.fetch, interceptors: interceptors ) + + let mutableStateRef = mutableState + let httpRef = http + let decoder = configuration.decoder + openAPIClient = Client( + serverURL: configuration.url, + transport: StorageOpenAPITransport(execute: { request in + try await Self.executeRequest( + request, headers: mutableStateRef.headers, http: httpRef, decoder: decoder) + }) + ) } /// Sets an HTTP header that will be included in all subsequent requests made by this instance. @@ -104,27 +120,32 @@ public class StorageApi: @unchecked Sendable { return self } - @discardableResult - func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + private static func executeRequest( + _ request: Helpers.HTTPRequest, + headers: [String: String], + http: any HTTPClientType, + decoder: JSONDecoder + ) async throws -> Helpers.HTTPResponse { var request = request - let headers = mutableState.headers request.headers = HTTPFields(headers).merging(with: request.headers) let response = try await http.send(request) guard (200..<300).contains(response.statusCode) else { - if let error = try? configuration.decoder.decode( - StorageError.self, - from: response.data - ) { + if let error = try? decoder.decode(StorageError.self, from: response.data) { throw error } - throw HTTPError(data: response.data, response: response.underlyingResponse) } return response } + + @discardableResult + func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + try await Self.executeRequest( + request, headers: mutableState.headers, http: http, decoder: configuration.decoder) + } } extension Helpers.HTTPRequest { diff --git a/Sources/Storage/Types.swift b/Sources/Storage/StorageTypes.swift similarity index 100% rename from Sources/Storage/Types.swift rename to Sources/Storage/StorageTypes.swift diff --git a/Tests/IntegrationTests/RealtimeIntegrationTests.swift b/Tests/IntegrationTests/RealtimeIntegrationTests.swift index 126cb3829..5ce6340b1 100644 --- a/Tests/IntegrationTests/RealtimeIntegrationTests.swift +++ b/Tests/IntegrationTests/RealtimeIntegrationTests.swift @@ -27,10 +27,10 @@ override func setUp() async throws { try await super.setUp() - // try XCTSkipUnless( - // ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil, - // "INTEGRATION_TESTS not defined. Set this environment variable to run integration tests." - // ) + try XCTSkipUnless( + ProcessInfo.processInfo.environment["INTEGRATION_TESTS"] != nil, + "INTEGRATION_TESTS not defined. Set this environment variable to run integration tests." + ) _clock = testClock @@ -66,12 +66,13 @@ } override func tearDown() async throws { - // Clean up channels and disconnect - await client.realtimeV2.removeAllChannels() - client.realtimeV2.disconnect() + // Clean up channels and disconnect. `client`/`client2` are nil when `setUp()` skipped via + // XCTSkipUnless (INTEGRATION_TESTS not set), so this must tolerate that case. + await client?.realtimeV2.removeAllChannels() + client?.realtimeV2.disconnect() - await client2.realtimeV2.removeAllChannels() - client2.realtimeV2.disconnect() + await client2?.realtimeV2.removeAllChannels() + client2?.realtimeV2.disconnect() try await super.tearDown() } diff --git a/Tests/StorageTests/StorageOpenAPITransportTests.swift b/Tests/StorageTests/StorageOpenAPITransportTests.swift new file mode 100644 index 000000000..6ab333f3c --- /dev/null +++ b/Tests/StorageTests/StorageOpenAPITransportTests.swift @@ -0,0 +1,64 @@ +import HTTPTypes +import OpenAPIRuntime +import XCTest + +@testable import Storage + +final class StorageOpenAPITransportTests: XCTestCase { + func testSendJoinsBaseURLAndPathAndQuery() async throws { + var capturedRequest: Helpers.HTTPRequest? + + let transport = StorageOpenAPITransport(execute: { request in + capturedRequest = request + return Helpers.HTTPResponse( + data: Data(#"{"ok":true}"#.utf8), + response: HTTPURLResponse( + url: URL(string: "http://localhost/storage/v1/bucket")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + ) + }) + + let (response, body) = try await transport.send( + HTTPTypes.HTTPRequest(method: .get, scheme: nil, authority: nil, path: "/bucket?limit=10"), + body: nil, + baseURL: URL(string: "http://localhost/storage/v1")!, + operationID: "bucketList" + ) + + XCTAssertEqual( + capturedRequest?.url.absoluteString, "http://localhost/storage/v1/bucket?limit=10") + XCTAssertEqual(capturedRequest?.method, .get) + XCTAssertEqual(response.status.code, 200) + let data = try await Data(collecting: body ?? HTTPBody(""), upTo: .max) + XCTAssertEqual(data, Data(#"{"ok":true}"#.utf8)) + } + + func testSendPropagatesBodyBytes() async throws { + var capturedRequest: Helpers.HTTPRequest? + + let transport = StorageOpenAPITransport(execute: { request in + capturedRequest = request + return Helpers.HTTPResponse( + data: Data(), + response: HTTPURLResponse( + url: URL(string: "http://localhost/storage/v1/bucket")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + ) + }) + + _ = try await transport.send( + HTTPTypes.HTTPRequest(method: .post, scheme: nil, authority: nil, path: "/bucket"), + body: HTTPBody(#"{"name":"avatars"}"#), + baseURL: URL(string: "http://localhost/storage/v1")!, + operationID: "bucketCreate" + ) + + XCTAssertEqual(capturedRequest?.body, Data(#"{"name":"avatars"}"#.utf8)) + } +} diff --git a/docs/superpowers/plans/2026-07-08-storage-openapi-generator.md b/docs/superpowers/plans/2026-07-08-storage-openapi-generator.md new file mode 100644 index 000000000..5119faa8f --- /dev/null +++ b/docs/superpowers/plans/2026-07-08-storage-openapi-generator.md @@ -0,0 +1,1072 @@ +# Storage OpenAPI Generator Adoption (Milestones 1–2) Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the hand-written request/response plumbing in `StorageBucketApi` with a +generated OpenAPI client (from `swift-openapi-generator`), sourced from +[supabase/storage#1215](https://github.com/supabase/storage/pull/1215)'s spec, with zero public +API or behavior change. + +**Architecture:** Vendor the spec, generate `Types.swift`/`Client.swift` once and commit them. +Add a small `StorageOpenAPITransport` that bridges the generated client's HTTP layer onto the +existing `execute()` request pipeline (same header merging, same `StorageError` decoding, same +`StorageHTTPSession` injection point — no behavior change). Migrate each `StorageBucketApi` +method one at a time to call the generated client internally while keeping its public signature +identical. + +**Tech Stack:** Swift 6.1, `apple/swift-openapi-generator` (dev-only codegen tool, not a runtime +dependency), `apple/swift-openapi-runtime` (runtime dependency, ships to consumers), XCTest + +Mocker (existing test infra, unchanged). + +**Design doc:** `docs/superpowers/specs/2026-07-08-storage-openapi-generator-design.md` + +## Global Constraints + +- No public API signature, type, or observable HTTP behavior may change. Every existing test in + `Tests/StorageTests/StorageBucketAPITests.swift` must keep passing after each migration task, + **except** where a test asserts the exact serialized JSON body of a request — those may need + their snapshot re-recorded (see Task 9/10 note) because the generated client's JSON encoder + doesn't necessarily preserve `configuration.encoder`'s `.sortedKeys` key ordering. The + `Content-Length` byte count must stay the same (same keys/values, just possibly reordered). +- `apple/swift-openapi-generator` must NOT be added to the root `Package.swift` — it's a dev-only + tool, isolated in `tools/openapi-generator`, following the existing `tools/node` precedent + (`tools/node/package.json`, used for cspell). +- `apple/swift-openapi-runtime` IS added to the root `Package.swift` as a real dependency of the + `Storage` target — the generated code imports `OpenAPIRuntime` at runtime. +- Run `./scripts/format.sh` before every commit that touches hand-written Swift files (not the + generated ones — see Task 2). +- Platforms: iOS 16+, macOS 13+, tvOS 16+, watchOS 9+, visionOS 1+ (per `AGENTS.md`) — `URL`, + `URLComponents` APIs used below are all available on these minimums. + +--- + +### Task 1: Vendor the OpenAPI spec from PR #1215 + +**Files:** +- Create: `Sources/Storage/OpenAPI/openapi.json` +- Create: `Sources/Storage/OpenAPI/openapi-generator-config.yaml` + +**Interfaces:** +- Produces: `Sources/Storage/OpenAPI/openapi.json` (OpenAPI 3.0.3 document, 25 paths under this + config, includes `bucketSchema`/`objectSchema`/`errorSchema`/`authSchema` components and + `bearerAuth` security scheme) — consumed by Task 2's codegen step. + +- [ ] **Step 1: Export the spec from the storage PR branch** + +Run (from any scratch directory, not this repo): + +```bash +git clone https://github.com/supabase/storage.git /tmp/storage-pr-1215 +cd /tmp/storage-pr-1215 +gh pr checkout 1215 +npm ci +PGRST_JWT_SECRET=dummydummydummydummydummydummy \ +AUTH_JWT_SECRET=dummydummydummydummydummydummy \ +ANON_KEY=dummy SERVICE_KEY=dummy \ +DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres \ +TENANT_ID=dummy STORAGE_BACKEND=file FILE_STORAGE_BACKEND_PATH=/tmp/storage \ +FILE_SIZE_LIMIT=1000000 REGION=local GLOBAL_S3_BUCKET=dummy \ +npm run docs:export +``` + +Expected: `static/api.json` is created (25 paths, no errors printed). + +- [ ] **Step 2: Copy the spec into this repo** + +```bash +cp /tmp/storage-pr-1215/static/api.json Sources/Storage/OpenAPI/openapi.json +rm -rf /tmp/storage-pr-1215 +``` + +- [ ] **Step 3: Add the generator config** + +Create `Sources/Storage/OpenAPI/openapi-generator-config.yaml`: + +```yaml +generate: + - types + - client +accessModifier: internal +``` + +- [ ] **Step 4: Verify the spec is valid JSON and has the expected shape** + +```bash +python3 -c " +import json +d = json.load(open('Sources/Storage/OpenAPI/openapi.json')) +assert d['openapi'] == '3.0.3' +assert 'bucketSchema' in d['components']['schemas'] +assert 'objectSchema' in d['components']['schemas'] +assert 'errorSchema' in d['components']['schemas'] +print('OK:', len(d['paths']), 'paths') +" +``` + +Expected: `OK: 25 paths` (or more, if run against a merged/updated spec — that's fine). + +- [ ] **Step 5: Commit** + +```bash +git add Sources/Storage/OpenAPI/openapi.json Sources/Storage/OpenAPI/openapi-generator-config.yaml +git commit -m "feat(storage): vendor OpenAPI spec from supabase/storage#1215" +``` + +--- + +### Task 2: Codegen tooling — generate and commit the client + +**Files:** +- Create: `tools/openapi-generator/Package.swift` +- Create: `scripts/generate-storage-openapi.sh` +- Create: `Sources/Storage/Generated/Types.swift` (generated output, do not hand-edit) +- Create: `Sources/Storage/Generated/Client.swift` (generated output, do not hand-edit) +- Modify: `Package.swift` (add `swift-openapi-runtime` dependency + `Storage` target dependency) + +**Interfaces:** +- Produces: `Sources/Storage/Generated/{Types,Client}.swift`, importable as `OpenAPIRuntime`-backed + `Components.Schemas.*`, `Operations.*`, and a `Client` type with one async throwing method per + operation (e.g. `Client.bucketList(_:) async throws -> Operations.bucketList.Output`). Consumed + by Task 4 onward. + +- [ ] **Step 1: Add the isolated codegen tool package** + +Create `tools/openapi-generator/Package.swift`: + +```swift +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "openapi-generator", + platforms: [.macOS(.v13)], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.5.0") + ], + targets: [] +) +``` + +- [ ] **Step 2: Add the generation script** + +Create `scripts/generate-storage-openapi.sh`: + +```bash +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." + +swift run --package-path tools/openapi-generator swift-openapi-generator generate \ + Sources/Storage/OpenAPI/openapi.json \ + --config Sources/Storage/OpenAPI/openapi-generator-config.yaml \ + --output-directory Sources/Storage/Generated +``` + +```bash +chmod +x scripts/generate-storage-openapi.sh +``` + +- [ ] **Step 3: Add `swift-openapi-runtime` as a real dependency** + +In the root `Package.swift`, add to the `dependencies:` array (after the `Mocker` line at +`Package.swift:32`): + +```swift + .package(url: "https://github.com/apple/swift-openapi-runtime", from: "1.6.0"), +``` + +Add to the `Storage` target's `dependencies:` (`Package.swift:169-173`): + +```swift + .target( + name: "Storage", + dependencies: [ + .product(name: "ConcurrencyExtras", package: "swift-concurrency-extras"), + .product(name: "HTTPTypes", package: "swift-http-types"), + .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), + "Helpers", + ] + ), +``` + +- [ ] **Step 4: Run codegen** + +```bash +./scripts/generate-storage-openapi.sh +``` + +Expected: `Sources/Storage/Generated/Types.swift` and `Sources/Storage/Generated/Client.swift` +are created. This may take a minute or two the first time (resolves and builds the generator +executable). + +- [ ] **Step 5: Inspect the generated `bucketSchema` type** + +```bash +grep -n "struct bucketSchema" -A 30 Sources/Storage/Generated/Types.swift +``` + +Confirm it has `id: Swift.String`, `name: Swift.String`, and a property for the JSON `public` +field (escaped somehow, e.g. `` `public`: Swift.Bool? `` or `_public: Swift.Bool?` — note the +exact spelling found here; Task 6 depends on it). Also note the exact spelling of the +`file_size_limit` property (should be a plain `Swift.Int?`, not an enum, since the response +schema declares it as `type: ["null", "integer"]`). + +- [ ] **Step 6: Verify the package builds** + +```bash +swift build --target Storage +``` + +Expected: builds successfully (the generated code isn't used anywhere yet, so this just proves +the new dependency and generated sources compile standalone). + +- [ ] **Step 7: Commit** + +```bash +git add tools/openapi-generator scripts/generate-storage-openapi.sh Package.swift \ + Package.resolved Sources/Storage/Generated +git commit -m "feat(storage): generate OpenAPI client from vendored spec" +``` + +--- + +### Task 3: `StorageOpenAPITransport` + unit tests + +**Files:** +- Create: `Sources/Storage/OpenAPI/StorageOpenAPITransport.swift` +- Test: `Tests/StorageTests/StorageOpenAPITransportTests.swift` + +**Interfaces:** +- Consumes: nothing from earlier tasks except the `OpenAPIRuntime` module (Task 2). +- Produces: `struct StorageOpenAPITransport: ClientTransport` with + `init(execute: @escaping @Sendable (Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse)`. + Consumed by Task 4. + +- [ ] **Step 1: Write the failing test** + +Create `Tests/StorageTests/StorageOpenAPITransportTests.swift`: + +```swift +import HTTPTypes +import OpenAPIRuntime +import XCTest + +@testable import Storage + +final class StorageOpenAPITransportTests: XCTestCase { + func testSendJoinsBaseURLAndPathAndQuery() async throws { + var capturedRequest: Helpers.HTTPRequest? + + let transport = StorageOpenAPITransport(execute: { request in + capturedRequest = request + return Helpers.HTTPResponse( + data: Data(#"{"ok":true}"#.utf8), + response: HTTPURLResponse( + url: URL(string: "http://localhost/storage/v1/bucket")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + ) + }) + + let (response, body) = try await transport.send( + HTTPTypes.HTTPRequest(method: .get, scheme: nil, authority: nil, path: "/bucket?limit=10"), + body: nil, + baseURL: URL(string: "http://localhost/storage/v1")!, + operationID: "bucketList" + ) + + XCTAssertEqual(capturedRequest?.url.absoluteString, "http://localhost/storage/v1/bucket?limit=10") + XCTAssertEqual(capturedRequest?.method, .get) + XCTAssertEqual(response.status.code, 200) + let data = try await Data(collecting: body ?? HTTPBody(""), upTo: .max) + XCTAssertEqual(data, Data(#"{"ok":true}"#.utf8)) + } + + func testSendPropagatesBodyBytes() async throws { + var capturedRequest: Helpers.HTTPRequest? + + let transport = StorageOpenAPITransport(execute: { request in + capturedRequest = request + return Helpers.HTTPResponse( + data: Data(), + response: HTTPURLResponse( + url: URL(string: "http://localhost/storage/v1/bucket")!, + statusCode: 200, + httpVersion: nil, + headerFields: nil + )! + ) + }) + + _ = try await transport.send( + HTTPTypes.HTTPRequest(method: .post, scheme: nil, authority: nil, path: "/bucket"), + body: HTTPBody(#"{"name":"avatars"}"#), + baseURL: URL(string: "http://localhost/storage/v1")!, + operationID: "bucketCreate" + ) + + XCTAssertEqual(capturedRequest?.body, Data(#"{"name":"avatars"}"#.utf8)) + } +} +``` + +- [ ] **Step 2: Run test to verify it fails** + +```bash +swift test --filter StorageOpenAPITransportTests +``` + +Expected: FAIL — `StorageOpenAPITransport` does not exist yet (compile error). + +- [ ] **Step 3: Write the implementation** + +Create `Sources/Storage/OpenAPI/StorageOpenAPITransport.swift`: + +```swift +import Foundation +import HTTPTypes +import OpenAPIRuntime + +#if canImport(FoundationNetworking) + import FoundationNetworking +#endif + +/// Bridges the generated OpenAPI client's HTTP layer onto ``StorageApi``'s existing +/// `execute(_:)` pipeline, so header merging, ``StorageError`` decoding, and the injectable +/// ``StorageHTTPSession`` all keep working unchanged for OpenAPI-routed requests. +struct StorageOpenAPITransport: ClientTransport { + var execute: @Sendable (Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse + + func send( + _ request: HTTPTypes.HTTPRequest, + body: OpenAPIRuntime.HTTPBody?, + baseURL: URL, + operationID: String + ) async throws -> (HTTPTypes.HTTPResponse, OpenAPIRuntime.HTTPBody?) { + let requestTarget = request.path ?? "" + let pathAndQuery = requestTarget.split(separator: "?", maxSplits: 1) + let path = String(pathAndQuery.first ?? "") + let query = pathAndQuery.count > 1 ? String(pathAndQuery[1]) : nil + + guard var components = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) else { + throw StorageOpenAPITransportError.invalidBaseURL(baseURL) + } + components.percentEncodedPath += path + components.percentEncodedQuery = query + + guard let url = components.url else { + throw StorageOpenAPITransportError.invalidRequestURL(path: requestTarget, baseURL: baseURL) + } + + let requestBody: Data? + if let body { + requestBody = try await Data(collecting: body, upTo: .max) + } else { + requestBody = nil + } + + var headers = HTTPFields() + for field in request.headerFields { + headers[field.name] = field.value + } + + let helpersRequest = Helpers.HTTPRequest( + url: url, + method: request.method, + headers: headers, + body: requestBody + ) + + let response = try await execute(helpersRequest) + + let responseBody: OpenAPIRuntime.HTTPBody? = + response.data.isEmpty ? nil : OpenAPIRuntime.HTTPBody(response.data) + + return ( + HTTPTypes.HTTPResponse(status: .init(code: response.statusCode)), + responseBody + ) + } +} + +enum StorageOpenAPITransportError: Error { + case invalidBaseURL(URL) + case invalidRequestURL(path: String, baseURL: URL) +} +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +swift test --filter StorageOpenAPITransportTests +``` + +Expected: PASS (2 tests). + +- [ ] **Step 5: Commit** + +```bash +git add Sources/Storage/OpenAPI/StorageOpenAPITransport.swift \ + Tests/StorageTests/StorageOpenAPITransportTests.swift +git commit -m "feat(storage): add StorageOpenAPITransport bridging generated client to execute()" +``` + +--- + +### Task 4: Wire the generated `Client` into `StorageApi` + +**Files:** +- Modify: `Sources/Storage/StorageApi.swift` + +**Interfaces:** +- Consumes: `StorageOpenAPITransport` (Task 3), generated `Client` (Task 2). +- Produces: `StorageApi.openAPIClient: Client` (internal, accessible to subclasses + `StorageBucketApi`/`StorageFileApi` in later tasks) and + `StorageApi.executeRequest(_:headers:http:decoder:) async throws -> Helpers.HTTPResponse` + (a `static` helper factoring out `execute(_:)`'s logic, reused by both `execute(_:)` itself and + the transport closure — no self-capture-before-init issue). + +- [ ] **Step 1: Write the failing test** + +Add to `Tests/StorageTests/StorageBucketAPITests.swift` (after `tearDown`, before +`testURLConstruction`): + +```swift + func testOpenAPIClientUsesConfiguredBaseURLAndHeaders() async throws { + Mock( + url: url.appendingPathComponent("bucket/bucket123"), + statusCode: 200, + data: [ + .get: Data( + """ + { + "id": "bucket123", + "name": "test-bucket", + "owner": "owner123", + "public": false, + "created_at": "2024-01-01T00:00:00.000Z", + "updated_at": "2024-01-01T00:00:00.000Z" + } + """.utf8 + ) + ] + ) + .register() + + let output = try await storage.openAPIClient.bucketGet( + .init(path: .init(bucketId: "bucket123")) + ) + guard case .ok(let okResponse) = output, case .json(let bucket) = okResponse.body else { + return XCTFail("expected .ok(.json) response") + } + XCTAssertEqual(bucket.id, "bucket123") + } +``` + +`storage` here is the `SupabaseStorageClient` built in `setUp()`. `SupabaseStorageClient` inherits +from `StorageBucketApi` which inherits from `StorageApi`, so `storage.openAPIClient` must resolve +once this task adds the property. + +- [ ] **Step 2: Run test to verify it fails** + +```bash +swift test --filter StorageBucketAPITests/testOpenAPIClientUsesConfiguredBaseURLAndHeaders +``` + +Expected: FAIL — `value of type 'SupabaseStorageClient' has no member 'openAPIClient'`. + +- [ ] **Step 3: Implement** + +In `Sources/Storage/StorageApi.swift`, add the import at the top (after `import HTTPTypes` at +line 3): + +```swift +import OpenAPIRuntime +``` + +Replace the `execute(_:)` method (`StorageApi.swift:107-127`) with a static helper plus a thin +instance wrapper, and add the `openAPIClient` property. The class becomes: + +```swift +public class StorageApi: @unchecked Sendable { + /// The configuration used to initialize this client instance. + public let configuration: StorageClientConfiguration + + /// The generated OpenAPI client for the Storage HTTP API. Internal implementation detail — + /// ``StorageBucketApi``/``StorageFileApi`` use this instead of hand-building requests. + let openAPIClient: Client + + private struct MutableState { + var headers: [String: String] + } + + private let mutableState: LockIsolated + private let http: any HTTPClientType + + public init(configuration: StorageClientConfiguration) { + var configuration = configuration + if configuration.headers["X-Client-Info"] == nil { + configuration.headers["X-Client-Info"] = "storage-swift/\(version)" + } + + if configuration.useNewHostname == true { + guard + var components = URLComponents(url: configuration.url, resolvingAgainstBaseURL: false), + let host = components.host + else { + fatalError("Client initialized with invalid URL: \(configuration.url)") + } + + let regex = try! NSRegularExpression(pattern: "supabase.(co|in|red)$") + + let isSupabaseHost = + regex.firstMatch(in: host, range: NSRange(location: 0, length: host.utf16.count)) != nil + + if isSupabaseHost, !host.contains("storage.supabase.") { + components.host = host.replacingOccurrences(of: "supabase.", with: "storage.supabase.") + } + + configuration.url = components.url! + } + + let initialHeaders = configuration.headers + self.configuration = configuration + self.mutableState = LockIsolated(MutableState(headers: initialHeaders)) + + var interceptors: [any HTTPClientInterceptor] = [] + if let logger = configuration.logger { + interceptors.append(LoggerInterceptor(logger: logger)) + } + + http = HTTPClient( + fetch: configuration.session.fetch, + interceptors: interceptors + ) + + let mutableStateRef = mutableState + let httpRef = http + let decoder = configuration.decoder + openAPIClient = Client( + serverURL: configuration.url, + transport: StorageOpenAPITransport(execute: { request in + try await Self.executeRequest( + request, headers: mutableStateRef.headers, http: httpRef, decoder: decoder) + }) + ) + } + + @discardableResult + public func setHeader(_ value: String, forKey key: String) -> Self { + mutableState.withValue { $0.headers[key.lowercased()] = value } + return self + } + + private static func executeRequest( + _ request: Helpers.HTTPRequest, + headers: [String: String], + http: any HTTPClientType, + decoder: JSONDecoder + ) async throws -> Helpers.HTTPResponse { + var request = request + request.headers = HTTPFields(headers).merging(with: request.headers) + + let response = try await http.send(request) + + guard (200..<300).contains(response.statusCode) else { + if let error = try? decoder.decode(StorageError.self, from: response.data) { + throw error + } + throw HTTPError(data: response.data, response: response.underlyingResponse) + } + + return response + } + + @discardableResult + func execute(_ request: Helpers.HTTPRequest) async throws -> Helpers.HTTPResponse { + try await Self.executeRequest( + request, headers: mutableState.headers, http: http, decoder: configuration.decoder) + } +} +``` + +Leave the `extension Helpers.HTTPRequest { init(url:method:query:formData:options:headers:) }` at +the bottom of the file (`StorageApi.swift:130-154`) unchanged. + +- [ ] **Step 4: Run test to verify it passes** + +```bash +swift test --filter StorageBucketAPITests +``` + +Expected: PASS (all existing `StorageBucketAPITests` tests still pass, plus the new +`testOpenAPIClientUsesConfiguredBaseURLAndHeaders`). + +- [ ] **Step 5: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageApi.swift Tests/StorageTests/StorageBucketAPITests.swift +git add Sources/Storage/StorageApi.swift Tests/StorageTests/StorageBucketAPITests.swift +git commit -m "feat(storage): wire generated OpenAPI client onto StorageApi" +``` + +--- + +### Task 5: Migrate `listBuckets()` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:37-45` + +**Interfaces:** +- Consumes: `openAPIClient.bucketList(_:) async throws -> Operations.bucketList.Output` (Task 4), + `Components.Schemas.bucketSchema` (Task 2). +- Produces: `Bucket(fromGenerated:)` — a `Bucket` initializer used by this and later bucket-read + migrations (Task 6). + +- [ ] **Step 1: Confirm the existing test still describes the desired behavior** + +`Tests/StorageTests/StorageBucketAPITests.swift:148` (`testListBuckets`) already asserts the +curl-equivalent request and the decoded result. No new test needed — this is a refactor with an +existing regression test as the acceptance gate. + +```bash +swift test --filter StorageBucketAPITests/testListBuckets +``` + +Expected: PASS (current hand-written implementation). + +- [ ] **Step 2: Add the `Bucket` ↔ generated-schema bridge** + +In `Sources/Storage/StorageTypes.swift`, immediately after the `Bucket` struct's `CodingKeys` (after +`Types.swift:649`, before the `// MARK: - StorageByteCount` comment), add: + +```swift +extension Bucket { + init(fromGenerated bucket: Components.Schemas.bucketSchema) { + self.init( + id: bucket.id, + name: bucket.name, + owner: bucket.owner ?? "", + isPublic: bucket.`public` ?? false, + createdAt: bucket.created_at.flatMap { ISO8601DateFormatter().date(from: $0) } ?? Date(timeIntervalSince1970: 0), + updatedAt: bucket.updated_at.flatMap { ISO8601DateFormatter().date(from: $0) } ?? Date(timeIntervalSince1970: 0), + allowedMimeTypes: bucket.allowed_mime_types, + fileSizeLimit: bucket.file_size_limit.map(Int64.init) + ) + } +} +``` + +> Before writing this, run +> `grep -n "struct bucketSchema" -A 30 Sources/Storage/Generated/Types.swift` (from Task 2 Step +> 5) and use the *exact* property names/types found there — property names for `created_at`, +> `updated_at`, `owner`, `public`, `allowed_mime_types`, `file_size_limit` may be camelCased by +> the generator's default naming strategy (e.g. `createdAt` instead of `created_at`), and the +> reserved word `public` may be escaped as `` `public` `` or `_public`. Adjust the field +> references above to match what's actually generated; the compiler will flag any mismatch with +> a clear "has no member" error naming the real property. + +- [ ] **Step 3: Migrate `listBuckets()`** + +Replace `Sources/Storage/StorageBucketApi.swift:37-45`: + +```swift + public func listBuckets() async throws -> [Bucket] { + let output = try await openAPIClient.bucketList(.init()) + guard case .ok(let response) = output, case .json(let buckets) = response.body else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + return buckets.map(Bucket.init(fromGenerated:)) + } +``` + +- [ ] **Step 4: Run test to verify it passes** + +```bash +swift test --filter StorageBucketAPITests/testListBuckets +``` + +Expected: PASS — same curl snapshot as before (GET request, no body, so no encoding differences +apply here). + +- [ ] **Step 5: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageTypes.swift Sources/Storage/StorageBucketApi.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageTypes.swift Sources/Storage/StorageBucketApi.swift +git commit -m "refactor(storage): migrate listBuckets() to generated OpenAPI client" +``` + +--- + +### Task 6: Migrate `getBucket(_:)` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:52-60` + +**Interfaces:** +- Consumes: `openAPIClient.bucketGet(_:) async throws -> Operations.bucketGet.Output`, + `Bucket.init(fromGenerated:)` (Task 5). + +- [ ] **Step 1: Confirm the existing test describes the desired behavior** + +```bash +swift test --filter StorageBucketAPITests/testGetBucket +``` + +Expected: PASS (current implementation). + +- [ ] **Step 2: Migrate** + +Replace `Sources/Storage/StorageBucketApi.swift:52-60`: + +```swift + public func getBucket(_ id: String) async throws -> Bucket { + let output = try await openAPIClient.bucketGet(.init(path: .init(bucketId: id))) + guard case .ok(let response) = output, case .json(let bucket) = response.body else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + return Bucket(fromGenerated: bucket) + } +``` + +- [ ] **Step 3: Run test to verify it passes** + +```bash +swift test --filter StorageBucketAPITests/testGetBucket +``` + +Expected: PASS, same curl snapshot as before. + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageBucketApi.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageBucketApi.swift +git commit -m "refactor(storage): migrate getBucket(_:) to generated OpenAPI client" +``` + +--- + +### Task 7: Migrate `deleteBucket(_:)` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:160-167` + +**Interfaces:** +- Consumes: `openAPIClient.bucketDelete(_:) async throws -> Operations.bucketDelete.Output`. + +- [ ] **Step 1: Confirm the existing test describes the desired behavior** + +```bash +swift test --filter StorageBucketAPITests/testDeleteBucket +``` + +Expected: PASS. + +- [ ] **Step 2: Migrate** + +`deleteBucket` doesn't decode a body today (`async throws`, no return value), so the migration +only needs to confirm success: + +```swift + public func deleteBucket(_ id: String) async throws { + let output = try await openAPIClient.bucketDelete(.init(path: .init(bucketId: id))) + guard case .ok = output else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + } +``` + +- [ ] **Step 3: Run test to verify it passes** + +```bash +swift test --filter StorageBucketAPITests/testDeleteBucket +``` + +Expected: PASS, same curl snapshot as before (DELETE, no body). + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageBucketApi.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageBucketApi.swift +git commit -m "refactor(storage): migrate deleteBucket(_:) to generated OpenAPI client" +``` + +--- + +### Task 8: Migrate `emptyBucket(_:)` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:143-150` + +**Interfaces:** +- Consumes: `openAPIClient.bucketEmpty(_:) async throws -> Operations.bucketEmpty.Output`. + +- [ ] **Step 1: Confirm the existing test describes the desired behavior** + +```bash +swift test --filter StorageBucketAPITests/testEmptyBucket +``` + +Expected: PASS. + +- [ ] **Step 2: Migrate** + +```swift + public func emptyBucket(_ id: String) async throws { + let output = try await openAPIClient.bucketEmpty(.init(path: .init(bucketId: id))) + guard case .ok = output else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + } +``` + +- [ ] **Step 3: Run test to verify it passes** + +```bash +swift test --filter StorageBucketAPITests/testEmptyBucket +``` + +Expected: PASS, same curl snapshot as before (POST, no body). + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageBucketApi.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageBucketApi.swift +git commit -m "refactor(storage): migrate emptyBucket(_:) to generated OpenAPI client" +``` + +--- + +### Task 9: Migrate `createBucket(_:options:)` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:70-103` +- Test: `Tests/StorageTests/StorageBucketAPITests.swift:184-222,308-383` (re-record snapshots) + +**Interfaces:** +- Consumes: `openAPIClient.bucketCreate(_:) async throws -> Operations.bucketCreate.Output`, + `Operations.bucketCreate.Input.Body.jsonPayload` (the request body's anyOf `file_size_limit` + needs inspection — see Step 1). + +- [ ] **Step 1: Inspect the generated request body type** + +```bash +grep -n "enum bucketCreate" -A 5 Sources/Storage/Generated/Types.swift +grep -n "struct Body" -A 20 Sources/Storage/Generated/Types.swift | grep -A 20 "bucketCreate" +grep -n "jsonPayload" -A 15 Sources/Storage/Generated/Types.swift | head -60 +``` + +Find the exact synthesized type for the request body (expected: +`Operations.bucketCreate.Input.Body.jsonPayload` with properties `name: Swift.String`, +`id: Swift.String?`, `public: Swift.Bool?` (name possibly escaped), `file_size_limit` typed as an +enum wrapping `Swift.Int` and `Swift.String` cases (since the spec declares +`anyOf: [integer, string]` for this field) — note the exact case names (e.g. `.case1`/`.case2`, +or generator-synthesized names). `BucketOptions.fileSizeLimit` (`Types.swift:965`) is already +always a `String?` by the time it reaches `StorageBucketApi`, so only the **string** case of +that enum is ever needed here. + +- [ ] **Step 2: Migrate** + +Replace `Sources/Storage/StorageBucketApi.swift:70-103` (keep the `BucketParameters` struct at +lines 62-68 removed as part of Task 11's cleanup, not here — other call sites still use it until +Task 10 migrates `updateBucket`): + +```swift + public func createBucket(_ id: String, options: BucketOptions = BucketOptions(isPublic: false)) + async throws + { + let output = try await openAPIClient.bucketCreate( + .init( + body: .json( + .init( + name: id, + id: id, + `public`: options.isPublic, + file_size_limit: options.fileSizeLimit.map { .case2($0) }, + allowed_mime_types: options.allowedMimeTypes + ) + ) + ) + ) + guard case .ok = output else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + } +``` + +> Use the exact property names and the exact `file_size_limit` enum case name found in Step 1. +> If the generated property for `public` is escaped differently (e.g. `_public`), or the +> enum case for the string variant isn't `.case2`, the compiler error at this call site will name +> the actual member — fix the reference to match. + +- [ ] **Step 3: Run the existing tests and re-record snapshots** + +```bash +swift test --filter StorageBucketAPITests/testCreateBucket +swift test --filter StorageBucketAPITests/testCreateBucketWithFileSizeLimit +swift test --filter StorageBucketAPITests/testCreateBucketWithHumanReadableFileSizeLimit +``` + +Expected: these may FAIL on the `--data` line of the snapshot if the generated client's JSON +encoder orders keys differently than `configuration.encoder`'s `.sortedKeys`. If so: + +1. Confirm the failure is *only* a key-order difference (same keys, same values, same total + `Content-Length`) — inspect the diff `InlineSnapshotTesting` prints. +2. Re-record by running with `--record` (this repo's snapshot tests use + `InlineSnapshotTesting`/`swift-snapshot-testing`; if failures show a suggested replacement + literal, apply it directly to the test file, or run + `SNAPSHOT_TESTING_RECORD=all swift test --filter StorageBucketAPITests/testCreateBucket` and + confirm the tool updates the inline `--data` string in place). +3. Re-run the three tests above to confirm they now pass with the re-recorded snapshot. + +If the `Content-Length` byte count changed, or a key/value is missing or different, STOP — this +means the request body isn't semantically equivalent, and the mapping in Step 2 needs fixing +before continuing. + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageBucketApi.swift Tests/StorageTests/StorageBucketAPITests.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageBucketApi.swift Tests/StorageTests/StorageBucketAPITests.swift +git commit -m "refactor(storage): migrate createBucket(_:options:) to generated OpenAPI client" +``` + +--- + +### Task 10: Migrate `updateBucket(_:options:)` + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift:118-134` +- Test: `Tests/StorageTests/StorageBucketAPITests.swift:224-262` (re-record snapshot if needed) + +**Interfaces:** +- Consumes: `openAPIClient.bucketUpdate(_:) async throws -> Operations.bucketUpdate.Output`. + +- [ ] **Step 1: Inspect the generated request body type** + +```bash +grep -n "enum bucketUpdate" -A 5 Sources/Storage/Generated/Types.swift +grep -n "jsonPayload" -A 15 Sources/Storage/Generated/Types.swift | head -80 +``` + +Same shape as `bucketCreate`'s body minus `name`/`id`/`type` (per the spec, `bucketUpdate`'s body +only has `public`, `file_size_limit`, `allowed_mime_types`, all optional). + +- [ ] **Step 2: Migrate** + +Replace `Sources/Storage/StorageBucketApi.swift:118-134`: + +```swift + public func updateBucket(_ id: String, options: BucketOptions) async throws { + let output = try await openAPIClient.bucketUpdate( + .init( + path: .init(bucketId: id), + body: .json( + .init( + `public`: options.isPublic, + file_size_limit: options.fileSizeLimit.map { .case2($0) }, + allowed_mime_types: options.allowedMimeTypes + ) + ) + ) + ) + guard case .ok = output else { + throw StorageError(statusCode: nil, message: "Unexpected response from Storage API") + } + } +``` + +> As in Task 9, confirm the exact property/case names against +> `Sources/Storage/Generated/Types.swift` and adjust if the compiler flags a mismatch. + +- [ ] **Step 3: Run the existing test and re-record the snapshot if needed** + +```bash +swift test --filter StorageBucketAPITests/testUpdateBucket +``` + +Same procedure as Task 9 Step 3 if the `--data` snapshot fails on key order only. + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh Sources/Storage/StorageBucketApi.swift Tests/StorageTests/StorageBucketAPITests.swift +swift test --filter StorageBucketAPITests +git add Sources/Storage/StorageBucketApi.swift Tests/StorageTests/StorageBucketAPITests.swift +git commit -m "refactor(storage): migrate updateBucket(_:options:) to generated OpenAPI client" +``` + +--- + +### Task 11: Remove dead code and run the full suite + +**Files:** +- Modify: `Sources/Storage/StorageBucketApi.swift` (remove the now-unused `BucketParameters` + struct) + +**Interfaces:** +- None — this is cleanup only. + +- [ ] **Step 1: Confirm `BucketParameters` is unused** + +```bash +grep -rn "BucketParameters" Sources/Storage Tests/StorageTests +``` + +Expected: only the struct definition itself remains (all six bucket methods were migrated in +Tasks 5–10). + +- [ ] **Step 2: Remove it** + +Delete the `BucketParameters` struct definition from `Sources/Storage/StorageBucketApi.swift` +(originally at lines 62-68). + +- [ ] **Step 3: Run the full test suite** + +```bash +make PLATFORM=IOS XCODEBUILD_ARGUMENT=test xcodebuild +``` + +Expected: all tests pass, including every test in `StorageBucketAPITests`, +`StorageOpenAPITransportTests`, and the rest of the existing suite (`StorageFileAPITests`, +`SupabaseStorageTests`, etc. — untouched by this plan, must show no regressions). + +- [ ] **Step 4: Format everything touched by this plan** + +```bash +./scripts/format.sh +git status +``` + +Expected: only files this plan touched show as modified (`Sources/Storage/*.swift`, +`Sources/Storage/OpenAPI/*`, `Sources/Storage/Generated/*`, `Tests/StorageTests/*`, +`Package.swift`, `Package.resolved`, `tools/openapi-generator/*`, `scripts/*`). If `format.sh` +reformats unrelated files, revert those with `git checkout -- ` — CI only format-checks +changed files. + +- [ ] **Step 5: Commit** + +```bash +git add -u +git commit -m "refactor(storage): remove BucketParameters, superseded by generated OpenAPI client" +``` + +--- + +## What's next + +Milestones 3 (JSON-only `StorageFileApi` operations) and 4 (multipart upload/update, binary +download) get their own follow-up plans once this one lands — the exact generated `Operations.*` +signatures for those endpoints should be verified against whatever spec is vendored at that time +(ideally the merged `master` version of supabase/storage#1215, per the design doc's rollout). diff --git a/docs/superpowers/plans/2026-07-08-storage-openapi-upload.md b/docs/superpowers/plans/2026-07-08-storage-openapi-upload.md new file mode 100644 index 000000000..36f0ad68b --- /dev/null +++ b/docs/superpowers/plans/2026-07-08-storage-openapi-upload.md @@ -0,0 +1,565 @@ +# Storage OpenAPI Upload Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Migrate `upload`/`update`/`uploadToSignedURL` (all multipart) from hand-built +`MultipartFormData` requests to the generated OpenAPI client, with zero public API or behavior +change. + +**Architecture:** Patch the vendored spec to declare a `multipart/form-data` requestBody (it's +currently entirely undeclared) for the three affected operations, regenerate, then replace the +two private helpers (`_uploadOrUpdate`, `_uploadToSignedURL`) that currently build +`MultipartFormData` by hand with calls to the generated client using its typed +`MultipartBody`/`MultipartPart` types. + +**Tech Stack:** Swift 6.1, `swift-openapi-generator`'s multipart support (confirmed working via a +throwaway spike — see design doc), existing `StorageOpenAPITransport`/error-handling +infrastructure from the bucket API migration (unchanged, already handles arbitrary request +bodies). + +**Design doc:** `docs/superpowers/specs/2026-07-08-storage-openapi-upload-design.md` + +## Global Constraints + +- No public API signature, type, or observable HTTP behavior may change. Every existing test in + `Tests/StorageTests/StorageFileAPITests.swift` covering `upload`/`update`/`uploadToSignedURL` + must keep passing after each task, **except** where a test asserts the exact serialized + multipart body bytes — those may need re-recording (same discipline as the bucket API + migration's JSON-body snapshots: only if the change is a semantically-inert reordering/framing + difference, never a real content change, and confirmed byte-for-byte reasoned through before + accepting). +- Run `./scripts/format.sh` before every commit that touches hand-written Swift files (it already + excludes `Sources/Storage/Generated`). +- Platforms: iOS 16+, macOS 13+, tvOS 16+, watchOS 9+, visionOS 1+ (per `AGENTS.md`). +- The current `_getFinalPath(_:)` helper (`Sources/Storage/StorageFileApi.swift:979-981`) returns + `"\(bucketId)/\(path)"` — a single combined path segment used when building a raw URL string. + The generated `Operations..Input.Path` splits this into two separate fields, `bucketName` + and `wildcard`. When migrating, pass `bucketId` and the **cleaned path alone** (not the + `_getFinalPath`-combined string) to `bucketName`/`wildcard` respectively — combining them again + would duplicate the bucket ID in the URL. + +--- + +### Task 1: Patch the spec to declare multipart request bodies, regenerate + +**Files:** +- Modify: `Sources/Storage/OpenAPI/openapi.json` +- Modify: `docs/superpowers/storage-openapi-upstream-issues.md` +- Modify (regenerated output): `Sources/Storage/Generated/Types.swift`, + `Sources/Storage/Generated/Client.swift` + +**Interfaces:** +- Produces: `Operations.objectUpload.Input.Body`, `Operations.objectUploadUpdate.Input.Body`, + `Operations.objectUploadSigned.Input.Body` — each an enum with case + `.multipartForm(OpenAPIRuntime.MultipartBody<...multipartFormPayload>)`, and + `...multipartFormPayload` an enum with cases `.cacheControl(MultipartPart)`, + `.metadata(MultipartPart)`, `.file(MultipartPart)`, + `.undocumented(MultipartRawPart)`. Each `*Payload` struct has a single `body: HTTPBody` field. + Consumed by Tasks 2-3. + +- [ ] **Step 1: Add the multipart requestBody to the three operations** + +Run this script to patch `Sources/Storage/OpenAPI/openapi.json` in place: + +```bash +python3 -c " +import json + +path = 'Sources/Storage/OpenAPI/openapi.json' +d = json.load(open(path)) + +multipart_schema = { + 'type': 'object', + 'properties': { + 'cacheControl': {'type': 'string'}, + 'metadata': {'type': 'string'}, + 'file': {'type': 'string', 'format': 'binary'} + }, + 'required': ['cacheControl', 'file'] +} + +request_body = { + 'required': True, + 'content': { + 'multipart/form-data': { + 'schema': multipart_schema + } + } +} + +targets = [ + ('/object/{bucketName}/{wildcard}', 'post'), # objectUpload + ('/object/{bucketName}/{wildcard}', 'put'), # objectUploadUpdate + ('/object/upload/sign/{bucketName}/{wildcard}', 'put'), # objectUploadSigned +] + +for path_key, method in targets: + op = d['paths'][path_key][method] + assert 'requestBody' not in op, f'{op[\"operationId\"]} already has a requestBody' + op['requestBody'] = request_body + print('patched', op['operationId']) + +with open(path, 'w') as f: + json.dump(d, f, separators=(',', ':')) +" +``` + +Expected output: +``` +patched objectUpload +patched objectUploadUpdate +patched objectUploadSigned +``` + +- [ ] **Step 2: Regenerate** + +```bash +./scripts/generate-storage-openapi.sh +``` + +Expected: completes with no errors (the throwaway spike documented in the design doc already +confirmed this exact multipart shape generates cleanly — if you see an error here, something +about the *current* committed spec differs from what the spike used, stop and investigate rather +than guessing). + +- [ ] **Step 3: Confirm the generated multipart types exist and inspect the file part's shape** + +```bash +grep -n "enum objectUpload {" -A 60 Sources/Storage/Generated/Types.swift | grep -n "multipartFormPayload\|case cacheControl\|case metadata\|case file\|case undocumented" +``` + +Expected: shows the four cases (`cacheControl`, `metadata`, `file`, `undocumented`) under +`Operations.objectUpload.Input.Body.multipartFormPayload`. + +Then check whether the generated `filePayload` struct or the `MultipartPart`/`MultipartBody` +machinery expose any way to set a per-part `Content-Type` header (the current hand-written +`MultipartFormData.append(_:withName:fileName:mimeType:)` sets one) — grep the generated file and +`OpenAPIRuntime`'s `MultipartPart`/`MultipartRawPart` types (find them via +`swift build --show-bin-path` then locate `.build/checkouts/swift-openapi-runtime/Sources/OpenAPIRuntime/Multipart/`) +for `headerFields`/`contentType`. Record what you find — Task 2 depends on this: if the typed +`.file(MultipartPart)` case has no way to set Content-Type, Task 2 must build the +file part via the `.undocumented(MultipartRawPart(headerFields:body:))` case instead, which does +expose full `HTTPFields` control. + +- [ ] **Step 4: Verify the package builds** + +```bash +swift build --target Storage +``` + +Expected: builds successfully (nothing consumes the new generated types yet). + +- [ ] **Step 5: Log the upstream spec gap** + +Add a new entry to `docs/superpowers/storage-openapi-upstream-issues.md` under `## Open`: + +```markdown +- **`objectUpload`/`objectUploadUpdate`/`objectUploadSigned` have no declared request body at + all.** Fastify's multipart handling isn't schema-validated the same way as JSON bodies, so the + storage service's OpenAPI export never documented these operations' `multipart/form-data` + request shape (`cacheControl`, optional `metadata`, and the file itself — currently sent with an + **empty** multipart field name in this SDK's hand-written client, which needed a real field name + to generate a usable typed member). Patched in the vendored copy at + `Sources/Storage/OpenAPI/openapi.json` by adding a `multipart/form-data` requestBody with + `cacheControl`/`metadata`/`file` fields to all three operations — see this repo's git history + for the commit that landed it — not yet reported/fixed upstream. The real fix upstream would + need to name the file field + server-side too (or confirm the server doesn't actually key off the field name, in which case + this SDK's request is already safe to send named without needing a server-side change first — + verify this against server behavior before relying on it for anything beyond documentation). +``` + +- [ ] **Step 6: Commit** + +```bash +git add Sources/Storage/OpenAPI/openapi.json Sources/Storage/Generated \ + docs/superpowers/storage-openapi-upstream-issues.md +git commit -m "feat(storage): add multipart requestBody to upload spec operations" +``` + +--- + +### Task 2: Migrate `upload`/`update` (`_uploadOrUpdate`) + +**Files:** +- Modify: `Sources/Storage/StorageFileApi.swift:139-179` (the `_uploadOrUpdate` helper) +- Test: `Tests/StorageTests/StorageFileAPITests.swift` (re-record multipart snapshots if needed) + +**Interfaces:** +- Consumes: `openAPIClient.objectUpload(_:)` / `.objectUploadUpdate(_:)`, both + `async throws -> Operations..Output`; `Operations..Input.Body.multipartForm(...)` + (Task 1). +- Produces: `_uploadOrUpdate` keeps its exact existing signature + (`method:path:file:options: async throws -> FileUploadResponse`) — no change visible to + `upload(_:data:)`, `upload(_:fileURL:)`, `update(_:data:)`, `update(_:fileURL:)`, which all call + it unchanged. + +- [ ] **Step 1: Confirm the existing tests describe current behavior** + +```bash +swift test --filter StorageFileAPITests +``` + +Expected: PASS (current hand-written implementation). Note which specific tests cover +`upload`/`update` (grep for `func test.*[Uu]pload` and `func test.*[Uu]pdate` in the test file) — +these are today's acceptance gate. + +- [ ] **Step 2: Build the multipart body** + +Replace the body-construction portion of `_uploadOrUpdate` +(`Sources/Storage/StorageFileApi.swift:139-179`). The exact shape depends on Task 1 Step 3's +finding about per-part Content-Type support. If the typed `.file(MultipartPart)` +case has no Content-Type control, build the file part via `.undocumented(MultipartRawPart(...))` +instead, as shown here (adjust to use the typed case if Task 1 found it does support Content-Type +— in that case use `.file(.init(payload: .init(body: fileBody), filename: path.fileName))` and +drop the manual `Content-Type` header line since it'd be redundant): + +```swift + private func _uploadOrUpdate( + method: HTTPTypes.HTTPRequest.Method, + path: String, + file: FileUpload, + options: FileOptions? + ) async throws -> FileUploadResponse { + let options = options ?? defaultFileOptions + + let cleanPath = _removeEmptyFolders(path) + + var parts: [Operations.objectUpload.Input.Body.multipartFormPayload] = [ + .cacheControl(.init(payload: .init(body: HTTPBody(options.cacheControl)))) + ] + + if let metadata = options.metadata { + parts.append( + .metadata(.init(payload: .init(body: HTTPBody(encodeMetadata(metadata))))) + ) + } + + let mimeType = options.contentType ?? mimeType(forPathExtension: path.pathExtension) + var fileHeaders = HTTPFields() + fileHeaders[.contentType] = mimeType + fileHeaders[.contentDisposition] = + #"form-data; name="file"; filename="\#(path.fileName)""# + + let fileBody: HTTPBody + switch file { + case .data(let data): + fileBody = HTTPBody(data) + case .url(let url): + fileBody = HTTPBody(try Data(contentsOf: url)) + } + + parts.append(.undocumented(MultipartRawPart(headerFields: fileHeaders, body: fileBody))) + + var headers = options.headers.map { HTTPFields($0) } ?? HTTPFields() + if method == .post { + headers[.xUpsert] = "\(options.upsert)" + } + headers[.duplex] = options.duplex + + let input = Operations.objectUpload.Input( + path: .init(bucketName: bucketId, wildcard: cleanPath), + headers: .init(), + body: .multipartForm(.init(parts)) + ) + + // continued in Step 3 — the output handling and the PUT (update) call site + } +``` + +> `Data(contentsOf:)` for the `.url` case reads the whole file into memory, same as the current +> `MultipartFormData.append(url, withName:)` path effectively does today via +> `formData.encode()` returning a single `Data` blob (confirm this in +> `Sources/Storage/MultipartFormData.swift` before assuming — if the current implementation +> genuinely streams from disk without loading it all into memory, and this migration would +> regress that, STOP and report it as a concern rather than silently accepting a memory-usage +> regression for large files). +> +> `options.headers` (extra custom headers) aren't expressible via the generated `Input.headers` +> struct (which only has `accept`) — they need to keep flowing through the transport's header +> merging (`StorageApi.executeRequestWithoutStatusCheck`, already shared with the bucket API +> migration), same as `x-upsert`/`Duplex` do today. Since the generated `Client` methods don't +> accept arbitrary extra headers directly, check whether `StorageApi.setHeader(_:forKey:)` +> (the per-instance mutable header bag already merged into every OpenAPI-routed request by +> `StorageOpenAPITransport`) is the right mechanism, or whether per-call custom headers +> (`options.headers`) need a different plumbing path — inspect how the bucket API migration +> handled per-instance headers (it didn't need per-CALL headers, only per-instance ones, so this +> is new territory Task 2 must resolve. If there's no clean way to pass per-call headers through +> the generated `Client`, that's a real gap — report it rather than silently dropping +> `options.headers` support). + +- [ ] **Step 3: Call the operation and handle the two methods (POST=upload, PUT=update)** + +`objectUpload` (POST) and `objectUploadUpdate` (PUT) are two different generated operations even +though today's hand-written code shares one method via an `HTTPTypes.HTTPRequest.Method` +parameter. Branch on `method` to call the right one, keeping the rest identical: + +```swift + let output: Operations.objectUpload.Output + if method == .post { + output = try await openAPIClient.objectUpload(input) + } else { + let updateOutput = try await openAPIClient.objectUploadUpdate( + .init(path: input.path, headers: .init(), body: input.body) + ) + // objectUploadUpdate.Output and objectUpload.Output are distinct generated types with the + // same shape (.ok/.forbidden/.clientError/.undocumented, same {Id?, Key} success body) — + // map updateOutput into the same handling below by writing the switch twice, once per + // concrete Output type (mirrors how the bucket API migration's six methods each got their + // own switch over their own concrete Output type — there is no shared protocol to unify + // them, confirmed during the bucket API work). + switch updateOutput { + case .ok(let response): + guard case .json(let body) = response.body else { + throw StorageError.unexpectedResponse() + } + return FileUploadResponse(id: body.Id ?? "", path: path, fullPath: body.Key) + case .forbidden(let response): + throw try StorageError(decoding: response.body.json) + case .clientError(let statusCode, let response): + throw try StorageError(statusCode: statusCode, decoding: response.body.json) + case .undocumented(let statusCode, let payload): + throw await StorageError( + statusCode: statusCode, undocumented: payload, decoder: configuration.decoder) + } + } + + switch output { + case .ok(let response): + guard case .json(let body) = response.body else { + throw StorageError.unexpectedResponse() + } + return FileUploadResponse(id: body.Id ?? "", path: path, fullPath: body.Key) + case .forbidden(let response): + throw try StorageError(decoding: response.body.json) + case .clientError(let statusCode, let response): + throw try StorageError(statusCode: statusCode, decoding: response.body.json) + case .undocumented(let statusCode, let payload): + throw await StorageError( + statusCode: statusCode, undocumented: payload, decoder: configuration.decoder) + } + } +``` + +> If this if/else duplication reads badly once you see it compiled, a cleaner shape is two +> completely separate private helpers (`_upload`/`_update`) instead of one `_uploadOrUpdate` +> branching on HTTP method — the current shared-helper design was chosen when both methods sent +> literally the same hand-built request differing only in `HTTPTypes.HTTPRequest.Method` and one +> header; now that they're two distinct generated operations with distinct `Output` types, that +> premise no longer holds. Use your judgment; either shape is acceptable as long as the four +> public methods' signatures and behavior stay identical — report which you chose and why. + +- [ ] **Step 4: Run tests, re-record multipart snapshots if needed** + +```bash +swift test --filter StorageFileAPITests +``` + +If a test fails only on the multipart `--data`/body bytes (boundary string, part ordering, +part framing) with the SAME logical content (same field names, same values, same file bytes), +that's expected — swift-openapi-generator's multipart encoder won't necessarily match +`MultipartFormData`'s exact byte-for-byte framing. Re-record following the same discipline as the +bucket API migration's JSON snapshots: confirm it's framing-only, not a real content difference, +before updating the test. + +If a test fails for any other reason (wrong field values, wrong Content-Type, wrong status code +handling), that's a real bug — fix the implementation, don't touch the test. + +- [ ] **Step 5: Format and commit** + +```bash +./scripts/format.sh +swift test --filter StorageFileAPITests +git add Sources/Storage/StorageFileApi.swift Tests/StorageTests/StorageFileAPITests.swift +git commit -m "refactor(storage): migrate upload/update to generated OpenAPI client" +``` + +--- + +### Task 3: Migrate `uploadToSignedURL` (`_uploadToSignedURL`) + +**Files:** +- Modify: `Sources/Storage/StorageFileApi.swift:939-974` (the `_uploadToSignedURL` helper) +- Test: `Tests/StorageTests/StorageFileAPITests.swift` (re-record multipart snapshots if needed) + +**Interfaces:** +- Consumes: `openAPIClient.objectUploadSigned(_:)`, `Operations.objectUploadSigned.Input` + (`path: .init(bucketName:wildcard:)`, `query: .init(token:)`, + `body: .multipartForm(...)` — same `multipartFormPayload` shape as Task 2, generated separately + per operation). +- Produces: `_uploadToSignedURL` keeps its exact existing signature + (`path:token:file:options: async throws -> SignedURLUploadResponse`) — no change visible to + `uploadToSignedURL(_:token:data:)`/`uploadToSignedURL(_:token:fileURL:)`. + +- [ ] **Step 1: Confirm the existing tests describe current behavior** + +```bash +swift test --filter StorageFileAPITests +``` + +Note which tests cover `uploadToSignedURL` (grep `func test.*SignedURL.*[Uu]pload` or similar). + +- [ ] **Step 2: Migrate** + +Reuse the exact multipart-part-building logic from Task 2 Step 2 (same `cacheControl`/`metadata`/ +file-part shape — factor it into a shared private helper if that reads cleanly, e.g. +`_buildMultipartParts(file:options:path:) -> [Operations.objectUpload.Input.Body.multipartFormPayload]`, +but note `Operations.objectUploadSigned.Input.Body.multipartFormPayload` is a **separate generated +type** even though structurally identical — a shared helper would need to either duplicate the +tiny part-building logic per operation, or you accept the duplication given there's no shared +protocol across generated types, same conclusion Task 2 and the bucket API migration both reached +independently). Replace `Sources/Storage/StorageFileApi.swift:939-974`: + +```swift + private func _uploadToSignedURL( + path: String, + token: String, + file: FileUpload, + options: FileOptions? + ) async throws -> SignedURLUploadResponse { + let options = options ?? defaultFileOptions + + var parts: [Operations.objectUploadSigned.Input.Body.multipartFormPayload] = [ + .cacheControl(.init(payload: .init(body: HTTPBody(options.cacheControl)))) + ] + + if let metadata = options.metadata { + parts.append( + .metadata(.init(payload: .init(body: HTTPBody(encodeMetadata(metadata))))) + ) + } + + let mimeType = options.contentType ?? mimeType(forPathExtension: path.pathExtension) + var fileHeaders = HTTPFields() + fileHeaders[.contentType] = mimeType + fileHeaders[.contentDisposition] = + #"form-data; name="file"; filename="\#(path.fileName)""# + + let fileBody: HTTPBody + switch file { + case .data(let data): + fileBody = HTTPBody(data) + case .url(let url): + fileBody = HTTPBody(try Data(contentsOf: url)) + } + + parts.append(.undocumented(MultipartRawPart(headerFields: fileHeaders, body: fileBody))) + + let output = try await openAPIClient.objectUploadSigned( + .init( + path: .init(bucketName: bucketId, wildcard: path), + query: .init(token: token), + headers: .init(), + body: .multipartForm(.init(parts)) + ) + ) + + switch output { + case .ok(let response): + guard case .json(let body) = response.body else { + throw StorageError.unexpectedResponse() + } + return SignedURLUploadResponse(path: path, fullPath: body.Key) + case .forbidden(let response): + throw try StorageError(decoding: response.body.json) + case .clientError(let statusCode, let response): + throw try StorageError(statusCode: statusCode, decoding: response.body.json) + case .undocumented(let statusCode, let payload): + throw await StorageError( + statusCode: statusCode, undocumented: payload, decoder: configuration.decoder) + } + } +``` + +> Note this operation's success response is `{Key}` only (no `Id`), matching +> `SignedURLUploadResponse`'s existing shape (`path`, `fullPath` — no `id` field) — confirmed +> against the vendored spec during planning. If the generated `Output.Ok.Body.jsonPayload` here +> has an `Id` field too, something changed since planning; adjust and note it in your report +> rather than silently ignoring the discrepancy. +> +> This operation applies `x-upsert`/`Duplex`/`options.headers` too (check the current +> pre-migration code at `Sources/Storage/StorageFileApi.swift:946-949`) — carry those over using +> whatever mechanism Task 2 settled on for the same problem (per-call custom headers through the +> generated client). + +- [ ] **Step 3: Run tests, re-record if needed** + +```bash +swift test --filter StorageFileAPITests +``` + +Same re-recording discipline as Task 2 Step 4 — framing-only differences are fine, content +differences are bugs. + +- [ ] **Step 4: Format and commit** + +```bash +./scripts/format.sh +swift test --filter StorageFileAPITests +git add Sources/Storage/StorageFileApi.swift Tests/StorageTests/StorageFileAPITests.swift +git commit -m "refactor(storage): migrate uploadToSignedURL to generated OpenAPI client" +``` + +--- + +### Task 4: Remove dead `MultipartFormData`, run the full suite + +**Files:** +- Delete: `Sources/Storage/MultipartFormData.swift` (if confirmed fully unused) +- Delete (if only used by the above): its paired test file, if one exists under + `Tests/StorageTests/` + +**Interfaces:** None — cleanup only. + +- [ ] **Step 1: Confirm `MultipartFormData` has no remaining callers** + +```bash +grep -rn "MultipartFormData(" Sources/Storage Tests/StorageTests +``` + +Expected: zero hits (Tasks 2-3 were its only callers, per the design doc's investigation). + +- [ ] **Step 2: Remove it** + +```bash +git rm Sources/Storage/MultipartFormData.swift +``` + +If a dedicated `Tests/StorageTests/MultipartFormDataTests.swift` (or similarly named) file exists +and only tests this now-deleted type, remove it too — check first with +`grep -l "MultipartFormData" Tests/StorageTests/*.swift`. + +- [ ] **Step 3: Run the full test suite** + +```bash +make PLATFORM=IOS XCODEBUILD_ARGUMENT=test xcodebuild +``` + +Expected: all tests pass, including every test in `StorageFileAPITests` and the rest of the +existing suite (no regressions in `StorageBucketAPITests`, `StorageOpenAPITransportTests`, etc.). + +- [ ] **Step 4: Format everything touched by this plan** + +```bash +./scripts/format.sh +git status +``` + +Expected: only files this plan touched show as modified. If `format.sh` reformats unrelated +files, revert those with `git checkout -- `. + +- [ ] **Step 5: Commit** + +```bash +git add -u +git commit -m "refactor(storage): remove MultipartFormData, superseded by generated OpenAPI client" +``` + +--- + +## What's next + +`download(path:options:query:cacheNonce:)` (binary response streaming) is the last piece of the +original Milestone 4 scope and gets its own follow-up plan, since it's a different concern +(response body streaming, not request multipart construction) with its own unknowns to verify +against the generated code first. diff --git a/docs/superpowers/specs/2026-07-08-storage-openapi-generator-design.md b/docs/superpowers/specs/2026-07-08-storage-openapi-generator-design.md new file mode 100644 index 000000000..84bffbaa1 --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-storage-openapi-generator-design.md @@ -0,0 +1,123 @@ +# Adopt swift-openapi-generator for the Storage HTTP layer + +Date: 2026-07-08 +Status: Proposed + +## Context + +`Sources/Storage/StorageBucketApi.swift` and `Sources/Storage/StorageFileApi.swift` hand-build +every request (URL, method, query, body encoding) and hand-decode every response. This is +error-prone to keep in sync with the storage service and duplicates work already captured in the +service's own OpenAPI spec. + +[supabase/storage#1215](https://github.com/supabase/storage/pull/1215) (open, targeting `master`) +fixes the generated OpenAPI spec's SDK-readiness: adds `operationId` to every operation, renames +the illegal `*` wildcard path param, registers `bucketSchema`/`objectSchema` as named components, +documents 401/403 responses, dedupes trailing-slash paths, and hides the schemaless S3 catch-all. +This makes the spec usable as codegen input for the first time. + +Exporting the spec from PR #1215's branch (`npm run docs:export`) currently produces (default +single-tenant config, no image-transform/S3 flags): + +- 25 paths, 54 operations (includes TUS resumable-upload routes, which don't exist in this SDK's + current public API and are out of scope here — see [Scope](#scope)) +- Named schemas: `authSchema`, `errorSchema`, `bucketSchema`, `objectSchema` +- Security scheme: `bearerAuth` + +## Scope + +This SDK's current (main-branch) public API surface: `StorageBucketApi` (bucket CRUD) and +`StorageFileApi` (object CRUD, list, move/copy, signed URLs, upload/update/download, info/exists, +transform options). TUS resumable uploads and the S3-compatible protocol don't exist in this SDK +today (they live only on the separate `refactor/storage-http-client` v3 branch, out of scope for +this effort) and are excluded. + +**No public API or behavior changes.** Every existing method signature, type, and error stays +exactly as-is; the generated client becomes an internal implementation detail. + +## Architecture + +The generated OpenAPI client sits behind the existing facade classes, which already translate +between public types and wire format (e.g. `BucketOptions` → `BucketParameters` today): + +``` +storage.createBucket(_:options:) ← public API, unchanged + → StorageBucketApi (facade, hand-written) + → maps public types to generated Operations input + → calls generated Client (Operations.bucketCreate, ...) + → StorageOpenAPITransport (new) → StorageHTTPSession.fetch/upload + → maps generated Output back to public types, or throws StorageError +``` + +Callers of the SDK see no difference. The translation seam is exactly where it already lives. + +## Spec vendoring & codegen tooling + +- Vendor the spec at `Sources/Storage/OpenAPI/openapi.yaml`, exported from PR #1215's branch now; + re-vendor once #1215 merges to `master`. +- Add `Sources/Storage/OpenAPI/openapi-generator-config.yaml` (generate `types` + `client`, + `internal` access modifier). +- **Generate once, commit the output** — not a SwiftPM build plugin. Rationale: a build plugin + regenerates on every build (cost paid by every consumer) and triggers Xcode's untrusted-plugin + approval prompt for anyone depending on this package. Generating once keeps generated code + diffable, formatted like the rest of the repo, and consumer-transparent. +- Don't add `apple/swift-openapi-generator` to the root `Package.swift` — it pulls in + `swift-syntax`/`swift-argument-parser`/etc. that every consumer would resolve for a dev-only + tool. Follow the existing `tools/node` precedent (used to isolate the cspell devDependency): + an isolated `tools/openapi-generator` package (or a documented `swift run` invocation), driven + by `scripts/generate-storage-openapi.sh`. Output committed into `Sources/Storage/Generated/`. +- Add `apple/swift-openapi-runtime` as a real dependency of the `Storage` target — the generated + code imports `OpenAPIRuntime` types at runtime, so this one does ship to consumers (unlike the + generator). +- CI: re-run generation and fail on diff, same spirit as the `./scripts/format.sh` check. + +## Transport bridge & error mapping + +- New `StorageOpenAPITransport: ClientTransport` + (`Sources/Storage/OpenAPI/StorageOpenAPITransport.swift`) converts + `OpenAPIRuntime.HTTPRequest`/`HTTPBody` to/from `URLRequest`/`Data` and calls + `configuration.session.fetch`/`.upload` — the same injectable closures `StorageHTTPSession` + already exposes. Existing Mocker-based tests keep working unchanged. +- The transport does not throw on non-2xx; it returns the raw response, matching + `OpenAPIURLSession`'s contract. Error mapping happens in the facade: switch on the generated + `Operations.*.Output` for documented error cases (401/403/404, now typed thanks to #1215) and + construct `StorageError` from the typed payload; fall back to decoding `StorageError` from + `.undocumented` responses for anything not in the spec (mirrors today's fallback in + `StorageApi.execute`, `StorageApi.swift:115`). +- Left for a later release, not blocking this effort: migrating the transport from + `StorageHTTPSession`-backed to `swift-openapi-urlsession` directly. Keeping + `StorageHTTPSession` for now avoids any behavior change to custom-session/background-upload + configuration. + +## Multipart bodies (upload/update/uploadToSignedURL) + +These use `multipart/form-data` today via the hand-written `MultipartFormData.swift`. +swift-openapi-generator supports typed multipart, but it's the least-proven part of this adoption. +Sequenced as its own milestone (see below) rather than blocking the rest of the migration on it. + +## Testing + +- Existing `StorageBucketAPITests`/`StorageFileAPITests` (Mocker-stubbed against `URLRequest`) + keep working unchanged for migrated methods — the transport bridge preserves the outgoing + request byte-for-byte. +- Add unit tests for `StorageOpenAPITransport` (request/response conversion round-trip) and for + the facade's error-mapping switch (documented vs. undocumented error cases). + +## Rollout + +One design, four independently-mergeable milestones: + +1. Vendor spec + tooling + `StorageOpenAPITransport`. No wiring, no behavior change. +2. Migrate `StorageBucketApi` (all JSON, smallest surface) — proves the pattern end-to-end. +3. Migrate JSON-only `StorageFileApi` operations: list, move, copy, remove, signed URLs, info, + exists. +4. Migrate multipart upload/update/uploadToSignedURL and binary download. + +Each milestone ships as its own `refactor(storage):` PR with no public API or behavior change. + +## Out of scope / future work + +- TUS resumable uploads and S3-compatible protocol (don't exist in this SDK today; live only on + `refactor/storage-http-client`). +- Migrating the transport to `swift-openapi-urlsession` (deferred to a future release). +- Exposing generated types as public API (would be source-breaking; not pursued here). diff --git a/docs/superpowers/specs/2026-07-08-storage-openapi-upload-design.md b/docs/superpowers/specs/2026-07-08-storage-openapi-upload-design.md new file mode 100644 index 000000000..91ad1080a --- /dev/null +++ b/docs/superpowers/specs/2026-07-08-storage-openapi-upload-design.md @@ -0,0 +1,103 @@ +# Migrate upload/update/uploadToSignedURL to the generated OpenAPI client + +Date: 2026-07-08 +Status: Proposed + +## Context + +This is Milestone 4 of [2026-07-08-storage-openapi-generator-design.md](2026-07-08-storage-openapi-generator-design.md) +("multipart upload/update/uploadToSignedURL and binary download"), picked up after Milestones 1-2 +(bucket API + plumbing) shipped and were reviewed clean. + +`StorageFileApi.upload(_:data:)`, `upload(_:fileURL:)`, `update(_:data:)`, `update(_:fileURL:)`, +and `uploadToSignedURL(...)` all funnel through a single private helper, +`_uploadOrUpdate(method:path:file:options:)` (`Sources/Storage/StorageFileApi.swift`), which: + +- Builds a `MultipartFormData` body by hand: a `cacheControl` field (always sent), an optional + `metadata` field (JSON-encoded), and the file itself as an **unnamed** part + (`formData.append(data, withName: "", fileName:, mimeType:)`). +- Sends `x-upsert` and `Duplex` as headers, not multipart fields. +- Decodes a `{ Key, Id }` JSON response into `FileUploadResponse`. + +The vendored spec (`Sources/Storage/OpenAPI/openapi.json`) declares the response schema for +`POST /object/{bucketName}/{wildcard}` (`objectUpload`) — `{Id, Key}`, matching the hand-written +`UploadResponse` exactly — but has **no `requestBody` at all**. Fastify's multipart handling isn't +schema-validated the same way as JSON bodies, so the storage service's OpenAPI export never +documented it. Without a declared request body, swift-openapi-generator's `Input` type for this +operation has no `body` parameter — there is nothing to send a request through today. + +## Scope + +- `upload(_:data:options:)`, `upload(_:fileURL:options:)`, `update(_:data:options:)`, + `update(_:fileURL:options:)`, `uploadToSignedURL(_:token:data:options:)`, + `uploadToSignedURL(_:token:fileURL:options:)` — all six route through the shared + `_uploadOrUpdate` helper and get migrated together as one unit of work. +- `download(path:options:query:cacheNonce:)` (binary response body) is explicitly **out of + scope** — different concern (streaming response, not multipart request), its own follow-up. +- No public API or behavior change, same bar as Milestones 1-2. + +## Architecture + +1. **Patch the vendored spec** to add a `multipart/form-data` requestBody to the three affected + operations (`POST /object/{bucketName}/{wildcard}`, `PUT /object/{bucketName}/{wildcard}`, + `PUT /object/upload/sign/{bucketName}/{wildcard}`), declaring the real parts: + `cacheControl` (string, required), `metadata` (string, optional), and the file part (binary). + Log this as a fourth entry in `docs/superpowers/storage-openapi-upstream-issues.md`, matching + the three fixes already tracked there from Milestones 1-2. +2. **De-risk the empty part name before writing any facade code.** The current file part has no + name (`withName: ""`) — multipart field names aren't observable to API callers, so naming it + explicitly in the spec (e.g. `""` → some placeholder name) is a safe, semantics-preserving + change on the wire, *if* the server's multipart parser matches by position/content-type rather + than by field name for this part. This needs verification against the real storage service + behavior (or its source, if inspectable) before assuming it's safe — this is the single biggest + unknown in this design, called out explicitly rather than glossed over. +3. **Spike first:** patch the spec, regenerate, and inspect the generated multipart `Input.body` + type in isolation (no facade changes yet) to confirm swift-openapi-generator produces something + usable. If it doesn't (e.g. errors, or an unworkable shape), fall back to option considered and + rejected in brainstorming — hand-built multipart body, generated client only for response + decoding — documented here so it isn't silently re-litigated if the spike fails. +4. **Migrate `_uploadOrUpdate`** to build the generated multipart `Input.body` instead of the + hand-written `MultipartFormData`, once the spike confirms feasibility. Keep the response + decoding and error handling identical to the bucket methods' established pattern (`.ok` + decodes `{Id, Key}` into `FileUploadResponse`; `.forbidden`/`.clientError`/`.undocumented` + decode `StorageError` from the real error body, per the fix already landed in Milestones 1-2). +5. **Verify `StorageOpenAPITransport` handles multipart bodies correctly** — it currently collects + `HTTPBody` into `Data` and forwards headers with two narrow normalizations (drop `Accept`, + strip `charset=utf-8` from JSON `Content-Type`). A multipart request's `Content-Type` is + `multipart/form-data; boundary=...`, which the charset-strip condition (exact match on + `application/json; charset=utf-8`) won't touch — should pass through unchanged, but confirm + with a test rather than assume. +6. **File-URL streaming**: the current `.url(let url)` case in `FileUpload.encode(to:withPath:)` + appends the URL directly to `MultipartFormData` for large-file handling. Confirm whether + swift-openapi-generator's multipart support has an equivalent (a body source that doesn't + require loading the whole file into memory) or whether this becomes a behavior-relevant gap + that needs explicit handling (e.g., falling back to reading the file into `Data` — acceptable + only if flagged and confirmed non-regressive for the file sizes this SDK is expected to handle). + +## Testing + +Existing `StorageFileAPITests` snapshot tests (multipart body assertions) are the acceptance gate, +same discipline as Milestones 1-2: request bytes should stay identical; if the generated +multipart encoder produces a different (but semantically equivalent) boundary/ordering, that's a +snapshot re-record, not silently accepted — same standard applied to `createBucket`'s JSON body +in Milestone 1-2. + +## Risks / Open Questions + +- **Empty multipart field name**: the biggest unknown (see Architecture step 2). This could + invalidate the "safe to add a name" assumption if the server actually inspects the field name. + Needs verification before or during the spike. +- **File-URL streaming parity**: swift-openapi-generator's multipart body types may not support + streaming from a file URL the way the current implementation intends to (see Architecture step + 6) — resolve during the spike, not assumed away. +- If the spike shows swift-openapi-generator's multipart support isn't viable for this shape, the + fallback (generated client for response/URL only, hand-written multipart body unchanged) is + documented above so the effort isn't wasted — that fallback still achieves typed response + decoding and shared error handling, just not typed request construction. + +## Out of scope / future work + +- `download(path:options:query:cacheNonce:)` — binary response streaming, separate design. +- The S3-compatible protocol and TUS resumable uploads remain out of scope for this SDK (per the + original design doc) — this only ever existed on the separate `refactor/storage-http-client` + branch. diff --git a/docs/superpowers/storage-openapi-upstream-issues.md b/docs/superpowers/storage-openapi-upstream-issues.md new file mode 100644 index 000000000..8125a13fb --- /dev/null +++ b/docs/superpowers/storage-openapi-upstream-issues.md @@ -0,0 +1,41 @@ +# Upstream issues found in supabase/storage's OpenAPI spec + +Tracking bugs/gaps found in the spec exported from +[supabase/storage#1215](https://github.com/supabase/storage/pull/1215) (`npm run docs:export`) +while adopting swift-openapi-generator in this repo. File these upstream once confirmed against +`master`. + +## Open + +- **Invalid OpenAPI 3.1 nullable-type-array syntax in a 3.0.3 document.** The document declares + `openapi: "3.0.3"` but 4 schema properties use OpenAPI 3.1-only array-form nullable types + (`"type": ["null", "integer"]`), which isn't valid in 3.0.x (nullable there must be + `"type": "integer", "nullable": true`). swift-openapi-generator (1.13.0) hard-fails parsing on + this. Found while running Task 2 of + `docs/superpowers/plans/2026-07-08-storage-openapi-generator.md`; patched in the vendored copy + at `Sources/Storage/OpenAPI/openapi.json` (commit a445a3a2), not yet reported/fixed upstream. + Locations in the exported spec: + - `components.schemas.bucketSchema.properties.file_size_limit` — `["null", "integer"]` + - `components.schemas.bucketSchema.properties.allowed_mime_types` — `["null", "array"]` + - `paths./object/sign/{bucketName}.post.responses.200.content.application/json.schema.items.properties.error` — `["null", "string"]` + - `paths./object/sign/{bucketName}.post.responses.200.content.application/json.schema.items.properties.signedURL` — `["null", "string"]` + +- **Second, larger batch of OpenAPI 3.1-only `anyOf` nullable syntax, also invalid under the + declared 3.0.3 version.** 12 properties use `anyOf: [, {"type": "null"}]` (the other + JSON-Schema-2020-12/3.1 way of expressing nullable), which 3.0.3 also doesn't support — nullable + there must be a plain `nullable: true` on the schema itself. swift-openapi-generator failed with + `Cannot initialize JSONType from invalid String value null` on `objectSchema.properties.id` and + the `/object/copy` response schema. Found while running Task 2 (after the first batch was + already fixed); patched by merging each non-null branch's keywords onto the parent schema and + setting `nullable: true` (commit 4651f5e3), not yet reported/fixed upstream. Exact locations not + individually enumerated here — see commit 4651f5e3's diff in this repo's history for the before + state if needed; a full re-scan after the fix confirmed zero remaining `type`-array or + `anyOf`-with-null occurrences anywhere in the document. + +- **`bucketUpdate`'s request body schema lists properties in `required` that don't exist at the + top level of `properties`.** swift-openapi-generator emits two non-fatal warnings for this + during generation (`public`/`file_size_limit`/`allowed_mime_types` appear in `required` without + a matching sibling in `properties` — likely because the real properties live inside an `anyOf` + branch rather than directly on the body schema). Doesn't block generation, not yet fixed on + either side. Reproduce with `./scripts/generate-storage-openapi.sh` and read the warnings on + `bucketUpdate`. diff --git a/scripts/generate-storage-openapi.sh b/scripts/generate-storage-openapi.sh new file mode 100755 index 000000000..3b3652ef6 --- /dev/null +++ b/scripts/generate-storage-openapi.sh @@ -0,0 +1,29 @@ +#!/bin/bash +set -euo pipefail +cd "$(dirname "$0")/.." + +SPEC=Sources/Storage/OpenAPI/openapi.json +NORMALIZED=$(mktemp -t openapi-normalized).json +trap 'rm -f "$NORMALIZED"' EXIT + +# swift-openapi-generator only understands OpenAPI 3.0's scalar `type` + +# `nullable: true`, not JSON Schema 2020-12's nullable idioms: a `"type"` +# array (e.g. `["null", "integer"]`) or an `anyOf` branch of `{"type": "null"}`. +# Rewrite both to the OpenAPI 3.0 form before feeding the generator. +jq 'walk( + if type == "object" then + if (.type? | type) == "array" and (.type | index("null")) + then . + {type: (.type - ["null"])[0], nullable: true} + elif (.anyOf? | type) == "array" and (.anyOf | length == 2) and (.anyOf | any(. == {"type":"null"})) + then (.anyOf | map(select(. != {"type":"null"})) | .[0]) as $other + | (del(.anyOf) + $other + {nullable: true}) + else . + end + else . + end +)' "$SPEC" > "$NORMALIZED" + +swift run --package-path tools/openapi-generator swift-openapi-generator generate \ + "$NORMALIZED" \ + --config Sources/Storage/OpenAPI/openapi-generator-config.yaml \ + --output-directory Sources/Storage/Generated diff --git a/tools/openapi-generator/Package.resolved b/tools/openapi-generator/Package.resolved new file mode 100644 index 000000000..721eef16f --- /dev/null +++ b/tools/openapi-generator/Package.resolved @@ -0,0 +1,59 @@ +{ + "pins" : [ + { + "identity" : "openapikit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/mattpolzin/OpenAPIKit", + "state" : { + "revision" : "57b6318128e3f901c93f4fbf98d1c1464ec168d3", + "version" : "6.2.0" + } + }, + { + "identity" : "swift-algorithms", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-algorithms", + "state" : { + "revision" : "87e50f483c54e6efd60e885f7f5aa946cee68023", + "version" : "1.2.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser", + "state" : { + "revision" : "6a52f3251125d74daf04fcbd5e6f08a75d074382", + "version" : "1.8.2" + } + }, + { + "identity" : "swift-numerics", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-numerics.git", + "state" : { + "revision" : "0c0290ff6b24942dadb83a929ffaaa1481df04a2", + "version" : "1.1.1" + } + }, + { + "identity" : "swift-openapi-generator", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-openapi-generator", + "state" : { + "revision" : "af9a2a1f5dcfb00a278d4bb29c6d75080932e99e", + "version" : "1.13.0" + } + }, + { + "identity" : "yams", + "kind" : "remoteSourceControl", + "location" : "https://github.com/jpsim/Yams", + "state" : { + "revision" : "a27b21e0c81c5bf42049b897a62aaf387e80f279", + "version" : "6.2.2" + } + } + ], + "version" : 2 +} diff --git a/tools/openapi-generator/Package.swift b/tools/openapi-generator/Package.swift new file mode 100644 index 000000000..806c8465f --- /dev/null +++ b/tools/openapi-generator/Package.swift @@ -0,0 +1,11 @@ +// swift-tools-version:5.9 +import PackageDescription + +let package = Package( + name: "openapi-generator", + platforms: [.macOS(.v13)], + dependencies: [ + .package(url: "https://github.com/apple/swift-openapi-generator", from: "1.5.0") + ], + targets: [] +)