Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Atlas functions support #973

Merged
merged 21 commits into from
Oct 25, 2022
Merged
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class _Address {
late String city;
}
```
* Added `User.functions`. This is the entry point for calling Atlas App functions. Functions allow you to define and execute server-side logic for your application. Atlas App functions are created on the server, written in modern JavaScript (ES6+) and executed in a serverless manner. When you call a function, you can dynamically access components of the current application as well as information about the request to execute the function and the logged in user that sent the request. ([#973](https://github.com/realm/realm-dart/pull/973))

### Fixed
* Added more validations when using `User.apiKeys` to return more meaningful errors when the user cannot perform API key actions - e.g. when the user has been logged in with API key credentials or when the user has been logged out. (Issue [#950](https://github.com/realm/realm-dart/issues/950))
Expand Down
8 changes: 4 additions & 4 deletions ffigen/README.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
This package configures the ffigen to generate bindings to realm-core library.
The is executed manually as needed and this package is never published.

Usage:
Usage:

dart run ffigen --config config.yaml

On linux you may need to install clang 11 dev tools. If you are using apt-get you can do:
```
sudo apt-get install libclang-11-dev
```
```

Note: Dart ffigen tool generated platform dependent bindings which are tailored for the host platform the ffigen is executed. Currently all of platforms generate identical output with the configuration workaround here: https://github.com/dart-lang/ffigen/pull/119.
Note: Dart ffigen tool generated platform dependent bindings which are tailored for the host platform the ffigen is executed. Currently all of platforms generate identical output with the configuration workaround here: https://github.com/dart-lang/ffigen/pull/119.

Dart has an issues to generate platform independent bindings here:
Dart has an issues to generate platform independent bindings here:
https://github.com/dart-lang/sdk/issues/42563
https://github.com/dart-lang/ffigen/issues/7
16 changes: 16 additions & 0 deletions lib/src/cli/atlas_apps/baas_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,22 @@ class BaasClient {
return { status: 'fail' };
}
};''';

static const String _authFuncSource = '''exports = (loginPayload) => {
return loginPayload["userId"];
};''';

static const String _userFuncNoArgs = '''exports = function(){
return {};
};''';

static const String _userFuncOneArg = '''exports = function(arg){
return {'arg': arg };
};''';

static const String _userFuncTwoArgs = '''exports = function(arg1, arg2){
return { 'arg1': arg1, 'arg2': arg2};
};''';
static const String defaultAppName = "flexible";

final String _baseUrl;
Expand Down Expand Up @@ -168,6 +181,9 @@ class BaasClient {
final confirmFuncId = await _createFunction(app, 'confirmFunc', _confirmFuncSource);
final resetFuncId = await _createFunction(app, 'resetFunc', _resetFuncSource);
final authFuncId = await _createFunction(app, 'authFunc', _authFuncSource);
await _createFunction(app, 'userFuncNoArgs', _userFuncNoArgs);
await _createFunction(app, 'userFuncOneArg', _userFuncOneArg);
await _createFunction(app, 'userFuncTwoArgs', _userFuncTwoArgs);

await enableProvider(app, 'anon-user');
await enableProvider(app, 'local-userpass', config: '''{
Expand Down
30 changes: 30 additions & 0 deletions lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2106,6 +2106,36 @@ class _RealmCore {
return completer.future;
});
}

static void _call_app_function_callback(Pointer<Void> userdata, Pointer<Char> response, Pointer<realm_app_error> error) {
final Completer<String>? completer = userdata.toObject(isPersistent: true);
if (completer == null) {
return;
}
if (error != nullptr) {
completer.completeWithAppError(error);
return;
}

final stringResponse = response.cast<Utf8>().toRealmDartString()!;
completer.complete(stringResponse);
}

Future<String> callAppFunction(App app, User user, String functionName, String? argsAsJSON) {
return using((arena) {
final completer = Completer<String>();
desistefanova marked this conversation as resolved.
Show resolved Hide resolved
_realmLib.invokeGetBool(() => _realmLib.realm_app_call_function(
app.handle._pointer,
user.handle._pointer,
functionName.toCharPtr(arena),
argsAsJSON?.toCharPtr(arena) ?? nullptr,
Pointer.fromFunction(_call_app_function_callback),
completer.toPersistentHandle(),
_realmLib.addresses.realm_dart_delete_persistent_handle,
));
return completer.future;
});
}
}

class LastError {
Expand Down
2 changes: 1 addition & 1 deletion lib/src/realm_class.dart
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ export 'realm_object.dart'
export 'realm_property.dart';
export 'results.dart' show RealmResults, RealmResultsChanges;
export 'subscription.dart' show Subscription, SubscriptionSet, SubscriptionSetState, MutableSubscriptionSet;
export 'user.dart' show User, UserState, UserIdentity, ApiKeyClient, ApiKey;
export 'user.dart' show User, UserState, UserIdentity, ApiKeyClient, ApiKey, FunctionsClient;
export 'session.dart' show Session, SessionState, ConnectionState, ProgressDirection, ProgressMode, SyncProgress, ConnectionStateChange;
export 'migration.dart' show Migration;
export 'package:cancellation_token/cancellation_token.dart' show CancellationToken, CancelledException;
Expand Down
27 changes: 27 additions & 0 deletions lib/src/user.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ class User {
}

late final ApiKeyClient _apiKeys = ApiKeyClient._(this);
FunctionsClient? _functions
blagoev marked this conversation as resolved.
Show resolved Hide resolved

/// Gets an [ApiKeyClient] instance that exposes functionality for managing
/// user API keys.
Expand All @@ -51,6 +52,15 @@ class User {
return _apiKeys;
}

/// Gets a [FunctionsClient] instance that exposes functionality for calling remote Atlas Functions.
/// A [FunctionsClient] instance scoped to this [User].
/// [Atlas Functions Docs](https://docs.mongodb.com/realm/functions/)
FunctionsClient get functions {
_ensureLoggedIn('access API keys');

return _functions ??= FunctionsClient._(this);;
desistefanova marked this conversation as resolved.
Show resolved Hide resolved
}

User._(this._handle, this._app);

/// The current state of this [User].
Expand Down Expand Up @@ -298,6 +308,23 @@ class ApiKey {
int get hashCode => id.hashCode;
}

/// A class exposing functionality for calling remote Atlas Functions.
class FunctionsClient {
final User _user;

FunctionsClient._(this._user);

/// Calls a remote function with the supplied arguments.
/// @name The name of the Atlas function to call.
/// @functionArgs - Arguments that will be sent to the Atlas function. They have to be json serializable values.
Future<dynamic> call(String name, [List<Object?>? functionArgs]) async {
nirinchev marked this conversation as resolved.
Show resolved Hide resolved
_user._ensureLoggedIn('call Atlas function');
final args = functionArgs != null ? jsonEncode(functionArgs) : null;
final response = await realmCore.callAppFunction(_user.app, _user, name, args);
return jsonDecode(response);
}
}

/// @nodoc
extension UserIdentityInternal on UserIdentity {
static UserIdentity create(String identity, AuthProviderType provider) => UserIdentity._(identity, provider);
Expand Down
65 changes: 65 additions & 0 deletions test/app_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -232,6 +232,66 @@ Future<void> main([List<String>? args]) async {

await expectLater(() => loginWithRetry(app, Credentials.emailPassword(username, strongPassword)), throws<AppException>("invalid username/password"));
});

baasTest('Call Atlas function that does not exist', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
await expectLater(user.functions.call('noFunc'), throws<AppException>("function not found: 'noFunc'"));
});

baasTest('Call Atlas function with no arguments', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
final dynamic response = await user.functions.call('userFuncNoArgs');
expect(response, isNotNull);
});

baasTest('Call Atlas function with one argument', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
final arg1 = 'Jhonatan';
final dynamic response = await user.functions.call('userFuncOneArg', [arg1]);
expect(response, isNotNull);
final map = response as Map<String, dynamic>;
expect(map['arg'], arg1);
});

baasTest('Call Atlas function with two arguments', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
final arg1 = 'Jhonatan';
final arg2 = 'Michael';
final dynamic response = await user.functions.call('userFuncTwoArgs', [arg1, arg2]);
expect(response, isNotNull);
final map = response as Map<String, dynamic>;
expect(map['arg1'], arg1);
expect(map['arg2'], arg2);
});

baasTest('Call Atlas function with two arguments but pass one', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
final arg1 = 'Jhonatan';
final dynamic response = await user.functions.call('userFuncTwoArgs', [arg1]);
expect(response, isNotNull);
final map = response as Map<String, dynamic>;
expect(map['arg1'], arg1);
expect(map['arg2'], <String, dynamic>{'\$undefined': true});
});

baasTest('Call Atlas function with two Object arguments', (configuration) async {
final app = App(configuration);
final user = await app.logIn(Credentials.anonymous());
final arg1 = Person("Jhonatan");
final arg2 = Person('Michael');
final dynamic response = await user.functions.call('userFuncTwoArgs', [arg1.toJson(), arg2.toJson()]);
expect(response, isNotNull);
final map = response as Map<String, Map<String, dynamic>>;
final receivedPerson1 = PersonJ.fromJson(map['arg1']);
final receivedPerson2 = PersonJ.fromJson(map['arg2']);
expect(receivedPerson1.name, arg1.name);
expect(receivedPerson2.name, arg2.name);
});
}

Future<void> testLogger(
Expand Down Expand Up @@ -267,3 +327,8 @@ Future<void> testLogger(
expect(e.value, greaterThanOrEqualTo(minExpectedCounts[e.key] ?? minInt), reason: '${e.key}');
}
}

extension PersonJ on Person {
static Person fromJson(Map<String, dynamic> json) => Person(json['name'] as String);
Map<String, dynamic> toJson() => <String, dynamic>{'name': name};
}