An open API service indexing awesome lists of open source software.

https://github.com/maxbritto/directus_api_manager

Dart & Flutter library to communicate with a Directus REST API
https://github.com/maxbritto/directus_api_manager

dart directus flutter rest rest-api

Last synced: about 1 month ago
JSON representation

Dart & Flutter library to communicate with a Directus REST API

Awesome Lists containing this project

README

          

Communicate with a Directus server using its REST API.

## Features

This packages can generate model classes for your each of your Directus collections.

## Install

You need to add 1 dependencies : (will be added to your app)
- directus_api_manager

And 2 dev dependencies : (will be only be used at build time to generate the model classes)
- reflectable_builder
- build_runner

Add the packages as a dependencies in your pubspec.yaml file :

```yaml
dependencies:
flutter:
sdk: flutter

directus_api_manager: ^1.11.0 #replace by latest version

dev_dependencies:
build_runner: any
reflectable_builder: any
```

## Getting started

### Create your models

For each directus model, create a new class that inherits `DirectusItem` and use annotations to specify the èndpointName`:

```dart
@DirectusCollection()
@CollectionMetadata(endpointName: "player")
class PlayerDirectusModel extends DirectusItem {
}
```

This `endpointName` is the name of the collection in Directus : use the exact same name you used when creating your directus collection, including capitalized letters.

**Important :** You must include the init method that calls the one from `super` and passes the raw received data, without adding any other parameter.

```dart
PlayerDirectusModel(super.rawReceivedData);
```

You can create other named constructors if you want.
If you intend to create new items and send them to your server, you should override the secondary init method named `newItem()` :

```dart
PlayerDirectusModel.newItem() : super.newItem();
```

Add any property you need as computed property using inner functions to access your data :

```dart
String get nickname => getValue(forKey: "nickname");

int get bestScore => getValue(forKey: "best_score");
set bestScore(int newBestScore) =>
setValue(newBestScore, forKey: "best_score");
```

The _key_ is the name of the property in your directus collection, you must use the same types in your directus collection as in your Dart computed properties.

## Generate the code for your models

Every time you add a new collection, you can trigger the generator for your project:
In your project folder, execute this line :

```bash
dart run build_runner build lib
```

It will add new `.reflectable.dart` files in your projects : do not include those files in your git repository.
**Tip :** Add this line at the end of your `.gitignore` file :

```
*.reflectable.dart
```

## Inititalize the library to use the generated models

```dart
void main() {
initializeReflectable();
//...
// rest of your app
}
```

### Create your DirectusApiManager

This object is the one that will handle everything for you :

- authentication and token management
- sending request
- parsing responses
- etc.

You should only create one object of this type and it only requires the url of your Directus instance :

```dart
DirectusApiManager _directusApiManager = DirectusApiManager(baseURL: "http://0.0.0.0:8055/");
```

### Manage users and Authentication

To authenticate use the `loginDirectusUser` method before making request that needs to be authorized:

```dart
final apiManager = DirectusApiManager(baseURL: "http://0.0.0.0:8055/");
final result = await apiManager.loginDirectusUser("will@acn.com", "will-password");
if (result.type == DirectusLoginResultType.success) {
print("User logged in");
} else if (result.type == DirectusLoginResultType.invalidCredentials) {
print("Please verify entered credentials");
} else if (result.type == DirectusLoginResultType.invalidOTP) {
print("Please provide OTP");
// Keep email and password for the next screen or extra field and resubmit using
// await apiManager.loginDirectusUser("will@acn.com", "will-password", oneTimePassword: "123456");
} else if (result.type == DirectusLoginResultType.error) {
print("An unknown error occured");
final additionalMessage = result.message;
if (additionalMessage != null) {
print("More information : $additionalMessage");
}
}
```

If the user's login requires MFA/OTP, you should present an extra field or page to the user to complete, and resend the authentication with the `oneTimePassword` parameter:

```dart
final result = await apiManager.loginDirectusUser("will@acn.com", "will-password", oneTimePassword: "123456");
```

All future request of this `apiManager` instance will include this user token.

### OTP Email Authentication

Directus does not yet support OTP email authentication natively. However, you can use our OTP email authentication extension to add this functionality to your Directus server : https://github.com/maxbritto/directus-extension-otp-auth

Then, with the OTP email authentication extension installed, you can use passwordless login via a one-time code sent by email.

First, request a code to be sent to the user's email:

```dart
final codeSent = await apiManager.requestOtpCode(email: "will@acn.com");
if (codeSent) {
print("A one-time code has been sent to your email");
}
```

Then, once the user has received and entered the code, verify it to log in:

```dart
final result = await apiManager.loginDirectusUserWithOtp(
email: "will@acn.com", otpCode: "123456");
if (result.type == DirectusLoginResultType.success) {
print("User logged in");
} else if (result.type == DirectusLoginResultType.invalidOTP) {
print("Invalid code, please try again");
} else if (result.type == DirectusLoginResultType.requestsExceeded) {
print("Too many attempts, please wait before trying again");
} else if (result.type == DirectusLoginResultType.error) {
print("An error occurred: ${result.message}");
}
```

On success, the user is fully authenticated and all future requests will include their token, just like with `loginDirectusUser`.

### CRUD for your collections

For each collection you can either :

- fetch one or multiple items
- update items
- create items
- delete items

## Creating new items

```dart
final newPlayer = PlayerDirectusModel.newItem(nickname: "Sheldon");
final creationResult =
await apiManager.createNewItem(objectToCreate: newPlayer);
if (creationResult.isSuccess) {
print("Created player!");
final createdPlayer = creationResult.createdItem;
// depending on your directus server authorization you might not have access to the created item
if (createdPlayer != null) {
print("The id of this new player is ${createdPlayer.id}");
}
} else {
final error = creationResult.error;
if (error != null) {
print("Error while creating player : $error");
}
}
```

## Fetching existing items

```dart
//Multiple items
final list = await apiManager.findListOfItems();
for (final player in list) {
print("${player.nickname} - ${player.bestScore}");
}

//One specific item from an ID
final PlayerDirectusModel onePlayer = await apiManager.getSpecificItem(id: "1");
print(onePlayer.nickname);
```

## Update existing items

```dart
final PlayerDirectusModel onePlayer = await apiManager.getSpecificItem(id: "1");
onePlayer.bestScore = 123;
final updatedPlayer = await apiManager.updateItem(objectToUpdate: onePlayer);
```

## Web Socket support

### DirectusWebSocket

Web sockets allow your directus server to send you events as they occur and your app can react to those changes. This lib helps you set up `DirectusWebSocketSubscription` objects to register for those events. The lib will handle the authentication, the refresh token process and keep the connection alive automatically. You just have to start a subscription and stop a subscription when needed.

### DirectusWebSocketSubscription

`DirectusWebSocketSubscription` represent a subscription to your Directus server. Here are the mandatory properties to use it :

- `uid` must be specified, the format can be any text that is unique between 2 subscriptions. When the server will send a message, this uid will be provided. This allow us to know from which subscription this message came from.
- `onCreate`, `onUpdate`, `onDelete` callbacks are trigger when a subscription receive a subscription message. They are all optional but the `DirectusWebSocketSubscription` must have at least one of them.

```dart
DirectusWebSocketSubscription(
uid: "directus_data_extension_uid",
onCreate: onCreate,
onUpdate: onUpdate,
onDelete: onDelete,
sort: const [SortProperty("id")],
limit: 10,
offset: 10
filter: const PropertyFilter(
field: "folder",
operator: FilterOperator.equals,
value: "folder_id"));
```

### start and stop a subscription

First create your subscription and then call the `startWebsocketSubscription` method on your `DirectusApiManager` instance to start it.
```dart
await apiManager.startWebsocketSubscription(subscription);
```

When you no longer need the subscription, you can stop it by calling the `stopWebsocketSubscription` method on your `DirectusApiManager` instance and providing the uid of the subscription.
```dart
await apiManager.stopWebsocketSubscription(subscription.uid);
```

## Cache system

### Enabling and configuring the Cache system

This api comes with a caching system that can be enabled by providing an instance of `ILocalDirectusCacheInterface` when creating your `DirectusApiManager` instance.

Currently 2 ready to use implementations are provided :

- The `JsonCacheEngine` class will store the data in a folder of your choosing using json files.
- The `MemoryCacheEngine` class will store the data in memory only. If you use it inside a Flutter app, the cache will emptied on each app restart

Example :

```dart
import 'package:path_provider/path_provider.dart';

void main() async {
final directory = await getApplicationCacheDirectory();
final apiManager = DirectusApiManager(baseURL: "http://0.0.0.0:8055/", cacheEngine: JsonCacheEngine(cacheFolderPath: "${directory.path}/directus_api_cache"));
// ...
}
```
You can decide to replace the json file implementation by creating and supplying your own implementation which implements the `ILocalDirectusCacheInterface` class.

### Using the Cache system for your requests

All read requests (get, find, currentUser, etc.) now have optional parameters to configure the cache. By default, most of those will save responses but will only use those if the next request fails.
If you want to also replace future responses by a local cache read, you can set the `canUseCacheForResponse` parameter to `true` and tweak the `maxCacheAge` parameter to set the maximum age of the cache (defaults to 1 day). This will prevent calling the directus server if a valid cache exists for the same request.

```dart
await apiManager.getSpecificItem(
id: "element1",
canUseCacheForResponse: true,
maxCacheAge: const Duration(days: 1));
```

If you want to disable the cache completely for a request, you can set the `canSaveResponseToCache` parameter to `false`.

```dart
await apiManager.getSpecificItem(
id: "element1",
canSaveResponseToCache: false);
```

By default, an expired cache can still be used if the real network request fails. If you want to disable this behavior, you can set the `canUseOldCachedResponseAsFallback` parameter to `false`.

```dart
await apiManager.getSpecificItem(
id: "element1",
canUseOldCachedResponseAsFallback: false);
```

Those parameters are available for all read based requests.

### Clearing the cache before it expires

The engine tries to be smart and will regularly invalidate caches when it performs modifications on the same type of data. For example :
- if you create a new item, the cache for the list of items will be invalidated.
- If you update an item, the cache for this specific item will be invalidated, as long as any list for that type of object.
- If you delete an item, the cache for this specific item will be invalidated, as long as any list for that type of object.

We suggest you rely mostly on automatic cache invalidation, but you can also manually clear the cache for specific requests.

#### Clearing cache for a specific object

You can use the [clearCacheForObject] function to clear the cache for a specific object. The object must be of type extending `DirectusData`.
If you only have the id of the object, you can use the [clearCacheForObjectWithId] function with the type hinted :

```dart
await apiManager.clearCacheForObjectWithId("element1");
```

#### Clearing the current user cache

The current user is a specific cache and for it, you can use the [discardCurrentUserCache] function.

#### Clearing all caches

Logging out the current user will automatically clear all the cached data.

#### Clearing specific caches with the cache key

Each cached object has a key that is used to store and retrieve it. This key is usually generated automatically based on the request, but you can provide your own cache key with the `requestIdentifier` parameter available on every `read` based method.

```dart
await apiManager.getSpecificItem(
id: "element1",
requestIdentifier: "my_custom_key");
```

Then you can use the [clearCacheWithKey] function to clear the cache associated with this key.

```dart
await apiManager.clearCacheWithKey("my_custom_key");
```

#### Clearing specific caches with tags

You can associate a set of tags with every read you perform. Those tags will be associated with the cached data, and can be use to invalidate those before the cache expiration time.

```dart
await sut.getSpecificItem(
id: "element1",
extraTags: ["tag1", "tag2"]);
await sut.getSpecificItem(
id: "element2",
extraTags: ["tag3"]);
await sut.getSpecificItem(
id: "element3",
extraTags: ["tag2", "tag4"]);

```
Those parameters are available for all read based requests.

Then you can use the [removeCacheEntriesWithTags] function to clear the caches entries associated with those tags.

```dart
await apiManager.removeCacheEntriesWithTags(["tag1", "tag3"]); //this will invalidate element1 and element2 from the example above
```

You can also use the `List extraTagsToClear` parameter present in each modification based method (create, update, delete) to clear the cache associated with those tags if the call succeeds.

## Additional information

### Install

If you want to use a specific version, it can be done in addition to the git ur:

```yaml
directus_api_manager:
git: https://github.com/maxbritto/directus_api_manager.git
version: ^1.2.0
```

### Advanced properties :

If your collections have advanced properties that needs specific parsing you can do it in the computed properties.
Here is an example of a properties of type _Tag list_ in Directus, inside we can enter some courses ids as number but Directus consider all tags as Strings. So we convert them in the dart code like this :

```dart
List get requiredCourseIdList {
final courseListJson = getValue(forKey: "required_course_id_list");
if (courseListJson is List) {
return courseListJson
.map((courseIdString) => int.parse(courseIdString))
.toList();
} else {
return [];
}
}
```

### Filtering

You can filter your items using the `filter` parameter. The filter can be a single filter or a combination of filters using logical operators.

#### Property Filter

A property filter is used to filter items based on a property value. Here are some examples:

```dart
// Filter by exact match
final filter = PropertyFilter(
field: "title",
operator: FilterOperator.equals,
value: "Hello World!");

// Filter by contains
final filter = PropertyFilter(
field: "title",
operator: FilterOperator.contains,
value: "Hello");

// Filter by greater than
final filter = PropertyFilter(
field: "score",
operator: FilterOperator.greaterThan,
value: 10);

// Filter by between
final filter = PropertyFilter(
field: "score",
operator: FilterOperator.between,
value: [10, 100]);

// Filter by is null
final filter = PropertyFilter(
field: "description",
operator: FilterOperator.isNull,
value: null);
```

#### Logical Filter

A logical filter is used to combine multiple filters using logical operators. Here are some examples:

```dart
// AND operator
final filter = LogicalOperatorFilter(
operator: LogicalOperator.and,
children: [
PropertyFilter(
field: "title",
operator: FilterOperator.contains,
value: "Hello"),
PropertyFilter(
field: "description",
operator: FilterOperator.equals,
value: "world"),
]);

// OR operator
final filter = LogicalOperatorFilter(
operator: LogicalOperator.or,
children: [
PropertyFilter(
field: "title",
operator: FilterOperator.contains,
value: "Hello"),
PropertyFilter(
field: "description",
operator: FilterOperator.equals,
value: "world"),
]);
```

#### Relation Filter

A relation filter is used to filter items based on related items. Here are some examples:

```dart
// Filter by related item
final filter = RelationFilter(
propertyName: "users",
linkedObjectFilter: PropertyFilter(
field: "id",
operator: FilterOperator.equals,
value: "23"));

// Filter by M2M relation
final filter = RelationFilter(
propertyName: "words",
linkedObjectFilter: RelationFilter(
propertyName: "idWord",
linkedObjectFilter: PropertyFilter(
field: "word",
operator: FilterOperator.equals,
value: "zelda")));
```

#### Geo Filter

A geo filter is used to filter items based on their geographical location. This is particularly useful for finding items within a specific geographical area. Here are some examples:

```dart
// Filter by rectangle area
final rectangle = GeoJsonPolygon.rectangle(
topLeft: [longitude1, latitude1],
bottomRight: [longitude2, latitude2],
);

final filter = GeoFilter(
field: "location",
operator: GeoFilterOperator.intersectsBbox,
feature: rectangle,
);

// Filter by custom polygon area
final polygon = GeoJsonPolygon.polygon(
points: [
[longitude1, latitude1],
[longitude2, latitude1],
[longitude2, latitude2],
[longitude1, latitude2],
],
);

final filter = GeoFilter(
field: "location",
operator: GeoFilterOperator.intersectsBbox,
feature: polygon,
);

// Filter by square area from center point
final square = GeoJsonPolygon.squareFromCenter(
center: [longitude, latitude],
distanceInMeters: 400, // Distance in meters
);

final filter = GeoFilter(
field: "location",
operator: GeoFilterOperator.intersectsBbox,
feature: square,
);

// Combine geo filter with other filters
final combinedFilter = LogicalOperatorFilter(
operator: LogicalOperator.and,
children: [
geoFilter,
PropertyFilter(
field: "type",
operator: FilterOperator.equals,
value: "restaurant",
),
],
);
```

The `GeoJsonPolygon` class provides three constructors for creating different types of geographical areas:
- `rectangle`: Creates a rectangular area defined by top-left and bottom-right coordinates
- `polygon`: Creates a custom polygon area defined by a list of coordinates
- `squareFromCenter`: Creates a square area centered at a specific point with a given distance in meters

All polygons are automatically closed (the last point connects back to the first point) to ensure valid GeoJSON format.