Skip to content

Commit

Permalink
v0.8 — Accounts, Transactions & Api Codes (#18)
Browse files Browse the repository at this point in the history
* Implemented Account model

* Implemented Account Routes & fixed Session#delete

* Implemented error codes

* Bumped version & updated CHANGELOG.md

* Implemented `Transaction`s

* Updated CHANGELOG.md

* dart fix

* Added missing .toUtc()

* Added some more logging

* Load accounts on startup

* Added `Account#retrieveTransactions`

* Added `Restrr#getCurrencies`

* Updated CHANGELOG.md

* Implemented `TransactionType`

* Implemented `Account#getCurrency` & `Account#retrieveCurrency`

* Implemented `Transaction#getSourceAccount`, `Transaction#getDestinationAccount`, `Transaction#retrieveSourceAccount` & `Transaction#retrieveDestinationAccount`

* Fixed `Account#update` (for now)

* Fixed `Restrr#createTransaction` (for now)

* Added missing toUtc()

* Correctly use _id suffix for Ids

* Implemented per-Entity Ids (#19)

* Implemented per-Entity Ids

* Cleaned up generics

* Id => EntityId, IdPrimitive => Id

* Fixed exposed id-parameters

* EntityId#id => EntityId#value + added toString, == & hashCode overrides

* Fixed entity building

* Made ErrorResponse#reference dynamic

* Fixed missing EntityId#value calls & added UserId to CustomCurrency

* Added Transaction#name

* Added RequestUtils#deleteSingle & fixed cache entity deletion

* Implemented previous cleanup on Account and User

* Updated CHANGELOG.md

* dart format

* Fixed & extended tests

* Fixed update methods

* Fixed Transaction#update

* Added try-catch for preflight cache requests

* fixed comment
  • Loading branch information
jasonlessenich committed Mar 31, 2024
1 parent a56d582 commit 4289ab8
Show file tree
Hide file tree
Showing 31 changed files with 843 additions and 134 deletions.
26 changes: 26 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
## 0.8
- Cache accounts & currencies on startup
- Added `Restrr#getAccounts`
- Added `Restrr#getCurrencies`
- Added entity-specific Ids:
- Added `EntityId<E>` methods: `get`, `retrieve`
- Added `AccountId`
- Added `TransactionId`
- Added `CurrencyId`
- Added `UserId`
- Added `PartialSessionId`
- Entities now feature the corresponding `EntityId<E>` instead of a `Id (int)`
- Added `Account`
- Added `AccountRoutes`
- Added `Account` methods: `delete`, `update`, `retrieveAllTransactions`
- Added `Restrr` methods: `createAccount`, `retrieveAccountById`, `retrieveAllAccounts`
- Added `Transaction`
- Added `TransactionRoutes`
- Added `Transaction` methods: `delete`, `update`
- Added `Restrr` methods: `createTransaction`, `retrieveTransactionById`, `retrieveAllTransactions`
- Fixed `Session#delete` using a wrong route
- Unified entity deletion
- Implemented actual `RestrrError` error codes
- Added `ErrorResponse#apiCode`
- Made `ErrorResponse#reference` `dynamic`

## 0.7
- Restructured package (many breaking changes!)
- Split package into `api` (abstraction) and `internal` (implementation)
Expand Down
5 changes: 4 additions & 1 deletion lib/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@ export 'src/api/entities/currency/currency.dart';
export 'src/api/entities/currency/custom_currency.dart';
export 'src/api/entities/session/partial_session.dart';
export 'src/api/entities/session/session.dart';
export 'src/api/entities/user.dart';
export 'src/api/entities/account.dart';
export 'src/api/entities/restrr_entity.dart';
export 'src/api/entities/transaction/transaction.dart';
export 'src/api/entities/transaction/transaction_type.dart';
export 'src/api/entities/user.dart';

/* [ /src/api/events ] */
export 'src/api/events/ready_event.dart';
Expand Down
22 changes: 22 additions & 0 deletions lib/src/api/entities/account.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import '../../../restrr.dart';

abstract class AccountId extends EntityId<Account> {}

abstract class Account extends RestrrEntity<Account, AccountId> {
@override
AccountId get id;

String get name;
String? get description;
String? get iban;
int get balance;
int get originalBalance;
CurrencyId get currencyId;
DateTime get createdAt;

Future<bool> delete();

Future<Account> update({String? name, String? description, String? iban, int? originalBalance, Id? currencyId});

Future<Paginated<Transaction>> retrieveAllTransactions({int page = 1, int limit = 25, bool forceRetrieve = false});
}
7 changes: 6 additions & 1 deletion lib/src/api/entities/currency/currency.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import '../../../../restrr.dart';

abstract class Currency extends RestrrEntity {
abstract class CurrencyId extends EntityId<Currency> {}

abstract class Currency extends RestrrEntity<Currency, CurrencyId> {
@override
CurrencyId get id;

String get name;
String get symbol;
int get decimalPlaces;
Expand Down
2 changes: 1 addition & 1 deletion lib/src/api/entities/currency/custom_currency.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:restrr/restrr.dart';

abstract class CustomCurrency extends Currency {
int? get user;
UserId? get userId;

bool isCreatedBy(User user);

Expand Down
26 changes: 24 additions & 2 deletions lib/src/api/entities/restrr_entity.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,33 @@ import '../../../restrr.dart';

typedef Id = int;

abstract class EntityId<E> {
Restrr get api;
Id get value;

E? get();
Future<E> retrieve({forceRetrieve = false});

@override
String toString() => value.toString();

@override
bool operator ==(Object other) {
if (other is EntityId<E>) {
return other.value == value;
}
return false;
}

@override
int get hashCode => value.hashCode;
}

/// The base class for all Restrr entities.
/// This simply provides a reference to the Restrr instance.
abstract class RestrrEntity {
abstract class RestrrEntity<E, ID extends EntityId<E>> {
/// A reference to the Restrr instance.
Restrr get api;

Id get id;
ID get id;
}
7 changes: 6 additions & 1 deletion lib/src/api/entities/session/partial_session.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import '../../../../restrr.dart';

abstract class PartialSession extends RestrrEntity {
abstract class PartialSessionId extends EntityId<PartialSession> {}

abstract class PartialSession extends RestrrEntity<PartialSession, PartialSessionId> {
@override
PartialSessionId get id;

String? get name;
DateTime get createdAt;
DateTime get expiresAt;
Expand Down
33 changes: 33 additions & 0 deletions lib/src/api/entities/transaction/transaction.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'package:restrr/restrr.dart';

abstract class TransactionId extends EntityId<Transaction> {}

abstract class Transaction extends RestrrEntity<Transaction, TransactionId> {
@override
TransactionId get id;

AccountId? get sourceId;
AccountId? get destinationId;
int get amount;
CurrencyId get currencyId;
String get name;
String? get description;
EntityId? get budgetId;
DateTime get createdAt;
DateTime get executedAt;

TransactionType get type;

Future<bool> delete();

Future<Transaction> update({
Id? sourceId,
Id? destinationId,
int? amount,
Id? currencyId,
String? name,
String? description,
Id? budgetId,
DateTime? executedAt,
});
}
1 change: 1 addition & 0 deletions lib/src/api/entities/transaction/transaction_type.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enum TransactionType { deposit, withdrawal, transfer }
4 changes: 3 additions & 1 deletion lib/src/api/entities/user.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import '../../../restrr.dart';

abstract class User extends RestrrEntity {
abstract class UserId extends EntityId<User> {}

abstract class User extends RestrrEntity<User, UserId> {
String get username;
String? get email;
String? get displayName;
Expand Down
17 changes: 11 additions & 6 deletions lib/src/api/requests/route.dart
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,17 @@ class CompiledRoute {
if (bearerToken != null) {
headers['Authorization'] = 'Bearer $bearerToken';
}
return dio.fetch(RequestOptions(
path: compiledRoute,
headers: headers,
data: body,
method: baseRoute.method,
baseUrl: _buildBaseUrl(routeOptions, baseRoute.isVersioned)));
return dio
.fetch(RequestOptions(
path: compiledRoute,
headers: headers,
data: body,
method: baseRoute.method,
baseUrl: _buildBaseUrl(routeOptions, baseRoute.isVersioned)))
.then((response) {
Restrr.log.info('${baseRoute.method} $compiledRoute => ${response.statusCode} ${response.statusMessage}');
return response;
});
}

String _buildBaseUrl(RouteOptions options, bool isVersioned) {
Expand Down
23 changes: 22 additions & 1 deletion lib/src/api/requests/route_definitions.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,33 @@ class UserRoutes {
static final Route create = Route.post('/user/register');
}

class AccountRoutes {
const AccountRoutes._();

static final Route getAll = Route.get('/account');
static final Route getById = Route.get('/account/{accountId}');
static final Route deleteById = Route.delete('/account/{accountId}');
static final Route patchById = Route.patch('/account/{accountId}');
static final Route getTransactions = Route.get('/account/{accountId}/transactions');
static final Route create = Route.post('/account');
}

class CurrencyRoutes {
const CurrencyRoutes._();

static final Route getAll = Route.get('/currency');
static final Route create = Route.post('/currency');
static final Route getById = Route.get('/currency/{currencyId}');
static final Route deleteById = Route.delete('/currency/{currencyId}');
static final Route updateById = Route.patch('/currency/{currencyId}');
static final Route patchById = Route.patch('/currency/{currencyId}');
}

class TransactionRoutes {
const TransactionRoutes._();

static final Route getAll = Route.get('/transaction');
static final Route getById = Route.get('/transaction/{transactionId}');
static final Route deleteById = Route.delete('/transaction/{transactionId}');
static final Route patchById = Route.patch('/transaction/{transactionId}');
static final Route create = Route.post('/transaction');
}
29 changes: 29 additions & 0 deletions lib/src/api/restrr.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,12 +60,41 @@ abstract class Restrr {

Future<bool> deleteAllSessions();

/* Accounts */

Future<Account> createAccount(
{required String name, required int originalBalance, required Id currencyId, String? description, String? iban});

List<Account> getAccounts();

Future<Account> retrieveAccountById(Id id, {bool forceRetrieve = false});

Future<Paginated<Account>> retrieveAllAccounts({int page = 1, int limit = 25, bool forceRetrieve = false});

/* Currencies */

Future<Currency> createCurrency(
{required String name, required String symbol, required int decimalPlaces, String? isoCode});

List<Currency> getCurrencies();

Future<Currency> retrieveCurrencyById(Id id, {bool forceRetrieve = false});

Future<Paginated<Currency>> retrieveAllCurrencies({int page = 1, int limit = 25, bool forceRetrieve = false});

/* Transactions */

Future<Transaction> createTransaction(
{required int amount,
required Id currencyId,
required DateTime executedAt,
required String name,
String? description,
Id? sourceId,
Id? destinationId,
Id? budgetId});

Future<Transaction> retrieveTransactionById(Id id, {bool forceRetrieve = false});

Future<Paginated<Transaction>> retrieveAllTransactions({int page = 1, int limit = 25, bool forceRetrieve = false});
}
26 changes: 23 additions & 3 deletions lib/src/api/restrr_builder.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../../restrr.dart';
import '../internal/requests/responses/rest_response.dart';
import '../internal/restrr_impl.dart';
import '../internal/utils/request_utils.dart';

/// A builder for creating a new [Restrr] instance.
/// The [Restrr] instance is created by calling [create].
Expand Down Expand Up @@ -28,7 +29,7 @@ class RestrrBuilder {
'session_name': sessionName,
},
noAuth: true,
mapper: (json) => apiImpl.entityBuilder.buildSession(json));
mapper: (json) => apiImpl.entityBuilder.buildPartialSession(json));
});
}

Expand All @@ -37,11 +38,12 @@ class RestrrBuilder {
return apiImpl.requestHandler.apiRequest(
route: SessionRoutes.refresh.compile(),
bearerTokenOverride: sessionToken,
mapper: (json) => apiImpl.entityBuilder.buildSession(json));
mapper: (json) => apiImpl.entityBuilder.buildPartialSession(json));
});
}

Future<Restrr> _handleAuthProcess({required Future<RestResponse<PartialSession>> Function(RestrrImpl) authFunction}) async {
Future<Restrr> _handleAuthProcess(
{required Future<RestResponse<PartialSession>> Function(RestrrImpl) authFunction}) async {
// check if the URI is valid and the API is healthy
final ServerInfo statusResponse = await Restrr.checkUri(uri, isWeb: options.isWeb);
Restrr.log.config('Host: $uri, API v${statusResponse.apiVersion}');
Expand All @@ -59,6 +61,24 @@ class RestrrBuilder {
throw ArgumentError('The response data is not a session');
}
apiImpl.session = response.data! as Session;

// Retrieve all accounts & currencies to make them available in the cache
try {
final List<Account> accounts =
await RequestUtils.fetchAllPaginated<Account, AccountId>(apiImpl, await apiImpl.retrieveAllAccounts(limit: 50));
Restrr.log.info('Cached ${accounts.length} account(s)');
} catch (e) {
Restrr.log.warning('Failed to cache accounts: $e');
}

try {
final List<Currency> currencies = await RequestUtils.fetchAllPaginated<Currency, CurrencyId>(
apiImpl, await apiImpl.retrieveAllCurrencies(limit: 50));
Restrr.log.info('Cached ${currencies.length} currencies');
} catch (e) {
Restrr.log.warning('Failed to cache currencies: $e');
}

apiImpl.eventHandler.fire(ReadyEvent(api: apiImpl));
return apiImpl;
}
Expand Down
8 changes: 4 additions & 4 deletions lib/src/internal/cache/batch_cache_view.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@ import 'package:restrr/src/internal/restrr_impl.dart';

import '../../../restrr.dart';

class BatchCacheView<T extends RestrrEntity> {
class BatchCacheView<E extends RestrrEntity<E, ID>, ID extends EntityId<E>> {
final RestrrImpl api;

BatchCacheView(this.api);

List<T>? _lastSnapshot;
List<E>? _lastSnapshot;

List<T>? get() => _lastSnapshot;
List<E>? get() => _lastSnapshot;

void update(List<T> value) => _lastSnapshot = value;
void update(List<E> value) => _lastSnapshot = value;

void clear() => _lastSnapshot = null;

Expand Down
11 changes: 8 additions & 3 deletions lib/src/internal/cache/cache_view.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import '../../../restrr.dart';
import '../restrr_impl.dart';

class EntityCacheView<E extends RestrrEntity> extends MapCacheView<Id, E> {
EntityCacheView(RestrrImpl api) : super(api, valueFunction: (entity) => entity.id);
class EntityCacheView<E extends RestrrEntity<E, ID>, ID extends EntityId<E>> extends MapCacheView<int, E> {
EntityCacheView(RestrrImpl api) : super(api, valueFunction: (entity) => entity.id.value);
}

class PageCacheView<T extends RestrrEntity> extends MapCacheView<(int, int), Paginated<T>> {
class PageCacheView<E extends RestrrEntity<E, ID>, ID extends EntityId<E>>
extends MapCacheView<(int, int), Paginated<E>> {
PageCacheView(RestrrImpl api) : super(api, valueFunction: (page) => (page.pageNumber, page.limit));
}

Expand All @@ -19,8 +20,12 @@ abstract class MapCacheView<K, V> {

V? get(K key) => _cache[key];

List<V> getAll() => _cache.values.toList();

V cache(V value) => _cache[valueFunction.call(value)] = value;

V? remove(K key) => _cache.remove(key);

void clear() => _cache.clear();

bool contains(K key) => _cache.containsKey(key);
Expand Down
Loading

0 comments on commit 4289ab8

Please sign in to comment.