From 8bec97dc73b2555580f0393fcb6bf17cad4c9b9e Mon Sep 17 00:00:00 2001 From: Dillon Nys <24740863+dnys1@users.noreply.github.com> Date: Tue, 28 May 2024 06:41:20 -0700 Subject: [PATCH] feat!: API refactor (#138) --- examples/gemini/celest/functions/gemini.dart | 8 +- examples/gemini/celest/generated/README.md | 9 + .../celest/{ => generated}/resources.dart | 6 +- .../celest/lib/src/client/functions.dart | 7 +- .../celest/lib/src/client/serializers.dart | 4 +- examples/openai/celest/functions/open_ai.dart | 8 +- examples/openai/celest/generated/README.md | 9 + .../celest/{ => generated}/resources.dart | 6 +- examples/openai/celest/lib/client.dart | 5 +- .../celest/lib/src/client/functions.dart | 7 +- .../celest/lib/src/client/serializers.dart | 4 +- examples/todo/celest/functions/tasks.dart | 6 + examples/todo/celest/generated/README.md | 9 + .../celest/{ => generated}/resources.dart | 0 .../todo/celest/lib/src/client/functions.dart | 2 +- packages/celest/analysis_options.yaml | 2 + .../example/celest/functions/greeting.dart | 2 + .../celest/example/celest/generated/README.md | 9 + .../example/celest/generated}/resources.dart | 0 .../celest/example/celest/lib/client.dart | 10 +- .../celest/lib/src/client/functions.dart | 13 +- .../celest/lib/src/client/serializers.dart | 35 +++ packages/celest/example/celest/pubspec.yaml | 3 +- packages/celest/example/celest/resources.dart | 24 -- packages/celest/lib/celest.dart | 1 + packages/celest/lib/fix_data.yaml | 13 ++ packages/celest/lib/http.dart | 12 + packages/celest/lib/src/core/annotations.dart | 43 ++++ packages/celest/lib/src/core/context.dart | 68 +++--- .../celest/lib/src/functions/http/http.dart | 20 ++ .../lib/src/functions/http/http_error.dart | 47 ++++ .../lib/src/functions/http/http_header.dart | 13 ++ .../lib/src/functions/http/http_method.dart | 17 ++ .../lib/src/functions/http/http_query.dart | 13 ++ .../lib/src/functions/http/http_status.dart | 211 ++++++++++++++++++ packages/celest/lib/src/runtime/serve.dart | 27 ++- packages/celest/pubspec.yaml | 2 +- .../example/celest/{auth => }/auth.dart | 0 .../example/celest/functions/greeting.dart | 3 +- .../example/celest/generated/README.md | 9 + .../example/celest/generated/resources.dart | 5 + .../example/celest/lib/client.dart | 6 +- .../celest/lib/src/client/functions.dart | 7 +- .../celest/lib/src/client/serializers.dart | 4 +- .../celest_auth/example/celest/pubspec.yaml | 3 +- .../lib/src/exception/cloud_exception.dart | 24 +- 46 files changed, 624 insertions(+), 112 deletions(-) create mode 100644 examples/gemini/celest/generated/README.md rename examples/gemini/celest/{ => generated}/resources.dart (80%) create mode 100644 examples/openai/celest/generated/README.md rename examples/openai/celest/{ => generated}/resources.dart (80%) create mode 100644 examples/todo/celest/generated/README.md rename examples/todo/celest/{ => generated}/resources.dart (100%) create mode 100644 packages/celest/example/celest/generated/README.md rename packages/{celest_auth/example/celest => celest/example/celest/generated}/resources.dart (100%) delete mode 100644 packages/celest/example/celest/resources.dart create mode 100644 packages/celest/lib/fix_data.yaml create mode 100644 packages/celest/lib/http.dart create mode 100644 packages/celest/lib/src/core/annotations.dart create mode 100644 packages/celest/lib/src/functions/http/http.dart create mode 100644 packages/celest/lib/src/functions/http/http_error.dart create mode 100644 packages/celest/lib/src/functions/http/http_header.dart create mode 100644 packages/celest/lib/src/functions/http/http_method.dart create mode 100644 packages/celest/lib/src/functions/http/http_query.dart create mode 100644 packages/celest/lib/src/functions/http/http_status.dart rename packages/celest_auth/example/celest/{auth => }/auth.dart (100%) create mode 100644 packages/celest_auth/example/celest/generated/README.md create mode 100644 packages/celest_auth/example/celest/generated/resources.dart diff --git a/examples/gemini/celest/functions/gemini.dart b/examples/gemini/celest/functions/gemini.dart index f2892a3c..9b589c47 100644 --- a/examples/gemini/celest/functions/gemini.dart +++ b/examples/gemini/celest/functions/gemini.dart @@ -6,9 +6,10 @@ import 'dart:convert'; import 'package:celest/celest.dart'; import 'package:google_generative_ai/google_generative_ai.dart'; -import '../resources.dart'; +import '../generated/resources.dart'; /// Returns a list of available models. +@cloud Future> availableModels() async => _availableModels; /// The list of available models. @@ -22,10 +23,11 @@ const _availableModels = [ /// Prompts the Gemini [modelName] with the given [prompt] and [parameters]. /// /// Returns the generated text. +@cloud Future generateContent({ required String modelName, required String prompt, - @Env.geminiApiKey required String apiKey, + @env.geminiApiKey required String apiKey, }) async { if (!_availableModels.contains(modelName)) { throw BadRequestException('Invalid model: $modelName'); @@ -45,7 +47,7 @@ Future generateContent({ print('Selected answer: $text'); return text; case _: - throw InternalServerException('Failed to generate content'); + throw InternalServerError('Failed to generate content'); } } diff --git a/examples/gemini/celest/generated/README.md b/examples/gemini/celest/generated/README.md new file mode 100644 index 00000000..a5b5cb79 --- /dev/null +++ b/examples/gemini/celest/generated/README.md @@ -0,0 +1,9 @@ +# Generated Celest code + +This directory contains code generated by the Celest CLI to assist in building +your backend. + +This code can be safely checked into version control, but it should not be +modified directly. + +It is planned to replace this directory with macros when they become stable. diff --git a/examples/gemini/celest/resources.dart b/examples/gemini/celest/generated/resources.dart similarity index 80% rename from examples/gemini/celest/resources.dart rename to examples/gemini/celest/generated/resources.dart index 8014ea1f..218da18a 100644 --- a/examples/gemini/celest/resources.dart +++ b/examples/gemini/celest/generated/resources.dart @@ -6,9 +6,9 @@ library; import 'package:celest/celest.dart'; -@Deprecated('Use `Env` instead.') -typedef env = Env; +@Deprecated('Use `env` instead.') +typedef Env = env; -abstract final class Env { +abstract final class env { static const geminiApiKey = EnvironmentVariable(name: r'GEMINI_API_KEY'); } diff --git a/examples/gemini/celest/lib/src/client/functions.dart b/examples/gemini/celest/lib/src/client/functions.dart index f2719a51..2d938e64 100644 --- a/examples/gemini/celest/lib/src/client/functions.dart +++ b/examples/gemini/celest/lib/src/client/functions.dart @@ -31,9 +31,8 @@ class CelestFunctionsGemini { throw Serializers.instance.deserialize($details); case r'UnauthorizedException': throw Serializers.instance.deserialize($details); - case r'InternalServerException': - throw Serializers.instance - .deserialize($details); + case r'InternalServerError': + throw Serializers.instance.deserialize($details); case r'SerializationException': throw Serializers.instance .deserialize($details); @@ -56,7 +55,7 @@ class CelestFunctionsGemini { case 400: throw BadRequestException($code); case _: - throw InternalServerException($code); + throw InternalServerError($code); } } } diff --git a/examples/gemini/celest/lib/src/client/serializers.dart b/examples/gemini/celest/lib/src/client/serializers.dart index 79e49349..57e6be77 100644 --- a/examples/gemini/celest/lib/src/client/serializers.dart +++ b/examples/gemini/celest/lib/src/client/serializers.dart @@ -16,10 +16,10 @@ void initSerializers() { }, )); Serializers.instance - .put(Serializer.define>( + .put(Serializer.define>( serialize: ($value) => {r'message': $value.message}, deserialize: ($serialized) { - return InternalServerException(($serialized[r'message'] as String)); + return InternalServerError(($serialized[r'message'] as String)); }, )); Serializers.instance diff --git a/examples/openai/celest/functions/open_ai.dart b/examples/openai/celest/functions/open_ai.dart index c69d6bee..a7fed53c 100644 --- a/examples/openai/celest/functions/open_ai.dart +++ b/examples/openai/celest/functions/open_ai.dart @@ -8,7 +8,7 @@ import 'package:celest_backend/models.dart'; import 'package:chat_gpt_sdk/chat_gpt_sdk.dart'; import 'package:chat_gpt_sdk/src/model/chat_complete/response/chat_choice.dart'; -import '../resources.dart'; +import '../generated/resources.dart'; /// Creates an instance of the OpenAI client. OpenAI _createOpenAI(String token) => OpenAI.instance.build( @@ -18,6 +18,7 @@ OpenAI _createOpenAI(String token) => OpenAI.instance.build( ); /// Returns a list of available models. +@cloud Future> availableModels() async => _availableModels; /// The list of available models. @@ -32,11 +33,12 @@ const _availableModels = [ /// Prompts the GPT [model] with the given [prompt] and [parameters]. /// /// Returns the generated text. +@cloud Future openAIRequest({ required String model, required String prompt, ModelParameters parameters = const ModelParameters(), - @Env.openAiToken required String openAiToken, + @env.openAiToken required String openAiToken, }) async { final openAI = _createOpenAI(openAiToken); @@ -63,7 +65,7 @@ Future openAIRequest({ case ChatCTResponse(choices: [ChatChoice(:final message?), ...]): return message.content.trim(); default: - throw InternalServerException( + throw InternalServerError( "Couldn't complete request. Please try again later.", ); } diff --git a/examples/openai/celest/generated/README.md b/examples/openai/celest/generated/README.md new file mode 100644 index 00000000..a5b5cb79 --- /dev/null +++ b/examples/openai/celest/generated/README.md @@ -0,0 +1,9 @@ +# Generated Celest code + +This directory contains code generated by the Celest CLI to assist in building +your backend. + +This code can be safely checked into version control, but it should not be +modified directly. + +It is planned to replace this directory with macros when they become stable. diff --git a/examples/openai/celest/resources.dart b/examples/openai/celest/generated/resources.dart similarity index 80% rename from examples/openai/celest/resources.dart rename to examples/openai/celest/generated/resources.dart index fb457dd2..e0a4b1cd 100644 --- a/examples/openai/celest/resources.dart +++ b/examples/openai/celest/generated/resources.dart @@ -6,9 +6,9 @@ library; import 'package:celest/celest.dart'; -@Deprecated('Use `Env` instead.') -typedef env = Env; +@Deprecated('Use `env` instead.') +typedef Env = env; -abstract final class Env { +abstract final class env { static const openAiToken = EnvironmentVariable(name: r'OPEN_AI_TOKEN'); } diff --git a/examples/openai/celest/lib/client.dart b/examples/openai/celest/lib/client.dart index bddf3640..72ea989c 100644 --- a/examples/openai/celest/lib/client.dart +++ b/examples/openai/celest/lib/client.dart @@ -16,15 +16,12 @@ import 'src/client/serializers.dart'; final Celest celest = Celest(); enum CelestEnvironment { - local, - production; + local; Uri get baseUri => switch (this) { local => kIsWeb || !_$io.Platform.isAndroid ? Uri.parse('http://localhost:7777') : Uri.parse('http://10.0.2.2:7777'), - production => - Uri.parse('https://openai-example-xmfv-v76lntiq7q-wn.a.run.app'), }; } diff --git a/examples/openai/celest/lib/src/client/functions.dart b/examples/openai/celest/lib/src/client/functions.dart index 3d21e0cf..6913b656 100644 --- a/examples/openai/celest/lib/src/client/functions.dart +++ b/examples/openai/celest/lib/src/client/functions.dart @@ -30,9 +30,8 @@ class CelestFunctionsOpenAi { throw Serializers.instance.deserialize($details); case r'UnauthorizedException': throw Serializers.instance.deserialize($details); - case r'InternalServerException': - throw Serializers.instance - .deserialize($details); + case r'InternalServerError': + throw Serializers.instance.deserialize($details); case r'SerializationException': throw Serializers.instance .deserialize($details); @@ -41,7 +40,7 @@ class CelestFunctionsOpenAi { case 400: throw BadRequestException($code); case _: - throw InternalServerException($code); + throw InternalServerError($code); } } } diff --git a/examples/openai/celest/lib/src/client/serializers.dart b/examples/openai/celest/lib/src/client/serializers.dart index b9483fc2..f7aaf268 100644 --- a/examples/openai/celest/lib/src/client/serializers.dart +++ b/examples/openai/celest/lib/src/client/serializers.dart @@ -28,10 +28,10 @@ void initSerializers() { }, )); Serializers.instance - .put(Serializer.define>( + .put(Serializer.define>( serialize: ($value) => {r'message': $value.message}, deserialize: ($serialized) { - return InternalServerException(($serialized[r'message'] as String)); + return InternalServerError(($serialized[r'message'] as String)); }, )); Serializers.instance diff --git a/examples/todo/celest/functions/tasks.dart b/examples/todo/celest/functions/tasks.dart index 84c92c42..f6d3a6c8 100644 --- a/examples/todo/celest/functions/tasks.dart +++ b/examples/todo/celest/functions/tasks.dart @@ -1,14 +1,17 @@ +import 'package:celest/celest.dart'; import 'package:celest_backend/exceptions.dart'; import 'package:celest_backend/models.dart'; import 'package:uuid/uuid.dart'; Map tasks = {}; +@cloud Future> listAllTasks() async { print('fetching tasks'); return tasks; } +@cloud Future addTask({ required String title, required Importance importance, @@ -26,11 +29,13 @@ Future addTask({ tasks[newTask.id] = newTask; } +@cloud Future deleteTask({required String id}) async { print('removing task $id'); tasks.remove(id); } +@cloud Future markAsCompleted({required String id}) async { print('marking as completed'); final task = tasks[id]; @@ -40,6 +45,7 @@ Future markAsCompleted({required String id}) async { tasks[id] = task.copyWith(isCompleted: true); } +@cloud Future markAsIncomplete({required String id}) async { print('marking as incomplete'); final task = tasks[id]; diff --git a/examples/todo/celest/generated/README.md b/examples/todo/celest/generated/README.md new file mode 100644 index 00000000..a5b5cb79 --- /dev/null +++ b/examples/todo/celest/generated/README.md @@ -0,0 +1,9 @@ +# Generated Celest code + +This directory contains code generated by the Celest CLI to assist in building +your backend. + +This code can be safely checked into version control, but it should not be +modified directly. + +It is planned to replace this directory with macros when they become stable. diff --git a/examples/todo/celest/resources.dart b/examples/todo/celest/generated/resources.dart similarity index 100% rename from examples/todo/celest/resources.dart rename to examples/todo/celest/generated/resources.dart diff --git a/examples/todo/celest/lib/src/client/functions.dart b/examples/todo/celest/lib/src/client/functions.dart index d2d2abfb..62d7d120 100644 --- a/examples/todo/celest/lib/src/client/functions.dart +++ b/examples/todo/celest/lib/src/client/functions.dart @@ -33,7 +33,7 @@ class CelestFunctionsTasks { case 400: throw BadRequestException($code); case _: - throw InternalServerException($code); + throw InternalServerError($code); } } } diff --git a/packages/celest/analysis_options.yaml b/packages/celest/analysis_options.yaml index 2cb8eb31..6d56cd09 100644 --- a/packages/celest/analysis_options.yaml +++ b/packages/celest/analysis_options.yaml @@ -6,6 +6,8 @@ analyzer: strict-inference: true strict-raw-types: true errors: + camel_case_types: ignore + # To prevent issues publishing. depend_on_referenced_packages: error public_member_api_docs: warning diff --git a/packages/celest/example/celest/functions/greeting.dart b/packages/celest/example/celest/functions/greeting.dart index b5692dfb..0f551b5d 100644 --- a/packages/celest/example/celest/functions/greeting.dart +++ b/packages/celest/example/celest/functions/greeting.dart @@ -1,10 +1,12 @@ // Cloud functions are top-level Dart functions defined in the `functions/` // folder of your Celest project. +import 'package:celest/celest.dart'; import 'package:celest_backend/exceptions/bad_name_exception.dart'; import 'package:celest_backend/models/person.dart'; /// Says hello to a [person]. +@cloud Future sayHello({required Person person}) async { if (person.name.isEmpty) { // Throw a custom exception defined in the `lib/exceptions/` and catch diff --git a/packages/celest/example/celest/generated/README.md b/packages/celest/example/celest/generated/README.md new file mode 100644 index 00000000..a5b5cb79 --- /dev/null +++ b/packages/celest/example/celest/generated/README.md @@ -0,0 +1,9 @@ +# Generated Celest code + +This directory contains code generated by the Celest CLI to assist in building +your backend. + +This code can be safely checked into version control, but it should not be +modified directly. + +It is planned to replace this directory with macros when they become stable. diff --git a/packages/celest_auth/example/celest/resources.dart b/packages/celest/example/celest/generated/resources.dart similarity index 100% rename from packages/celest_auth/example/celest/resources.dart rename to packages/celest/example/celest/generated/resources.dart diff --git a/packages/celest/example/celest/lib/client.dart b/packages/celest/example/celest/lib/client.dart index 1f1cfc16..72ea989c 100644 --- a/packages/celest/example/celest/lib/client.dart +++ b/packages/celest/example/celest/lib/client.dart @@ -6,6 +6,7 @@ library; // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:io' as _$io; +import 'package:celest_core/_internal.dart'; import 'package:celest_core/src/util/globals.dart'; import 'package:http/http.dart' as _$http; @@ -24,12 +25,16 @@ enum CelestEnvironment { }; } -class Celest { +class Celest with CelestBase { var _initialized = false; late CelestEnvironment _currentEnvironment; - late _$http.Client httpClient = _$http.Client(); + late final NativeStorage _storage = NativeStorage(scope: 'celest'); + + @override + late _$http.Client httpClient = + CelestHttpClient(secureStorage: _storage.secure); late Uri _baseUri; @@ -46,6 +51,7 @@ class Celest { CelestEnvironment get currentEnvironment => _checkInitialized(() => _currentEnvironment); + @override Uri get baseUri => _checkInitialized(() => _baseUri); CelestFunctions get functions => _checkInitialized(() => _functions); diff --git a/packages/celest/example/celest/lib/src/client/functions.dart b/packages/celest/example/celest/lib/src/client/functions.dart index 97ac8f14..f40a557a 100644 --- a/packages/celest/example/celest/lib/src/client/functions.dart +++ b/packages/celest/example/celest/lib/src/client/functions.dart @@ -10,6 +10,8 @@ import 'package:celest/celest.dart'; import 'package:celest_backend/exceptions/bad_name_exception.dart' as _$bad_name_exception; import 'package:celest_backend/models/person.dart' as _$person; +import 'package:celest_core/src/exception/cloud_exception.dart'; +import 'package:celest_core/src/exception/serialization_exception.dart'; import '../../client.dart'; @@ -26,6 +28,15 @@ class CelestFunctionsGreeting { final $code = ($error['code'] as String); final $details = ($error['details'] as Map?); switch ($code) { + case r'BadRequestException': + throw Serializers.instance.deserialize($details); + case r'UnauthorizedException': + throw Serializers.instance.deserialize($details); + case r'InternalServerError': + throw Serializers.instance.deserialize($details); + case r'SerializationException': + throw Serializers.instance + .deserialize($details); case r'BadNameException': throw Serializers.instance .deserialize<_$bad_name_exception.BadNameException>($details); @@ -34,7 +45,7 @@ class CelestFunctionsGreeting { case 400: throw BadRequestException($code); case _: - throw InternalServerException($code); + throw InternalServerError($code); } } } diff --git a/packages/celest/example/celest/lib/src/client/serializers.dart b/packages/celest/example/celest/lib/src/client/serializers.dart index 381836e8..711e4d11 100644 --- a/packages/celest/example/celest/lib/src/client/serializers.dart +++ b/packages/celest/example/celest/lib/src/client/serializers.dart @@ -5,6 +5,8 @@ import 'package:celest/celest.dart'; import 'package:celest_backend/exceptions/bad_name_exception.dart' as _$bad_name_exception; import 'package:celest_backend/models/person.dart' as _$person; +import 'package:celest_core/src/exception/cloud_exception.dart'; +import 'package:celest_core/src/exception/serialization_exception.dart'; void initSerializers() { Serializers.instance.put(Serializer.define< @@ -22,4 +24,37 @@ void initSerializers() { return _$person.Person(name: ($serialized[r'name'] as String)); }, )); + Serializers.instance + .put(Serializer.define>( + serialize: ($value) => {r'message': $value.message}, + deserialize: ($serialized) { + return BadRequestException(($serialized[r'message'] as String)); + }, + )); + Serializers.instance + .put(Serializer.define>( + serialize: ($value) => {r'message': $value.message}, + deserialize: ($serialized) { + return InternalServerError(($serialized[r'message'] as String)); + }, + )); + Serializers.instance + .put(Serializer.define?>( + serialize: ($value) => {r'message': $value.message}, + deserialize: ($serialized) { + return UnauthorizedException( + (($serialized?[r'message'] as String?)) ?? 'Unauthorized'); + }, + )); + Serializers.instance + .put(Serializer.define>( + serialize: ($value) => { + r'message': $value.message, + r'offset': $value.offset, + r'source': $value.source, + }, + deserialize: ($serialized) { + return SerializationException(($serialized[r'message'] as String)); + }, + )); } diff --git a/packages/celest/example/celest/pubspec.yaml b/packages/celest/example/celest/pubspec.yaml index da068f6f..81e49e75 100644 --- a/packages/celest/example/celest/pubspec.yaml +++ b/packages/celest/example/celest/pubspec.yaml @@ -6,8 +6,7 @@ environment: sdk: ^3.3.0 dependencies: - celest: ^0.2.0 - celest_core: ^0.2.0 + celest: ^0.4.0-0 http: ">=0.13.0 <2.0.0" dependency_overrides: diff --git a/packages/celest/example/celest/resources.dart b/packages/celest/example/celest/resources.dart deleted file mode 100644 index 9e08d4c3..00000000 --- a/packages/celest/example/celest/resources.dart +++ /dev/null @@ -1,24 +0,0 @@ -// Generated by Celest. This file should not be modified manually, but -// it can be checked into version control. -// ignore_for_file: type=lint, unused_local_variable, unnecessary_cast, unnecessary_import - -library; - -import 'package:celest/celest.dart'; - -@Deprecated('Use `Apis` instead.') -typedef apis = Apis; - -abstract final class Apis { - static const greeting = CloudApi(name: r'greeting'); -} - -@Deprecated('Use `Functions` instead.') -typedef functions = Functions; - -abstract final class Functions { - static const greetingSayHello = CloudFunction( - api: r'greeting', - functionName: r'sayHello', - ); -} diff --git a/packages/celest/lib/celest.dart b/packages/celest/lib/celest.dart index 51f01b06..ffed66da 100644 --- a/packages/celest/lib/celest.dart +++ b/packages/celest/lib/celest.dart @@ -11,6 +11,7 @@ export 'src/auth/auth_provider.dart'; export 'src/config/env.dart'; /// Core +export 'src/core/annotations.dart'; export 'src/core/cloud_widget.dart'; export 'src/core/context.dart'; export 'src/core/project.dart'; diff --git a/packages/celest/lib/fix_data.yaml b/packages/celest/lib/fix_data.yaml new file mode 100644 index 00000000..2af94adf --- /dev/null +++ b/packages/celest/lib/fix_data.yaml @@ -0,0 +1,13 @@ +version: 1 +transforms: + - title: "Change '@Context.user' to '@principal' for accessing user data" + date: 2024-05-28 + element: + uris: ["celest.dart"] + field: user + inClass: Context + changes: + - kind: replacedBy + newElement: + uris: ["celest.dart"] + variable: principal diff --git a/packages/celest/lib/http.dart b/packages/celest/lib/http.dart new file mode 100644 index 00000000..21d6fb29 --- /dev/null +++ b/packages/celest/lib/http.dart @@ -0,0 +1,12 @@ +/// HTTP annotations for Celest Functions. +/// +/// See the [docs](https://celest.dev/docs/functions/http/customization) for +/// usage and examples. +library http; + +export 'src/functions/http/http.dart'; +export 'src/functions/http/http_error.dart'; +export 'src/functions/http/http_header.dart'; +export 'src/functions/http/http_method.dart'; +export 'src/functions/http/http_query.dart'; +export 'src/functions/http/http_status.dart'; diff --git a/packages/celest/lib/src/core/annotations.dart b/packages/celest/lib/src/core/annotations.dart new file mode 100644 index 00000000..22918737 --- /dev/null +++ b/packages/celest/lib/src/core/annotations.dart @@ -0,0 +1,43 @@ +import 'package:meta/meta_meta.dart'; + +/// Marks a function or library as a cloud API. +/// +/// Celest Functions are written as normal Dart functions in the `celest/functions` +/// folder of your project. +/// +/// To turn a function into a cloud function, add the `@cloud` annotation: +/// +/// ```dart +/// // A helper function (not a cloud function). +/// String greet(String name) => 'Hello, $name!'; +/// +/// // A cloud function which exposes the greeting logic. +/// @cloud +/// Future sayHello(Person person) async { +/// return greet(person.name); +/// } +/// ``` +/// +/// For more information, see [Creating functions](https://celest.dev/docs/functions/creating-functions). +const cloud = _Cloud(); + +@Target({TargetKind.function, TargetKind.library}) +final class _Cloud { + const _Cloud(); +} + +/// Marks an extension type definition as a custom implementation or its +/// representation type. +/// +/// Custom implementations can be used to redefine the behavior of a type +/// in Celest by changing some aspects of its interface. The most common +/// use case for custom implementations is to customize serialization logic +/// for a type which you do not own. +/// +/// See the [docs](https://celest.dev/docs/functions/data-types#custom-implementations) for more information. +const customOverride = _CustomOverride(); + +@Target({TargetKind.extensionType}) +final class _CustomOverride { + const _CustomOverride(); +} diff --git a/packages/celest/lib/src/core/context.dart b/packages/celest/lib/src/core/context.dart index c4bcaf13..3d925c8f 100644 --- a/packages/celest/lib/src/core/context.dart +++ b/packages/celest/lib/src/core/context.dart @@ -1,34 +1,46 @@ import 'package:celest/celest.dart'; +/// {@template celest.core.principal} +/// A contextual reference to the principal ([User]) invoking a [CloudFunction]. +/// +/// For more information, see [Authorizing your functions](https://celest.dev/docs/functions/authorizing-functions). +/// +/// ## Example +/// +/// To inject a user into an `@authenticated` function: +/// +/// ```dart +/// @authenticated +/// Future sayHello({ +/// @principal required User user, +/// }) async { +/// print('Hello, ${user.displayName}!'); +/// } +/// ``` +/// +/// If a user is injected to a `@public` or private function, then the +/// user parameter must be nullable: +/// +/// ```dart +/// @public +/// Future sayHello({ +/// @principal User? user, +/// }) async { +/// print('Hello, ${user?.displayName ?? 'stranger'}!'); +/// } +/// ``` +/// {@endtemplate} +const principal = _UserContext(); + +/// {@template celest.core.context} /// The context of a [CloudFunction] invocation. -abstract final class Context { - /// A context reference to the [User] invoking a [CloudFunction]. - /// - /// ## Example - /// - /// To inject a user into an `@authenticated` function: - /// - /// ```dart - /// @authenticated - /// Future sayHello({ - /// @Context.user required User user, - /// }) async { - /// print('Hello, ${user.displayName}!'); - /// } - /// ``` - /// - /// If a user is injected to a `@public` or private function, then the - /// user parameter must be nullable: - /// - /// ```dart - /// @public - /// Future sayHello({ - /// @Context.user User? user, - /// }) async { - /// print('Hello, ${user?.displayName ?? 'stranger'}!'); - /// } - /// ``` - static const user = _UserContext(); +/// {@endtemplate} +final class Context { + const Context._(); + + /// {@macro celest.core.principal} + @Deprecated('Use @principal instead.') + static const Context user = principal; } final class _UserContext implements Context { diff --git a/packages/celest/lib/src/functions/http/http.dart b/packages/celest/lib/src/functions/http/http.dart new file mode 100644 index 00000000..7aebf49a --- /dev/null +++ b/packages/celest/lib/src/functions/http/http.dart @@ -0,0 +1,20 @@ +import 'package:celest/http.dart'; +import 'package:meta/meta_meta.dart'; + +/// {@template celest.functions.http} +/// HTTP configuration options for cloud functions. +/// {@endtemplate} +@Target({TargetKind.library, TargetKind.function}) +final class http { + /// {@macro celest.functions.http} + const http({ + this.method = HttpMethod.post, + this.statusCode = HttpStatus.ok, + }); + + /// The HTTP method this function supports. + final HttpMethod method; + + /// The status code returned for a successful response. + final HttpStatus statusCode; +} diff --git a/packages/celest/lib/src/functions/http/http_error.dart b/packages/celest/lib/src/functions/http/http_error.dart new file mode 100644 index 00000000..968ab849 --- /dev/null +++ b/packages/celest/lib/src/functions/http/http_error.dart @@ -0,0 +1,47 @@ +import 'package:meta/meta_meta.dart'; + +/// {@template celest.http.http_error} +/// Configures an HTTP error response for a cloud function. +/// {@endtemplate} +@Target({TargetKind.library, TargetKind.function}) +final class httpError { + /// {@macro celest.http.http_error} + const httpError( + this.statusCode, + this.type, [ + this.type1, + this.type2, + this.type3, + this.type4, + this.type5, + this.type6, + this.type7, + ]); + + /// The status code returned when any of the specified types are thrown. + final int statusCode; + + /// The error type this configuration applies to. + final Type type; + + /// Additional error type this configuration applies to. + final Type? type1; + + /// Additional error type this configuration applies to. + final Type? type2; + + /// Additional error type this configuration applies to. + final Type? type3; + + /// Additional error type this configuration applies to. + final Type? type4; + + /// Additional error type this configuration applies to. + final Type? type5; + + /// Additional error type this configuration applies to. + final Type? type6; + + /// Additional error type this configuration applies to. + final Type? type7; +} diff --git a/packages/celest/lib/src/functions/http/http_header.dart b/packages/celest/lib/src/functions/http/http_header.dart new file mode 100644 index 00000000..bf3a42cf --- /dev/null +++ b/packages/celest/lib/src/functions/http/http_header.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta_meta.dart'; + +/// {@template celest.http.http_header} +/// An HTTP header key. +/// {@endtemplate} +@Target({TargetKind.parameter}) +final class httpHeader { + /// {@macro celest.http.http_header} + const httpHeader(this.name); + + /// The name of the HTTP header. + final String name; +} diff --git a/packages/celest/lib/src/functions/http/http_method.dart b/packages/celest/lib/src/functions/http/http_method.dart new file mode 100644 index 00000000..6aeafe99 --- /dev/null +++ b/packages/celest/lib/src/functions/http/http_method.dart @@ -0,0 +1,17 @@ +/// Supported HTTP methods in Celest. +extension type const HttpMethod._(String method) implements String { + /// `GET` + static const get = HttpMethod._('GET'); + + /// `POST` + static const post = HttpMethod._('POST'); + + /// `PUT` + static const put = HttpMethod._('PUT'); + + /// `DELETE` + static const delete = HttpMethod._('DELETE'); + + /// `PATCH` + static const patch = HttpMethod._('PATCH'); +} diff --git a/packages/celest/lib/src/functions/http/http_query.dart b/packages/celest/lib/src/functions/http/http_query.dart new file mode 100644 index 00000000..ddf43d59 --- /dev/null +++ b/packages/celest/lib/src/functions/http/http_query.dart @@ -0,0 +1,13 @@ +import 'package:meta/meta_meta.dart'; + +/// {@template celest.functions.http.http_query} +/// An HTTP query parameter key. +/// {@endtemplate} +@Target({TargetKind.parameter}) +final class httpQuery { + /// {@macro celest.functions.http.http_query} + const httpQuery(this.name); + + /// The name of the HTTP query parameter. + final String name; +} diff --git a/packages/celest/lib/src/functions/http/http_status.dart b/packages/celest/lib/src/functions/http/http_status.dart new file mode 100644 index 00000000..a0906a74 --- /dev/null +++ b/packages/celest/lib/src/functions/http/http_status.dart @@ -0,0 +1,211 @@ +/// {@template celest.http.http_status} +/// An HTTP status code. +/// {@endtemplate} +extension type const HttpStatus._(int code) implements int { + /// {@macro celest.http.http_status} + const HttpStatus(this.code) + : assert( + code >= 100 && code <= 999, + 'code must be in the range 100-999', + ); + + /// `200 OK` The request succeeded. + /// + /// The result meaning of "success" depends on the HTTP method: + /// + /// - `GET`: The resource has been fetched and is transmitted in the message + /// body. + /// - `HEAD`: The representation headers are included in the response without + /// any message body. + /// - `PUT` or `POST`: The resource describing the result of the action is + /// transmitted in the message body. + /// - `TRACE`: The message body contains the request message as received by + /// the server. + static const ok = HttpStatus(200); + + /// `201 Created` The request succeeded, and a new resource was created as a + /// result. + /// + /// This is typically the response sent after `POST` requests, or some `PUT` + /// requests. + static const created = HttpStatus(201); + + /// `202 Accepted` The request has been received but not yet acted upon. + /// + /// It is noncommittal, since there is no way in HTTP to later send an + /// asynchronous response indicating the outcome of the request. It is + /// intended for cases where another process or server handles the request, + /// or for batch processing. + static const accepted = HttpStatus(202); + + /// `203 Non-Authoritative Information` This response code means the returned metadata is not exactly the + /// same as is available from the origin server, but is collected from a local + /// or a third-party copy. + /// + /// This is mostly used for mirrors or backups of another resource. Except + /// for that specific case, the `200` [ok] response is preferred to this + /// status. + static const nonAuthoritativeInformation = HttpStatus(203); + + /// `204 No Content` There is no content to send for this request, but the headers may be + /// useful. + /// + /// The user agent may update its cached headers for this resource with the + /// new ones. + static const noContent = HttpStatus(204); + + /// `205 Reset Content` Tells the user agent to reset the document which sent + /// this request. + static const resetContent = HttpStatus(205); + + /// `206 Partial Content` This response code is used when the Range header is + /// sent from the client to request only part of a resource. + static const partialContent = HttpStatus(206); + + /// `400 Bad Request` + /// + /// The server cannot or will not process the request due to something that is + /// perceived to be a client error (e.g., malformed request syntax, invalid + /// request message framing, or deceptive request routing). + static const badRequest = HttpStatus(400); + + /// `401 Unauthorized` + /// + /// Although the HTTP standard specifies "unauthorized", semantically this + /// response means "unauthenticated". That is, the client must authenticate + /// itself to get the requested response. + static const unauthorized = HttpStatus(401); + + /// `403 Forbidden` + /// + /// The client does not have access rights to the content; that is, it is + /// unauthorized, so the server is refusing to give the requested resource. + /// Unlike `401` [unauthorized], the client's identity is known to the server. + static const forbidden = HttpStatus(403); + + /// `404 Not Found` + /// + /// The server can not find the requested resource. In the browser, this means + /// the URL is not recognized. In an API, this can also mean that the endpoint + /// is valid but the resource itself does not exist. Servers may also send + /// this response instead of `403` [forbidden] to hide the existence of a + /// resource from an unauthorized client. + static const notFound = HttpStatus(404); + + /// `405 Method Not Allowed` + /// + /// The request method is known by the server but is not supported by the + /// target resource. For example, an API may not allow calling `DELETE` to + /// remove a resource. + static const methodNotAllowed = HttpStatus(405); + + /// `406 Not Acceptable` + /// + /// This response is sent when the web server, after performing server-driven + /// content negotiation, doesn't find any content that conforms to the + /// criteria given by the user agent. + static const notAcceptable = HttpStatus(406); + + /// `408 Request Timeout` + /// + /// This response is sent on an idle connection by some servers, even without + /// any previous request by the client. It means that the server would like + /// to shut down this unused connection. This response is used much more since + /// some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection + /// mechanisms to speed up surfing. Also note that some servers merely shut + /// down the connection without sending this message. + static const requestTimeout = HttpStatus(408); + + /// `409 Conflict` + /// + /// This response is sent when a request conflicts with the current state of + /// the server. + static const conflict = HttpStatus(409); + + /// `410 Gone` + /// + /// This response is sent when the requested content has been permanently + /// deleted from the server, with no forwarding address. Clients are expected + /// to remove their caches and links to the resource. The HTTP specification + /// intends this status code to be used for "limited-time, promotional + /// services". APIs should not feel compelled to indicate resources that have + /// been deleted with this status code. + static const gone = HttpStatus(410); + + /// `411 Length Required` + /// + /// Server rejected the request because the Content-Length header field is + /// not defined and the server requires it. + static const lengthRequired = HttpStatus(411); + + /// `412 Precondition Failed` + /// + /// The client has indicated preconditions in its headers which the server + /// does not meet. + static const preconditionFailed = HttpStatus(412); + + /// `413 Payload Too Large` + /// + /// Request entity is larger than limits defined by server; the server might + /// close the connection or return a `Retry-After` header field. + static const payloadTooLarge = HttpStatus(413); + + /// `414 URI Too Long` + /// + /// The URI requested by the client is longer than the server is willing to + /// interpret. + static const uriTooLong = HttpStatus(414); + + /// `415 Unsupported Media Type` + /// + /// The media format of the requested data is not supported by the server, so + /// the server is rejecting the request. + static const unsupportedMediaType = HttpStatus(415); + + /// `416 Range Not Satisfiable` + /// + /// The range specified by the `Range` header field in the request can't be + /// fulfilled; it's possible that the range is outside the size of the target + /// URI's data. + static const rangeNotSatisfiable = HttpStatus(416); + + /// `429 Too Many Requests` + /// + /// The user has sent too many requests in a given amount of time ("rate + /// limiting"). + static const tooManyRequests = HttpStatus(429); + + /// `500 Internal Server Error` + /// + /// The server has encountered a situation it doesn't know how to handle. + static const internalServerError = HttpStatus(500); + + /// `501 Not Implemented` + /// + /// The request method is not supported by the server and cannot be handled. + /// The only methods that servers are required to support (and therefore that + /// must not return this code) are `GET` and `HEAD`. + static const notImplemented = HttpStatus(501); + + /// `502 Bad Gateway` + /// + /// This error response means that the server, while working as a gateway to + /// get a response needed to handle the request, got an invalid response. + static const badGateway = HttpStatus(502); + + /// `503 Service Unavailable` + /// + /// The server is not ready to handle the request. Common causes are a server + /// that is down for maintenance or that is overloaded. Note that together + /// with this response, a user-friendly page explaining the problem should be + /// sent. This responses should be used for temporary conditions and the + /// `Retry-After` HTTP header should, if possible, contain the estimated time + /// before the recovery of the service. + static const serviceUnavailable = HttpStatus(503); + + /// `504 Gateway Timeout` + /// + /// This error response is given when the server is acting as a gateway and + /// cannot get a response in time. + static const gatewayTimeout = HttpStatus(504); +} diff --git a/packages/celest/lib/src/runtime/serve.dart b/packages/celest/lib/src/runtime/serve.dart index 0ecb38f0..f771ed0b 100644 --- a/packages/celest/lib/src/runtime/serve.dart +++ b/packages/celest/lib/src/runtime/serve.dart @@ -20,7 +20,7 @@ Future serve({ }) async { final router = Router()..get('/_health', (_) => Response.ok('OK')); for (final MapEntry(key: route, value: target) in targets.entries) { - router.post(route, target._handler); + target._apply(router, route); } final pipeline = const Pipeline() .addMiddleware(_heartbeatMiddleware) @@ -36,6 +36,7 @@ Future serve({ pipeline, InternetAddress.anyIPv4, port, + shared: true, ); print('Serving on http://localhost:$port'); await StreamGroup.merge([ @@ -73,10 +74,12 @@ abstract base class CloudFunctionTarget { } }); final response = await runZoned( - () => handle({ - r'$context': context, - ...bodyJson, - }), + () => handle( + bodyJson, + context: context, + headers: request.headersAll, + queryParameters: request.url.queryParametersAll, + ), zoneSpecification: ZoneSpecification( print: (self, parent, zone, message) { parent.print(zone, '[$name] $message'); @@ -95,13 +98,25 @@ abstract base class CloudFunctionTarget { /// The name of the [CloudFunction] this class targets. String get name; + /// The HTTP method of the [CloudFunction] this class targets. + String get method => 'POST'; + + void _apply(Router router, String route) { + router.add(method, route, _handler); + } + /// Initializes this target. /// /// This is called once when the target is instantiated. void init() {} /// Handles a JSON [request] to this target. - Future handle(Map request); + Future handle( + Map request, { + required Map context, + required Map> headers, + required Map> queryParameters, + }); } Handler _heartbeatMiddleware(Handler inner) { diff --git a/packages/celest/pubspec.yaml b/packages/celest/pubspec.yaml index 4c851a14..8ca3b135 100644 --- a/packages/celest/pubspec.yaml +++ b/packages/celest/pubspec.yaml @@ -12,7 +12,7 @@ dependencies: celest_auth: ^0.4.0-0 celest_core: ^0.4.0-0 chunked_stream: ^1.4.2 - meta: ^1.9.0 + meta: ^1.11.0 shelf: ^1.4.1 shelf_router: ^1.1.4 diff --git a/packages/celest_auth/example/celest/auth/auth.dart b/packages/celest_auth/example/celest/auth.dart similarity index 100% rename from packages/celest_auth/example/celest/auth/auth.dart rename to packages/celest_auth/example/celest/auth.dart diff --git a/packages/celest_auth/example/celest/functions/greeting.dart b/packages/celest_auth/example/celest/functions/greeting.dart index e8343b5c..68149a3e 100644 --- a/packages/celest_auth/example/celest/functions/greeting.dart +++ b/packages/celest_auth/example/celest/functions/greeting.dart @@ -4,9 +4,10 @@ import 'package:celest/celest.dart'; /// Says hello to the authenticated [user]. +@cloud @authenticated Future sayHello({ - @Context.user required User user, + @principal required User user, }) async { if (!user.emailVerified) { throw UnauthorizedException('Email not verified'); diff --git a/packages/celest_auth/example/celest/generated/README.md b/packages/celest_auth/example/celest/generated/README.md new file mode 100644 index 00000000..a5b5cb79 --- /dev/null +++ b/packages/celest_auth/example/celest/generated/README.md @@ -0,0 +1,9 @@ +# Generated Celest code + +This directory contains code generated by the Celest CLI to assist in building +your backend. + +This code can be safely checked into version control, but it should not be +modified directly. + +It is planned to replace this directory with macros when they become stable. diff --git a/packages/celest_auth/example/celest/generated/resources.dart b/packages/celest_auth/example/celest/generated/resources.dart new file mode 100644 index 00000000..ffc4fd72 --- /dev/null +++ b/packages/celest_auth/example/celest/generated/resources.dart @@ -0,0 +1,5 @@ +// Generated by Celest. This file should not be modified manually, but +// it can be checked into version control. +// ignore_for_file: type=lint, unused_local_variable, unnecessary_cast, unnecessary_import + +library; diff --git a/packages/celest_auth/example/celest/lib/client.dart b/packages/celest_auth/example/celest/lib/client.dart index c431f830..9167e2c9 100644 --- a/packages/celest_auth/example/celest/lib/client.dart +++ b/packages/celest_auth/example/celest/lib/client.dart @@ -24,8 +24,8 @@ enum CelestEnvironment { Uri get baseUri => switch (this) { local => kIsWeb || !_$io.Platform.isAndroid - ? Uri.parse('http://localhost:8888') - : Uri.parse('http://10.0.2.2:8888'), + ? Uri.parse('http://localhost:7777') + : Uri.parse('http://10.0.2.2:7777'), }; } @@ -34,7 +34,7 @@ class Celest with CelestBase { late CelestEnvironment _currentEnvironment; - late final NativeStorage _storage = NativeStorage().scoped('celest'); + late final NativeStorage _storage = NativeStorage(scope: 'celest'); @override late _$http.Client httpClient = diff --git a/packages/celest_auth/example/celest/lib/src/client/functions.dart b/packages/celest_auth/example/celest/lib/src/client/functions.dart index 48f1df90..f1560448 100644 --- a/packages/celest_auth/example/celest/lib/src/client/functions.dart +++ b/packages/celest_auth/example/celest/lib/src/client/functions.dart @@ -29,9 +29,8 @@ class CelestFunctionsGreeting { throw Serializers.instance.deserialize($details); case r'UnauthorizedException': throw Serializers.instance.deserialize($details); - case r'InternalServerException': - throw Serializers.instance - .deserialize($details); + case r'InternalServerError': + throw Serializers.instance.deserialize($details); case r'SerializationException': throw Serializers.instance .deserialize($details); @@ -40,7 +39,7 @@ class CelestFunctionsGreeting { case 400: throw BadRequestException($code); case _: - throw InternalServerException($code); + throw InternalServerError($code); } } } diff --git a/packages/celest_auth/example/celest/lib/src/client/serializers.dart b/packages/celest_auth/example/celest/lib/src/client/serializers.dart index fd4cdea5..7a68dcbf 100644 --- a/packages/celest_auth/example/celest/lib/src/client/serializers.dart +++ b/packages/celest_auth/example/celest/lib/src/client/serializers.dart @@ -13,10 +13,10 @@ void initSerializers() { }, )); Serializers.instance - .put(Serializer.define>( + .put(Serializer.define>( serialize: ($value) => {r'message': $value.message}, deserialize: ($serialized) { - return InternalServerException(($serialized[r'message'] as String)); + return InternalServerError(($serialized[r'message'] as String)); }, )); Serializers.instance diff --git a/packages/celest_auth/example/celest/pubspec.yaml b/packages/celest_auth/example/celest/pubspec.yaml index 4dda133c..4680b9cd 100644 --- a/packages/celest_auth/example/celest/pubspec.yaml +++ b/packages/celest_auth/example/celest/pubspec.yaml @@ -6,8 +6,7 @@ environment: sdk: ^3.3.0 dependencies: - celest: ^0.3.0 - celest_core: ^0.3.0 + celest: ^0.4.0-0 http: ">=0.13.0 <2.0.0" dependency_overrides: diff --git a/packages/celest_core/lib/src/exception/cloud_exception.dart b/packages/celest_core/lib/src/exception/cloud_exception.dart index 19f5998c..3f19e8eb 100644 --- a/packages/celest_core/lib/src/exception/cloud_exception.dart +++ b/packages/celest_core/lib/src/exception/cloud_exception.dart @@ -20,7 +20,13 @@ class BadRequestException implements CloudException { String toString() => 'BadRequestException: $message'; } -final class UnauthorizedException implements CloudException { +/// {@template celest_core.exception.unauthorized_exception} +/// An exception thrown by a Cloud Function when a request is not authorized. +/// {@endtemplate} +class UnauthorizedException implements CloudException { + /// Creates a [UnauthorizedException] with the given [message]. + /// + /// {@macro celest_core.exception.unauthorized_exception} const UnauthorizedException([this.message = 'Unauthorized']); @override @@ -30,19 +36,23 @@ final class UnauthorizedException implements CloudException { String toString() => 'UnauthorizedException: $message'; } -/// {@template celest_core.exception.internal_server_exception} +/// {@macro celest_core.exception.internal_server_error} +@Deprecated('Use InternalServerError instead.') +typedef InternalServerException = InternalServerError; + +/// {@template celest_core.exception.internal_server_error} /// An exception thrown by a Cloud Function when an unrecoverable internal error /// occurs. /// {@endtemplate} -class InternalServerException implements CloudException { - /// Creates a [InternalServerException] with the given [message]. +class InternalServerError extends Error implements CloudException { + /// Creates a [InternalServerError] with the given [message]. /// - /// {@macro celest_core_exception_internal_server_exception} - const InternalServerException(this.message); + /// {@macro celest_core.exception.internal_server_error} + InternalServerError(this.message); @override final String message; @override - String toString() => 'InternalServerException: $message'; + String toString() => 'InternalServerError: $message'; }