diff --git a/.sdk-parse-ignore b/.sdk-parse-ignore index 2171714af..9c6d91b03 100644 --- a/.sdk-parse-ignore +++ b/.sdk-parse-ignore @@ -8,6 +8,9 @@ # extractor honours it and excludes them from the scan. Use this file only for # paths that cannot carry that annotation, e.g. generated sources. +# Code generation tooling, not part of the Supabase SDK capability surface. +packages/supabase_openapi_generator/ + # supabase_common is an internal, cross-package helper library. Its public # symbols are imported by the other packages, so they cannot be marked # @internal (that would trigger invalid_use_of_internal_member in the diff --git a/packages/supabase_openapi_generator/.gitignore b/packages/supabase_openapi_generator/.gitignore new file mode 100644 index 000000000..05d029502 --- /dev/null +++ b/packages/supabase_openapi_generator/.gitignore @@ -0,0 +1,2 @@ +.dart_tool/ +pubspec.lock diff --git a/packages/supabase_openapi_generator/README.md b/packages/supabase_openapi_generator/README.md new file mode 100644 index 000000000..a14ae7b60 --- /dev/null +++ b/packages/supabase_openapi_generator/README.md @@ -0,0 +1,76 @@ +# supabase_openapi_generator + +Generates idiomatic Dart HTTP clients for Supabase services from OpenAPI 3.0 +specifications. The generated clients are built on the `http` package, need no +`build_runner`, and run unchanged on mobile, desktop, web and WASM. + +The emitter turns an OpenAPI document into: + +- immutable model classes (`final` fields, named constructor, `fromJson`/`toJson`, + snake_case wire keys mapped to camelCase Dart fields), and +- a client class with one typed method per operation. + +Supported transport features: + +- **Streaming uploads** via `Stream>` request bodies, never buffered + into memory. +- **Streaming responses** handed back as a live byte stream. +- **Multipart** uploads with a streaming file part. +- **Per-request header injection** for runtime values such as auth tokens. +- **Percent-encoded path parameters** so object keys with reserved characters + address the correct resource. + +## Layout + +``` +supabase_openapi_generator/ + openapi/ # OpenAPI 3.0 input documents + bin/generate.dart # the emitter + lib/ + src/runtime.dart # transport: ApiClient, streaming, errors + src/generated/*.g.dart # generated clients (checked in) +``` + +## Generating + +Point the emitter at the OpenAPI documents in `openapi/` and run it. Output is +written to `lib/src/generated/` and formatted automatically. + +```bash +dart pub get +dart run bin/generate.dart +``` + +To generate from different documents or to different targets, edit the +`_generate(...)` calls in `bin/generate.dart`. + +## Using a generated client + +Construct an `ApiClient` with the base URL and, optionally, an `httpClient`, +default headers, and a `headerProvider` that supplies runtime headers before +every request: + +```dart +final client = ApiClient( + baseUrl: 'https://.supabase.co/storage/v1', + defaultHeaders: {'apikey': anonKey}, + headerProvider: () => {'Authorization': 'Bearer ${session.accessToken}'}, +); + +final storage = StorageApi(client); + +// JSON operation. +final buckets = await storage.listBuckets(); + +// Streaming upload (bytes flow straight from the source to the socket). +await storage.uploadChunk( + uploadId: uploadId, + tusResumable: '1.0.0', + uploadOffset: 0, + body: file.openRead(), + contentLength: length, +); +``` + +Non-2xx responses throw `ApiException` with the decoded error body. +Octet-stream responses return a `StreamedApiResponse` exposing the live stream. diff --git a/packages/supabase_openapi_generator/bin/generate.dart b/packages/supabase_openapi_generator/bin/generate.dart new file mode 100644 index 000000000..cf313a891 --- /dev/null +++ b/packages/supabase_openapi_generator/bin/generate.dart @@ -0,0 +1,613 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:code_builder/code_builder.dart'; +import 'package:dart_style/dart_style.dart'; + +/// A minimal OpenAPI 3.0 -> idiomatic Dart emitter. It consumes the OpenAPI +/// documents in `openapi/` and writes `http`-based clients into +/// `lib/src/generated/`. +/// +/// The Dart source is built with `package:code_builder` (class/field/method +/// structure and imports) and formatted in-process with `package:dart_style`, +/// so there is no hand-rolled brace/comma bookkeeping and no shelling out to +/// `dart format`. Procedural method bodies are supplied as code blocks. +/// +/// Run with: `dart run bin/generate.dart` +void main() { + _generate( + specPath: 'openapi/StorageService.openapi.json', + className: 'StorageApi', + outputPath: 'lib/src/generated/storage_api.g.dart', + ); + _generate( + specPath: 'openapi/FunctionsService.openapi.json', + className: 'FunctionsApi', + outputPath: 'lib/src/generated/functions_api.g.dart', + ); + _generate( + specPath: 'openapi/DatabaseService.openapi.json', + className: 'DatabaseApi', + outputPath: 'lib/src/generated/database_api.g.dart', + ); +} + +/// The HTTP methods an operation can use. The [Enum.name] of each value is the +/// lowercase key used in an OpenAPI path item (`get`, `post`, …). +enum HttpMethod { get, post, put, patch, delete, head } + +HttpMethod? _httpMethodFrom(String name) { + for (final method in HttpMethod.values) { + if (method.name == name) return method; + } + return null; +} + +void _generate({ + required String specPath, + required String className, + required String outputPath, +}) { + final spec = jsonDecode(File(specPath).readAsStringSync()) as Map; + final schemas = + (spec['components']?['schemas'] as Map?)?.cast() ?? {}; + final paths = (spec['paths'] as Map).cast(); + + final classes = [ + for (final entry in schemas.entries) + if (_isModel((entry.value as Map).cast())) + _buildModel(entry.key, (entry.value as Map).cast()), + _buildClient(className, paths), + ]; + + final library = Library( + (builder) => builder + ..directives.addAll([ + if (_needsConvert(paths)) Directive.import('dart:convert'), + Directive.import('package:http/http.dart', as: 'http'), + Directive.import('../runtime.dart'), + ]) + ..body.addAll(classes), + ); + + final rendered = library + .accept(DartEmitter(orderDirectives: true, useNullSafetySyntax: true)) + .toString(); + + final header = '// GENERATED CODE - DO NOT MODIFY BY HAND.\n' + '// Generated from $specPath by bin/generate.dart.\n' + '// ignore_for_file: prefer_final_locals, ' + 'unnecessary_brace_in_string_interps\n\n'; + + final formatted = DartFormatter( + languageVersion: DartFormatter.latestLanguageVersion, + ).format('$header$rendered'); + + File(outputPath).writeAsStringSync(formatted); + stdout.writeln('Generated $outputPath'); +} + +/// True when any operation carries or returns `application/json` (and therefore +/// the generated file needs `dart:convert`). Error responses are ignored: those +/// are decoded by the runtime, not the generated code. +bool _needsConvert(Map paths) { + for (final operations in paths.values) { + for (final operation in (operations as Map).values) { + if (operation is! Map) continue; + final requestBody = operation['requestBody']?['content'] as Map?; + if (requestBody?.containsKey('application/json') ?? false) return true; + final responses = (operation['responses'] as Map?) ?? {}; + final successKey = ['200', '201', '204', '202'] + .firstWhere(responses.containsKey, orElse: () => ''); + final content = (responses[successKey] as Map?)?['content'] as Map?; + if (content?.containsKey('application/json') ?? false) return true; + } + } + return false; +} + +// ─── Models ────────────────────────────────────────────────────────────── + +bool _isModel(Map schema) => + schema['type'] == 'object' && schema['properties'] != null; + +Class _buildModel(String name, Map schema) { + final properties = (schema['properties'] as Map).cast(); + final required = ((schema['required'] as List?) ?? []).cast().toSet(); + + final fields = [ + for (final entry in properties.entries) + _Field( + jsonKey: entry.key, + dartName: _camelCase(entry.key), + schema: (entry.value as Map).cast(), + isRequired: required.contains(entry.key), + ), + ]; + + final fromJsonArguments = fields + .map((field) => + "${field.dartName}: ${_fromJson(field.schema, "json['${field.jsonKey}']", field.isRequired)},") + .join('\n'); + + final toJsonEntries = fields.map((field) { + final value = _toJson(field.schema, field.dartName, field.isRequired); + return field.isRequired + ? "'${field.jsonKey}': $value," + : "if (${field.dartName} != null) '${field.jsonKey}': $value,"; + }).join('\n'); + + return Class( + (classBuilder) => classBuilder + ..name = name + ..constructors.add( + Constructor( + (constructorBuilder) => constructorBuilder + ..optionalParameters.addAll([ + for (final field in fields) + Parameter((parameterBuilder) => parameterBuilder + ..named = true + ..toThis = true + ..required = field.isRequired + ..name = field.dartName), + ]), + ), + ) + ..constructors.add( + Constructor( + (constructorBuilder) => constructorBuilder + ..factory = true + ..name = 'fromJson' + ..requiredParameters.add( + Parameter((parameterBuilder) => parameterBuilder + ..type = refer('Map') + ..name = 'json'), + ) + ..body = Code('return $name($fromJsonArguments);'), + ), + ) + ..fields.addAll([ + for (final field in fields) + Field((fieldBuilder) => fieldBuilder + ..modifier = FieldModifier.final$ + ..type = + refer(field.isRequired ? field.dartType : '${field.dartType}?') + ..name = field.dartName), + ]) + ..methods.add( + Method( + (methodBuilder) => methodBuilder + ..name = 'toJson' + ..returns = refer('Map') + ..body = Code('return {$toJsonEntries};'), + ), + ), + ); +} + +// ─── Client ────────────────────────────────────────────────────────────── + +Class _buildClient(String className, Map paths) { + final methods = []; + for (final pathEntry in paths.entries) { + final operations = (pathEntry.value as Map).cast(); + for (final operationEntry in operations.entries) { + final method = _httpMethodFrom(operationEntry.key); + if (method == null) continue; + methods.add( + _buildOperation( + pathEntry.key, + method, + (operationEntry.value as Map).cast(), + ), + ); + } + } + + return Class( + (classBuilder) => classBuilder + ..name = className + ..docs.addAll([ + '/// Generated HTTP client. Every operation goes through the', + '/// hand-written [ApiClient] runtime for headers and transport.', + ]) + ..constructors.add( + Constructor( + (constructorBuilder) => constructorBuilder + ..requiredParameters.add( + Parameter((parameterBuilder) => parameterBuilder + ..toThis = true + ..name = '_client'), + ), + ), + ) + ..fields.add( + Field((fieldBuilder) => fieldBuilder + ..modifier = FieldModifier.final$ + ..type = refer('ApiClient') + ..name = '_client'), + ) + ..methods.addAll(methods), + ); +} + +Method _buildOperation( + String path, + HttpMethod method, + Map operation, +) { + final operationId = operation['operationId'] as String; + final parameters = ((operation['parameters'] as List?) ?? []) + .cast() + .map((parameter) => parameter.cast()) + .toList(); + + final pathParameters = parameters.where((p) => p['in'] == 'path').toList(); + final headerParameters = parameters.where((p) => p['in'] == 'header').toList(); + final queryParameters = parameters.where((p) => p['in'] == 'query').toList(); + + final body = _resolveBody(operation); + final response = _resolveResponse(operation); + + final namedParameters = [ + for (final parameter in pathParameters) + _namedParameter( + 'String', _camelCase(_stripWildcard(parameter['name'] as String)), + required: true), + for (final parameter in headerParameters) + _namedParameter(_headerDartType(parameter['schema'] as Map?), + _camelCase(parameter['name'] as String), + required: parameter['required'] == true), + for (final parameter in queryParameters) + _namedParameter(_headerDartType(parameter['schema'] as Map?), + _camelCase(parameter['name'] as String), + required: false), + ...body.parameters, + ]; + + final buffer = StringBuffer(); + + // URI. Path values are percent-encoded so keys with reserved characters + // (spaces, `?`, `#`, …) don't corrupt the URL. Wildcard segments keep `/`. + var dartPath = path; + for (final parameter in pathParameters) { + final wire = parameter['name'] as String; + final name = _camelCase(_stripWildcard(wire)); + final encoded = + wire.endsWith('+') ? 'encodePath($name)' : 'Uri.encodeComponent($name)'; + dartPath = dartPath.replaceAll('{$wire}', '\${$encoded}'); + } + if (queryParameters.isEmpty) { + buffer.writeln("final uri = _client.uri('$dartPath');"); + } else { + buffer.writeln("final uri = _client.uri('$dartPath', {"); + for (final parameter in queryParameters) { + final name = _camelCase(parameter['name'] as String); + final type = _headerDartType(parameter['schema'] as Map?); + final valueExpression = type == 'String' ? name : '$name?.toString()'; + buffer.writeln("'${parameter['name']}': $valueExpression,"); + } + buffer.writeln('});'); + } + + buffer.writeln('final headers = await _client.headers({'); + for (final parameter in headerParameters) { + final wire = parameter['name'] as String; + final name = _camelCase(wire); + final type = _headerDartType(parameter['schema'] as Map?); + final valueExpression = type == 'String' ? name : '\'\$$name\''; + if (parameter['required'] == true) { + buffer.writeln("'$wire': $valueExpression,"); + } else { + buffer.writeln("if ($name != null) '$wire': $valueExpression,"); + } + } + buffer.writeln('});'); + + // Operation-owned headers (e.g. the JSON content-type) are applied after + // addAll so caller/default headers can't clobber them. + buffer.write(body.buildRequest(method.name.toUpperCase())); + buffer.writeln('request.headers.addAll(headers);'); + buffer.write(body.afterHeaders); + buffer.writeln('final streamed = await _client.send(request);'); + buffer.write(response.handle); + + return Method( + (methodBuilder) => methodBuilder + ..name = _lowerFirst(operationId) + ..modifier = MethodModifier.async + ..returns = refer('Future<${response.returnType}>') + ..optionalParameters.addAll(namedParameters) + ..body = Code(buffer.toString()), + ); +} + +Parameter _namedParameter(String type, String name, {required bool required}) => + Parameter((parameterBuilder) => parameterBuilder + ..named = true + ..required = required + ..type = refer(required ? type : '$type?') + ..name = name); + +// ─── Request body resolution ───────────────────────────────────────────────── + +class _Body { + _Body({ + required this.parameters, + required this.buildRequest, + this.afterHeaders = '', + }); + + final List parameters; + final String Function(String method) buildRequest; + + /// Emitted after `request.headers.addAll(headers)` so operation-owned headers + /// win over caller/default headers. + final String afterHeaders; +} + +_Body _resolveBody(Map operation) { + final content = + (operation['requestBody']?['content'] as Map?)?.cast(); + if (content == null) { + return _Body( + parameters: const [], + buildRequest: (method) => "final request = http.Request('$method', uri);\n", + ); + } + + if (content.containsKey('multipart/form-data')) { + return _multipartBody(content['multipart/form-data'] as Map); + } + + final jsonContent = content['application/json']; + if (jsonContent != null) { + final schema = (jsonContent['schema'] as Map).cast(); + final type = _referenceName(schema[r'$ref'] as String); + return _Body( + parameters: [_namedParameter(type, 'body', required: true)], + buildRequest: (method) => "final request = http.Request('$method', uri)\n" + '..body = jsonEncode(body.toJson());\n', + afterHeaders: "request.headers['content-type'] = 'application/json';\n", + ); + } + + // Binary / streaming payload (e.g. TUS UploadChunk). + return _Body( + parameters: [ + _namedParameter('Stream>', 'body', required: true), + _namedParameter('int', 'contentLength', required: false), + ], + buildRequest: (method) => "final request = streamingRequest('$method', uri, " + 'body: body, contentLength: contentLength);\n', + ); +} + +_Body _multipartBody(Map content) { + final schema = (content['schema'] as Map).cast(); + final properties = (schema['properties'] as Map).cast(); + + final fieldParameters = []; + final fieldWrites = []; + String? fileField; + properties.forEach((key, raw) { + final propertySchema = (raw as Map).cast(); + final name = _camelCase(key); + if (propertySchema['format'] == 'binary') { + fileField = key; + } else if (propertySchema['type'] == 'object') { + fieldParameters + .add(_namedParameter('Map', name, required: false)); + fieldWrites + .add("if ($name != null) request.fields['$key'] = jsonEncode($name);"); + } else { + fieldParameters.add(_namedParameter('String', name, required: false)); + fieldWrites.add("if ($name != null) request.fields['$key'] = $name;"); + } + }); + + final parameters = [ + _namedParameter('Stream>', 'file', required: true), + _namedParameter('int', 'fileLength', required: true), + ...fieldParameters, + _namedParameter('String', 'fileName', required: false), + ]; + + return _Body( + parameters: parameters, + buildRequest: (method) { + final buffer = StringBuffer() + ..writeln("final request = http.MultipartRequest('$method', uri);") + ..writeln('request.files.add(http.MultipartFile(') + ..writeln("'${fileField ?? 'file'}',") + ..writeln('file,') + ..writeln('fileLength,') + ..writeln('filename: fileName,') + ..writeln('));'); + for (final write in fieldWrites) { + buffer.writeln(write); + } + return buffer.toString(); + }, + ); +} + +// ─── Response resolution ───────────────────────────────────────────────────── + +class _Response { + _Response({required this.returnType, required this.handle}); + + final String returnType; + final String handle; +} + +_Response _resolveResponse(Map operation) { + final responses = (operation['responses'] as Map).cast(); + final successKey = ['200', '201', '204', '202'] + .firstWhere(responses.containsKey, orElse: () => ''); + final success = + (responses[successKey] as Map?)?.cast() ?? {}; + + final content = (success['content'] as Map?)?.cast(); + if (content != null) { + final jsonContent = content['application/json']; + if (jsonContent != null) { + final schema = (jsonContent['schema'] as Map).cast(); + final type = _referenceName(schema[r'$ref'] as String); + return _Response( + returnType: type, + handle: 'final response = await readOrThrow(streamed);\n' + 'return $type.fromJson(jsonDecode(response.body) as Map);\n', + ); + } + // Binary response streamed straight to the caller. + return _Response( + returnType: 'StreamedApiResponse', + handle: 'if (streamed.statusCode < 200 || streamed.statusCode >= 300) {\n' + 'await readOrThrow(streamed);\n' + '}\n' + 'return StreamedApiResponse(\n' + 'statusCode: streamed.statusCode,\n' + 'headers: streamed.headers,\n' + 'stream: streamed.stream,\n' + ');\n', + ); + } + + // Header-only success (e.g. TUS Upload-Offset). Expose the typed headers. + final headers = (success['headers'] as Map?)?.cast(); + if (headers != null && headers.isNotEmpty) { + final buffer = StringBuffer() + ..writeln('final response = await readOrThrow(streamed);') + ..writeln('return {'); + for (final entry in headers.entries) { + final wire = entry.key; + final schema = + ((entry.value as Map)['schema'] as Map?)?.cast(); + final isNumber = + schema?['type'] == 'number' || schema?['type'] == 'integer'; + final read = isNumber + ? "parseIntHeader(response.headers['${wire.toLowerCase()}'])" + : "response.headers['${wire.toLowerCase()}']"; + buffer.writeln("'${_camelCase(wire)}': $read,"); + } + buffer.writeln('};'); + return _Response(returnType: 'Map', handle: buffer.toString()); + } + + // No content. + return _Response( + returnType: 'void', + handle: 'await readOrThrow(streamed);\n', + ); +} + +// ─── Type + serialization helpers ──────────────────────────────────────────── + +class _Field { + _Field({ + required this.jsonKey, + required this.dartName, + required this.schema, + required this.isRequired, + }); + + final String jsonKey; + final String dartName; + final Map schema; + final bool isRequired; + + String get dartType => _dartType(schema); +} + +String _dartType(Map schema) { + if (schema.containsKey(r'$ref')) { + return _referenceName(schema[r'$ref'] as String); + } + switch (schema['type']) { + case 'string': + final format = schema['format']; + if (format == 'binary' || format == 'byte') return 'Stream>'; + return 'String'; + case 'boolean': + return 'bool'; + case 'integer': + return 'int'; + case 'number': + return 'num'; + case 'array': + return 'List<${_dartType((schema['items'] as Map))}>'; + case 'object': + return 'Map'; + default: + return 'dynamic'; + } +} + +String _fromJson(Map schema, String expression, bool isRequired) { + final suffix = isRequired ? '' : '?'; + if (schema.containsKey(r'$ref')) { + final type = _referenceName(schema[r'$ref'] as String); + if (isRequired) { + return '$type.fromJson($expression as Map)'; + } + return '$expression == null ? null : $type.fromJson($expression as Map)'; + } + switch (schema['type']) { + case 'array': + final items = schema['items'] as Map; + if (items.containsKey(r'$ref')) { + final type = _referenceName(items[r'$ref'] as String); + final mapped = + '($expression as List).map((element) => $type.fromJson(element as Map)).toList()'; + return isRequired ? mapped : '$expression == null ? null : $mapped'; + } + final inner = _dartType(items); + final cast = '($expression as List).cast<$inner>()'; + return isRequired ? cast : '$expression == null ? null : $cast'; + case 'object': + return '$expression as Map$suffix'; + default: + return '$expression as ${_dartType(schema)}$suffix'; + } +} + +String _toJson(Map schema, String name, bool isRequired) { + final access = isRequired ? name : '$name!'; + if (schema.containsKey(r'$ref')) { + return '$access.toJson()'; + } + if (schema['type'] == 'array') { + final items = schema['items'] as Map; + if (items.containsKey(r'$ref')) { + return '$access.map((element) => element.toJson()).toList()'; + } + } + return name; +} + +String _headerDartType(Map? schema) { + final type = schema?['type']; + if (type == 'number' || type == 'integer') return 'int'; + if (type == 'boolean') return 'bool'; + return 'String'; +} + +String _referenceName(String reference) => reference.split('/').last; + +String _stripWildcard(String name) => + name.endsWith('+') ? name.substring(0, name.length - 1) : name; + +String _camelCase(String input) { + final parts = input.split(RegExp('[_-]')); + if (parts.isEmpty) return input; + final buffer = StringBuffer(_lowerFirst(parts.first)); + for (final part in parts.skip(1)) { + if (part.isEmpty) continue; + buffer.write(part[0].toUpperCase() + part.substring(1)); + } + return buffer.toString(); +} + +String _lowerFirst(String input) => + input.isEmpty ? input : input[0].toLowerCase() + input.substring(1); diff --git a/packages/supabase_openapi_generator/lib/src/generated/database_api.g.dart b/packages/supabase_openapi_generator/lib/src/generated/database_api.g.dart new file mode 100644 index 000000000..b79aa6ff2 --- /dev/null +++ b/packages/supabase_openapi_generator/lib/src/generated/database_api.g.dart @@ -0,0 +1,284 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND. +// Generated from openapi/DatabaseService.openapi.json by bin/generate.dart. +// ignore_for_file: prefer_final_locals, unnecessary_brace_in_string_interps + +import 'package:http/http.dart' as http; + +import '../runtime.dart'; + +class DatabaseErrorResponseContent { + DatabaseErrorResponseContent({ + this.code, + this.message, + this.details, + this.hint, + }); + + factory DatabaseErrorResponseContent.fromJson(Map json) { + return DatabaseErrorResponseContent( + code: json['code'] as String?, + message: json['message'] as String?, + details: json['details'] as String?, + hint: json['hint'] as String?, + ); + } + + final String? code; + + final String? message; + + final String? details; + + final String? hint; + + Map toJson() { + return { + if (code != null) 'code': code, + if (message != null) 'message': message, + if (details != null) 'details': details, + if (hint != null) 'hint': hint, + }; + } +} + +/// Generated HTTP client. Every operation goes through the +/// hand-written [ApiClient] runtime for headers and transport. +class DatabaseApi { + DatabaseApi(this._client); + + final ApiClient _client; + + Future callRpcGet({ + required String functionName, + String? acceptProfile, + String? select, + String? args, + }) async { + final uri = _client.uri('/rpc/${Uri.encodeComponent(functionName)}', { + 'select': select, + 'args': args, + }); + final headers = await _client.headers({ + if (acceptProfile != null) 'Accept-Profile': acceptProfile, + }); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future callRpcPost({ + required String functionName, + String? contentProfile, + String? prefer, + String? select, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri('/rpc/${Uri.encodeComponent(functionName)}', { + 'select': select, + }); + final headers = await _client.headers({ + if (contentProfile != null) 'Content-Profile': contentProfile, + if (prefer != null) 'Prefer': prefer, + }); + final request = streamingRequest( + 'POST', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future deleteRows({ + required String table, + String? contentProfile, + String? prefer, + String? select, + String? filters, + }) async { + final uri = _client.uri('/${Uri.encodeComponent(table)}', { + 'select': select, + 'filters': filters, + }); + final headers = await _client.headers({ + if (contentProfile != null) 'Content-Profile': contentProfile, + if (prefer != null) 'Prefer': prefer, + }); + final request = http.Request('DELETE', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future selectRows({ + required String table, + String? accept, + String? acceptProfile, + String? prefer, + String? range, + String? rangeUnit, + String? select, + String? order, + int? limit, + int? offset, + String? filters, + }) async { + final uri = _client.uri('/${Uri.encodeComponent(table)}', { + 'select': select, + 'order': order, + 'limit': limit?.toString(), + 'offset': offset?.toString(), + 'filters': filters, + }); + final headers = await _client.headers({ + if (accept != null) 'Accept': accept, + if (acceptProfile != null) 'Accept-Profile': acceptProfile, + if (prefer != null) 'Prefer': prefer, + if (range != null) 'Range': range, + if (rangeUnit != null) 'Range-Unit': rangeUnit, + }); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future updateRows({ + required String table, + String? contentProfile, + String? prefer, + String? select, + String? filters, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri('/${Uri.encodeComponent(table)}', { + 'select': select, + 'filters': filters, + }); + final headers = await _client.headers({ + if (contentProfile != null) 'Content-Profile': contentProfile, + if (prefer != null) 'Prefer': prefer, + }); + final request = streamingRequest( + 'PATCH', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future insertRows({ + required String table, + String? contentProfile, + String? prefer, + String? select, + String? columns, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri('/${Uri.encodeComponent(table)}', { + 'select': select, + 'columns': columns, + }); + final headers = await _client.headers({ + if (contentProfile != null) 'Content-Profile': contentProfile, + if (prefer != null) 'Prefer': prefer, + }); + final request = streamingRequest( + 'POST', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future upsertRows({ + required String table, + String? contentProfile, + String? prefer, + String? select, + String? onConflict, + String? filters, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri('/${Uri.encodeComponent(table)}', { + 'select': select, + 'on_conflict': onConflict, + 'filters': filters, + }); + final headers = await _client.headers({ + if (contentProfile != null) 'Content-Profile': contentProfile, + if (prefer != null) 'Prefer': prefer, + }); + final request = streamingRequest( + 'PUT', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } +} diff --git a/packages/supabase_openapi_generator/lib/src/generated/functions_api.g.dart b/packages/supabase_openapi_generator/lib/src/generated/functions_api.g.dart new file mode 100644 index 000000000..ddfc38af9 --- /dev/null +++ b/packages/supabase_openapi_generator/lib/src/generated/functions_api.g.dart @@ -0,0 +1,172 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND. +// Generated from openapi/FunctionsService.openapi.json by bin/generate.dart. +// ignore_for_file: prefer_final_locals, unnecessary_brace_in_string_interps + +import 'package:http/http.dart' as http; + +import '../runtime.dart'; + +class FunctionsErrorResponseContent { + FunctionsErrorResponseContent({this.message}); + + factory FunctionsErrorResponseContent.fromJson(Map json) { + return FunctionsErrorResponseContent(message: json['message'] as String?); + } + + final String? message; + + Map toJson() { + return {if (message != null) 'message': message}; + } +} + +/// Generated HTTP client. Every operation goes through the +/// hand-written [ApiClient] runtime for headers and transport. +class FunctionsApi { + FunctionsApi(this._client); + + final ApiClient _client; + + Future invokeFunctionDelete({ + required String functionName, + String? xRegion, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri( + '/functions/v1/${Uri.encodeComponent(functionName)}', + ); + final headers = await _client.headers({ + if (xRegion != null) 'x-region': xRegion, + }); + final request = streamingRequest( + 'DELETE', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future invokeFunctionGet({ + required String functionName, + String? xRegion, + }) async { + final uri = _client.uri( + '/functions/v1/${Uri.encodeComponent(functionName)}', + ); + final headers = await _client.headers({ + if (xRegion != null) 'x-region': xRegion, + }); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future invokeFunctionPatch({ + required String functionName, + String? xRegion, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri( + '/functions/v1/${Uri.encodeComponent(functionName)}', + ); + final headers = await _client.headers({ + if (xRegion != null) 'x-region': xRegion, + }); + final request = streamingRequest( + 'PATCH', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future invokeFunctionPost({ + required String functionName, + String? xRegion, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri( + '/functions/v1/${Uri.encodeComponent(functionName)}', + ); + final headers = await _client.headers({ + if (xRegion != null) 'x-region': xRegion, + }); + final request = streamingRequest( + 'POST', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } + + Future invokeFunctionPut({ + required String functionName, + String? xRegion, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri( + '/functions/v1/${Uri.encodeComponent(functionName)}', + ); + final headers = await _client.headers({ + if (xRegion != null) 'x-region': xRegion, + }); + final request = streamingRequest( + 'PUT', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + if (streamed.statusCode < 200 || streamed.statusCode >= 300) { + await readOrThrow(streamed); + } + return StreamedApiResponse( + statusCode: streamed.statusCode, + headers: streamed.headers, + stream: streamed.stream, + ); + } +} diff --git a/packages/supabase_openapi_generator/lib/src/generated/storage_api.g.dart b/packages/supabase_openapi_generator/lib/src/generated/storage_api.g.dart new file mode 100644 index 000000000..ed96f38b6 --- /dev/null +++ b/packages/supabase_openapi_generator/lib/src/generated/storage_api.g.dart @@ -0,0 +1,1046 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND. +// Generated from openapi/StorageService.openapi.json by bin/generate.dart. +// ignore_for_file: prefer_final_locals, unnecessary_brace_in_string_interps + +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +import '../runtime.dart'; + +class Bucket { + Bucket({ + required this.id, + required this.name, + required this.public, + this.fileSizeLimit, + this.allowedMimeTypes, + this.createdAt, + this.updatedAt, + }); + + factory Bucket.fromJson(Map json) { + return Bucket( + id: json['id'] as String, + name: json['name'] as String, + public: json['public'] as bool, + fileSizeLimit: json['file_size_limit'] as num?, + allowedMimeTypes: json['allowed_mime_types'] == null + ? null + : (json['allowed_mime_types'] as List).cast(), + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, + ); + } + + final String id; + + final String name; + + final bool public; + + final num? fileSizeLimit; + + final List? allowedMimeTypes; + + final String? createdAt; + + final String? updatedAt; + + Map toJson() { + return { + 'id': id, + 'name': name, + 'public': public, + if (fileSizeLimit != null) 'file_size_limit': fileSizeLimit, + if (allowedMimeTypes != null) 'allowed_mime_types': allowedMimeTypes, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + }; + } +} + +class CopyObjectRequestContent { + CopyObjectRequestContent({ + required this.bucketId, + required this.sourceKey, + required this.destinationKey, + this.destinationBucket, + }); + + factory CopyObjectRequestContent.fromJson(Map json) { + return CopyObjectRequestContent( + bucketId: json['bucketId'] as String, + sourceKey: json['sourceKey'] as String, + destinationKey: json['destinationKey'] as String, + destinationBucket: json['destinationBucket'] as String?, + ); + } + + final String bucketId; + + final String sourceKey; + + final String destinationKey; + + final String? destinationBucket; + + Map toJson() { + return { + 'bucketId': bucketId, + 'sourceKey': sourceKey, + 'destinationKey': destinationKey, + if (destinationBucket != null) 'destinationBucket': destinationBucket, + }; + } +} + +class CopyObjectResponseContent { + CopyObjectResponseContent({required this.key}); + + factory CopyObjectResponseContent.fromJson(Map json) { + return CopyObjectResponseContent(key: json['Key'] as String); + } + + final String key; + + Map toJson() { + return {'Key': key}; + } +} + +class CreateBucketRequestContent { + CreateBucketRequestContent({ + required this.id, + required this.name, + required this.public, + this.fileSizeLimit, + this.allowedMimeTypes, + }); + + factory CreateBucketRequestContent.fromJson(Map json) { + return CreateBucketRequestContent( + id: json['id'] as String, + name: json['name'] as String, + public: json['public'] as bool, + fileSizeLimit: json['file_size_limit'] as num?, + allowedMimeTypes: json['allowed_mime_types'] == null + ? null + : (json['allowed_mime_types'] as List).cast(), + ); + } + + final String id; + + final String name; + + final bool public; + + final num? fileSizeLimit; + + final List? allowedMimeTypes; + + Map toJson() { + return { + 'id': id, + 'name': name, + 'public': public, + if (fileSizeLimit != null) 'file_size_limit': fileSizeLimit, + if (allowedMimeTypes != null) 'allowed_mime_types': allowedMimeTypes, + }; + } +} + +class CreateSignedUploadUrlResponseContent { + CreateSignedUploadUrlResponseContent({required this.url}); + + factory CreateSignedUploadUrlResponseContent.fromJson( + Map json, + ) { + return CreateSignedUploadUrlResponseContent(url: json['url'] as String); + } + + final String url; + + Map toJson() { + return {'url': url}; + } +} + +class CreateSignedUrlRequestContent { + CreateSignedUrlRequestContent({required this.expiresIn}); + + factory CreateSignedUrlRequestContent.fromJson(Map json) { + return CreateSignedUrlRequestContent(expiresIn: json['expiresIn'] as num); + } + + final num expiresIn; + + Map toJson() { + return {'expiresIn': expiresIn}; + } +} + +class CreateSignedUrlResponseContent { + CreateSignedUrlResponseContent({required this.signedURL}); + + factory CreateSignedUrlResponseContent.fromJson(Map json) { + return CreateSignedUrlResponseContent( + signedURL: json['signedURL'] as String, + ); + } + + final String signedURL; + + Map toJson() { + return {'signedURL': signedURL}; + } +} + +class CreateSignedUrlsRequestContent { + CreateSignedUrlsRequestContent({ + required this.expiresIn, + required this.paths, + }); + + factory CreateSignedUrlsRequestContent.fromJson(Map json) { + return CreateSignedUrlsRequestContent( + expiresIn: json['expiresIn'] as num, + paths: (json['paths'] as List).cast(), + ); + } + + final num expiresIn; + + final List paths; + + Map toJson() { + return {'expiresIn': expiresIn, 'paths': paths}; + } +} + +class CreateSignedUrlsResponseContent { + CreateSignedUrlsResponseContent({required this.items}); + + factory CreateSignedUrlsResponseContent.fromJson(Map json) { + return CreateSignedUrlsResponseContent( + items: (json['items'] as List) + .map( + (element) => + SignedUrlResult.fromJson(element as Map), + ) + .toList(), + ); + } + + final List items; + + Map toJson() { + return {'items': items.map((element) => element.toJson()).toList()}; + } +} + +class DeleteObjectsRequestContent { + DeleteObjectsRequestContent({required this.prefixes}); + + factory DeleteObjectsRequestContent.fromJson(Map json) { + return DeleteObjectsRequestContent( + prefixes: (json['prefixes'] as List).cast(), + ); + } + + final List prefixes; + + Map toJson() { + return {'prefixes': prefixes}; + } +} + +class DeleteObjectsResponseContent { + DeleteObjectsResponseContent({required this.items}); + + factory DeleteObjectsResponseContent.fromJson(Map json) { + return DeleteObjectsResponseContent( + items: (json['items'] as List) + .map( + (element) => FileObject.fromJson(element as Map), + ) + .toList(), + ); + } + + final List items; + + Map toJson() { + return {'items': items.map((element) => element.toJson()).toList()}; + } +} + +class FileMetadata { + FileMetadata({ + this.eTag, + this.size, + this.mimetype, + this.cacheControl, + this.lastModified, + this.contentLength, + this.httpStatusCode, + }); + + factory FileMetadata.fromJson(Map json) { + return FileMetadata( + eTag: json['eTag'] as String?, + size: json['size'] as num?, + mimetype: json['mimetype'] as String?, + cacheControl: json['cacheControl'] as String?, + lastModified: json['lastModified'] as String?, + contentLength: json['contentLength'] as num?, + httpStatusCode: json['httpStatusCode'] as num?, + ); + } + + final String? eTag; + + final num? size; + + final String? mimetype; + + final String? cacheControl; + + final String? lastModified; + + final num? contentLength; + + final num? httpStatusCode; + + Map toJson() { + return { + if (eTag != null) 'eTag': eTag, + if (size != null) 'size': size, + if (mimetype != null) 'mimetype': mimetype, + if (cacheControl != null) 'cacheControl': cacheControl, + if (lastModified != null) 'lastModified': lastModified, + if (contentLength != null) 'contentLength': contentLength, + if (httpStatusCode != null) 'httpStatusCode': httpStatusCode, + }; + } +} + +class FileObject { + FileObject({ + required this.name, + this.id, + this.updatedAt, + this.createdAt, + this.lastAccessedAt, + this.metadata, + }); + + factory FileObject.fromJson(Map json) { + return FileObject( + name: json['name'] as String, + id: json['id'] as String?, + updatedAt: json['updated_at'] as String?, + createdAt: json['created_at'] as String?, + lastAccessedAt: json['last_accessed_at'] as String?, + metadata: json['metadata'] == null + ? null + : FileMetadata.fromJson(json['metadata'] as Map), + ); + } + + final String name; + + final String? id; + + final String? updatedAt; + + final String? createdAt; + + final String? lastAccessedAt; + + final FileMetadata? metadata; + + Map toJson() { + return { + 'name': name, + if (id != null) 'id': id, + if (updatedAt != null) 'updated_at': updatedAt, + if (createdAt != null) 'created_at': createdAt, + if (lastAccessedAt != null) 'last_accessed_at': lastAccessedAt, + if (metadata != null) 'metadata': metadata!.toJson(), + }; + } +} + +class GetBucketResponseContent { + GetBucketResponseContent({ + required this.id, + required this.name, + required this.public, + this.fileSizeLimit, + this.allowedMimeTypes, + this.createdAt, + this.updatedAt, + }); + + factory GetBucketResponseContent.fromJson(Map json) { + return GetBucketResponseContent( + id: json['id'] as String, + name: json['name'] as String, + public: json['public'] as bool, + fileSizeLimit: json['file_size_limit'] as num?, + allowedMimeTypes: json['allowed_mime_types'] == null + ? null + : (json['allowed_mime_types'] as List).cast(), + createdAt: json['created_at'] as String?, + updatedAt: json['updated_at'] as String?, + ); + } + + final String id; + + final String name; + + final bool public; + + final num? fileSizeLimit; + + final List? allowedMimeTypes; + + final String? createdAt; + + final String? updatedAt; + + Map toJson() { + return { + 'id': id, + 'name': name, + 'public': public, + if (fileSizeLimit != null) 'file_size_limit': fileSizeLimit, + if (allowedMimeTypes != null) 'allowed_mime_types': allowedMimeTypes, + if (createdAt != null) 'created_at': createdAt, + if (updatedAt != null) 'updated_at': updatedAt, + }; + } +} + +class GetObjectInfoResponseContent { + GetObjectInfoResponseContent({ + this.eTag, + this.size, + this.mimetype, + this.cacheControl, + this.lastModified, + this.contentLength, + this.httpStatusCode, + }); + + factory GetObjectInfoResponseContent.fromJson(Map json) { + return GetObjectInfoResponseContent( + eTag: json['eTag'] as String?, + size: json['size'] as num?, + mimetype: json['mimetype'] as String?, + cacheControl: json['cacheControl'] as String?, + lastModified: json['lastModified'] as String?, + contentLength: json['contentLength'] as num?, + httpStatusCode: json['httpStatusCode'] as num?, + ); + } + + final String? eTag; + + final num? size; + + final String? mimetype; + + final String? cacheControl; + + final String? lastModified; + + final num? contentLength; + + final num? httpStatusCode; + + Map toJson() { + return { + if (eTag != null) 'eTag': eTag, + if (size != null) 'size': size, + if (mimetype != null) 'mimetype': mimetype, + if (cacheControl != null) 'cacheControl': cacheControl, + if (lastModified != null) 'lastModified': lastModified, + if (contentLength != null) 'contentLength': contentLength, + if (httpStatusCode != null) 'httpStatusCode': httpStatusCode, + }; + } +} + +class ListBucketsResponseContent { + ListBucketsResponseContent({required this.items}); + + factory ListBucketsResponseContent.fromJson(Map json) { + return ListBucketsResponseContent( + items: (json['items'] as List) + .map((element) => Bucket.fromJson(element as Map)) + .toList(), + ); + } + + final List items; + + Map toJson() { + return {'items': items.map((element) => element.toJson()).toList()}; + } +} + +class ListObjectsRequestContent { + ListObjectsRequestContent({ + required this.prefix, + this.limit, + this.offset, + this.sortBy, + }); + + factory ListObjectsRequestContent.fromJson(Map json) { + return ListObjectsRequestContent( + prefix: json['prefix'] as String, + limit: json['limit'] as num?, + offset: json['offset'] as num?, + sortBy: json['sortBy'] == null + ? null + : SortBy.fromJson(json['sortBy'] as Map), + ); + } + + final String prefix; + + final num? limit; + + final num? offset; + + final SortBy? sortBy; + + Map toJson() { + return { + 'prefix': prefix, + if (limit != null) 'limit': limit, + if (offset != null) 'offset': offset, + if (sortBy != null) 'sortBy': sortBy!.toJson(), + }; + } +} + +class ListObjectsResponseContent { + ListObjectsResponseContent({required this.items}); + + factory ListObjectsResponseContent.fromJson(Map json) { + return ListObjectsResponseContent( + items: (json['items'] as List) + .map( + (element) => FileObject.fromJson(element as Map), + ) + .toList(), + ); + } + + final List items; + + Map toJson() { + return {'items': items.map((element) => element.toJson()).toList()}; + } +} + +class MoveObjectRequestContent { + MoveObjectRequestContent({ + required this.bucketId, + required this.sourceKey, + required this.destinationKey, + this.destinationBucket, + }); + + factory MoveObjectRequestContent.fromJson(Map json) { + return MoveObjectRequestContent( + bucketId: json['bucketId'] as String, + sourceKey: json['sourceKey'] as String, + destinationKey: json['destinationKey'] as String, + destinationBucket: json['destinationBucket'] as String?, + ); + } + + final String bucketId; + + final String sourceKey; + + final String destinationKey; + + final String? destinationBucket; + + Map toJson() { + return { + 'bucketId': bucketId, + 'sourceKey': sourceKey, + 'destinationKey': destinationKey, + if (destinationBucket != null) 'destinationBucket': destinationBucket, + }; + } +} + +class SignedUrlResult { + SignedUrlResult({this.signedURL, required this.path, this.error}); + + factory SignedUrlResult.fromJson(Map json) { + return SignedUrlResult( + signedURL: json['signedURL'] as String?, + path: json['path'] as String, + error: json['error'] as String?, + ); + } + + final String? signedURL; + + final String path; + + final String? error; + + Map toJson() { + return { + if (signedURL != null) 'signedURL': signedURL, + 'path': path, + if (error != null) 'error': error, + }; + } +} + +class SortBy { + SortBy({this.column, this.order}); + + factory SortBy.fromJson(Map json) { + return SortBy( + column: json['column'] as String?, + order: json['order'] as String?, + ); + } + + final String? column; + + final String? order; + + Map toJson() { + return { + if (column != null) 'column': column, + if (order != null) 'order': order, + }; + } +} + +class StorageErrorResponseContent { + StorageErrorResponseContent({this.message, this.error, this.statusCode}); + + factory StorageErrorResponseContent.fromJson(Map json) { + return StorageErrorResponseContent( + message: json['message'] as String?, + error: json['error'] as String?, + statusCode: json['statusCode'] as String?, + ); + } + + final String? message; + + final String? error; + + final String? statusCode; + + Map toJson() { + return { + if (message != null) 'message': message, + if (error != null) 'error': error, + if (statusCode != null) 'statusCode': statusCode, + }; + } +} + +class UpdateBucketRequestContent { + UpdateBucketRequestContent({ + required this.public, + this.fileSizeLimit, + this.allowedMimeTypes, + }); + + factory UpdateBucketRequestContent.fromJson(Map json) { + return UpdateBucketRequestContent( + public: json['public'] as bool, + fileSizeLimit: json['file_size_limit'] as num?, + allowedMimeTypes: json['allowed_mime_types'] == null + ? null + : (json['allowed_mime_types'] as List).cast(), + ); + } + + final bool public; + + final num? fileSizeLimit; + + final List? allowedMimeTypes; + + Map toJson() { + return { + 'public': public, + if (fileSizeLimit != null) 'file_size_limit': fileSizeLimit, + if (allowedMimeTypes != null) 'allowed_mime_types': allowedMimeTypes, + }; + } +} + +class FileUploadedResponse { + FileUploadedResponse({required this.key, required this.id}); + + factory FileUploadedResponse.fromJson(Map json) { + return FileUploadedResponse( + key: json['Key'] as String, + id: json['Id'] as String, + ); + } + + final String key; + + final String id; + + Map toJson() { + return {'Key': key, 'Id': id}; + } +} + +/// Generated HTTP client. Every operation goes through the +/// hand-written [ApiClient] runtime for headers and transport. +class StorageApi { + StorageApi(this._client); + + final ApiClient _client; + + Future listBuckets() async { + final uri = _client.uri('/bucket'); + final headers = await _client.headers({}); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return ListBucketsResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future createBucket({required CreateBucketRequestContent body}) async { + final uri = _client.uri('/bucket'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future deleteBucket({required String id}) async { + final uri = _client.uri('/bucket/${Uri.encodeComponent(id)}'); + final headers = await _client.headers({}); + final request = http.Request('DELETE', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future getBucket({required String id}) async { + final uri = _client.uri('/bucket/${Uri.encodeComponent(id)}'); + final headers = await _client.headers({}); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return GetBucketResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future updateBucket({ + required String id, + required UpdateBucketRequestContent body, + }) async { + final uri = _client.uri('/bucket/${Uri.encodeComponent(id)}'); + final headers = await _client.headers({}); + final request = http.Request('PUT', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future emptyBucket({required String id}) async { + final uri = _client.uri('/bucket/${Uri.encodeComponent(id)}/empty'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future copyObject({ + required CopyObjectRequestContent body, + }) async { + final uri = _client.uri('/object/copy'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return CopyObjectResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future getObjectInfo({ + required String bucketId, + required String wildcardPath, + }) async { + final uri = _client.uri( + '/object/info/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({}); + final request = http.Request('GET', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return GetObjectInfoResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future listObjects({ + required String bucketId, + required ListObjectsRequestContent body, + }) async { + final uri = _client.uri('/object/list/${Uri.encodeComponent(bucketId)}'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return ListObjectsResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future moveObject({required MoveObjectRequestContent body}) async { + final uri = _client.uri('/object/move'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future createSignedUrls({ + required String bucketId, + required CreateSignedUrlsRequestContent body, + }) async { + final uri = _client.uri('/object/sign/${Uri.encodeComponent(bucketId)}'); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return CreateSignedUrlsResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future createSignedUrl({ + required String bucketId, + required String wildcardPath, + required CreateSignedUrlRequestContent body, + }) async { + final uri = _client.uri( + '/object/sign/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({}); + final request = http.Request('POST', uri)..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return CreateSignedUrlResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future createSignedUploadUrl({ + required String bucketId, + required String wildcardPath, + String? xUpsert, + }) async { + final uri = _client.uri( + '/object/upload/sign/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({ + if (xUpsert != null) 'x-upsert': xUpsert, + }); + final request = http.Request('POST', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return CreateSignedUploadUrlResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future deleteObjects({ + required String bucketId, + required DeleteObjectsRequestContent body, + }) async { + final uri = _client.uri('/object/${Uri.encodeComponent(bucketId)}'); + final headers = await _client.headers({}); + final request = http.Request('DELETE', uri) + ..body = jsonEncode(body.toJson()); + request.headers.addAll(headers); + request.headers['content-type'] = 'application/json'; + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return DeleteObjectsResponseContent.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future headObject({ + required String bucketId, + required String wildcardPath, + }) async { + final uri = _client.uri( + '/object/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({}); + final request = http.Request('HEAD', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + await readOrThrow(streamed); + } + + Future uploadObject({ + required String bucketId, + required String wildcardPath, + String? xUpsert, + required Stream> file, + required int fileLength, + String? cacheControl, + Map? metadata, + String? fileName, + }) async { + final uri = _client.uri( + '/object/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({ + if (xUpsert != null) 'x-upsert': xUpsert, + }); + final request = http.MultipartRequest('POST', uri); + request.files.add( + http.MultipartFile('file', file, fileLength, filename: fileName), + ); + if (cacheControl != null) request.fields['cacheControl'] = cacheControl; + if (metadata != null) request.fields['metadata'] = jsonEncode(metadata); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return FileUploadedResponse.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future updateObject({ + required String bucketId, + required String wildcardPath, + required Stream> file, + required int fileLength, + String? cacheControl, + Map? metadata, + String? fileName, + }) async { + final uri = _client.uri( + '/object/${Uri.encodeComponent(bucketId)}/${encodePath(wildcardPath)}', + ); + final headers = await _client.headers({}); + final request = http.MultipartRequest('PUT', uri); + request.files.add( + http.MultipartFile('file', file, fileLength, filename: fileName), + ); + if (cacheControl != null) request.fields['cacheControl'] = cacheControl; + if (metadata != null) request.fields['metadata'] = jsonEncode(metadata); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return FileUploadedResponse.fromJson( + jsonDecode(response.body) as Map, + ); + } + + Future> createTusUpload({ + required String tusResumable, + required int uploadLength, + required String uploadMetadata, + String? xUpsert, + }) async { + final uri = _client.uri('/upload/resumable'); + final headers = await _client.headers({ + 'Tus-Resumable': tusResumable, + 'Upload-Length': '$uploadLength', + 'Upload-Metadata': uploadMetadata, + if (xUpsert != null) 'x-upsert': xUpsert, + }); + final request = http.Request('POST', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return {'location': response.headers['location']}; + } + + Future> getUploadOffset({ + required String uploadId, + required String tusResumable, + }) async { + final uri = _client.uri( + '/upload/resumable/${Uri.encodeComponent(uploadId)}', + ); + final headers = await _client.headers({'Tus-Resumable': tusResumable}); + final request = http.Request('HEAD', uri); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return {'uploadOffset': parseIntHeader(response.headers['upload-offset'])}; + } + + Future> uploadChunk({ + required String uploadId, + required String tusResumable, + required int uploadOffset, + required Stream> body, + int? contentLength, + }) async { + final uri = _client.uri( + '/upload/resumable/${Uri.encodeComponent(uploadId)}', + ); + final headers = await _client.headers({ + 'Tus-Resumable': tusResumable, + 'Upload-Offset': '$uploadOffset', + }); + final request = streamingRequest( + 'PATCH', + uri, + body: body, + contentLength: contentLength, + ); + request.headers.addAll(headers); + final streamed = await _client.send(request); + final response = await readOrThrow(streamed); + return {'uploadOffset': parseIntHeader(response.headers['upload-offset'])}; + } +} diff --git a/packages/supabase_openapi_generator/lib/src/runtime.dart b/packages/supabase_openapi_generator/lib/src/runtime.dart new file mode 100644 index 000000000..c69537a65 --- /dev/null +++ b/packages/supabase_openapi_generator/lib/src/runtime.dart @@ -0,0 +1,144 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:http/http.dart' as http; + +/// A callback invoked before every request to supply headers whose values can +/// change at runtime, most importantly the `Authorization` bearer token. +/// +/// This is the interceptor/middleware hook. Because it runs per request, a +/// token refreshed by the auth loop is always picked up without rebuilding the +/// client. +typedef HeaderProvider = FutureOr> Function(); + +/// Thrown for any non-2xx response. [body] is the decoded error payload when +/// the server returned JSON, otherwise the raw string. +class ApiException implements Exception { + ApiException({ + required this.statusCode, + required this.body, + this.reasonPhrase, + }); + + final int statusCode; + final Object? body; + final String? reasonPhrase; + + @override + String toString() => 'ApiException($statusCode $reasonPhrase): $body'; +} + +/// A response whose body is handed to the caller as a live byte stream instead +/// of being buffered. Used for operations that return +/// `application/octet-stream`, e.g. Functions streaming/SSE responses. +class StreamedApiResponse { + StreamedApiResponse({ + required this.statusCode, + required this.headers, + required this.stream, + }); + + final int statusCode; + final Map headers; + final Stream> stream; +} + +/// Minimal transport shared by the generated clients. Deliberately built on the +/// `http` package that supabase-dart already depends on, with no `build_runner` +/// and no platform-specific imports so the same code runs on mobile, desktop +/// and web. +class ApiClient { + ApiClient({ + required this.baseUrl, + http.Client? httpClient, + this.headerProvider, + Map defaultHeaders = const {}, + }) : _httpClient = httpClient ?? http.Client(), + _defaultHeaders = defaultHeaders; + + final String baseUrl; + final http.Client _httpClient; + final Map _defaultHeaders; + final HeaderProvider? headerProvider; + + /// Merges default headers, the runtime [headerProvider] result and the + /// per-operation [extra] headers. Later entries win. + Future> headers([ + Map extra = const {}, + ]) async { + final provided = await headerProvider?.call() ?? const {}; + return {..._defaultHeaders, ...provided, ...extra}; + } + + /// Builds a request URI, dropping query entries whose value is null. + Uri uri(String path, [Map query = const {}]) { + final filtered = { + for (final entry in query.entries) + if (entry.value != null) entry.key: entry.value!, + }; + return Uri.parse('$baseUrl$path').replace( + queryParameters: filtered.isEmpty ? null : filtered, + ); + } + + Future send(http.BaseRequest request) { + return _httpClient.send(request); + } + + void close() => _httpClient.close(); +} + +/// Reads a streamed response into memory and raises [ApiException] on a non-2xx +/// status. Shared by every generated JSON operation. +Future readOrThrow(http.StreamedResponse streamed) async { + final response = await http.Response.fromStream(streamed); + if (response.statusCode < 200 || response.statusCode >= 300) { + throw _errorFrom(response); + } + return response; +} + +ApiException _errorFrom(http.Response response) { + Object? body = response.body; + final contentType = response.headers['content-type'] ?? ''; + if (contentType.contains('application/json') && response.body.isNotEmpty) { + try { + body = jsonDecode(response.body); + } catch (_) { + // Keep the raw string body. + } + } + return ApiException( + statusCode: response.statusCode, + body: body, + reasonPhrase: response.reasonPhrase, + ); +} + +/// Percent-encodes a path value per RFC 3986 path-segment rules, preserving +/// `/` separators (so wildcard object keys with sub-paths stay intact). Mirrors +/// the encoding the hand-written storage client applies to object keys. +String encodePath(String value) => Uri(pathSegments: value.split('/')).path; + +/// Reads an integer-valued response header without crashing when it is absent +/// or non-integer. Returns null instead of throwing. +int? parseIntHeader(String? value) => + value == null ? null : num.tryParse(value)?.toInt(); + +/// Feeds [body] into a [http.StreamedRequest] without collecting it into memory +/// first. The bytes flow straight from the source stream to +/// the socket. Uses [StreamSink.addStream] so a source error propagates to the +/// send future instead of becoming an unhandled error after the sink closes. +http.StreamedRequest streamingRequest( + String method, + Uri uri, { + required Stream> body, + int? contentLength, +}) { + final request = http.StreamedRequest(method, uri); + if (contentLength != null) { + request.contentLength = contentLength; + } + request.sink.addStream(body).whenComplete(request.sink.close); + return request; +} diff --git a/packages/supabase_openapi_generator/lib/supabase_openapi_generator.dart b/packages/supabase_openapi_generator/lib/supabase_openapi_generator.dart new file mode 100644 index 000000000..46115aa4c --- /dev/null +++ b/packages/supabase_openapi_generator/lib/supabase_openapi_generator.dart @@ -0,0 +1,4 @@ +export 'src/generated/database_api.g.dart'; +export 'src/generated/functions_api.g.dart'; +export 'src/generated/storage_api.g.dart'; +export 'src/runtime.dart'; diff --git a/packages/supabase_openapi_generator/openapi/DatabaseService.openapi.json b/packages/supabase_openapi_generator/openapi/DatabaseService.openapi.json new file mode 100644 index 000000000..51263cdd7 --- /dev/null +++ b/packages/supabase_openapi_generator/openapi/DatabaseService.openapi.json @@ -0,0 +1,741 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Database API", + "version": "1.0", + "description": "PostgREST-backed database API.\n\nBase URL: https://{project-ref}.supabase.co/rest/v1\n\nKnown limitations:\n 1. Write operations return 204 (no body) by default and 200 with a body when\n Prefer: return=representation \u2014 the model uses 200 throughout so generators\n always produce body-parsing code; clients must tolerate empty bodies.\n 2. RPC GET arguments are function-specific; they are expressed via the same\n @httpQueryParams map as row filters, with function-defined keys." + }, + "paths": { + "/rpc/{functionName}": { + "get": { + "operationId": "CallRpcGet", + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "args", + "in": "query", + "description": "Function arguments \u2014 each entry becomes a query parameter.\nKeys and value formats are defined by the PostgreSQL function signature.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Accept-Profile", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "CallRpcGet 200 response", + "headers": { + "Content-Range": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcGetOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CallRpcPost", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcPostInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "description": "e.g. \"params=single-object\" \u2014 treat the entire body as a single parameter.", + "schema": { + "type": "string", + "description": "e.g. \"params=single-object\" \u2014 treat the entire body as a single parameter." + } + } + ], + "responses": { + "200": { + "description": "CallRpcPost 200 response", + "headers": { + "Content-Range": { + "schema": { + "type": "string" + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/CallRpcPostOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + } + }, + "/{table}": { + "delete": { + "operationId": "DeleteRows", + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be deleted.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "DeleteRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/DeleteRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "SelectRows", + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "description": "Column selection \u2014 comma-separated, supports aliasing, casting, embedded\nresources, and JSON operators. e.g. \"id,name,orders(total,status)\".", + "schema": { + "type": "string", + "description": "Column selection \u2014 comma-separated, supports aliasing, casting, embedded\nresources, and JSON operators. e.g. \"id,name,orders(total,status)\"." + } + }, + { + "name": "order", + "in": "query", + "description": "Ordering \u2014 e.g. \"name.asc,age.desc.nullslast\"", + "schema": { + "type": "string", + "description": "Ordering \u2014 e.g. \"name.asc,age.desc.nullslast\"" + } + }, + { + "name": "limit", + "in": "query", + "description": "Maximum number of rows to return.", + "schema": { + "type": "number", + "description": "Maximum number of rows to return." + } + }, + { + "name": "offset", + "in": "query", + "description": "Row offset for pagination.", + "schema": { + "type": "number", + "description": "Row offset for pagination." + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 each entry becomes a query parameter.\nKey: column name (or \"or\"/\"and\" for logical groups).\nValue: \"{operator}.{value}\" e.g. {\"id\": \"eq.5\", \"name\": \"like.foo*\"}.\nSee FilterOperator for the full list of operators.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Accept", + "in": "header", + "description": "Response format. e.g. \"application/json\" (default), \"text/csv\",\n\"application/vnd.pgrst.object+json\" (singular-row mode).", + "schema": { + "type": "string", + "description": "Response format. e.g. \"application/json\" (default), \"text/csv\",\n\"application/vnd.pgrst.object+json\" (singular-row mode)." + } + }, + { + "name": "Accept-Profile", + "in": "header", + "description": "Target a non-default schema exposed by PostgREST.", + "schema": { + "type": "string", + "description": "Target a non-default schema exposed by PostgREST." + } + }, + { + "name": "Prefer", + "in": "header", + "description": "Counting mode. e.g. \"count=exact\", \"count=planned\", \"count=estimated\".", + "schema": { + "type": "string", + "description": "Counting mode. e.g. \"count=exact\", \"count=planned\", \"count=estimated\"." + } + }, + { + "name": "Range", + "in": "header", + "description": "Range-based pagination \u2014 e.g. \"0-9\" (ten rows starting at 0).", + "schema": { + "type": "string", + "description": "Range-based pagination \u2014 e.g. \"0-9\" (ten rows starting at 0)." + } + }, + { + "name": "Range-Unit", + "in": "header", + "description": "Unit for the Range header. Defaults to \"items\".", + "schema": { + "type": "string", + "description": "Unit for the Range header. Defaults to \"items\"." + } + } + ], + "responses": { + "200": { + "description": "SelectRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/SelectRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "UpdateRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpdateRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be updated.\nKey: column name. Value: \"{operator}.{value}\" e.g. {\"id\": \"eq.5\"}.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "UpdateRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpdateRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "InsertRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InsertRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "description": "Columns to select in the returned representation (requires return=representation).", + "schema": { + "type": "string", + "description": "Columns to select in the returned representation (requires return=representation)." + } + }, + { + "name": "columns", + "in": "query", + "description": "Restrict which columns may be populated (useful with CSV uploads).", + "schema": { + "type": "string", + "description": "Restrict which columns may be populated (useful with CSV uploads)." + } + }, + { + "name": "Content-Profile", + "in": "header", + "description": "Target a non-default schema for the write.", + "schema": { + "type": "string", + "description": "Target a non-default schema for the write." + } + }, + { + "name": "Prefer", + "in": "header", + "description": "Return behavior and conflict handling.\ne.g. \"return=representation\", \"return=minimal\" (default),\n \"return=headers-only\", \"resolution=merge-duplicates\".", + "schema": { + "type": "string", + "description": "Return behavior and conflict handling.\ne.g. \"return=representation\", \"return=minimal\" (default),\n \"return=headers-only\", \"resolution=merge-duplicates\"." + } + } + ], + "responses": { + "201": { + "description": "InsertRows 201 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InsertRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpsertRows", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpsertRowsInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "table", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "select", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "on_conflict", + "in": "query", + "description": "Columns to match for conflict detection (if not the primary key).", + "schema": { + "type": "string", + "description": "Columns to match for conflict detection (if not the primary key)." + } + }, + { + "name": "filters", + "in": "query", + "description": "Horizontal filters \u2014 rows matching these filters will be upserted.", + "style": "form", + "schema": { + "$ref": "#/components/schemas/StringMap" + } + }, + { + "name": "Content-Profile", + "in": "header", + "schema": { + "type": "string" + } + }, + { + "name": "Prefer", + "in": "header", + "description": "e.g. \"return=representation\", \"resolution=merge-duplicates\",\n \"resolution=ignore-duplicates\".", + "schema": { + "type": "string", + "description": "e.g. \"return=representation\", \"resolution=merge-duplicates\",\n \"resolution=ignore-duplicates\"." + } + } + ], + "responses": { + "200": { + "description": "UpsertRows 200 response", + "headers": { + "Content-Range": { + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count).", + "schema": { + "type": "string", + "description": "Pagination info \u2014 e.g. \"0-9/200\" (range/total) or \"0-9/*\" (unknown count)." + } + } + }, + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UpsertRowsOutputPayload" + } + } + } + }, + "400": { + "description": "DatabaseError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DatabaseErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "CallRpcGetOutputPayload": { + "type": "string", + "format": "byte" + }, + "CallRpcPostInputPayload": { + "type": "string", + "description": "Named parameters as a JSON object, or a single argument when combined\nwith Prefer: params=single-object.", + "format": "byte" + }, + "CallRpcPostOutputPayload": { + "type": "string", + "format": "byte" + }, + "DatabaseErrorResponseContent": { + "type": "object", + "properties": { + "code": { + "type": "string", + "description": "PostgreSQL error code (e.g. \"23505\") or PostgREST error code (e.g. \"PGRST301\")." + }, + "message": { + "type": "string", + "description": "Human-readable error message." + }, + "details": { + "type": "string", + "description": "Extra context \u2014 constraint name, offending column, etc." + }, + "hint": { + "type": "string", + "description": "Hint from PostgreSQL." + } + } + }, + "DeleteRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "InsertRowsInputPayload": { + "type": "string", + "description": "JSON object or array of objects to insert.", + "format": "byte" + }, + "InsertRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "SelectRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "StringMap": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Generic string-to-string map \u2014 used for arbitrary query parameter collections\n(e.g. PostgREST filter params, RPC GET arguments)." + }, + "UpdateRowsInputPayload": { + "type": "string", + "description": "Partial JSON object with fields to update.", + "format": "byte" + }, + "UpdateRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "UpsertRowsInputPayload": { + "type": "string", + "description": "JSON object or array of objects to upsert.", + "format": "byte" + }, + "UpsertRowsOutputPayload": { + "type": "string", + "format": "byte" + }, + "FilterOperator": { + "type": "string", + "description": "PostgREST column filter operators. Format a filter value as \"{operator}.{value}\", e.g. \"eq.5\". Prefix with \"not.\" to negate: \"not.eq.5\". For logical grouping use keys \"or\" / \"and\" in the filters map.", + "enum": [ + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "like", + "ilike", + "match", + "imatch", + "is", + "isdistinct", + "in", + "cs", + "cd", + "ov", + "sl", + "sr", + "nxl", + "nxr", + "adj", + "fts", + "plfts", + "phfts", + "wfts" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/supabase_openapi_generator/openapi/FunctionsService.openapi.json b/packages/supabase_openapi_generator/openapi/FunctionsService.openapi.json new file mode 100644 index 000000000..9f4f9db18 --- /dev/null +++ b/packages/supabase_openapi_generator/openapi/FunctionsService.openapi.json @@ -0,0 +1,305 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Functions API", + "version": "1.0" + }, + "paths": { + "/functions/v1/{functionName}": { + "delete": { + "operationId": "InvokeFunctionDelete", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionDeleteInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionDelete 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionDeleteOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "InvokeFunctionGet", + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionGet 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionGetOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "operationId": "InvokeFunctionPatch", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPatchInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPatch 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPatchOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "InvokeFunctionPost", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPostInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPost 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPostOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "InvokeFunctionPut", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPutInputPayload" + } + } + } + }, + "parameters": [ + { + "name": "functionName", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-region", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "InvokeFunctionPut 200 response", + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/InvokeFunctionPutOutputPayload" + } + } + } + }, + "400": { + "description": "FunctionsError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FunctionsErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "FunctionsErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + } + } + }, + "InvokeFunctionDeleteInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionDeleteOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionGetOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPatchInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPatchOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPostInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPostOutputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPutInputPayload": { + "type": "string", + "format": "byte" + }, + "InvokeFunctionPutOutputPayload": { + "type": "string", + "format": "byte" + } + } + } +} diff --git a/packages/supabase_openapi_generator/openapi/StorageService.openapi.json b/packages/supabase_openapi_generator/openapi/StorageService.openapi.json new file mode 100644 index 000000000..0e40c0007 --- /dev/null +++ b/packages/supabase_openapi_generator/openapi/StorageService.openapi.json @@ -0,0 +1,1388 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Supabase Storage API", + "version": "1.0" + }, + "paths": { + "/bucket": { + "get": { + "operationId": "ListBuckets", + "responses": { + "200": { + "description": "ListBuckets 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListBucketsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "CreateBucket", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateBucketRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CreateBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/bucket/{id}": { + "delete": { + "operationId": "DeleteBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "get": { + "operationId": "GetBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetBucket 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetBucketResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpdateBucket", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpdateBucketRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "UpdateBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/bucket/{id}/empty": { + "post": { + "operationId": "EmptyBucket", + "parameters": [ + { + "name": "id", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "EmptyBucket 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/copy": { + "post": { + "operationId": "CopyObject", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopyObjectRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "CopyObject 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CopyObjectResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/info/{bucketId}/{wildcardPath+}": { + "get": { + "operationId": "GetObjectInfo", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetObjectInfo 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetObjectInfoResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/list/{bucketId}": { + "post": { + "operationId": "ListObjects", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListObjectsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "ListObjects 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ListObjectsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/move": { + "post": { + "operationId": "MoveObject", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/MoveObjectRequestContent" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "MoveObject 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/sign/{bucketId}": { + "post": { + "operationId": "CreateSignedUrls", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateSignedUrls 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/sign/{bucketId}/{wildcardPath+}": { + "post": { + "operationId": "CreateSignedUrl", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "CreateSignedUrl 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUrlResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/upload/sign/{bucketId}/{wildcardPath+}": { + "post": { + "operationId": "CreateSignedUploadUrl", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "CreateSignedUploadUrl 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateSignedUploadUrlResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/{bucketId}": { + "delete": { + "operationId": "DeleteObjects", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteObjectsRequestContent" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "DeleteObjects 200 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/DeleteObjectsResponseContent" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/object/{bucketId}/{wildcardPath+}": { + "head": { + "operationId": "HeadObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "HeadObject 200 response" + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "post": { + "operationId": "UploadObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "schema": { + "type": "string" + }, + "required": false + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "file" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Upload successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadedResponse" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "put": { + "operationId": "UpdateObject", + "parameters": [ + { + "name": "bucketId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "wildcardPath+", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "cacheControl": { + "type": "string" + }, + "metadata": { + "type": "object", + "additionalProperties": true + }, + "file": { + "type": "string", + "format": "binary" + } + }, + "required": [ + "file" + ] + } + } + } + }, + "responses": { + "200": { + "description": "Upload successful", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadedResponse" + } + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/upload/resumable": { + "post": { + "description": "Step 1: Create a new TUS upload session.\nThe server responds with a Location header containing the upload URL.", + "operationId": "CreateTusUpload", + "parameters": [ + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Upload-Length", + "in": "header", + "description": "Total size of the file in bytes.", + "schema": { + "type": "number", + "description": "Total size of the file in bytes." + }, + "required": true + }, + { + "name": "Upload-Metadata", + "in": "header", + "description": "Base64-encoded TUS metadata (bucketName, objectName, contentType, cacheControl).", + "schema": { + "type": "string", + "description": "Base64-encoded TUS metadata (bucketName, objectName, contentType, cacheControl)." + }, + "required": true + }, + { + "name": "x-upsert", + "in": "header", + "description": "Set to \"true\" to overwrite an existing object at the same path.", + "schema": { + "type": "string", + "description": "Set to \"true\" to overwrite an existing object at the same path." + } + } + ], + "responses": { + "201": { + "description": "CreateTusUpload 201 response", + "headers": { + "Location": { + "description": "Full URL of the created upload session. Used in subsequent PATCH/HEAD requests.", + "schema": { + "type": "string", + "description": "Full URL of the created upload session. Used in subsequent PATCH/HEAD requests." + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + }, + "/upload/resumable/{uploadId}": { + "head": { + "description": "Step 3: Query the server-side offset of a TUS session (used when resuming).", + "operationId": "GetUploadOffset", + "parameters": [ + { + "name": "uploadId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + } + ], + "responses": { + "200": { + "description": "GetUploadOffset 200 response", + "headers": { + "Upload-Offset": { + "schema": { + "type": "number" + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + }, + "patch": { + "description": "Step 2: Upload a chunk of data to an existing TUS session.\nRepeat with increasing Upload-Offset until all bytes are sent.", + "operationId": "UploadChunk", + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "$ref": "#/components/schemas/UploadChunkInputPayload" + } + } + }, + "required": true + }, + "parameters": [ + { + "name": "uploadId", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Tus-Resumable", + "in": "header", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "Upload-Offset", + "in": "header", + "description": "Byte offset at which this chunk begins.", + "schema": { + "type": "number", + "description": "Byte offset at which this chunk begins." + }, + "required": true + } + ], + "responses": { + "204": { + "description": "UploadChunk 204 response", + "headers": { + "Upload-Offset": { + "description": "New server-side offset after the chunk was accepted.", + "schema": { + "type": "number", + "description": "New server-side offset after the chunk was accepted." + }, + "required": true + } + } + }, + "400": { + "description": "StorageError 400 response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StorageErrorResponseContent" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "Bucket": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "CopyObjectRequestContent": { + "type": "object", + "properties": { + "bucketId": { + "type": "string" + }, + "sourceKey": { + "type": "string" + }, + "destinationKey": { + "type": "string" + }, + "destinationBucket": { + "type": "string" + } + }, + "required": [ + "bucketId", + "destinationKey", + "sourceKey" + ] + }, + "CopyObjectResponseContent": { + "type": "object", + "properties": { + "Key": { + "type": "string" + } + }, + "required": [ + "Key" + ] + }, + "CreateBucketRequestContent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "CreateSignedUploadUrlResponseContent": { + "type": "object", + "properties": { + "url": { + "type": "string" + } + }, + "required": [ + "url" + ] + }, + "CreateSignedUrlRequestContent": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number" + } + }, + "required": [ + "expiresIn" + ] + }, + "CreateSignedUrlResponseContent": { + "type": "object", + "properties": { + "signedURL": { + "type": "string" + } + }, + "required": [ + "signedURL" + ] + }, + "CreateSignedUrlsRequestContent": { + "type": "object", + "properties": { + "expiresIn": { + "type": "number" + }, + "paths": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "expiresIn", + "paths" + ] + }, + "CreateSignedUrlsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SignedUrlResult" + } + } + }, + "required": [ + "items" + ] + }, + "DeleteObjectsRequestContent": { + "type": "object", + "properties": { + "prefixes": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "prefixes" + ] + }, + "DeleteObjectsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } + } + }, + "required": [ + "items" + ] + }, + "FileMetadata": { + "type": "object", + "properties": { + "eTag": { + "type": "string" + }, + "size": { + "type": "number" + }, + "mimetype": { + "type": "string" + }, + "cacheControl": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "contentLength": { + "type": "number" + }, + "httpStatusCode": { + "type": "number" + } + } + }, + "FileObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "string" + }, + "updated_at": { + "type": "string" + }, + "created_at": { + "type": "string" + }, + "last_accessed_at": { + "type": "string" + }, + "metadata": { + "$ref": "#/components/schemas/FileMetadata" + } + }, + "required": [ + "name" + ] + }, + "GetBucketResponseContent": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + }, + "created_at": { + "type": "string" + }, + "updated_at": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "public" + ] + }, + "GetObjectInfoResponseContent": { + "type": "object", + "properties": { + "eTag": { + "type": "string" + }, + "size": { + "type": "number" + }, + "mimetype": { + "type": "string" + }, + "cacheControl": { + "type": "string" + }, + "lastModified": { + "type": "string" + }, + "contentLength": { + "type": "number" + }, + "httpStatusCode": { + "type": "number" + } + } + }, + "ListBucketsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Bucket" + } + } + }, + "required": [ + "items" + ] + }, + "ListObjectsRequestContent": { + "type": "object", + "properties": { + "prefix": { + "type": "string" + }, + "limit": { + "type": "number" + }, + "offset": { + "type": "number" + }, + "sortBy": { + "$ref": "#/components/schemas/SortBy" + } + }, + "required": [ + "prefix" + ] + }, + "ListObjectsResponseContent": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileObject" + } + } + }, + "required": [ + "items" + ] + }, + "MoveObjectRequestContent": { + "type": "object", + "properties": { + "bucketId": { + "type": "string" + }, + "sourceKey": { + "type": "string" + }, + "destinationKey": { + "type": "string" + }, + "destinationBucket": { + "type": "string" + } + }, + "required": [ + "bucketId", + "destinationKey", + "sourceKey" + ] + }, + "SignedUrlResult": { + "type": "object", + "properties": { + "signedURL": { + "type": "string" + }, + "path": { + "type": "string" + }, + "error": { + "type": "string" + } + }, + "required": [ + "path" + ] + }, + "SortBy": { + "type": "object", + "properties": { + "column": { + "type": "string" + }, + "order": { + "type": "string" + } + } + }, + "StorageErrorResponseContent": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "error": { + "type": "string" + }, + "statusCode": { + "type": "string" + } + } + }, + "UpdateBucketRequestContent": { + "type": "object", + "properties": { + "public": { + "type": "boolean" + }, + "file_size_limit": { + "type": "number" + }, + "allowed_mime_types": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Common string list shape reused across services." + } + }, + "required": [ + "public" + ] + }, + "UploadChunkInputPayload": { + "type": "string", + "description": "Raw chunk bytes, streamed directly \u2014 never buffered.", + "format": "binary" + }, + "FileUploadedResponse": { + "type": "object", + "properties": { + "Key": { + "type": "string" + }, + "Id": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "Key", + "Id" + ] + } + } + } +} \ No newline at end of file diff --git a/packages/supabase_openapi_generator/pubspec.yaml b/packages/supabase_openapi_generator/pubspec.yaml new file mode 100644 index 000000000..a6d9d262e --- /dev/null +++ b/packages/supabase_openapi_generator/pubspec.yaml @@ -0,0 +1,19 @@ +name: supabase_openapi_generator +description: >- + Generates idiomatic Dart HTTP clients for Supabase services from OpenAPI 3.0 + specifications, with streaming upload, streaming response and multipart + support. +publish_to: none +version: 0.0.1 + +environment: + sdk: ^3.5.0 + +dependencies: + code_builder: ^4.11.1 + dart_style: ^3.1.9 + http: ^1.2.2 + +dev_dependencies: + supabase_lints: ^0.1.0 + test: ^1.24.0 diff --git a/packages/supabase_openapi_generator/test/generated_client_test.dart b/packages/supabase_openapi_generator/test/generated_client_test.dart new file mode 100644 index 000000000..6868bf978 --- /dev/null +++ b/packages/supabase_openapi_generator/test/generated_client_test.dart @@ -0,0 +1,333 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:typed_data'; + +import 'package:http/http.dart' as http; +import 'package:supabase_openapi_generator/supabase_openapi_generator.dart'; +import 'package:test/test.dart'; + +/// A test double that records the outgoing request and returns a canned +/// response. It never touches the network. +class _RecordingClient extends http.BaseClient { + _RecordingClient(this._handler); + + final Future Function(http.BaseRequest request) + _handler; + + http.BaseRequest? lastRequest; + + @override + Future send(http.BaseRequest request) { + lastRequest = request; + return _handler(request); + } +} + +http.StreamedResponse _json(Object body, {int status = 200}) { + final bytes = utf8.encode(jsonEncode(body)); + return http.StreamedResponse( + Stream.value(bytes), + status, + headers: {'content-type': 'application/json'}, + ); +} + +void main() { + group('JSON operations', () { + test('listBuckets decodes into models', () async { + final http = _RecordingClient( + (_) async => _json({ + 'items': [ + {'id': 'avatars', 'name': 'avatars', 'public': true}, + ], + }), + ); + final api = StorageApi(ApiClient(baseUrl: 'https://x', httpClient: http)); + + final result = await api.listBuckets(); + + expect(result.items, hasLength(1)); + expect(result.items.first.id, 'avatars'); + expect(result.items.first.public, isTrue); + expect(http.lastRequest!.method, 'GET'); + expect(http.lastRequest!.url.path, '/bucket'); + }); + + test('createBucket serializes the request model', () async { + Map? sentBody; + final client = _RecordingClient((request) async { + sentBody = jsonDecode( + await (request as http.Request).finalize().bytesToString(), + ) as Map; + return _json({'name': 'photos'}); + }); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + await api.createBucket( + body: CreateBucketRequestContent( + id: 'photos', + name: 'photos', + public: false, + ), + ); + + expect(sentBody, {'id': 'photos', 'name': 'photos', 'public': false}); + }); + + test('non-2xx maps to ApiException with decoded body', () async { + final api = StorageApi( + ApiClient( + baseUrl: 'https://x', + httpClient: _RecordingClient( + (_) async => _json({'message': 'not found'}, status: 404), + ), + ), + ); + + await expectLater( + api.getBucket(id: 'missing'), + throwsA( + isA() + .having((e) => e.statusCode, 'statusCode', 404) + .having((e) => (e.body as Map)['message'], 'body', 'not found'), + ), + ); + }); + }); + + group('Streaming', () { + test('upload streams the request body without buffering (question 1)', + () async { + final received = []; + final client = _RecordingClient((request) async { + // Pull the finalized body chunk by chunk; nothing was collected into a + // Uint8List by the generated client before this point. + await for (final chunk in request.finalize()) { + received.addAll(chunk); + } + return http.StreamedResponse( + const Stream.empty(), + 204, + headers: {'upload-offset': '9'}, + ); + }); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + final source = Stream>.fromIterable([ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + ]); + + final result = await api.uploadChunk( + uploadId: 'abc', + tusResumable: '1.0.0', + uploadOffset: 0, + body: source, + contentLength: 9, + ); + + expect(client.lastRequest, isA()); + expect(received, [1, 2, 3, 4, 5, 6, 7, 8, 9]); + expect(result['uploadOffset'], 9); + expect(client.lastRequest!.headers['Upload-Offset'], '0'); + expect(client.lastRequest!.headers['Tus-Resumable'], '1.0.0'); + }); + + test('response is handed back as a live stream (question 2)', () async { + final controller = StreamController>(); + final api = FunctionsApi( + ApiClient( + baseUrl: 'https://x', + httpClient: _RecordingClient( + (_) async => http.StreamedResponse(controller.stream, 200), + ), + ), + ); + + final response = await api.invokeFunctionGet(functionName: 'sse'); + expect(response, isA()); + + final received = []; + final done = response.stream + .transform(utf8.decoder) + .listen(received.add) + .asFuture(); + + // Emit events over time; the caller receives them incrementally. + controller.add(utf8.encode('event-1')); + await Future.delayed(Duration.zero); + expect(received, ['event-1']); + controller.add(utf8.encode('event-2')); + await controller.close(); + await done; + + expect(received, ['event-1', 'event-2']); + }); + + test('multipart upload streams the file part (question 3)', () async { + http.BaseRequest? captured; + final client = _RecordingClient((request) async { + captured = request; + await request.finalize().drain(); + return _json({'Key': 'avatars/a.png', 'Id': 'uuid'}); + }); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + final result = await api.uploadObject( + bucketId: 'avatars', + wildcardPath: 'a.png', + file: Stream.value(Uint8List.fromList([1, 2, 3])), + fileLength: 3, + fileName: 'a.png', + cacheControl: '3600', + ); + + expect(captured, isA()); + expect(result.key, 'avatars/a.png'); + }); + }); + + group('Correctness fixes', () { + test('path parameters are percent-encoded, wildcard keeps slashes', + () async { + final client = _RecordingClient((_) async => _json({})); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + await api.getObjectInfo( + bucketId: 'my bucket', + wildcardPath: 'sub dir/a?b.png', + ); + + final url = client.lastRequest!.url; + // Decoded segments round-trip to the original values, and the `?` did not + // leak into the query string. + expect(url.pathSegments, [ + 'object', + 'info', + 'my bucket', + 'sub dir', + 'a?b.png', + ]); + expect(url.query, isEmpty); + }); + + test('operation content-type wins over provider headers', () async { + final client = _RecordingClient((_) async => _json({'name': 'photos'})); + final api = StorageApi( + ApiClient( + baseUrl: 'https://x', + httpClient: client, + headerProvider: () => {'content-type': 'text/plain'}, + ), + ); + + await api.createBucket( + body: CreateBucketRequestContent( + id: 'photos', + name: 'photos', + public: false, + ), + ); + + expect(client.lastRequest!.headers['content-type'], 'application/json'); + }); + + test('missing numeric response header yields null instead of crashing', + () async { + final client = _RecordingClient( + (_) async => http.StreamedResponse(const Stream.empty(), 200), + ); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + final result = + await api.getUploadOffset(uploadId: 'abc', tusResumable: '1.0.0'); + + expect(result['uploadOffset'], isNull); + }); + + test('a body-stream error surfaces as a catchable exception', () async { + final client = _RecordingClient((request) async { + await request.finalize().drain(); + return http.StreamedResponse(const Stream.empty(), 204); + }); + final api = + StorageApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + final failing = Stream>.error(StateError('read failed')); + + await expectLater( + api.uploadChunk( + uploadId: 'abc', + tusResumable: '1.0.0', + uploadOffset: 0, + body: failing, + ), + throwsA(isA()), + ); + }); + }); + + group('PostgREST (DatabaseService) transport', () { + test('selectRows serializes typed query params and streams the response', + () async { + final client = _RecordingClient( + (_) async => http.StreamedResponse( + Stream.value(utf8.encode('[{"id":1}]')), + 200, + ), + ); + final api = + DatabaseApi(ApiClient(baseUrl: 'https://x', httpClient: client)); + + final response = await api.selectRows( + table: 'my table', + select: 'id,name', + limit: 10, + offset: 20, + ); + + final url = client.lastRequest!.url; + expect(url.pathSegments, ['my table']); + expect(url.queryParameters['select'], 'id,name'); + expect(url.queryParameters['limit'], '10'); + expect(url.queryParameters['offset'], '20'); + expect(response, isA()); + expect( + await response.stream.transform(utf8.decoder).join(), + '[{"id":1}]', + ); + }); + }); + + group('Middleware / header injection (question 4)', () { + test('headerProvider is invoked per request for fresh auth tokens', + () async { + // The token lives in a mutable holder so the provider closure reads the + // latest value on every request. + final token = ['first']; + final client = _RecordingClient((_) async => _json({'items': []})); + final api = StorageApi( + ApiClient( + baseUrl: 'https://x', + httpClient: client, + defaultHeaders: {'apikey': 'anon'}, + headerProvider: () => {'Authorization': 'Bearer ${token.first}'}, + ), + ); + + await api.listBuckets(); + expect(client.lastRequest!.headers['Authorization'], 'Bearer first'); + expect(client.lastRequest!.headers['apikey'], 'anon'); + + token[0] = 'refreshed'; + await api.listBuckets(); + expect(client.lastRequest!.headers['Authorization'], 'Bearer refreshed'); + }); + }); +}