diff --git a/.claude/skills/add-api-caller/SKILL.md b/.claude/skills/add-api-caller/SKILL.md new file mode 100644 index 00000000..60d0e75c --- /dev/null +++ b/.claude/skills/add-api-caller/SKILL.md @@ -0,0 +1,409 @@ +--- +name: add-api-caller +description: Add GraphQL API caller methods (fetch, fetchAll, save, delete/expire) to an existing model pair in a layrz_models sub-library module. +argument-hint: +--- + +# Add GraphQL API caller methods to a model pair + +Add `fetch()`, `fetchAll()`, `delete()`/`expire()` methods to a `@freezed` model and a `save()` method +to its `@unfreezed` input variant, following the established pattern from `Locator`/`LocatorInput`. + +## Usage + +``` +/add-api-caller +``` + +Where `` is the sub-library directory name (e.g. `locator`, `map`) and `` is the +PascalCase class name (e.g. `Locator`, `Poi`). + +## Workflow + +### 0. Enter plan mode + +Use the `EnterPlanMode` tool immediately before doing any work. Present the full plan to the user +and wait for approval via `ExitPlanMode` before writing any files or running any commands. + +### 1. Identify the target model pair + +Read `lib/src//src/.dart` (or whichever file contains the model). + +- Confirm both `@freezed ModelName` and `@unfreezed ModelNameInput` exist. +- Note the field names, especially `id` (needed for single-entity operations). + +### 2. Collect GraphQL details from the user + +Ask the user for the following. Accept a backend schema or operation list as a single paste if available. + +| Detail | Example | +|---|---| +| GraphQL **query name** for fetching | `locators`, `pois` | +| GraphQL **add mutation name** | `addLocator`, `addPoi` | +| GraphQL **edit mutation name** | `editLocator`, `editPoi` | +| GraphQL **delete/expire mutation name** | `expireLocators`, `deletePois` (or skip if none) | +| Destructive operation style | `delete` or `expire` | +| GraphQL **input type** name | `LocatorInput!`, `PoiInput!` | +| GraphQL **fragment fields** | Paste or describe; can also be derived from the `@freezed` model fields | +| `fetchAll` query style | Full fragment, or lighter subset of fields (ask the user) | +| Fragment name (camelCase) | `locatorFragment`, `poiFragment` | +| GraphQL type name (PascalCase) | `Locator`, `Poi` | + +If the fragment composes fragments from other models (e.g. `RegisteredApp.registeredAppFragment`), +ask the user which fragments to include. + +### 3. Add API methods to the `@freezed` model + +If `const ModelName._();` is not already present, add it right after the opening `{` of the class body, +before the `const factory` constructor. + +Then add the following **after** the `fromJson` factory, inside the class body: + +#### `fetch()` — instance method + +```dart +/// [fetch] fetches a single [ModelName] from the server by its ID +Future fetch({ + /// [apiToken] is the API token to use for authentication + required String apiToken, + + /// [uri] is the GraphQL endpoint to use + required Uri uri, + + /// [onResponse] is an optional callback invoked with the status code + void Function(String statusCode)? onResponse, +}) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: fetchSingleQuery, + variables: {'apiToken': apiToken, 'id': id}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/fetch(): No response from server"); + return null; + } + + final result = data['data']['queryName']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/fetch(): No result from server"); + return null; + } + + if (result['status'] != 'OK') { + onResponse?.call(result['status']); + return null; + } + if (result['result'] == null || (result['result'] as List).isEmpty) { + onResponse?.call('NOT_FOUND'); + return null; + } + + return ModelName.fromJson(Map.from(result['result'][0] as Map)); + } catch (e, stack) { + Log.critical("layrz_models/ModelName/fetch(): General exception => $e\n$stack"); + return null; + } +} +``` + +#### `fetchAll()` — static method + +```dart +/// [fetchAll] fetches all [ModelName] from the server +static Future> fetchAll({ + required String apiToken, + required Uri uri, + void Function(String statusCode)? onResponse, +}) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: fetchAllGraphqlQuery, + variables: {'apiToken': apiToken}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/fetchAll(): No response from server"); + return []; + } + + final result = data['data']['queryName']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/fetchAll(): No result from server"); + return []; + } + + if (result['status'] != 'OK') { + onResponse?.call(result['status']); + return []; + } + + return (result['result'] as List?) + ?.map((e) => ModelName.fromJson(Map.from(e as Map))) + .toList() ?? + []; + } catch (e, stack) { + Log.critical("layrz_models/ModelName/fetchAll(): General exception => $e\n$stack"); + return []; + } +} +``` + +#### `delete()`/`expire()` — instance method (skip if not applicable) + +```dart +/// [expire] expires this [ModelName] +Future expire({ + required String apiToken, + required Uri uri, + void Function(String statusCode)? onResponse, +}) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: expireGraphqlMutation, + variables: {'apiToken': apiToken, 'ids': [id]}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/expire(): No response from server"); + return false; + } + + final result = data['data']['expireModelNames']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/expire(): No result from server"); + return false; + } + + if (result['status'] != 'OK') { + onResponse?.call(result['status']); + return false; + } + + return true; + } catch (e, stack) { + Log.critical("layrz_models/ModelName/expire(): General exception => $e\n$stack"); + return false; + } +} +``` + +#### `deleteMultiple()`/`expireMultiple()` — static method (skip if not applicable) + +```dart +/// [expireMultiple] expires multiple [ModelName] by their IDs +static Future expireMultiple({ + required String apiToken, + required Uri uri, + required List ids, + void Function(String statusCode)? onResponse, +}) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: expireGraphqlMutation, + variables: {'apiToken': apiToken, 'ids': ids}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/expireMultiple(): No response from server"); + return false; + } + + final result = data['data']['expireModelNames']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelName/expireMultiple(): No result from server"); + return false; + } + + if (result['status'] != 'OK') { + onResponse?.call(result['status']); + return false; + } + + return true; + } catch (e, stack) { + Log.critical("layrz_models/ModelName/expireMultiple(): General exception => $e\n$stack"); + return false; + } +} +``` + +#### Static GraphQL string getters + +```dart +/// [fetchSingleQuery] is the GraphQL query to fetch a single [ModelName] by its ID +static String get fetchSingleQuery => + '${ModelName.graphqlFragment}' + r''' + query fetchModelNames($apiToken: String!, $id: ID) { + queryName(apiToken: $apiToken, id: $id) { + status + errors + result { + ...modelNameFragment + } + } + } + '''; + +/// [fetchAllGraphqlQuery] is the GraphQL query to fetch all [ModelName]s +static String get fetchAllGraphqlQuery => r''' + query fetchModelNames($apiToken: String!) { + queryName(apiToken: $apiToken) { + status + errors + result { + // fields here — full fragment or lighter subset per user preference + } + } + } + '''; + +/// [graphqlFragment] is the GraphQL fragment for [ModelName] +static String get graphqlFragment => ''' + // compose sub-fragments first, e.g.: \${OtherModel.otherFragment} + + fragment modelNameFragment on GraphQLTypeName { + // all fields + } + '''; + +/// [expireGraphqlMutation] is the GraphQL mutation to expire one or more [ModelName]s +static String get expireGraphqlMutation => r''' + mutation expireModelName($apiToken: String!, $ids: [ID!]!) { + expireModelNames(apiToken: $apiToken, ids: $ids) { + status + errors + } + } + '''; +``` + +### 4. Add `save()` to the `@unfreezed` input model + +If `const ModelNameInput._();` is not already present, add it right after the opening `{` of the class body, +before the `factory` constructor. + +Then add the following **after** the `fromJson` factory, inside the class body: + +```dart +/// [save] creates or updates this [ModelName] on the server +/// Returns an [ApiResponse] with the saved [ModelName] on success, or errors on failure. +/// Returns `null` on a network/server error. +Future>?> save({ + required String apiToken, + required Uri uri, + void Function(String statusCode)? onResponse, +}) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: id == null ? addGraphqlMutation : editGraphqlMutation, + variables: {'apiToken': apiToken, 'data': toJson()}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelNameInput/save(): No response from server"); + return null; + } + + final result = id == null ? data['data']['addModelName'] : data['data']['editModelName']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/ModelNameInput/save(): No result from server"); + return null; + } + + if (result['status'] != 'OK') { + onResponse?.call(result['status']); + return ApiResponse( + status: ApiStatus.fromJson(result['status']), + errors: Map.from(result['errors'] ?? {}), + ); + } + + return ApiResponse(status: ApiStatus.ok, result: ModelName.fromJson(result['result'])); + } catch (e, stack) { + Log.critical("layrz_models/ModelNameInput/save(): General exception => $e\n$stack"); + return null; + } +} + +/// [addGraphqlMutation] is the GraphQL mutation to add a [ModelName] +static String get addGraphqlMutation => + '${ModelName.graphqlFragment}' + r''' + mutation addModelName($apiToken: String!, $data: ModelNameInput!) { + addModelName(data: $data, apiToken: $apiToken) { + status + errors + result { + ...modelNameFragment + } + } + } + '''; + +/// [editGraphqlMutation] is the GraphQL mutation to edit a [ModelName] +static String get editGraphqlMutation => + '${ModelName.graphqlFragment}' + r''' + mutation editModelName($apiToken: String!, $data: ModelNameInput!) { + editModelName(data: $data, apiToken: $apiToken) { + status + errors + result { + ...modelNameFragment + } + } + } + '''; +``` + +### 5. Add required imports to the module's main `.dart` file + +Ensure `lib/src//.dart` contains these imports (add if missing): + +```dart +import 'package:layrz_models/src/utils/utils.dart'; +import 'package:layrz_models/src/api/api.dart'; +``` + +### 6. Run code generation + +```bash +make freezed +``` + +### 7. Report + +List the modified files and whether the build succeeded. + +## Rules + +- **Never** manually edit `.freezed.dart` or `.g.dart` files. +- Follow the exact patterns from `Locator`/`LocatorInput` (`lib/src/locator/`) — do not invent new patterns. +- Every added method and getter must have a dartdoc `/// [name] description` comment. +- GraphQL fragments can compose sub-fragments from other models: `'${OtherModel.fragment}' r'''...'''` +- The `fetchAll` query body may differ from the `fetchSingleQuery` (lighter fields for list views) — always ask the user. +- If the model has no `id` field or no destructive operation, skip those methods. +- If the user says the backend does not support a specific operation (e.g., no delete), omit it. +- Log paths must follow the convention: `"layrz_models/ClassName/methodName(): message"`. diff --git a/.claude/skills/add-model/SKILL.md b/.claude/skills/add-model/SKILL.md index ba5ed10b..38fb174f 100644 --- a/.claude/skills/add-model/SKILL.md +++ b/.claude/skills/add-model/SKILL.md @@ -20,6 +20,11 @@ Where `` is the target sub-library directory name (e.g. `locator`, `asse ## Workflow +### 0. Enter plan mode + +Use the `EnterPlanMode` tool immediately before doing any work. Present the full plan to the user +and wait for approval via `ExitPlanMode` before writing any files or running any commands. + ### 1. Collect the backend struct/schema Ask the user to paste or describe the backend definition (Python ObjectType, GraphQL schema, @@ -146,6 +151,13 @@ make freezed List the created and modified files and whether the build succeeded. +If the model needs GraphQL API caller methods (`fetch`, `fetchAll`, `save`, `delete`/`expire`), +suggest the user run: + +``` +/add-api-caller +``` + ## Rules - Never manually edit `.freezed.dart` or `.g.dart` files. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1693b7c0..fe4827f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Changelog +## 3.7.0 + +- Added `unknown`, `userNotFound`, `wrongPassword`, `accountBlocked`, and `passwordUsedBefore` values to `ApiStatus`. +- Removed deprecated `telegramUnauthorized` and `telegramBadRequest` values from `ApiStatus`. +- Added `MapLayerInput` model with full API callers (`fetch`, `fetchAll`, `save`, `delete`). +- Removed `LocatorApiResponse` in favour of the generic `ApiResponse>`. +- Removed `PoiApiResponse` in favour of the generic `ApiResponse>`. +- Replaced hardcoded `'INTERNAL_ERROR'` strings with `ApiStatus.internalError.toJson()` across all API callers. + ## 3.6.29 - Updated `Locator.fetchAllGraphqlQuery` to include `description` in the query result fields. diff --git a/lib/src/access/src/access_input.dart b/lib/src/access/src/access_input.dart index 6859e758..1a19db51 100644 --- a/lib/src/access/src/access_input.dart +++ b/lib/src/access/src/access_input.dart @@ -55,7 +55,7 @@ abstract class AccessInput with _$AccessInput { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/AccessInput/save(): No response from server"); return false; } @@ -64,13 +64,14 @@ abstract class AccessInput with _$AccessInput { ? (useUuid ? data['data']['addAccessPermissionUuid'] : data['data']['addAccessPermission']) : (useUuid ? data['data']['editAccessPermissionUuid'] : data['data']['editAccessPermission']); if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/AccessInput/save(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } @@ -106,20 +107,21 @@ abstract class AccessInput with _$AccessInput { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Access/delete(): No response from server"); return false; } final result = data['data'][useUuid ? 'deleteAccessPermissionUuid' : 'deleteAccessPermission']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Access/delete(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } diff --git a/lib/src/api/api.g.dart b/lib/src/api/api.g.dart index 73ff44be..8e85c86c 100644 --- a/lib/src/api/api.g.dart +++ b/lib/src/api/api.g.dart @@ -27,8 +27,12 @@ Map _$ApiResponseToJson( }; const _$ApiStatusEnumMap = { + ApiStatus.unknown: 'UNKNOWN', ApiStatus.ok: 'OK', ApiStatus.notfound: 'NOTFOUND', + ApiStatus.userNotFound: 'USER_NOT_FOUND', + ApiStatus.wrongPassword: 'WRONG_PASSWORD', + ApiStatus.accountBlocked: 'ACCOUNT_BLOCKED', ApiStatus.internalError: 'INTERNALERROR', ApiStatus.unprocessable: 'UNPROCESSABLE', ApiStatus.unauthorized: 'UNAUTHORIZED', @@ -39,11 +43,6 @@ const _$ApiStatusEnumMap = { ApiStatus.paymentRequired: 'PAYMENTREQUIRED', ApiStatus.serviceUnavailable: 'SERVICEUNAVAILABLE', ApiStatus.limitReached: 'LIMITREACHED', - ApiStatus.telegramUnauthorized: 'TELEGRAMUNAUTHORIZED', - ApiStatus.telegramBadRequest: 'TELEGRAMBADREQUEST', - ApiStatus.malformedPlan: 'MALFORMEDPLAN', - ApiStatus.subscriptionAlreadyAdded: 'SUBSCRIPTIONALREADYADDED', - ApiStatus.malformedSubscription: 'MALFORMEDSUBSCRIPTION', ApiStatus.fileNotFound: 'FILE_NOT_FOUND', ApiStatus.checkumError: 'CHECKUM_ERROR', ApiStatus.downloadDone: 'DOWNLOAD_DONE', @@ -66,6 +65,7 @@ const _$ApiStatusEnumMap = { ApiStatus.cannotEdit: 'CANNOT_EDIT', ApiStatus.cannotDelete: 'CANNOT_DELETE', ApiStatus.gptDisabled: 'GPT_DISABLED', + ApiStatus.passwordUsedBefore: 'PASSWORD_USED_BEFORE', }; T? _$nullableGenericFromJson( diff --git a/lib/src/api/src/status.dart b/lib/src/api/src/status.dart index 93934577..4c544eb0 100644 --- a/lib/src/api/src/status.dart +++ b/lib/src/api/src/status.dart @@ -2,6 +2,10 @@ part of '../api.dart'; @JsonEnum(alwaysCreate: true) enum ApiStatus { + /// [unknown] - Unknown or unrecognized status. Used as the default fallback. + @JsonValue('UNKNOWN') + unknown, + /// [ok] - The request was successful. @JsonValue('OK') ok, @@ -10,6 +14,18 @@ enum ApiStatus { @JsonValue('NOTFOUND') notfound, + /// [userNotFound] - The user was not found, please check the credentials and try again. + @JsonValue('USER_NOT_FOUND') + userNotFound, + + /// [wrongPassword] - The submitted password is incorrect. Please check your credentials and try again. + @JsonValue('WRONG_PASSWORD') + wrongPassword, + + /// [accountBlocked] - Your account has been blocked due to too many wrong password attempts. + @JsonValue('ACCOUNT_BLOCKED') + accountBlocked, + /// [internalError] - Internal server error, please try again later. If the problem persists, /// please contact us through support@layrz.com. (English or Spanish support) @JsonValue('INTERNALERROR') @@ -56,29 +72,6 @@ enum ApiStatus { @JsonValue('LIMITREACHED') limitReached, - /// [telegramUnauthorized] - We are sorry, but the request for telegram hooks failed, please verify the bot - /// token and chat id sended in the request. The 401 means that the token is invalid. - @JsonValue('TELEGRAMUNAUTHORIZED') - telegramUnauthorized, - - /// [telegramBadRequest] - We are sorry, but the request for telegram hooks failed, please verify - /// the bot token and chat id sended in the request. The 400 means that the chat id provide is invalid. - @JsonValue('TELEGRAMBADREQUEST') - telegramBadRequest, - - /// [malformedPlan] - The provided plan data is malformed or invalid. - @JsonValue('MALFORMEDPLAN') - malformedPlan, - - /// [subscriptionAlreadyAdded] - Means the subscription what you want to add is already added previously. - @JsonValue('SUBSCRIPTIONALREADYADDED') - subscriptionAlreadyAdded, - - /// [malformedSubscription] - Means the subscription item cannot be saved because it has more items in the - /// ecosystem than the quantity limit. - @JsonValue('MALFORMEDSUBSCRIPTION') - malformedSubscription, - /// [fileNotFound] - The file was not found in our storage server. @JsonValue('FILE_NOT_FOUND') fileNotFound, @@ -169,19 +162,23 @@ enum ApiStatus { /// [gptDisabled] - Layo AI is disabled right now, please try again later @JsonValue('GPT_DISABLED') - gptDisabled; + gptDisabled, + + /// [passwordUsedBefore] - The submitted password was used before, please choose a different password. + @JsonValue('PASSWORD_USED_BEFORE') + passwordUsedBefore; @override String toString() => toJson(); /// [toJson] - Converts an [ApiStatus] to a JSON string. - String toJson() => _$ApiStatusEnumMap[this] ?? 'INTERNALERROR'; + String toJson() => _$ApiStatusEnumMap[this] ?? 'UNKNOWN'; /// [fromJson] - Converts a JSON string to an [ApiStatus]. static ApiStatus fromJson(String json) => _$ApiStatusEnumMap.entries .firstWhere( (element) => element.value == json, - orElse: () => const MapEntry(ApiStatus.internalError, 'INTERNALERROR'), + orElse: () => const MapEntry(ApiStatus.unknown, 'UNKNOWN'), ) .key; } diff --git a/lib/src/app/src/registered_app.dart b/lib/src/app/src/registered_app.dart index 1a4089f6..c6e8fa36 100644 --- a/lib/src/app/src/registered_app.dart +++ b/lib/src/app/src/registered_app.dart @@ -66,20 +66,21 @@ abstract class RegisteredApp with _$RegisteredApp { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/RegisteredApp/fetchAll(): No response from server"); return []; } final result = data['data']['registeredApps']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/RegisteredApp/fetchAll(): No result from server"); return []; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return []; } diff --git a/lib/src/locator/locator.dart b/lib/src/locator/locator.dart index a3079266..4293e022 100644 --- a/lib/src/locator/locator.dart +++ b/lib/src/locator/locator.dart @@ -3,6 +3,7 @@ library; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:layrz_logging/layrz_logging.dart'; +import 'package:layrz_models/src/api/api.dart'; import 'package:layrz_models/src/app/app.dart'; import 'package:layrz_models/src/assets/assets.dart'; import 'package:layrz_models/src/converters/converters.dart'; @@ -20,9 +21,4 @@ part 'src/locator.dart'; part 'src/mqtt_config.dart'; part 'src/locator_input.dart'; -class LocatorApiResponse { - final Locator? locator; - final Map errors; - LocatorApiResponse({this.locator, this.errors = const {}}); -} diff --git a/lib/src/locator/src/locator.dart b/lib/src/locator/src/locator.dart index 71c076b5..6493ae48 100644 --- a/lib/src/locator/src/locator.dart +++ b/lib/src/locator/src/locator.dart @@ -118,20 +118,21 @@ abstract class Locator with _$Locator { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/fetch(): No response from server"); return null; } final result = data['data']['locators']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/fetch(): No result from server"); return null; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return null; } if (result['result'] == null || (result['result'] as List).isEmpty) { @@ -165,20 +166,21 @@ abstract class Locator with _$Locator { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/fetchAll(): No response from server"); return []; } final result = data['data']['locators']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/fetchAll(): No result from server"); return []; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return []; } @@ -218,20 +220,21 @@ abstract class Locator with _$Locator { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/expire(): No response from server"); return false; } final result = data['data']['expireLocators']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/expire(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } @@ -267,20 +270,21 @@ abstract class Locator with _$Locator { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/expireMultiple(): No response from server"); return false; } final result = data['data']['expireLocators']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Locator/expireMultiple(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } diff --git a/lib/src/locator/src/locator_input.dart b/lib/src/locator/src/locator_input.dart index b9ce82fb..62a5dcea 100644 --- a/lib/src/locator/src/locator_input.dart +++ b/lib/src/locator/src/locator_input.dart @@ -51,8 +51,8 @@ abstract class LocatorInput with _$LocatorInput { } /// [save] saves the locator input to the server - /// It returns a [LocatorApiResponse] with the saved locator or errors if any - Future save({ + /// It returns a [ApiResponse] with the saved locator or errors if any + Future>?> save({ /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation /// on the GraphQL API. required String apiToken, @@ -72,24 +72,28 @@ abstract class LocatorInput with _$LocatorInput { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/LocatorInput/save(): No response from server"); return null; } final result = id == null ? data['data']['addLocator'] : data['data']['editLocator']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/LocatorInput/save(): No result from server"); return null; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); - return LocatorApiResponse(errors: Map.from(result['errors'] ?? {})); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return ApiResponse( + status: status, + errors: Map.from(result['errors'] ?? {}), + ); } - return LocatorApiResponse(locator: Locator.fromJson(result['result'])); + return ApiResponse(status: ApiStatus.ok, result: Locator.fromJson(result['result'])); } catch (e, stack) { Log.critical("layrz_models/LocatorInput/save(): General exception => $e\n$stack"); return null; diff --git a/lib/src/map/map.dart b/lib/src/map/map.dart index c3967bae..c8007be7 100644 --- a/lib/src/map/map.dart +++ b/lib/src/map/map.dart @@ -5,6 +5,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:layrz_icons/layrz_icons.dart'; import 'package:layrz_logging/layrz_logging.dart'; import 'package:layrz_models/src/access/access.dart'; +import 'package:layrz_models/src/api/api.dart'; import 'package:layrz_models/src/converters/converters.dart'; import 'package:layrz_models/src/utils/src/api_connector.dart'; @@ -14,6 +15,7 @@ part 'map.g.dart'; // Modules part 'src/layer.dart'; +part 'src/layer_input.dart'; part 'src/here_styles.dart'; part 'src/google_layer.dart'; part 'src/mapbox_style.dart'; @@ -21,9 +23,4 @@ part 'src/map_source.dart'; part 'src/poi.dart'; part 'src/poi_input.dart'; -class PoiApiResponse { - final Poi? poi; - final Map errors; - PoiApiResponse({this.poi, this.errors = const {}}); -} diff --git a/lib/src/map/map.freezed.dart b/lib/src/map/map.freezed.dart index 2d8ec72b..fb80b90a 100644 --- a/lib/src/map/map.freezed.dart +++ b/lib/src/map/map.freezed.dart @@ -253,8 +253,8 @@ return $default(_that.id,_that.name,_that.source,_that.rasterServerLight,_that.r /// @nodoc @JsonSerializable() -class _MapLayer implements MapLayer { - const _MapLayer({required this.id, required this.name, @JsonKey(unknownEnumValue: MapSource.custom) required this.source, this.rasterServerLight, this.rasterServerDark, this.googleToken, @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) final List? googleLayers, this.mapboxToken, @JsonKey(unknownEnumValue: MapboxStyle.navigation) final List? mapboxLayers, this.mapboxCustomUsername, this.mapboxCustomStyleId, this.hereToken, @JsonKey(unknownEnumValue: HereStyle.lite) final List? hereLayers, this.attributionUrl = 'https://cdn.layrz.com/resources/layrz/logo/normal.png', this.attributionUrlDark, this.attributionWidth = 100, this.attributionHeight = 30, final List appsIds = const []}): _googleLayers = googleLayers,_mapboxLayers = mapboxLayers,_hereLayers = hereLayers,_appsIds = appsIds; +class _MapLayer extends MapLayer { + const _MapLayer({required this.id, required this.name, @JsonKey(unknownEnumValue: MapSource.custom) required this.source, this.rasterServerLight, this.rasterServerDark, this.googleToken, @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) final List? googleLayers, this.mapboxToken, @JsonKey(unknownEnumValue: MapboxStyle.navigation) final List? mapboxLayers, this.mapboxCustomUsername, this.mapboxCustomStyleId, this.hereToken, @JsonKey(unknownEnumValue: HereStyle.lite) final List? hereLayers, this.attributionUrl = 'https://cdn.layrz.com/resources/layrz/logo/normal.png', this.attributionUrlDark, this.attributionWidth = 100, this.attributionHeight = 30, final List appsIds = const []}): _googleLayers = googleLayers,_mapboxLayers = mapboxLayers,_hereLayers = hereLayers,_appsIds = appsIds,super._(); factory _MapLayer.fromJson(Map json) => _$MapLayerFromJson(json); /// [id] is the unique identifier for the layer. @@ -687,8 +687,8 @@ return $default(_that.id,_that.name,_that.source,_that.rasterServerLight,_that.r /// @nodoc @JsonSerializable() -class _MapLayerInput implements MapLayerInput { - _MapLayerInput({this.id, this.name = '', @JsonKey(unknownEnumValue: MapSource.custom) this.source = MapSource.custom, this.rasterServerLight, this.rasterServerDark, this.googleToken, @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) this.googleLayers, this.mapboxToken, @JsonKey(unknownEnumValue: MapboxStyle.navigation) this.mapboxLayers, this.mapboxCustomUsername, this.mapboxCustomStyleId, this.hereToken, @JsonKey(unknownEnumValue: HereStyle.lite) this.hereLayers, this.attributionUrl = 'https://cdn.layrz.com/resources/layrz/logo/normal.png', this.attributionUrlDark, this.attributionWidth = 100, this.attributionHeight = 30, this.appsIds = const [], this.mapLayerId, this.poisIds = const []}); +class _MapLayerInput extends MapLayerInput { + _MapLayerInput({this.id, this.name = '', @JsonKey(unknownEnumValue: MapSource.custom) this.source = MapSource.custom, this.rasterServerLight, this.rasterServerDark, this.googleToken, @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) this.googleLayers, this.mapboxToken, @JsonKey(unknownEnumValue: MapboxStyle.navigation) this.mapboxLayers, this.mapboxCustomUsername, this.mapboxCustomStyleId, this.hereToken, @JsonKey(unknownEnumValue: HereStyle.lite) this.hereLayers, this.attributionUrl = 'https://cdn.layrz.com/resources/layrz/logo/normal.png', this.attributionUrlDark, this.attributionWidth = 100, this.attributionHeight = 30, this.appsIds = const [], this.mapLayerId, this.poisIds = const []}): super._(); factory _MapLayerInput.fromJson(Map json) => _$MapLayerInputFromJson(json); /// [id] is the unique identifier for the layer. diff --git a/lib/src/map/src/layer.dart b/lib/src/map/src/layer.dart index f30f7383..92bf19d5 100644 --- a/lib/src/map/src/layer.dart +++ b/lib/src/map/src/layer.dart @@ -2,6 +2,8 @@ part of '../map.dart'; @freezed abstract class MapLayer with _$MapLayer { + const MapLayer._(); + /// [MapLayer] is the model for a map layer. /// It is used to define the layers that are available in the app. /// This model only can be getted from the [RegisteredApp] model. @@ -72,84 +74,218 @@ abstract class MapLayer with _$MapLayer { }) = _MapLayer; factory MapLayer.fromJson(Map json) => _$MapLayerFromJson(json); -} - -@unfreezed -abstract class MapLayerInput with _$MapLayerInput { - /// [MapLayerInput] is the model for a map layer. - /// It is used to define the layers that are available in the app. - /// This model only can be getted from the [RegisteredApp] model. - factory MapLayerInput({ - /// [id] is the unique identifier for the layer. - String? id, - - /// [name] is the name of the layer. - @Default('') String name, - - /// [source] is the source of the layer. - @JsonKey(unknownEnumValue: MapSource.custom) @Default(MapSource.custom) MapSource source, - - /// [rasterServerLight] is the raster server for light mode and default. - /// Only used when the [source] is [MapSource.custom]. - String? rasterServerLight, - - /// [rasterServerDark] is the raster server for dark mode. - /// Only used when the [source] is [MapSource.custom]. - String? rasterServerDark, - - /// [googleToken] is the Google Maps token with Map Tiles API capabilities. - /// Only used when the [source] is [MapSource.google]. - String? googleToken, - - /// [googleLayers] is the list of enabled layers for the Google Maps. - /// Only used when the [source] is [MapSource.google]. - @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) List? googleLayers, - - /// [mapboxToken] is the Mapbox token with Static Tiles API capabilities. - /// Only used when the [source] is [MapSource.mapbox]. - String? mapboxToken, - - /// [mapboxStyle] is the Mapbox style for the layer. - /// Only used when the [source] is [MapSource.mapbox]. - @JsonKey(unknownEnumValue: MapboxStyle.navigation) List? mapboxLayers, - - /// [mapboxCustomUsername] is the Mapbox custom username. - /// Only used when the [source] is [MapSource.mapbox] and the [mapboxStyle] is [MapboxStyle.custom]. - String? mapboxCustomUsername, - - /// [mapboxCustomStyleId] is the Mapbox custom style id. - /// Only used when the [source] is [MapSource.mapbox] and the [mapboxStyle] is [MapboxStyle.custom]. - String? mapboxCustomStyleId, - - /// [hereToken] is the HERE token with Map Tiles API capabilities. - /// Only used when the [source] is [MapSource.here]. - String? hereToken, - - /// [hereLayers] is the list of enabled layers for the HERE Maps. - /// Only used when the [source] is [MapSource.here]. - @JsonKey(unknownEnumValue: HereStyle.lite) List? hereLayers, - - /// [attributionUrl] is the URI for the attribution of the layer. - @Default('https://cdn.layrz.com/resources/layrz/logo/normal.png') String attributionUrl, - - /// [attributionUrlDark] is the URI for the attribution of the layer in dark mode. - String? attributionUrlDark, - - /// [attributionWidth] is the width of the attribution of the layer. - @Default(100) double attributionWidth, - - /// [attributionHeight] is the height of the attribution of the layer. - @Default(30) double attributionHeight, - - /// [appsIds] is the list of [App]s that are associated with the layer. - @Default([]) List appsIds, - - /// [mapLayerId] is the id of the map layer to use for the locators that are using this layer. - String? mapLayerId, - - /// [poisIds] is the list of [Poi]s that are associated with the layer. - @Default([]) List poisIds, - }) = _MapLayerInput; - factory MapLayerInput.fromJson(Map json) => _$MapLayerInputFromJson(json); + /// [fetch] fetches a single [MapLayer] from the server by its ID + Future fetch({ + /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation + /// on the GraphQL API. + required String apiToken, + + /// [uri] is the GraphQL endpoint to use + required Uri uri, + + /// [onResponse] is the callback to call when the response is received + void Function(String statusCode)? onResponse, + }) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: fetchSingleQuery, + variables: {'apiToken': apiToken, 'id': id}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/fetch(): No response from server"); + return null; + } + + final result = data['data']['mapLayers']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/fetch(): No result from server"); + return null; + } + + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return null; + } + if (result['result'] == null || (result['result'] as List).isEmpty) { + onResponse?.call('NOT_FOUND'); + return null; + } + + return MapLayer.fromJson(Map.from(result['result'][0] as Map)); + } catch (e, stack) { + Log.critical("layrz_models/MapLayer/fetch(): General exception => $e\n$stack"); + return null; + } + } + + /// [fetchAll] fetches all [MapLayer]s from the server + static Future> fetchAll({ + /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation + /// on the GraphQL API. + required String apiToken, + + /// [uri] is the GraphQL endpoint to use + required Uri uri, + + /// [onResponse] is the callback to call when the response is received + void Function(String statusCode)? onResponse, + }) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: fetchAllGraphqlQuery, + variables: {'apiToken': apiToken}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/fetchAll(): No response from server"); + return []; + } + + final result = data['data']['mapLayers']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/fetchAll(): No result from server"); + return []; + } + + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return []; + } + + return (result['result'] as List?) + ?.map((e) => MapLayer.fromJson(Map.from(e as Map))) + .toList() ?? + []; + } catch (e, stack) { + Log.critical("layrz_models/MapLayer/fetchAll(): General exception => $e\n$stack"); + return []; + } + } + + /// [delete] deletes multiple [MapLayer]s from the server by their IDs + static Future delete({ + /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation + /// on the GraphQL API. + required String apiToken, + + /// [uri] is the GraphQL endpoint to use + required Uri uri, + + /// [ids] is the list of [MapLayer] IDs to delete + required List ids, + + /// [onResponse] is the callback to call when the response is received + void Function(String statusCode)? onResponse, + }) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: deleteMapLayers, + variables: {'apiToken': apiToken, 'ids': ids}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/delete(): No response from server"); + return false; + } + + final result = data['data']['deleteMapLayers']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayer/delete(): No result from server"); + return false; + } + + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return false; + } + + return true; + } catch (e, stack) { + Log.critical("layrz_models/MapLayer/delete(): General exception => $e\n$stack"); + return false; + } + } + + /// [fetchSingleQuery] is the GraphQL query to fetch a single [MapLayer] by its ID + /// It uses the [MapLayer.graphqlFragment] to get the map layer data + static String get fetchSingleQuery => + '${MapLayer.graphqlFragment}' + r''' + query mapLayers($apiToken: String!, $id: ID) { + mapLayers(apiToken: $apiToken, id: $id) { + status + errors + result { + ...mapLayerFragment + } + } + } + '''; + + /// [fetchAllGraphqlQuery] is the GraphQL query to fetch all [MapLayer]s + /// It uses a lighter subset of fields to reduce the amount of data transferred + static String get fetchAllGraphqlQuery => r''' + query mapLayers($apiToken: String!) { + mapLayers(apiToken: $apiToken) { + status + errors + result { + id + name + source + } + } + } + '''; + + /// [graphqlFragment] is the GraphQL fragment to fetch the [MapLayer] data + static String get graphqlFragment => ''' + fragment mapLayerFragment on MapLayer { + id + name + source + rasterServerLight + rasterServerDark + googleToken + googleLayers + mapboxToken + mapboxLayers + mapboxCustomUsername + mapboxCustomStyleId + hereToken + hereLayers + attributionUrl + attributionUrlDark + attributionWidth + attributionHeight + appsIds + } + '''; + + /// [deleteMapLayers] is the GraphQL mutation to delete one or more [MapLayer]s by their IDs + static String get deleteMapLayers => r''' + mutation deleteMapLayers($apiToken: String!, $ids: [ID!]!) { + deleteMapLayers(apiToken: $apiToken, ids: $ids) { + status + errors + } + } + '''; } diff --git a/lib/src/map/src/layer_input.dart b/lib/src/map/src/layer_input.dart new file mode 100644 index 00000000..34d27ad5 --- /dev/null +++ b/lib/src/map/src/layer_input.dart @@ -0,0 +1,166 @@ +part of '../map.dart'; + +@unfreezed +abstract class MapLayerInput with _$MapLayerInput { + const MapLayerInput._(); + + /// [MapLayerInput] is the model for a map layer. + /// It is used to define the layers that are available in the app. + /// This model only can be getted from the [RegisteredApp] model. + factory MapLayerInput({ + /// [id] is the unique identifier for the layer. + String? id, + + /// [name] is the name of the layer. + @Default('') String name, + + /// [source] is the source of the layer. + @JsonKey(unknownEnumValue: MapSource.custom) @Default(MapSource.custom) MapSource source, + + /// [rasterServerLight] is the raster server for light mode and default. + /// Only used when the [source] is [MapSource.custom]. + String? rasterServerLight, + + /// [rasterServerDark] is the raster server for dark mode. + /// Only used when the [source] is [MapSource.custom]. + String? rasterServerDark, + + /// [googleToken] is the Google Maps token with Map Tiles API capabilities. + /// Only used when the [source] is [MapSource.google]. + String? googleToken, + + /// [googleLayers] is the list of enabled layers for the Google Maps. + /// Only used when the [source] is [MapSource.google]. + @JsonKey(unknownEnumValue: GoogleMapLayer.roadmap) List? googleLayers, + + /// [mapboxToken] is the Mapbox token with Static Tiles API capabilities. + /// Only used when the [source] is [MapSource.mapbox]. + String? mapboxToken, + + /// [mapboxStyle] is the Mapbox style for the layer. + /// Only used when the [source] is [MapSource.mapbox]. + @JsonKey(unknownEnumValue: MapboxStyle.navigation) List? mapboxLayers, + + /// [mapboxCustomUsername] is the Mapbox custom username. + /// Only used when the [source] is [MapSource.mapbox] and the [mapboxStyle] is [MapboxStyle.custom]. + String? mapboxCustomUsername, + + /// [mapboxCustomStyleId] is the Mapbox custom style id. + /// Only used when the [source] is [MapSource.mapbox] and the [mapboxStyle] is [MapboxStyle.custom]. + String? mapboxCustomStyleId, + + /// [hereToken] is the HERE token with Map Tiles API capabilities. + /// Only used when the [source] is [MapSource.here]. + String? hereToken, + + /// [hereLayers] is the list of enabled layers for the HERE Maps. + /// Only used when the [source] is [MapSource.here]. + @JsonKey(unknownEnumValue: HereStyle.lite) List? hereLayers, + + /// [attributionUrl] is the URI for the attribution of the layer. + @Default('https://cdn.layrz.com/resources/layrz/logo/normal.png') String attributionUrl, + + /// [attributionUrlDark] is the URI for the attribution of the layer in dark mode. + String? attributionUrlDark, + + /// [attributionWidth] is the width of the attribution of the layer. + @Default(100) double attributionWidth, + + /// [attributionHeight] is the height of the attribution of the layer. + @Default(30) double attributionHeight, + + /// [appsIds] is the list of [App]s that are associated with the layer. + @Default([]) List appsIds, + + /// [mapLayerId] is the id of the map layer to use for the locators that are using this layer. + String? mapLayerId, + + /// [poisIds] is the list of [Poi]s that are associated with the layer. + @Default([]) List poisIds, + }) = _MapLayerInput; + + factory MapLayerInput.fromJson(Map json) => _$MapLayerInputFromJson(json); + + /// [save] saves the [MapLayerInput] to the server + /// It returns an [ApiResponse] with the saved [MapLayer] on success, or errors on failure. + /// Returns `null` on a network/server error. + Future>?> save({ + /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation + /// on the GraphQL API. + required String apiToken, + + /// [uri] is the GraphQL endpoint to use + required Uri uri, + + /// [onResponse] is the callback to call when the response is received + void Function(String statusCode)? onResponse, + }) async { + final connector = LayrzConnector(uri: uri); + try { + final response = await connector.perform( + query: id == null ? addGraphqlMutation : editGraphqlMutation, + variables: {'apiToken': apiToken, 'data': toJson()}, + ); + + final data = response.data; + if (data == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayerInput/save(): No response from server"); + return null; + } + + final result = id == null ? data['data']['addMapLayer'] : data['data']['editMapLayer']; + if (result == null) { + onResponse?.call(ApiStatus.internalError.toJson()); + Log.error("layrz_models/MapLayerInput/save(): No result from server"); + return null; + } + + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return ApiResponse( + status: status, + errors: Map.from(result['errors'] ?? {}), + ); + } + + return ApiResponse(status: ApiStatus.ok, result: MapLayer.fromJson(result['result'])); + } catch (e, stack) { + Log.critical("layrz_models/MapLayerInput/save(): General exception => $e\n$stack"); + return null; + } + } + + /// [addGraphqlMutation] is the GraphQL mutation to add a [MapLayer] + /// It uses the [MapLayer.graphqlFragment] to get the map layer data + static String get addGraphqlMutation => + '${MapLayer.graphqlFragment}' + r''' + mutation addMapLayer($apiToken: String!, $data: MapLayerInput!) { + addMapLayer(data: $data, apiToken: $apiToken) { + status + errors + result { + ...mapLayerFragment + } + } + } + '''; + + /// [editGraphqlMutation] is the GraphQL mutation to edit a [MapLayer] + /// It uses the [MapLayer.graphqlFragment] to get the map layer data + static String get editGraphqlMutation => + '${MapLayer.graphqlFragment}' + r''' + mutation editMapLayer($apiToken: String!, $data: MapLayerInput!) { + editMapLayer(data: $data, apiToken: $apiToken) { + status + errors + result { + ...mapLayerFragment + } + } + } + '''; +} diff --git a/lib/src/map/src/poi.dart b/lib/src/map/src/poi.dart index 571961a7..b9c1285f 100644 --- a/lib/src/map/src/poi.dart +++ b/lib/src/map/src/poi.dart @@ -50,20 +50,21 @@ abstract class Poi with _$Poi { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/fetch(): No response from server"); return null; } final result = data['data']['pois']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/fetch(): No result from server"); return null; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return null; } if (result['result'] == null || (result['result'] as List).isEmpty) { @@ -97,20 +98,21 @@ abstract class Poi with _$Poi { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/fetchAll(): No response from server"); return []; } final result = data['data']['pois']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/fetchAll(): No result from server"); return []; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return []; } @@ -149,20 +151,21 @@ abstract class Poi with _$Poi { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/delete(): No response from server"); return false; } final result = data['data']['deletePois']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Poi/delete(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } diff --git a/lib/src/map/src/poi_input.dart b/lib/src/map/src/poi_input.dart index e2975e57..7f54ca05 100644 --- a/lib/src/map/src/poi_input.dart +++ b/lib/src/map/src/poi_input.dart @@ -30,8 +30,8 @@ abstract class PoiInput with _$PoiInput { factory PoiInput.fromJson(Map json) => _$PoiInputFromJson(json); /// [save] saves the POI input to the server - /// It returns a [PoiApiResponse] with the saved POI or errors if any - Future save({ + /// It returns a [ApiResponse] with the saved POI or errors if any + Future>?> save({ /// [apiToken] is the API token to use for authentication. You can get one using the `login` mutation /// on the GraphQL API. required String apiToken, @@ -51,24 +51,28 @@ abstract class PoiInput with _$PoiInput { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/PoiInput/save(): No response from server"); return null; } final result = id == null ? data['data']['addPoi'] : data['data']['editPoi']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/PoiInput/save(): No result from server"); return null; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); - return PoiApiResponse(errors: Map.from(result['errors'] ?? {})); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); + return ApiResponse( + status: status, + errors: Map.from(result['errors'] ?? {}), + ); } - return PoiApiResponse(poi: Poi.fromJson(result['result'])); + return ApiResponse(status: ApiStatus.ok, result: Poi.fromJson(result['result'])); } catch (e, stack) { Log.critical("layrz_models/PoiInput/save(): General exception => $e\n$stack"); return null; diff --git a/lib/src/token/src/token.dart b/lib/src/token/src/token.dart index 61150454..140643a7 100644 --- a/lib/src/token/src/token.dart +++ b/lib/src/token/src/token.dart @@ -45,20 +45,21 @@ abstract class Token with _$Token { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/fetchAll(): No response from server"); return []; } final result = data['data']['tokens']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/fetchAll(): No result from server"); return []; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return []; } @@ -97,20 +98,21 @@ abstract class Token with _$Token { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/expire(): No response from server"); return false; } final result = data['data']['expireToken']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/expire(): No result from server"); return false; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return false; } @@ -151,20 +153,21 @@ abstract class Token with _$Token { final data = response.data; if (data == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/createUsingDuration(): No response from server"); return null; } final result = data['data']['createToken']; if (result == null) { - onResponse?.call('INTERNAL_ERROR'); + onResponse?.call(ApiStatus.internalError.toJson()); Log.error("layrz_models/Token/createUsingDuration(): No result from server"); return null; } - if (result['status'] != 'OK') { - onResponse?.call(result['status']); + final status = ApiStatus.fromJson(result['status']); + if (status != ApiStatus.ok) { + onResponse?.call(status.toJson()); return null; } diff --git a/lib/src/token/token.dart b/lib/src/token/token.dart index 5ba3bffa..b7193fad 100644 --- a/lib/src/token/token.dart +++ b/lib/src/token/token.dart @@ -3,6 +3,7 @@ library; import 'package:collection/collection.dart'; import 'package:freezed_annotation/freezed_annotation.dart'; import 'package:layrz_logging/layrz_logging.dart'; +import 'package:layrz_models/src/api/api.dart'; import 'package:layrz_models/src/converters/converters.dart'; import 'package:layrz_models/src/utils/src/api_connector.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index 053c23ca..d12aa329 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ description: Layrz API models for Dart/Flutter. This package contains the models used by the Layrz API. name: layrz_models -version: "3.6.29" +version: "3.7.0" repository: https://github.com/goldenm-software/layrz_models environment: