Skip to content

Commit 86aafce

Browse files
authored
Merge pull request #180 from goldenm-software/development
feat: gql_builder module, LayrzConnector migration, and v3.8.0 release
2 parents b9263cb + 3dc8ced commit 86aafce

31 files changed

Lines changed: 1376 additions & 791 deletions

CHANGELOG.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Changelog
22

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

518
- Added `StockClosing` entity model

lib/src/access/src/access.dart

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,29 +20,23 @@ abstract class Access with _$Access {
2020

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

23-
/// [graphqlIdFragment] GraphQL fragment for Access
24-
static const String graphqlIdFragment = '''
25-
fragment accessFragment on AccessPermission {
26-
id
27-
read
28-
write
29-
manage
30-
objectId
31-
userId
32-
module
33-
}
34-
''';
23+
/// [graphqlIdFragment] GqlFragment for Access using integer ID
24+
static GqlFragment get graphqlIdFragment => GqlFragment(name: 'accessFragment', onType: 'AccessPermission')
25+
..add(GqlField(name: 'id'))
26+
..add(GqlField(name: 'read'))
27+
..add(GqlField(name: 'write'))
28+
..add(GqlField(name: 'manage'))
29+
..add(GqlField(name: 'objectId'))
30+
..add(GqlField(name: 'userId'))
31+
..add(GqlField(name: 'module'));
3532

36-
/// [graphqlUuidFragment] GraphQL fragment for Access using UUID
37-
static const String graphqlUuidFragment = '''
38-
fragment accessUuidFragment on AccessPermissionUuid {
39-
id
40-
read
41-
write
42-
manage
43-
objectId
44-
userId
45-
module
46-
}
47-
''';
33+
/// [graphqlUuidFragment] GqlFragment for Access using UUID
34+
static GqlFragment get graphqlUuidFragment => GqlFragment(name: 'accessUuidFragment', onType: 'AccessPermissionUuid')
35+
..add(GqlField(name: 'id'))
36+
..add(GqlField(name: 'read'))
37+
..add(GqlField(name: 'write'))
38+
..add(GqlField(name: 'manage'))
39+
..add(GqlField(name: 'objectId'))
40+
..add(GqlField(name: 'userId'))
41+
..add(GqlField(name: 'module'));
4842
}

lib/src/access/src/access_input.dart

Lines changed: 31 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -45,12 +45,24 @@ abstract class AccessInput with _$AccessInput {
4545
bool useUuid = false,
4646
}) async {
4747
final connector = LayrzConnector(uri: uri);
48+
final isNew = id == null;
49+
final opName = isNew
50+
? (useUuid ? 'addAccessPermissionUuid' : 'addAccessPermission')
51+
: (useUuid ? 'editAccessPermissionUuid' : 'editAccessPermission');
52+
final inputName = useUuid ? 'AccessPermissionUuidInput' : 'AccessPermissionInput';
4853
try {
4954
final response = await connector.perform(
50-
query: id == null
51-
? (useUuid ? AccessInput.addUuidGraphqlMutation : AccessInput.addIdGraphqlMutation)
52-
: (useUuid ? AccessInput.editUuidGraphqlMutation : AccessInput.editIdGraphqlMutation),
53-
variables: {'apiToken': apiToken, 'data': toJson()},
55+
GqlMutation(
56+
variables: [
57+
GqlVariable(name: 'apiToken', type: .string, req: true, value: apiToken),
58+
GqlVariable(name: 'data', type: .input, req: true, inputName: inputName, value: toJson()),
59+
],
60+
name: opName,
61+
)..add(
62+
GqlField(name: opName, args: {'apiToken': 'apiToken', 'data': 'data'})
63+
..add(GqlField(name: 'status'))
64+
..add(GqlField(name: 'errors')),
65+
),
5466
);
5567

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

63-
final result = id == null
64-
? (useUuid ? data['data']['addAccessPermissionUuid'] : data['data']['addAccessPermission'])
65-
: (useUuid ? data['data']['editAccessPermissionUuid'] : data['data']['editAccessPermission']);
75+
final result = data['data'][opName];
6676
if (result == null) {
6777
onResponse?.call(ApiStatus.internalError.toJson());
6878
Log.error("layrz_models/AccessInput/save(): No result from server");
@@ -98,11 +108,22 @@ abstract class AccessInput with _$AccessInput {
98108
bool useUuid = false,
99109
}) async {
100110
final connector = LayrzConnector(uri: uri);
111+
final opName = useUuid ? 'deleteAccessPermissionUuid' : 'deleteAccessPermission';
112+
final inputName = useUuid ? 'AccessPermissionUuidInput' : 'AccessPermissionInput';
101113

102114
try {
103115
final response = await connector.perform(
104-
query: useUuid ? AccessInput.deleteUuidGraphqlMutation : AccessInput.deleteIdGraphqlMutation,
105-
variables: {'apiToken': apiToken, 'data': toJson()},
116+
GqlMutation(
117+
variables: [
118+
GqlVariable(name: 'apiToken', type: .string, req: true, value: apiToken),
119+
GqlVariable(name: 'data', type: .input, req: true, inputName: inputName, value: toJson()),
120+
],
121+
name: opName,
122+
)..add(
123+
GqlField(name: opName, args: {'apiToken': 'apiToken', 'data': 'data'})
124+
..add(GqlField(name: 'status'))
125+
..add(GqlField(name: 'errors')),
126+
),
106127
);
107128

108129
final data = response.data;
@@ -112,7 +133,7 @@ abstract class AccessInput with _$AccessInput {
112133
return false;
113134
}
114135

115-
final result = data['data'][useUuid ? 'deleteAccessPermissionUuid' : 'deleteAccessPermission'];
136+
final result = data['data'][opName];
116137
if (result == null) {
117138
onResponse?.call(ApiStatus.internalError.toJson());
118139
Log.error("layrz_models/Access/delete(): No result from server");
@@ -131,64 +152,4 @@ abstract class AccessInput with _$AccessInput {
131152
return false;
132153
}
133154
}
134-
135-
/// [addIdGraphqlMutation] GraphQL mutation for adding an access permission
136-
static String get addIdGraphqlMutation => r'''
137-
mutation($apiToken: String!, $data: AccessPermissionInput!) {
138-
addAccessPermission(apiToken: $apiToken, data: $data) {
139-
status
140-
errors
141-
}
142-
}
143-
''';
144-
145-
/// [addUuidGraphqlMutation] GraphQL mutation for adding an access permission
146-
static String get addUuidGraphqlMutation => r'''
147-
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
148-
addAccessPermissionUuid(apiToken: $apiToken, data: $data) {
149-
status
150-
errors
151-
}
152-
}
153-
''';
154-
155-
/// [editIdGraphqlMutation] GraphQL mutation for updating an access permission
156-
static String get editIdGraphqlMutation => r'''
157-
mutation($apiToken: String!, $data: AccessPermissionInput!) {
158-
editAccessPermission(apiToken: $apiToken, data: $data) {
159-
status
160-
errors
161-
}
162-
}
163-
''';
164-
165-
/// [editUuidGraphqlMutation] GraphQL mutation for updating an access permission
166-
static String get editUuidGraphqlMutation => r'''
167-
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
168-
editAccessPermissionUuid(apiToken: $apiToken, data: $data) {
169-
status
170-
errors
171-
}
172-
}
173-
''';
174-
175-
/// [deleteIdGraphqlMutation] GraphQL mutation for deleting an access permission
176-
static String get deleteIdGraphqlMutation => r'''
177-
mutation($apiToken: String!, $data: AccessPermissionInput!) {
178-
deleteAccessPermission(apiToken: $apiToken, data: $data) {
179-
status
180-
errors
181-
}
182-
}
183-
''';
184-
185-
/// [deleteUuidGraphqlMutation] GraphQL mutation for deleting an access permission
186-
static String get deleteUuidGraphqlMutation => r'''
187-
mutation($apiToken: String!, $data: AccessPermissionUuidInput!) {
188-
deleteAccessPermissionUuid(apiToken: $apiToken, data: $data) {
189-
status
190-
errors
191-
}
192-
}
193-
''';
194155
}

lib/src/api/api.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,18 @@
11
library;
22

3+
import 'dart:convert';
4+
5+
import 'package:dio/dio.dart';
36
import 'package:freezed_annotation/freezed_annotation.dart';
47

58
part 'api.freezed.dart';
69
part 'api.g.dart';
710

11+
part 'src/api_connector.dart';
812
part 'src/response.dart';
913
part 'src/status.dart';
14+
15+
part 'src/gql_builder/variables.dart';
16+
part 'src/gql_builder/fragment.dart';
17+
part 'src/gql_builder/field.dart';
18+
part 'src/gql_builder/gql.dart';
Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import 'dart:convert';
2-
3-
import 'package:dio/dio.dart';
1+
part of '../api.dart';
42

53
class LayrzConnector {
64
final Uri uri;
@@ -40,13 +38,18 @@ class LayrzConnector {
4038

4139
late final Dio _dio;
4240

43-
Future<Response> perform({
44-
required String query,
45-
required Map<String, dynamic> variables,
46-
}) =>
47-
_dio.post('', data: {
48-
'query': query,
49-
'variables': variables,
50-
'operationName': null,
51-
});
41+
/// [perform] executes a [Gql] object built with the gql_builder.
42+
/// Each [GqlVariable] with a non-null `value` is included in the variables map;
43+
/// variables without a value are omitted from the wire payload.
44+
Future<Response> perform(Gql gql) {
45+
final variables = <String, dynamic>{
46+
for (final v in gql.variables)
47+
if (v.value != null) v.name: v.value,
48+
};
49+
return _dio.post('', data: {
50+
'query': gql.generated,
51+
'variables': variables,
52+
'operationName': null,
53+
});
54+
}
5255
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
part of '../../api.dart';
2+
3+
class GqlField<T> {
4+
/// [name] is the GraphQL field name, e.g. `charts`, `addChart`, `id`, `name`.
5+
final String name;
6+
7+
/// [alias] is the GraphQL field alias, e.g. `myCharts: charts`. Optional.
8+
final String? alias;
9+
10+
/// [fields] are the child fields to query on this field, e.g. for `charts { id name }`
11+
/// the `charts` field has two child fields `id` and `name`.
12+
late List<GqlField> fields;
13+
14+
/// [parser] is an optional function to parse the raw JSON value of this field into a Dart type.
15+
final T? Function(Object?)? parser;
16+
17+
/// [fragment] is an optional fragment to spread on this field, e.g. `...ChartFragment`.
18+
final GqlFragment? fragment;
19+
20+
/// Key is the GraphQL argument name, value is the variable name (without `$`).
21+
/// Example: `{'apiToken': 'apiToken', 'id': 'id'}` renders as `(apiToken: $apiToken, id: $id)`.
22+
final Map<String, String> args;
23+
24+
/// [GqlField] represents a field in a GraphQL query or mutation, with
25+
/// optional child fields, arguments, and fragments.
26+
GqlField({
27+
required this.name,
28+
this.alias,
29+
this.parser,
30+
List<GqlField>? fields,
31+
this.fragment,
32+
Map<String, String>? args,
33+
}) : args = args ?? const {} {
34+
this.fields = List.from(fields ?? [], growable: true);
35+
}
36+
37+
/// Adds a child field to this field, e.g. adding `id` and `name` to `charts` in `charts { id name }`.
38+
void add(GqlField field) => fields.add(field);
39+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
part of '../../api.dart';
2+
3+
class GqlFragment {
4+
/// [name] is the name of the fragment, e.g. `ChartFragment`.
5+
final String name;
6+
7+
/// [onType] is the GraphQL type on which this fragment is defined, e.g. `Chart`.
8+
final String onType;
9+
10+
/// [fields] are the fields included in this fragment.
11+
late List<GqlField> fields;
12+
13+
/// [GqlFragment] represents a GraphQL fragment, which is a reusable selection of fields on a specific type.
14+
/// Example usage: `fragment ChartFragment on Chart { id name type }` defines a
15+
/// fragment named `ChartFragment` on the `Chart` type, which includes the fields `id`, `name`, and `type`.
16+
GqlFragment({
17+
required this.name,
18+
required this.onType,
19+
List<GqlField>? fields,
20+
}) {
21+
this.fields = List.from(fields ?? [], growable: true);
22+
}
23+
24+
/// Adds a field to this fragment, e.g. adding `id` to `ChartFragment` in `fragment ChartFragment on Chart { id }`.
25+
void add(GqlField field) => fields.add(field);
26+
27+
@override
28+
bool operator ==(Object other) {
29+
if (identical(this, other)) return true;
30+
return other is GqlFragment && other.name == name && other.onType == onType;
31+
}
32+
33+
@override
34+
int get hashCode => name.hashCode ^ onType.hashCode;
35+
}

0 commit comments

Comments
 (0)