Skip to content

Commit

Permalink
feat: fix complete interface, change resp to res, improve test covera…
Browse files Browse the repository at this point in the history
…ge, add exists to buckets (#23)

* fix return type of task complete
* add file event worker
* change resp to res
* improve test coverage
* improve consistency of permissions
* fix late initialisation of workers
* consolidate channel handling
  • Loading branch information
HomelessDinosaur authored Apr 22, 2024
1 parent 370cdb4 commit 126e47d
Show file tree
Hide file tree
Showing 60 changed files with 2,186 additions and 546 deletions.
29 changes: 13 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ The SDK is in early stage development and APIs and interfaces are still subject

# Building a REST API with Nitric

This guide will show you how to build a serverless REST API with the Nitric framework using Dart. The example API enables reading, writing and editing basic user profile information using a Nitric [collection](https://nitric.io/docs/collections) to store user data. Once the API is created we'll test it locally, then optionally deploy it to a cloud of your choice.
This guide will show you how to build a serverless REST API with the Nitric framework using Dart. The example API enables reading, writing and editing basic user profile information using a Nitric [key value store](https://nitric.io/docs/keyvalue) to store user data. Once the API is created we'll test it locally, then optionally deploy it to a cloud of your choice.

The example API enables reading, writing, and deleting profile information from a Nitric [collection](https://nitric.io/docs/collection).
The example API enables reading, writing, and deleting profile information from a Nitric [key value store](https://nitric.io/docs/keyvalue).

The API will provide the following routes:

Expand Down Expand Up @@ -72,10 +72,8 @@ dart create -t console my-profile-api
Add the Nitric SDK by adding the repository URL to your `pubspec.yaml`.

```yaml
nitric_sdk:
git:
url: https://github.com/nitrictech/dart-sdk.git
ref: main
dependencies:
nitric_sdk: ^1.2.0
```
Next, open the project in your editor of choice.
Expand Down Expand Up @@ -112,7 +110,7 @@ services:
## Create a Profile class
We will create a class to represent the profiles that we will store in the collection. We will add `toJson` and `fromJson` functions to assist.
We will create a class to represent the profiles that we will store in the key value store. We will add `toJson` and `fromJson` functions to assist.

```dart
class Profile {
Expand Down Expand Up @@ -143,19 +141,18 @@ Applications built with Nitric can contain many APIs, let's start by adding one
```dart
import 'package:nitric_sdk/nitric.dart';
import 'package:nitric_sdk/resources.dart';
import 'package:nitric_sdk/src/context/common.dart';
import 'package:uuid/uuid.dart';
void main() {
// Create an API named 'public'
final profileApi = api("public");
final profileApi = Nitric.api("public");
// Define a key value store named 'profiles', then request getting, setting and deleting permissions.
final profiles = store("profiles").requires([
KeyValuePermission.getting,
KeyValuePermission.setting,
KeyValuePermission.deleting
// Define a key value store named 'profiles', then request get, set and delete permissions.
final profiles = Nitric.kv("profiles").allow([
KeyValuePermission.get,
KeyValuePermission.set,
KeyValuePermission.delete
]);
}
```
Expand Down Expand Up @@ -188,7 +185,7 @@ profileApi.post("/profiles", (ctx) async {
final profile = Profile.fromJson(ctx.req.json());
// Store the new profile in the profiles collection
// Store the new profile in the profiles kv store
await profiles.set(id, profile);
// Send a success response.
Expand Down Expand Up @@ -335,7 +332,7 @@ If you want to go a bit deeper and create some other resources with Nitric, why
Define a bucket named `profilesImg` with reading/writing permissions.

```dart
final profilesImg = Nitric.bucket("profilesImg").requires([BucketPermission.reading, BucketPermission.writing]);
final profilesImg = Nitric.bucket("profilesImg").allow([BucketPermission.read, BucketPermission.write]);
```

### Get a URL to upload a profile image
Expand Down
8 changes: 4 additions & 4 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,17 @@ include: package:lints/recommended.yaml

# Uncomment the following section to specify additional rules.

# linter:
# rules:
# - camel_case_types
linter:
rules:
- camel_case_types
- unawaited_futures

analyzer:
exclude:
- lib/**/*.pb.dart
- lib/**/*.pbenum.dart
- lib/**/*.pbgrpb.dart
- lib/**/*.pbjson.dart

# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints

Expand Down
3 changes: 2 additions & 1 deletion example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ environment:
sdk: ^3.2.5

dependencies:
nitric_sdk: ^1.0.0
nitric_sdk:
path: ../
uuid: ^4.3.3

dev_dependencies:
Expand Down
42 changes: 17 additions & 25 deletions example/services/nitric_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,26 +21,18 @@ class Profile {
}

void main() {
var oidc = Nitric.oidcRule(
"profile security",
"https://dev-w7gm5ldb.us.auth0.com",
["https://test-security-definition/"]);

// Create an API named 'public'
final profileApi = Nitric.api("public",
opts: ApiOptions(security: [
oidc(["user:read"])
]));
final profileApi = Nitric.api("public");

// Define a collection named 'profiles', then request reading and writing permissions.
final profiles = Nitric.store("profiles").requires([
KeyValueStorePermission.getting,
KeyValueStorePermission.deleting,
KeyValueStorePermission.setting
final profiles = Nitric.kv("profiles").allow([
KeyValueStorePermission.get,
KeyValueStorePermission.delete,
KeyValueStorePermission.set
]);

final profilesImg = Nitric.bucket("profilesImg")
.requires([BucketPermission.reading, BucketPermission.writing]);
.allow([BucketPermission.read, BucketPermission.write]);

profileApi.post("/profiles", (ctx) async {
final uuid = Uuid();
Expand All @@ -53,10 +45,10 @@ void main() {
await profiles.set(id, profile.toJson());

// Send a success response.
ctx.resp.body = "Profile $id created.";
ctx.res.body = "Profile $id created.";
} on Exception catch (e) {
ctx.resp.status = 400;
ctx.resp.body = "An error occurred: $e";
ctx.res.status = 400;
ctx.res.body = "An error occurred: $e";
}

return ctx;
Expand All @@ -68,11 +60,11 @@ void main() {
try {
// Retrieve and return the profile data
final profile = await profiles.get(id);
ctx.resp.json(profile);
ctx.res.json(profile);
} on Exception catch (e) {
print(e);
ctx.resp.status = 404;
ctx.resp.body = "Profile $id not found.";
ctx.res.status = 404;
ctx.res.body = "Profile $id not found.";
}

return ctx;
Expand All @@ -84,10 +76,10 @@ void main() {
// Delete the profile
try {
await profiles.delete(id);
ctx.resp.body = "Profile $id removed.";
ctx.res.body = "Profile $id removed.";
} on Exception catch (e) {
ctx.resp.status = 404;
ctx.resp.body = "Profile $id not found. $e";
ctx.res.status = 404;
ctx.res.body = "Profile $id not found. $e";
}

return ctx;
Expand Down Expand Up @@ -124,8 +116,8 @@ void main() {
final photoUrl =
await profilesImg.file("images/$id/photo.png").getDownloadUrl();

ctx.resp.status = 303;
ctx.resp.headers["Location"] = [photoUrl];
ctx.res.status = 303;
ctx.res.headers["Location"] = [photoUrl];

return ctx;
});
Expand Down
1 change: 0 additions & 1 deletion lib/api.dart

This file was deleted.

1 change: 1 addition & 0 deletions lib/nitric.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
library;

export 'src/nitric.dart';
export 'src/context/common.dart';
3 changes: 1 addition & 2 deletions lib/resources.dart
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
export 'src/resources/resources.dart';
export 'src/context/common.dart';
export 'src/resources/common.dart';
69 changes: 13 additions & 56 deletions lib/src/api/bucket.dart
Original file line number Diff line number Diff line change
@@ -1,28 +1,31 @@
import 'dart:async';
import 'dart:convert';

import 'package:nitric_sdk/nitric.dart';
import 'package:nitric_sdk/src/context/common.dart';
import 'package:nitric_sdk/src/grpc_helper.dart';
import 'package:nitric_sdk/src/nitric/proto/storage/v1/storage.pbgrpc.dart'
as $p;
import 'package:nitric_sdk/src/google/protobuf/duration.pb.dart' as $d;
import 'package:fixnum/fixnum.dart';
import 'package:grpc/grpc.dart';
import 'package:nitric_sdk/src/workers/common.dart';

class Bucket {
late $p.StorageClient _storageClient;
late $p.StorageListenerClient? _storageListenerClient;

String name;

Bucket(this.name, {$p.StorageClient? client}) {
Bucket(this.name,
{$p.StorageClient? client,
$p.StorageListenerClient? storageListenerClient}) {
if (client == null) {
final channel = createClientChannelFromEnvVar();

_storageClient = $p.StorageClient(channel);
_storageClient =
$p.StorageClient(ClientChannelSingleton.instance.clientChannel);
} else {
_storageClient = client;
}

_storageListenerClient = storageListenerClient;
}

/// Get a reference to a file by it's [key].
Expand All @@ -42,7 +45,7 @@ class Bucket {

/// Create a blob event subscription triggered on the [blobEventType] filtered by files that match the [keyPrefixFilter].
Future<void> on(BlobEventType blobEventType, String keyPrefixFilter,
BlobEventHandler handler) async {
FileEventHandler handler) async {
// Create the request to register the Storage listener with the membrane
final eventType = switch (blobEventType) {
BlobEventType.write => $p.BlobEventType.Created,
Expand All @@ -55,9 +58,10 @@ class Bucket {
blobEventType: eventType,
);

var worker = BlobEventWorker(registrationRequest, handler, this);
var worker = FileEventWorker(registrationRequest, handler, this,
client: _storageListenerClient);

worker.start();
await worker.start();
}
}

Expand Down Expand Up @@ -149,50 +153,3 @@ class File {
return resp.url;
}
}

class BlobEventWorker implements Worker {
$p.RegistrationRequest registrationRequest;
BlobEventHandler middleware;
Bucket bucket;

BlobEventWorker(this.registrationRequest, this.middleware, this.bucket);

@override
Future<void> start() async {
// Create Storage listener client
final channel = createClientChannelFromEnvVar();
final client = $p.StorageListenerClient(channel);

final initMsg = $p.ClientMessage(registrationRequest: registrationRequest);

// Create the request stream and send the initial message
final requestStream = StreamController<$p.ClientMessage>();
requestStream.add(initMsg);

final response = client.listen(
requestStream.stream,
);

await for (final msg in response) {
if (msg.hasRegistrationResponse()) {
// Blob Notification has connected with Nitric server
} else if (msg.hasBlobEventRequest()) {
var ctx = BlobEventContext.fromRequest(msg, bucket);

try {
ctx = await middleware(ctx);
} on GrpcError catch (e) {
print("caught a GrpcError: $e");
} catch (e) {
print("unhandled application error: $e");

ctx.resp.success = false;
}

requestStream.add(ctx.toResponse());
}
}

await channel.shutdown();
}
}
5 changes: 2 additions & 3 deletions lib/src/api/keyvalue.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ class KeyValueStore {

KeyValueStore(this.name, {$p.KvStoreClient? client}) {
if (client == null) {
var channel = createClientChannelFromEnvVar();

_keyValueClient = $p.KvStoreClient(channel);
_keyValueClient =
$p.KvStoreClient(ClientChannelSingleton.instance.clientChannel);
} else {
_keyValueClient = client;
}
Expand Down
9 changes: 4 additions & 5 deletions lib/src/api/queue.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:async';

import 'package:nitric_sdk/api.dart';
import 'package:nitric_sdk/src/api/api.dart';
import 'package:nitric_sdk/src/grpc_helper.dart';
import 'package:nitric_sdk/src/nitric/proto/queues/v1/queues.pbgrpc.dart' as $p;

Expand All @@ -13,9 +13,8 @@ class Queue {
/// Construct a new queue.
Queue(this.name, {$p.QueuesClient? client}) {
if (client == null) {
final channel = createClientChannelFromEnvVar();

_queuesClient = $p.QueuesClient(channel);
_queuesClient =
$p.QueuesClient(ClientChannelSingleton.instance.clientChannel);
} else {
_queuesClient = client;
}
Expand Down Expand Up @@ -61,7 +60,7 @@ class DequeuedMessage {
}

/// Inform the queue that the message was handled successfully.
void complete() async {
Future<void> complete() async {
var req =
$p.QueueCompleteRequest(leaseId: _leaseId, queueName: _queue.name);

Expand Down
5 changes: 2 additions & 3 deletions lib/src/api/secret.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,8 @@ class Secret {

Secret(this.name, {$p.SecretManagerClient? client}) {
if (client == null) {
final channel = createClientChannelFromEnvVar();

_secretClient = $p.SecretManagerClient(channel);
_secretClient =
$p.SecretManagerClient(ClientChannelSingleton.instance.clientChannel);
} else {
_secretClient = client;
}
Expand Down
5 changes: 2 additions & 3 deletions lib/src/api/topic.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,8 @@ class Topic {

Topic(this.name, {$p.TopicsClient? client}) {
if (client == null) {
final channel = createClientChannelFromEnvVar();

_topicsClient = $p.TopicsClient(channel);
_topicsClient =
$p.TopicsClient(ClientChannelSingleton.instance.clientChannel);
} else {
_topicsClient = client;
}
Expand Down
Loading

0 comments on commit 126e47d

Please sign in to comment.