@simtlix/simfinity-js-client 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,10 @@
1
- import fetch from 'node-fetch';
1
+ if (typeof globalThis.fetch !== 'function') {
2
+ throw new Error(
3
+ 'SimfinityClient requires a global fetch implementation. ' +
4
+ 'Use Node.js 18+ (which includes native fetch) or provide a fetch polyfill.'
5
+ );
6
+ }
7
+ const _fetch = globalThis.fetch.bind(globalThis);
2
8
 
3
9
  const INTROSPECTION_QUERY = `
4
10
  query IntrospectionQuery {
@@ -11,28 +17,47 @@ const INTROSPECTION_QUERY = `
11
17
  fields {
12
18
  name
13
19
  type {
14
- name kind
20
+ kind name
15
21
  ofType {
16
- name kind
22
+ kind name
17
23
  ofType {
18
- name kind
19
- ofType { name kind }
24
+ kind name
25
+ ofType {
26
+ kind name
27
+ ofType {
28
+ kind name
29
+ }
30
+ }
20
31
  }
21
32
  }
22
33
  }
23
34
  args {
24
35
  name
25
36
  type {
26
- name kind
37
+ kind name
27
38
  ofType {
28
- name kind
39
+ kind name
29
40
  ofType {
30
- name kind
31
- ofType { name kind }
41
+ kind name
42
+ ofType {
43
+ kind name
44
+ ofType {
45
+ kind name
46
+ }
47
+ }
32
48
  }
33
49
  }
34
50
  }
35
51
  }
52
+ extensions {
53
+ relation {
54
+ displayField
55
+ embedded
56
+ connectionField
57
+ }
58
+ stateMachine
59
+ readOnly
60
+ }
36
61
  }
37
62
  enumValues { name }
38
63
  }
@@ -52,6 +77,48 @@ function unwrapType(typeRef) {
52
77
  return { name: current?.name, kind: current?.kind, isList, isNonNull };
53
78
  }
54
79
 
80
+ function getActualScalarType(name) {
81
+ if (!name) return null;
82
+ return name.includes('_') ? name.split('_').pop() || name : name;
83
+ }
84
+
85
+ function isNumericScalar(name) {
86
+ if (!name) return false;
87
+ const actual = getActualScalarType(name);
88
+ if (!actual) return false;
89
+ const n = actual.toLowerCase();
90
+ return n === 'int' || n === 'float' || n === 'idnumber';
91
+ }
92
+
93
+ function isBooleanScalar(name) {
94
+ if (!name) return false;
95
+ const actual = getActualScalarType(name);
96
+ if (!actual) return false;
97
+ return actual.toLowerCase() === 'boolean';
98
+ }
99
+
100
+ function isDateTimeScalar(name) {
101
+ if (!name) return false;
102
+ const actual = getActualScalarType(name);
103
+ if (!actual) return false;
104
+ const n = actual.toLowerCase();
105
+ return n === 'date' || n === 'datetime' || n === 'timestamp'
106
+ || n === 'isodate' || n === 'graphqldate' || n === 'graphqldatetime';
107
+ }
108
+
109
+ function isScalarOrEnum(kind) {
110
+ return kind === 'SCALAR' || kind === 'ENUM';
111
+ }
112
+
113
+ function isListType(rawType) {
114
+ let current = rawType;
115
+ while (current) {
116
+ if (current.kind === 'LIST') return true;
117
+ current = current.ofType || null;
118
+ }
119
+ return false;
120
+ }
121
+
55
122
  function typeRefToString(typeRef) {
56
123
  if (!typeRef) return '';
57
124
  if (typeRef.kind === 'NON_NULL') return typeRefToString(typeRef.ofType) + '!';
@@ -76,6 +143,33 @@ class SelectionTree {
76
143
  }
77
144
  }
78
145
 
146
+ addSelection(selectionString) {
147
+ const tokens = selectionString.trim().split(/\s+/);
148
+ let i = 0;
149
+ while (i < tokens.length) {
150
+ const token = tokens[i];
151
+ if (token === '{' || token === '}') { i++; continue; }
152
+ if (i + 1 < tokens.length && tokens[i + 1] === '{') {
153
+ let depth = 1;
154
+ let j = i + 2;
155
+ while (j < tokens.length && depth > 0) {
156
+ if (tokens[j] === '{') depth++;
157
+ else if (tokens[j] === '}') depth--;
158
+ j++;
159
+ }
160
+ const inner = tokens.slice(i + 2, j - 1).join(' ');
161
+ if (!this.children.has(token)) {
162
+ this.children.set(token, new SelectionTree());
163
+ }
164
+ this.children.get(token).addSelection(inner);
165
+ i = j;
166
+ } else {
167
+ this.scalars.add(token);
168
+ i++;
169
+ }
170
+ }
171
+ }
172
+
79
173
  addNested(dottedPath, fieldNames) {
80
174
  const segments = dottedPath.split('.');
81
175
  let node = this;
@@ -156,7 +250,7 @@ class QueryBuilder {
156
250
  // -- Result Shaping ------------------------------------------------------
157
251
 
158
252
  fields(selectionString) {
159
- this._selectionTree.addScalars(selectionString);
253
+ this._selectionTree.addSelection(selectionString);
160
254
  this._hasExplicitFields = true;
161
255
  return this;
162
256
  }
@@ -174,6 +268,20 @@ class QueryBuilder {
174
268
  return this;
175
269
  }
176
270
 
271
+ clearSort() {
272
+ this._sortTerms = [];
273
+ delete this._variables.sort;
274
+ return this;
275
+ }
276
+
277
+ autoSelect() {
278
+ const result = this._client.buildSelectionSet(this._typeName);
279
+ this._hasExplicitFields = true;
280
+ this._autoSelection = result.selection;
281
+ this._selectionMeta = result;
282
+ return this;
283
+ }
284
+
177
285
  // -- Execution -----------------------------------------------------------
178
286
 
179
287
  async exec() {
@@ -187,7 +295,7 @@ class QueryBuilder {
187
295
  this._selectionTree.addScalars(autoFields);
188
296
  }
189
297
 
190
- const selectionSet = this._selectionTree.serialize();
298
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
191
299
  if (!selectionSet) {
192
300
  throw new Error(`No fields to select for type '${this._typeName}'`);
193
301
  }
@@ -210,12 +318,64 @@ class QueryBuilder {
210
318
  const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
211
319
 
212
320
  const response = await this._client._sendRequest(query, variables);
213
- if (response.errors) {
321
+ if (response.errors && !response.data?.[queryInfo.name]) {
214
322
  const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
215
323
  error.graphQLErrors = response.errors;
216
324
  throw error;
217
325
  }
218
- return response.data[queryInfo.name];
326
+
327
+ const result = response.data[queryInfo.name];
328
+
329
+ if (response.extensions) {
330
+ result.__extensions = response.extensions;
331
+ }
332
+
333
+ return result;
334
+ }
335
+
336
+ async execWithMeta() {
337
+ const queryInfo = this._client._findPluralQuery(this._typeName);
338
+ if (!queryInfo) {
339
+ throw new Error(`No plural query found for type '${this._typeName}'`);
340
+ }
341
+
342
+ if (!this._hasExplicitFields) {
343
+ const autoFields = this._client._getScalarFields(this._typeName);
344
+ this._selectionTree.addScalars(autoFields);
345
+ }
346
+
347
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
348
+ if (!selectionSet) {
349
+ throw new Error(`No fields to select for type '${this._typeName}'`);
350
+ }
351
+
352
+ const varDefs = [];
353
+ const variables = {};
354
+ const argMappings = [];
355
+
356
+ for (const [key, val] of Object.entries(this._variables)) {
357
+ const arg = queryInfo.args.find(a => a.name === key);
358
+ if (arg) {
359
+ varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
360
+ variables[key] = val;
361
+ argMappings.push(`${key}: $${key}`);
362
+ }
363
+ }
364
+
365
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
366
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
367
+ const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
368
+
369
+ const response = await this._client._sendRequest(query, variables);
370
+ if (response.errors && !response.data?.[queryInfo.name]) {
371
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
372
+ error.graphQLErrors = response.errors;
373
+ throw error;
374
+ }
375
+ return {
376
+ data: response.data[queryInfo.name],
377
+ extensions: response.extensions || null,
378
+ };
219
379
  }
220
380
  }
221
381
 
@@ -329,6 +489,7 @@ export default class SimfinityClient {
329
489
  this._typeNameToPlural = new Map();
330
490
  this._typeNameToSingular = new Map();
331
491
  this._typeNameToAggregate = new Map();
492
+ this._queryNameToType = new Map();
332
493
  this._initialized = false;
333
494
  }
334
495
 
@@ -366,6 +527,7 @@ export default class SimfinityClient {
366
527
  type: unwrapType(f.type),
367
528
  rawType: f.type,
368
529
  args: (f.args || []).map(a => ({ name: a.name, type: a.type })),
530
+ extensions: f.extensions || null,
369
531
  })),
370
532
  enumValues: (type.enumValues || []).map(v => v.name),
371
533
  };
@@ -401,6 +563,14 @@ export default class SimfinityClient {
401
563
  this._typeNameToAggregate.set(typeName, aggName);
402
564
  }
403
565
  }
566
+
567
+ this._queryNameToType = new Map();
568
+ for (const [typeName, queryName] of this._typeNameToPlural) {
569
+ this._queryNameToType.set(queryName, typeName);
570
+ }
571
+ for (const [typeName, queryName] of this._typeNameToSingular) {
572
+ this._queryNameToType.set(queryName, typeName);
573
+ }
404
574
  }
405
575
 
406
576
  _buildMutationsRegistry(types, mutationTypeName) {
@@ -494,7 +664,7 @@ export default class SimfinityClient {
494
664
  if (variables && Object.keys(variables).length > 0) {
495
665
  body.variables = variables;
496
666
  }
497
- const response = await fetch(this._endpoint, {
667
+ const response = await _fetch(this._endpoint, {
498
668
  method: 'POST',
499
669
  headers: { 'Content-Type': 'application/json' },
500
670
  body: JSON.stringify(body),
@@ -514,6 +684,64 @@ export default class SimfinityClient {
514
684
  return new AggregateBuilder(this, typeName);
515
685
  }
516
686
 
687
+ findByParent(typeName, connectionField, parentId) {
688
+ this._ensureInitialized();
689
+ const builder = new QueryBuilder(this, typeName);
690
+ builder.where(connectionField, [{ path: 'id', operator: 'EQ', value: parentId }]);
691
+ builder.sort('id', 'ASC');
692
+ return builder;
693
+ }
694
+
695
+ async search(typeName, searchTerm, options = {}) {
696
+ this._ensureInitialized();
697
+ const { page = 1, size = 10, displayField = null } = options;
698
+
699
+ let searchField = displayField;
700
+ if (!searchField) {
701
+ const typeInfo = this._types.get(typeName);
702
+ if (typeInfo?.fields) {
703
+ const nameField = typeInfo.fields.find(f => f.name === 'name');
704
+ if (nameField) {
705
+ searchField = 'name';
706
+ } else {
707
+ const firstScalar = typeInfo.fields.find(f =>
708
+ isScalarOrEnum(f.type.kind) && f.name !== 'id'
709
+ );
710
+ searchField = firstScalar?.name || 'id';
711
+ }
712
+ }
713
+ }
714
+
715
+ const typeInfo = this._types.get(typeName);
716
+ const fieldInfo = typeInfo?.fields.find(f => f.name === searchField);
717
+ const fieldTypeName = fieldInfo?.type?.name || 'String';
718
+
719
+ const isString = !isNumericScalar(fieldTypeName) && !isBooleanScalar(fieldTypeName);
720
+ const operator = isString ? 'LIKE' : 'EQ';
721
+
722
+ let castedTerm = searchTerm;
723
+ if (isNumericScalar(fieldTypeName)) {
724
+ const actualType = getActualScalarType(fieldTypeName);
725
+ if (actualType?.toLowerCase() === 'int') {
726
+ const parsed = parseInt(searchTerm, 10);
727
+ if (!isNaN(parsed)) castedTerm = parsed;
728
+ } else {
729
+ const parsed = parseFloat(searchTerm);
730
+ if (!isNaN(parsed)) castedTerm = parsed;
731
+ }
732
+ } else if (isBooleanScalar(fieldTypeName)) {
733
+ if (searchTerm.toLowerCase() === 'true') castedTerm = true;
734
+ else if (searchTerm.toLowerCase() === 'false') castedTerm = false;
735
+ }
736
+
737
+ const builder = this.find(typeName)
738
+ .fields(`id ${searchField}`)
739
+ .page(page, size, false)
740
+ .where(searchField, operator, castedTerm);
741
+
742
+ return builder.exec();
743
+ }
744
+
517
745
  async getById(typeName, id, fields) {
518
746
  this._ensureInitialized();
519
747
  const queryInfo = this._findSingularQuery(typeName);
@@ -528,7 +756,7 @@ export default class SimfinityClient {
528
756
  const query = `query($id: ${idType}) { ${queryInfo.name}(id: $id) { ${selectionSet} } }`;
529
757
  const response = await this._sendRequest(query, { id });
530
758
 
531
- if (response.errors) {
759
+ if (response.errors && !response.data?.[queryInfo.name]) {
532
760
  const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
533
761
  error.graphQLErrors = response.errors;
534
762
  throw error;
@@ -538,14 +766,24 @@ export default class SimfinityClient {
538
766
 
539
767
  // -- CRUD Mutation Methods -----------------------------------------------
540
768
 
541
- async add(typeName, input, fields) {
769
+ async add(typeName, input, fields, options = {}) {
542
770
  this._ensureInitialized();
543
- return this._executeMutation(`add${typeName}`, typeName, { input }, fields);
771
+ const transformedInput = options.transform
772
+ ? this.transformInput(typeName, input, { mode: 'create', ...options })
773
+ : input;
774
+ return this._executeMutation(`add${typeName}`, typeName, { input: transformedInput }, fields);
544
775
  }
545
776
 
546
- async update(typeName, id, input, fields) {
777
+ async update(typeName, id, input, fields, options = {}) {
547
778
  this._ensureInitialized();
548
- return this._executeMutation(`update${typeName}`, typeName, { input: { id, ...input } }, fields);
779
+ const transformedInput = options.transform
780
+ ? this.transformInput(typeName, input, { mode: 'update', ...options })
781
+ : input;
782
+ return this._executeMutation(
783
+ `update${typeName}`, typeName,
784
+ { input: { id, ...transformedInput } },
785
+ fields,
786
+ );
549
787
  }
550
788
 
551
789
  async delete(typeName, id, fields) {
@@ -585,6 +823,29 @@ export default class SimfinityClient {
585
823
  return null;
586
824
  }
587
825
 
826
+ getStateMachineFields(typeName) {
827
+ this._ensureInitialized();
828
+ const typeInfo = this._types.get(typeName);
829
+ if (!typeInfo?.fields) return [];
830
+ return typeInfo.fields
831
+ .filter(f => f.extensions?.stateMachine === true)
832
+ .map(f => f.name);
833
+ }
834
+
835
+ getAvailableTransitions(typeName) {
836
+ this._ensureInitialized();
837
+ const transitions = [];
838
+ for (const info of this._mutations.values()) {
839
+ if (info.category === 'stateTransition' && info.typeName === typeName) {
840
+ transitions.push({
841
+ action: info.action,
842
+ mutationName: info.name,
843
+ });
844
+ }
845
+ }
846
+ return transitions;
847
+ }
848
+
588
849
  // -- Custom Mutations ----------------------------------------------------
589
850
 
590
851
  async customMutation(mutationName, args, fields) {
@@ -655,6 +916,409 @@ export default class SimfinityClient {
655
916
  return Object.fromEntries(this._mutations);
656
917
  }
657
918
 
919
+ // -- Selection Set Building ------------------------------------------------
920
+
921
+ buildSelectionSet(typeName) {
922
+ this._ensureInitialized();
923
+ const typeInfo = this._types.get(typeName);
924
+ if (!typeInfo?.fields) {
925
+ return { selection: 'id', columns: ['id'], sortFieldByColumn: {}, fieldTypeByColumn: {} };
926
+ }
927
+
928
+ const columns = [];
929
+ const sortFieldByColumn = {};
930
+ const fieldTypeByColumn = {};
931
+ const fieldSelections = [];
932
+
933
+ for (const field of typeInfo.fields) {
934
+ const { name, type: unwrapped, rawType, extensions } = field;
935
+
936
+ if (isListType(rawType)) continue;
937
+
938
+ if (isScalarOrEnum(unwrapped.kind)) {
939
+ fieldSelections.push(name);
940
+ if (name !== 'id') columns.push(name);
941
+ sortFieldByColumn[name] = name;
942
+ fieldTypeByColumn[name] = unwrapped.name || unwrapped.kind || 'SCALAR';
943
+ } else if (unwrapped.kind === 'OBJECT' && unwrapped.name) {
944
+ const objType = this._types.get(unwrapped.name);
945
+ const objFields = objType?.fields || [];
946
+ const objFieldNames = new Set(objFields.map(f => f.name));
947
+ const isEmbedded = extensions?.relation?.embedded === true;
948
+
949
+ let chosenDisplay = null;
950
+ const extDisplay = extensions?.relation?.displayField;
951
+ if (extDisplay && objFieldNames.has(extDisplay)) {
952
+ chosenDisplay = extDisplay;
953
+ } else if (objFieldNames.has('name')) {
954
+ chosenDisplay = 'name';
955
+ } else {
956
+ const firstScalar = objFields.find(f =>
957
+ isScalarOrEnum(f.type.kind) && !f.type.isList
958
+ );
959
+ chosenDisplay = firstScalar?.name || null;
960
+ }
961
+
962
+ const subFields = new Set();
963
+ if (chosenDisplay) subFields.add(chosenDisplay);
964
+ if (!isEmbedded && objFieldNames.has('id')) subFields.add('id');
965
+ if (subFields.size === 0) {
966
+ if (objFieldNames.has('id') && !isEmbedded) subFields.add('id');
967
+ else if (objFieldNames.has('name')) subFields.add('name');
968
+ else if (objFields[0]) subFields.add(objFields[0].name);
969
+ }
970
+
971
+ const subSelection = [...subFields].join(' ');
972
+ fieldSelections.push(`${name} { ${subSelection} }`);
973
+ columns.push(name);
974
+
975
+ if (chosenDisplay) {
976
+ sortFieldByColumn[name] = `${name}.${chosenDisplay}`;
977
+ const displayFieldInfo = objFields.find(f => f.name === chosenDisplay);
978
+ fieldTypeByColumn[name] = displayFieldInfo?.type?.name || 'OBJECT';
979
+ } else {
980
+ sortFieldByColumn[name] = name;
981
+ fieldTypeByColumn[name] = 'OBJECT';
982
+ }
983
+ }
984
+ }
985
+
986
+ const hasId = typeInfo.fields.some(f => f.name === 'id');
987
+ if (hasId && !fieldSelections.includes('id')) {
988
+ fieldSelections.unshift('id');
989
+ }
990
+
991
+ return {
992
+ selection: fieldSelections.join('\n'),
993
+ columns,
994
+ sortFieldByColumn,
995
+ fieldTypeByColumn,
996
+ };
997
+ }
998
+
999
+ // -- Schema Metadata Access -----------------------------------------------
1000
+
1001
+ getFieldExtensions(typeName, fieldName) {
1002
+ this._ensureInitialized();
1003
+ const typeInfo = this._types.get(typeName);
1004
+ if (!typeInfo) return null;
1005
+ const field = typeInfo.fields.find(f => f.name === fieldName);
1006
+ return field?.extensions || null;
1007
+ }
1008
+
1009
+ getDisplayField(typeName, fieldName) {
1010
+ this._ensureInitialized();
1011
+ const ext = this.getFieldExtensions(typeName, fieldName);
1012
+ const displayField = ext?.relation?.displayField;
1013
+ if (displayField) return displayField;
1014
+
1015
+ const typeInfo = this._types.get(typeName);
1016
+ const field = typeInfo?.fields.find(f => f.name === fieldName);
1017
+ if (!field || field.type.kind !== 'OBJECT') return null;
1018
+
1019
+ const objectType = this._types.get(field.type.name);
1020
+ if (!objectType?.fields) return null;
1021
+
1022
+ const objFieldNames = objectType.fields.map(f => f.name);
1023
+ if (objFieldNames.includes('name')) return 'name';
1024
+
1025
+ const firstScalar = objectType.fields.find(f =>
1026
+ isScalarOrEnum(f.type.kind) && !f.type.isList
1027
+ );
1028
+ return firstScalar?.name || null;
1029
+ }
1030
+
1031
+ isEmbeddedField(typeName, fieldName) {
1032
+ this._ensureInitialized();
1033
+ const ext = this.getFieldExtensions(typeName, fieldName);
1034
+ return ext?.relation?.embedded === true;
1035
+ }
1036
+
1037
+ getConnectionField(typeName, fieldName) {
1038
+ this._ensureInitialized();
1039
+ const ext = this.getFieldExtensions(typeName, fieldName);
1040
+ return ext?.relation?.connectionField || null;
1041
+ }
1042
+
1043
+ isStateMachineField(typeName, fieldName) {
1044
+ this._ensureInitialized();
1045
+ const ext = this.getFieldExtensions(typeName, fieldName);
1046
+ return ext?.stateMachine === true;
1047
+ }
1048
+
1049
+ isReadOnlyField(typeName, fieldName) {
1050
+ this._ensureInitialized();
1051
+ const ext = this.getFieldExtensions(typeName, fieldName);
1052
+ return ext?.readOnly === true;
1053
+ }
1054
+
1055
+ getEnumValues(typeName) {
1056
+ this._ensureInitialized();
1057
+ const typeInfo = this._types.get(typeName);
1058
+ if (!typeInfo || typeInfo.kind !== 'ENUM') return [];
1059
+ return typeInfo.enumValues || [];
1060
+ }
1061
+
1062
+ getFieldsOfType(typeName) {
1063
+ this._ensureInitialized();
1064
+ const typeInfo = this._types.get(typeName);
1065
+ if (!typeInfo) return [];
1066
+ return typeInfo.fields;
1067
+ }
1068
+
1069
+ getDescriptionFieldType(typeName, descriptionField) {
1070
+ this._ensureInitialized();
1071
+ const typeInfo = this._types.get(typeName);
1072
+ if (!typeInfo?.fields) return 'String';
1073
+ const field = typeInfo.fields.find(f => f.name === descriptionField);
1074
+ if (!field) return 'String';
1075
+ return field.type.name || 'String';
1076
+ }
1077
+
1078
+ // -- Entity & Query Name Resolution ----------------------------------------
1079
+
1080
+ getTypeNameForQuery(queryName) {
1081
+ this._ensureInitialized();
1082
+ const queryInfo = this._queries.get(queryName);
1083
+ return queryInfo?.returnType?.name || null;
1084
+ }
1085
+
1086
+ getPluralQueryName(typeName) {
1087
+ this._ensureInitialized();
1088
+ return this._typeNameToPlural.get(typeName) || null;
1089
+ }
1090
+
1091
+ getSingularQueryName(typeName) {
1092
+ this._ensureInitialized();
1093
+ return this._typeNameToSingular.get(typeName) || null;
1094
+ }
1095
+
1096
+ getListEntityNames() {
1097
+ this._ensureInitialized();
1098
+ const names = [];
1099
+ for (const [queryName, queryInfo] of this._queries) {
1100
+ if (queryInfo.returnType.isList && !queryName.endsWith('_aggregate')) {
1101
+ names.push(queryName);
1102
+ }
1103
+ }
1104
+ return names.sort();
1105
+ }
1106
+
1107
+ getListEntityNamesOfType(typeName) {
1108
+ this._ensureInitialized();
1109
+ const names = [];
1110
+ for (const [queryName, queryInfo] of this._queries) {
1111
+ if (queryInfo.returnType.isList
1112
+ && queryInfo.returnType.name === typeName
1113
+ && !queryName.endsWith('_aggregate')) {
1114
+ names.push(queryName);
1115
+ }
1116
+ }
1117
+ return names.sort();
1118
+ }
1119
+
1120
+ getQueryNamesForType(typeName) {
1121
+ this._ensureInitialized();
1122
+ return {
1123
+ pluralQueryName: this._typeNameToPlural.get(typeName) || null,
1124
+ singularQueryName: this._typeNameToSingular.get(typeName) || null,
1125
+ aggregateQueryName: this._typeNameToAggregate.get(typeName) || null,
1126
+ };
1127
+ }
1128
+
1129
+ // -- Scalar Type Utilities -----------------------------------------------
1130
+
1131
+ getActualScalarType(scalarName) { return getActualScalarType(scalarName); }
1132
+ isNumericScalar(scalarName) { return isNumericScalar(scalarName); }
1133
+ isBooleanScalar(scalarName) { return isBooleanScalar(scalarName); }
1134
+ isDateTimeScalar(scalarName) { return isDateTimeScalar(scalarName); }
1135
+
1136
+ // -- Mutation Input Transformation -----------------------------------------
1137
+
1138
+ transformInput(typeName, rawInput, options = {}) {
1139
+ this._ensureInitialized();
1140
+ const { skipFields = [], transientFields = [], mode = 'create' } = options;
1141
+ const typeInfo = this._types.get(typeName);
1142
+ if (!typeInfo) return { ...rawInput };
1143
+
1144
+ const transformed = {};
1145
+
1146
+ for (const field of typeInfo.fields) {
1147
+ const { name, type: unwrapped, rawType, extensions } = field;
1148
+
1149
+ if (rawInput[name] === undefined) continue;
1150
+ if (name === 'id') continue;
1151
+ if (skipFields.includes(name) || transientFields.includes(name)) continue;
1152
+ if (extensions?.stateMachine === true) continue;
1153
+ if (extensions?.readOnly === true) continue;
1154
+ if (isListType(rawType) && unwrapped.kind === 'OBJECT') continue;
1155
+
1156
+ let value = rawInput[name];
1157
+
1158
+ if (unwrapped.kind === 'OBJECT' && extensions?.relation?.embedded === true) {
1159
+ value = this._transformEmbeddedInput(unwrapped.name, value, options);
1160
+ } else if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1161
+ value = this._cleanObjectFieldForMutation(value);
1162
+ } else {
1163
+ value = this._coerceScalarValue(value, unwrapped.name);
1164
+ }
1165
+
1166
+ if (value !== undefined && value !== null && value !== '') {
1167
+ transformed[name] = value;
1168
+ }
1169
+ }
1170
+
1171
+ return this._deepRemoveTypename(transformed);
1172
+ }
1173
+
1174
+ _transformEmbeddedInput(objectTypeName, value, options = {}) {
1175
+ if (!value || typeof value !== 'object') return value;
1176
+ const typeInfo = this._types.get(objectTypeName);
1177
+ if (!typeInfo?.fields) return value;
1178
+
1179
+ const cleaned = {};
1180
+ for (const field of typeInfo.fields) {
1181
+ const { name, type: unwrapped, extensions } = field;
1182
+ if (value[name] === undefined) continue;
1183
+ if (extensions?.readOnly === true) continue;
1184
+
1185
+ let fieldValue = value[name];
1186
+ fieldValue = this._coerceScalarValue(fieldValue, unwrapped.name);
1187
+
1188
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
1189
+ cleaned[name] = fieldValue;
1190
+ }
1191
+ }
1192
+ return Object.keys(cleaned).length > 0 ? cleaned : undefined;
1193
+ }
1194
+
1195
+ _cleanObjectFieldForMutation(value) {
1196
+ if (!value) return value;
1197
+ if (typeof value === 'string') return { id: value };
1198
+ if (typeof value === 'object' && value.id) return { id: value.id };
1199
+ return value;
1200
+ }
1201
+
1202
+ _coerceScalarValue(value, scalarTypeName) {
1203
+ if (value === null || value === undefined) return value;
1204
+
1205
+ if (isNumericScalar(scalarTypeName) && typeof value === 'string') {
1206
+ return Number(value);
1207
+ }
1208
+
1209
+ if (isDateTimeScalar(scalarTypeName) && typeof value === 'string') {
1210
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1211
+ return `${value}T00:00:00.000Z`;
1212
+ }
1213
+ }
1214
+
1215
+ return value;
1216
+ }
1217
+
1218
+ _deepRemoveTypename(obj) {
1219
+ if (typeof obj !== 'object' || obj === null) return obj;
1220
+ if (Array.isArray(obj)) return obj.map(item => this._deepRemoveTypename(item));
1221
+
1222
+ const cleaned = {};
1223
+ for (const [key, value] of Object.entries(obj)) {
1224
+ if (key === '__typename') continue;
1225
+ cleaned[key] = this._deepRemoveTypename(value);
1226
+ }
1227
+ return cleaned;
1228
+ }
1229
+
1230
+ // -- Collection Delta Mutations --------------------------------------------
1231
+
1232
+ transformCollectionDelta(collectionTypeName, delta, options = {}) {
1233
+ this._ensureInitialized();
1234
+ const { connectionField = null } = options;
1235
+ const result = {};
1236
+
1237
+ if (delta.added && delta.added.length > 0) {
1238
+ result.added = delta.added.map(item => {
1239
+ let clean = { ...item };
1240
+
1241
+ delete clean.__status;
1242
+ delete clean.__originalData;
1243
+
1244
+ if (connectionField && clean[connectionField] !== undefined) {
1245
+ delete clean[connectionField];
1246
+ }
1247
+
1248
+ if (clean.id && typeof clean.id === 'string' && clean.id.startsWith('temp_')) {
1249
+ delete clean.id;
1250
+ }
1251
+
1252
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1253
+ clean = this._deepRemoveTypename(clean);
1254
+ return clean;
1255
+ });
1256
+ }
1257
+
1258
+ if (delta.updated && delta.updated.length > 0) {
1259
+ result.updated = delta.updated.map(item => {
1260
+ let clean = { ...item };
1261
+
1262
+ delete clean.__status;
1263
+ delete clean.__originalData;
1264
+
1265
+ if (connectionField && clean[connectionField] !== undefined) {
1266
+ delete clean[connectionField];
1267
+ }
1268
+
1269
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1270
+ clean = this._deepRemoveTypename(clean);
1271
+ return clean;
1272
+ });
1273
+ }
1274
+
1275
+ if (delta.deleted && delta.deleted.length > 0) {
1276
+ result.deleted = delta.deleted.map(item => {
1277
+ if (typeof item === 'string') return item;
1278
+ if (typeof item === 'object' && item.id) return item.id;
1279
+ return item;
1280
+ });
1281
+ }
1282
+
1283
+ return result;
1284
+ }
1285
+
1286
+ _cleanCollectionItem(collectionTypeName, item) {
1287
+ const typeInfo = this._types.get(collectionTypeName);
1288
+ if (!typeInfo?.fields) return item;
1289
+
1290
+ const cleaned = { ...item };
1291
+
1292
+ for (const field of typeInfo.fields) {
1293
+ const { name, type: unwrapped, rawType, extensions } = field;
1294
+ if (cleaned[name] === undefined) continue;
1295
+
1296
+ if (extensions?.stateMachine === true) {
1297
+ delete cleaned[name];
1298
+ continue;
1299
+ }
1300
+
1301
+ if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1302
+ const val = cleaned[name];
1303
+ if (val && typeof val === 'object' && 'id' in val) {
1304
+ cleaned[name] = { id: val.id };
1305
+ }
1306
+ continue;
1307
+ }
1308
+
1309
+ if (isNumericScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1310
+ cleaned[name] = Number(cleaned[name]);
1311
+ }
1312
+ if (isDateTimeScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1313
+ if (/^\d{4}-\d{2}-\d{2}$/.test(cleaned[name])) {
1314
+ cleaned[name] = `${cleaned[name]}T00:00:00.000Z`;
1315
+ }
1316
+ }
1317
+ }
1318
+
1319
+ return cleaned;
1320
+ }
1321
+
658
1322
  // -- Guard ---------------------------------------------------------------
659
1323
 
660
1324
  _ensureInitialized() {