Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 3.8.0

- Added `gql_builder` module (`GqlQuery`, `GqlMutation`, `GqlFragment`, `GqlField`, `GqlVariable`) for composable, type-safe GraphQL query construction.
- Moved `LayrzConnector` from `lib/src/utils/` into the `api/` module; still accessible via the top-level barrel.
- Added `LayrzConnector.perform(Gql)` replacing the raw-string `perform(query:, variables:)` method.
- Added `GqlVariableType.list` with `listOf` and `nestedRequired` for `[ID]!` and `[ID!]!` list types.
- Added `GqlVariableType.input` with `inputName` for GraphQL input object variables.
- Added `GqlField.args` for rendering field-level arguments (e.g. `charts(apiToken: $apiToken)`).
- Added `Avatar.gqlFragment` reusable fragment.
- Migrated all API callers (`LayrzChart`, `LayrzChartInput`, `Access`, `User`, `Locator`, `LocatorInput`, `MapLayer`, `MapLayerInput`, `Poi`, `PoiInput`, `Token`, `RegisteredApp`) to the `gql_builder`.
- Added `assets`, `assetsIds`, and `enableLttb` fields to `LayrzChart` and `LayrzChartInput`.
- Fragment collection is now automatic — the builder walks the field tree at generation time; no explicit `fragments:` list required.

## 3.7.10

- Added `StockClosing` entity model
Expand Down
42 changes: 18 additions & 24 deletions lib/src/access/src/access.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,29 +20,23 @@ abstract class Access with _$Access {

factory Access.fromJson(Map<String, dynamic> json) => _$AccessFromJson(json);

/// [graphqlIdFragment] GraphQL fragment for Access
static const String graphqlIdFragment = '''
fragment accessFragment on AccessPermission {
id
read
write
manage
objectId
userId
module
}
''';
/// [graphqlIdFragment] GqlFragment for Access using integer ID
static GqlFragment get graphqlIdFragment => GqlFragment(name: 'accessFragment', onType: 'AccessPermission')
..add(GqlField(name: 'id'))
..add(GqlField(name: 'read'))
..add(GqlField(name: 'write'))
..add(GqlField(name: 'manage'))
..add(GqlField(name: 'objectId'))
..add(GqlField(name: 'userId'))
..add(GqlField(name: 'module'));

/// [graphqlUuidFragment] GraphQL fragment for Access using UUID
static const String graphqlUuidFragment = '''
fragment accessUuidFragment on AccessPermissionUuid {
id
read
write
manage
objectId
userId
module
}
''';
/// [graphqlUuidFragment] GqlFragment for Access using UUID
static GqlFragment get graphqlUuidFragment => GqlFragment(name: 'accessUuidFragment', onType: 'AccessPermissionUuid')
..add(GqlField(name: 'id'))
..add(GqlField(name: 'read'))
..add(GqlField(name: 'write'))
..add(GqlField(name: 'manage'))
..add(GqlField(name: 'objectId'))
..add(GqlField(name: 'userId'))
..add(GqlField(name: 'module'));
}
101 changes: 31 additions & 70 deletions lib/src/access/src/access_input.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,24 @@ abstract class AccessInput with _$AccessInput {
bool useUuid = false,
}) async {
final connector = LayrzConnector(uri: uri);
final isNew = id == null;
final opName = isNew
? (useUuid ? 'addAccessPermissionUuid' : 'addAccessPermission')
: (useUuid ? 'editAccessPermissionUuid' : 'editAccessPermission');
final inputName = useUuid ? 'AccessPermissionUuidInput' : 'AccessPermissionInput';
try {
final response = await connector.perform(
query: id == null
? (useUuid ? AccessInput.addUuidGraphqlMutation : AccessInput.addIdGraphqlMutation)
: (useUuid ? AccessInput.editUuidGraphqlMutation : AccessInput.editIdGraphqlMutation),
variables: {'apiToken': apiToken, 'data': toJson()},
GqlMutation(
variables: [
GqlVariable(name: 'apiToken', type: .string, req: true, value: apiToken),
GqlVariable(name: 'data', type: .input, req: true, inputName: inputName, value: toJson()),
],
name: opName,
)..add(
GqlField(name: opName, args: {'apiToken': 'apiToken', 'data': 'data'})
..add(GqlField(name: 'status'))
..add(GqlField(name: 'errors')),
),
);

final data = response.data;
Expand All @@ -60,9 +72,7 @@ abstract class AccessInput with _$AccessInput {
return false;
}

final result = id == null
? (useUuid ? data['data']['addAccessPermissionUuid'] : data['data']['addAccessPermission'])
: (useUuid ? data['data']['editAccessPermissionUuid'] : data['data']['editAccessPermission']);
final result = data['data'][opName];
if (result == null) {
onResponse?.call(ApiStatus.internalError.toJson());
Log.error("layrz_models/AccessInput/save(): No result from server");
Expand Down Expand Up @@ -98,11 +108,22 @@ abstract class AccessInput with _$AccessInput {
bool useUuid = false,
}) async {
final connector = LayrzConnector(uri: uri);
final opName = useUuid ? 'deleteAccessPermissionUuid' : 'deleteAccessPermission';
final inputName = useUuid ? 'AccessPermissionUuidInput' : 'AccessPermissionInput';

try {
final response = await connector.perform(
query: useUuid ? AccessInput.deleteUuidGraphqlMutation : AccessInput.deleteIdGraphqlMutation,
variables: {'apiToken': apiToken, 'data': toJson()},
GqlMutation(
variables: [
GqlVariable(name: 'apiToken', type: .string, req: true, value: apiToken),
GqlVariable(name: 'data', type: .input, req: true, inputName: inputName, value: toJson()),
],
name: opName,
)..add(
GqlField(name: opName, args: {'apiToken': 'apiToken', 'data': 'data'})
..add(GqlField(name: 'status'))
..add(GqlField(name: 'errors')),
),
);

final data = response.data;
Expand All @@ -112,7 +133,7 @@ abstract class AccessInput with _$AccessInput {
return false;
}

final result = data['data'][useUuid ? 'deleteAccessPermissionUuid' : 'deleteAccessPermission'];
final result = data['data'][opName];
if (result == null) {
onResponse?.call(ApiStatus.internalError.toJson());
Log.error("layrz_models/Access/delete(): No result from server");
Expand All @@ -131,64 +152,4 @@ abstract class AccessInput with _$AccessInput {
return false;
}
}

/// [addIdGraphqlMutation] GraphQL mutation for adding an access permission
static String get addIdGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionInput!) {
addAccessPermission(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';

/// [addUuidGraphqlMutation] GraphQL mutation for adding an access permission
static String get addUuidGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
addAccessPermissionUuid(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';

/// [editIdGraphqlMutation] GraphQL mutation for updating an access permission
static String get editIdGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionInput!) {
editAccessPermission(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';

/// [editUuidGraphqlMutation] GraphQL mutation for updating an access permission
static String get editUuidGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
editAccessPermissionUuid(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';

/// [deleteIdGraphqlMutation] GraphQL mutation for deleting an access permission
static String get deleteIdGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionInput!) {
deleteAccessPermission(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';

/// [deleteUuidGraphqlMutation] GraphQL mutation for deleting an access permission
static String get deleteUuidGraphqlMutation => r'''
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
deleteAccessPermissionUuid(apiToken: $apiToken, data: $data) {
status
errors
}
}
''';
}
9 changes: 9 additions & 0 deletions lib/src/api/api.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,18 @@
library;

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:freezed_annotation/freezed_annotation.dart';

part 'api.freezed.dart';
part 'api.g.dart';

part 'src/api_connector.dart';
part 'src/response.dart';
part 'src/status.dart';

part 'src/gql_builder/variables.dart';
part 'src/gql_builder/fragment.dart';
part 'src/gql_builder/field.dart';
part 'src/gql_builder/gql.dart';
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import 'dart:convert';

import 'package:dio/dio.dart';
part of '../api.dart';

class LayrzConnector {
final Uri uri;
Expand Down Expand Up @@ -40,13 +38,18 @@ class LayrzConnector {

late final Dio _dio;

Future<Response> perform({
required String query,
required Map<String, dynamic> variables,
}) =>
_dio.post('', data: {
'query': query,
'variables': variables,
'operationName': null,
});
/// [perform] executes a [Gql] object built with the gql_builder.
/// Each [GqlVariable] with a non-null `value` is included in the variables map;
/// variables without a value are omitted from the wire payload.
Future<Response> perform(Gql gql) {
final variables = <String, dynamic>{
for (final v in gql.variables)
if (v.value != null) v.name: v.value,
};
return _dio.post('', data: {
'query': gql.generated,
'variables': variables,
'operationName': null,
});
}
}
39 changes: 39 additions & 0 deletions lib/src/api/src/gql_builder/field.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
part of '../../api.dart';

class GqlField<T> {
/// [name] is the GraphQL field name, e.g. `charts`, `addChart`, `id`, `name`.
final String name;

/// [alias] is the GraphQL field alias, e.g. `myCharts: charts`. Optional.
final String? alias;

/// [fields] are the child fields to query on this field, e.g. for `charts { id name }`
/// the `charts` field has two child fields `id` and `name`.
late List<GqlField> fields;

/// [parser] is an optional function to parse the raw JSON value of this field into a Dart type.
final T? Function(Object?)? parser;

/// [fragment] is an optional fragment to spread on this field, e.g. `...ChartFragment`.
final GqlFragment? fragment;

/// Key is the GraphQL argument name, value is the variable name (without `$`).
/// Example: `{'apiToken': 'apiToken', 'id': 'id'}` renders as `(apiToken: $apiToken, id: $id)`.
final Map<String, String> args;

/// [GqlField] represents a field in a GraphQL query or mutation, with
/// optional child fields, arguments, and fragments.
GqlField({
required this.name,
this.alias,
this.parser,
List<GqlField>? fields,
this.fragment,
Map<String, String>? args,
}) : args = args ?? const {} {
this.fields = List.from(fields ?? [], growable: true);
}

/// Adds a child field to this field, e.g. adding `id` and `name` to `charts` in `charts { id name }`.
void add(GqlField field) => fields.add(field);
}
35 changes: 35 additions & 0 deletions lib/src/api/src/gql_builder/fragment.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
part of '../../api.dart';

class GqlFragment {
/// [name] is the name of the fragment, e.g. `ChartFragment`.
final String name;

/// [onType] is the GraphQL type on which this fragment is defined, e.g. `Chart`.
final String onType;

/// [fields] are the fields included in this fragment.
late List<GqlField> fields;

/// [GqlFragment] represents a GraphQL fragment, which is a reusable selection of fields on a specific type.
/// Example usage: `fragment ChartFragment on Chart { id name type }` defines a
/// fragment named `ChartFragment` on the `Chart` type, which includes the fields `id`, `name`, and `type`.
GqlFragment({
required this.name,
required this.onType,
List<GqlField>? fields,
}) {
this.fields = List.from(fields ?? [], growable: true);
}

/// Adds a field to this fragment, e.g. adding `id` to `ChartFragment` in `fragment ChartFragment on Chart { id }`.
void add(GqlField field) => fields.add(field);

@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is GqlFragment && other.name == name && other.onType == onType;
}

@override
int get hashCode => name.hashCode ^ onType.hashCode;
}
Loading
Loading