Skip to content

Commit

Permalink
Add user context
Browse files Browse the repository at this point in the history
  • Loading branch information
dnys1 committed Mar 9, 2024
1 parent 9dda3fc commit 5a7c4f9
Show file tree
Hide file tree
Showing 8 changed files with 57 additions and 83 deletions.
26 changes: 16 additions & 10 deletions packages/celest/lib/src/core/context.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import 'package:celest/celest.dart';

/// The context of a [CloudFunction] invocation.
abstract final class Context {
const Context._();

/// A context reference to the [User] invoking a [CloudFunction].
///
/// ## Example
Expand All @@ -13,18 +11,26 @@ abstract final class Context {
/// ```dart
/// @authenticated
/// Future<void> sayHello({
/// @Context.user() required User user,
/// @Context.user required User user,
/// }) async {
/// print('Hello, ${user.displayName}!');
/// }
/// ```
const factory Context.user() = _ContextKey.user;
///
/// If a user is injected to a `@public` or private function, then the
/// user parameter must be nullable:
///
/// ```dart
/// @public
/// Future<void> sayHello({
/// @Context.user User? user,
/// }) async {
/// print('Hello, ${user?.displayName ?? 'stranger'}!');
/// }
/// ```
static const user = _UserContext();
}

final class _ContextKey extends Context {
const _ContextKey._(this.key) : super._();

const _ContextKey.user() : this._('user');

final String key;
final class _UserContext implements Context {
const _UserContext();
}
8 changes: 7 additions & 1 deletion packages/celest/lib/src/runtime/serve.dart
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,14 @@ abstract base class CloudFunctionTarget {

Future<Response> _handler(Request request) async {
final bodyJson = await request.decodeJson();
const contextHeaderPrefix = 'X-Celest-Context-';
final response = await runZoned(
() => handle(bodyJson),
() => handle({
for (final MapEntry(:key, :value) in request.headers.entries)
if (key.startsWith(contextHeaderPrefix))
key.substring(contextHeaderPrefix.length): value,
...bodyJson,
}),
zoneSpecification: ZoneSpecification(
print: (self, parent, zone, message) {
parent.print(zone, '[$name] $message');
Expand Down
20 changes: 8 additions & 12 deletions packages/celest_auth/example/celest/functions/greeting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,24 +2,20 @@
// 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 the authenticated [user].
// @authenticated
@authenticated
Future<String> sayHello({
required Person person,
// @Context.user() required User user,
@Context.user required User user,
}) async {
if (person.name.isEmpty) {
// Throw a custom exception defined in the `lib/exceptions/` and catch
// it on the frontend.
throw BadNameException('Name cannot be empty');
if (!user.emailVerified) {
throw UnauthorizedException('Email not verified');
}

// Logging is handled automatically when you print to the console.
print('Saying hello to ${person.name}');
print('Saying hello to user: $user');

return 'Hello, ${person.name}!';
if (user.displayName case final name?) {
return 'Hello, $name!';
}
return 'Hello!';
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ library; // ignore_for_file: no_leading_underscores_for_library_prefixes
import 'dart:convert' as _$convert;

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';

Expand Down Expand Up @@ -38,9 +35,6 @@ class CelestFunctionsGreeting {
case r'SerializationException':
throw Serializers.instance
.deserialize<SerializationException>($details);
case r'BadNameException':
throw Serializers.instance
.deserialize<_$bad_name_exception.BadNameException>($details);
case _:
switch ($statusCode) {
case 400:
Expand All @@ -52,12 +46,10 @@ class CelestFunctionsGreeting {
}

/// Says hello to the authenticated [user].
Future<String> sayHello({required _$person.Person person}) async {
Future<String> sayHello() async {
final $response = await celest.httpClient.post(
celest.baseUri.resolve('/greeting/say-hello'),
headers: const {'Content-Type': 'application/json; charset=utf-8'},
body: _$convert.jsonEncode(
{r'person': Serializers.instance.serialize<_$person.Person>(person)}),
);
final $body =
(_$convert.jsonDecode($response.body) as Map<String, Object?>);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,10 @@
// ignore_for_file: type=lint, unused_local_variable, unnecessary_cast, unnecessary_import

// ignore_for_file: no_leading_underscores_for_library_prefixes
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<
_$bad_name_exception.BadNameException, Map<String, Object?>>(
serialize: ($value) => {r'message': $value.message},
deserialize: ($serialized) {
return _$bad_name_exception.BadNameException(
($serialized[r'message'] as String));
},
));
Serializers.instance
.put(Serializer.define<_$person.Person, Map<String, Object?>>(
serialize: ($value) => {r'name': $value.name},
deserialize: ($serialized) {
return _$person.Person(name: ($serialized[r'name'] as String));
},
));
Serializers.instance
.put(Serializer.define<BadRequestException, Map<String, Object?>>(
serialize: ($value) => {r'message': $value.message},
Expand Down
19 changes: 0 additions & 19 deletions packages/celest_auth/example/celest/resources.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,3 @@
// 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',
);
}
Original file line number Diff line number Diff line change
@@ -1,18 +1,34 @@
import 'package:celest_backend/exceptions/bad_name_exception.dart';
import 'package:celest_backend/models/person.dart';
import 'package:celest/celest.dart';
import 'package:test/test.dart';

import '../../functions/greeting.dart';

void main() {
group('greeting', () {
test('sayHello', () async {
expect(await sayHello(person: Person(name: 'Celest')), 'Hello, Celest!');
expect(
await sayHello(
user: User(
userId: '123',
email: '[email protected]',
emailVerified: true,
displayName: 'Celest',
),
),
'Hello, Celest!',
);
});
test('sayHello (empty name)', () async {
test('sayHello (email not verified)', () async {
expect(
sayHello(person: Person(name: '')),
throwsA(isA<BadNameException>()),
sayHello(
user: User(
userId: '123',
email: '[email protected]',
emailVerified: false,
displayName: 'Celest',
),
),
throwsA(isA<UnauthorizedException>()),
);
});
});
Expand Down
10 changes: 3 additions & 7 deletions packages/celest_auth/example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:celest_backend/client.dart';
import 'package:celest_backend/models/person.dart';
import 'package:flutter/material.dart';

void main() {
Expand Down Expand Up @@ -53,12 +52,9 @@ class _MainAppState extends State<MainApp> {
const SizedBox(height: 16),
TextButton(
onPressed: () {
_request = celest.functions.greeting.sayHello(
person: const Person(name: 'Dillon'),
);
if (context.mounted) {
setState(() {});
}
setState(() {
_request = celest.functions.greeting.sayHello();
});
},
child: const Text('Make authenticated request'),
),
Expand Down

0 comments on commit 5a7c4f9

Please sign in to comment.