@simtlix/simfinity-js-client 1.0.0 → 1.0.1

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,14 @@ class QueryBuilder {
174
268
  return this;
175
269
  }
176
270
 
271
+ autoSelect() {
272
+ const result = this._client.buildSelectionSet(this._typeName);
273
+ this._hasExplicitFields = true;
274
+ this._autoSelection = result.selection;
275
+ this._selectionMeta = result;
276
+ return this;
277
+ }
278
+
177
279
  // -- Execution -----------------------------------------------------------
178
280
 
179
281
  async exec() {
@@ -187,7 +289,7 @@ class QueryBuilder {
187
289
  this._selectionTree.addScalars(autoFields);
188
290
  }
189
291
 
190
- const selectionSet = this._selectionTree.serialize();
292
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
191
293
  if (!selectionSet) {
192
294
  throw new Error(`No fields to select for type '${this._typeName}'`);
193
295
  }
@@ -215,7 +317,59 @@ class QueryBuilder {
215
317
  error.graphQLErrors = response.errors;
216
318
  throw error;
217
319
  }
218
- return response.data[queryInfo.name];
320
+
321
+ const result = response.data[queryInfo.name];
322
+
323
+ if (response.extensions) {
324
+ result.__extensions = response.extensions;
325
+ }
326
+
327
+ return result;
328
+ }
329
+
330
+ async execWithMeta() {
331
+ const queryInfo = this._client._findPluralQuery(this._typeName);
332
+ if (!queryInfo) {
333
+ throw new Error(`No plural query found for type '${this._typeName}'`);
334
+ }
335
+
336
+ if (!this._hasExplicitFields) {
337
+ const autoFields = this._client._getScalarFields(this._typeName);
338
+ this._selectionTree.addScalars(autoFields);
339
+ }
340
+
341
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
342
+ if (!selectionSet) {
343
+ throw new Error(`No fields to select for type '${this._typeName}'`);
344
+ }
345
+
346
+ const varDefs = [];
347
+ const variables = {};
348
+ const argMappings = [];
349
+
350
+ for (const [key, val] of Object.entries(this._variables)) {
351
+ const arg = queryInfo.args.find(a => a.name === key);
352
+ if (arg) {
353
+ varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
354
+ variables[key] = val;
355
+ argMappings.push(`${key}: $${key}`);
356
+ }
357
+ }
358
+
359
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
360
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
361
+ const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
362
+
363
+ const response = await this._client._sendRequest(query, variables);
364
+ if (response.errors) {
365
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
366
+ error.graphQLErrors = response.errors;
367
+ throw error;
368
+ }
369
+ return {
370
+ data: response.data[queryInfo.name],
371
+ extensions: response.extensions || null,
372
+ };
219
373
  }
220
374
  }
221
375
 
@@ -329,6 +483,7 @@ export default class SimfinityClient {
329
483
  this._typeNameToPlural = new Map();
330
484
  this._typeNameToSingular = new Map();
331
485
  this._typeNameToAggregate = new Map();
486
+ this._queryNameToType = new Map();
332
487
  this._initialized = false;
333
488
  }
334
489
 
@@ -366,6 +521,7 @@ export default class SimfinityClient {
366
521
  type: unwrapType(f.type),
367
522
  rawType: f.type,
368
523
  args: (f.args || []).map(a => ({ name: a.name, type: a.type })),
524
+ extensions: f.extensions || null,
369
525
  })),
370
526
  enumValues: (type.enumValues || []).map(v => v.name),
371
527
  };
@@ -401,6 +557,14 @@ export default class SimfinityClient {
401
557
  this._typeNameToAggregate.set(typeName, aggName);
402
558
  }
403
559
  }
560
+
561
+ this._queryNameToType = new Map();
562
+ for (const [typeName, queryName] of this._typeNameToPlural) {
563
+ this._queryNameToType.set(queryName, typeName);
564
+ }
565
+ for (const [typeName, queryName] of this._typeNameToSingular) {
566
+ this._queryNameToType.set(queryName, typeName);
567
+ }
404
568
  }
405
569
 
406
570
  _buildMutationsRegistry(types, mutationTypeName) {
@@ -494,7 +658,7 @@ export default class SimfinityClient {
494
658
  if (variables && Object.keys(variables).length > 0) {
495
659
  body.variables = variables;
496
660
  }
497
- const response = await fetch(this._endpoint, {
661
+ const response = await _fetch(this._endpoint, {
498
662
  method: 'POST',
499
663
  headers: { 'Content-Type': 'application/json' },
500
664
  body: JSON.stringify(body),
@@ -514,6 +678,64 @@ export default class SimfinityClient {
514
678
  return new AggregateBuilder(this, typeName);
515
679
  }
516
680
 
681
+ findByParent(typeName, connectionField, parentId) {
682
+ this._ensureInitialized();
683
+ const builder = new QueryBuilder(this, typeName);
684
+ builder.where(connectionField, [{ path: 'id', operator: 'EQ', value: parentId }]);
685
+ builder.sort('id', 'ASC');
686
+ return builder;
687
+ }
688
+
689
+ async search(typeName, searchTerm, options = {}) {
690
+ this._ensureInitialized();
691
+ const { page = 1, size = 10, displayField = null } = options;
692
+
693
+ let searchField = displayField;
694
+ if (!searchField) {
695
+ const typeInfo = this._types.get(typeName);
696
+ if (typeInfo?.fields) {
697
+ const nameField = typeInfo.fields.find(f => f.name === 'name');
698
+ if (nameField) {
699
+ searchField = 'name';
700
+ } else {
701
+ const firstScalar = typeInfo.fields.find(f =>
702
+ isScalarOrEnum(f.type.kind) && f.name !== 'id'
703
+ );
704
+ searchField = firstScalar?.name || 'id';
705
+ }
706
+ }
707
+ }
708
+
709
+ const typeInfo = this._types.get(typeName);
710
+ const fieldInfo = typeInfo?.fields.find(f => f.name === searchField);
711
+ const fieldTypeName = fieldInfo?.type?.name || 'String';
712
+
713
+ const isString = !isNumericScalar(fieldTypeName) && !isBooleanScalar(fieldTypeName);
714
+ const operator = isString ? 'LIKE' : 'EQ';
715
+
716
+ let castedTerm = searchTerm;
717
+ if (isNumericScalar(fieldTypeName)) {
718
+ const actualType = getActualScalarType(fieldTypeName);
719
+ if (actualType?.toLowerCase() === 'int') {
720
+ const parsed = parseInt(searchTerm, 10);
721
+ if (!isNaN(parsed)) castedTerm = parsed;
722
+ } else {
723
+ const parsed = parseFloat(searchTerm);
724
+ if (!isNaN(parsed)) castedTerm = parsed;
725
+ }
726
+ } else if (isBooleanScalar(fieldTypeName)) {
727
+ if (searchTerm.toLowerCase() === 'true') castedTerm = true;
728
+ else if (searchTerm.toLowerCase() === 'false') castedTerm = false;
729
+ }
730
+
731
+ const builder = this.find(typeName)
732
+ .fields(`id ${searchField}`)
733
+ .page(page, size, false)
734
+ .where(searchField, operator, castedTerm);
735
+
736
+ return builder.exec();
737
+ }
738
+
517
739
  async getById(typeName, id, fields) {
518
740
  this._ensureInitialized();
519
741
  const queryInfo = this._findSingularQuery(typeName);
@@ -538,14 +760,24 @@ export default class SimfinityClient {
538
760
 
539
761
  // -- CRUD Mutation Methods -----------------------------------------------
540
762
 
541
- async add(typeName, input, fields) {
763
+ async add(typeName, input, fields, options = {}) {
542
764
  this._ensureInitialized();
543
- return this._executeMutation(`add${typeName}`, typeName, { input }, fields);
765
+ const transformedInput = options.transform
766
+ ? this.transformInput(typeName, input, { mode: 'create', ...options })
767
+ : input;
768
+ return this._executeMutation(`add${typeName}`, typeName, { input: transformedInput }, fields);
544
769
  }
545
770
 
546
- async update(typeName, id, input, fields) {
771
+ async update(typeName, id, input, fields, options = {}) {
547
772
  this._ensureInitialized();
548
- return this._executeMutation(`update${typeName}`, typeName, { input: { id, ...input } }, fields);
773
+ const transformedInput = options.transform
774
+ ? this.transformInput(typeName, input, { mode: 'update', ...options })
775
+ : input;
776
+ return this._executeMutation(
777
+ `update${typeName}`, typeName,
778
+ { input: { id, ...transformedInput } },
779
+ fields,
780
+ );
549
781
  }
550
782
 
551
783
  async delete(typeName, id, fields) {
@@ -585,6 +817,29 @@ export default class SimfinityClient {
585
817
  return null;
586
818
  }
587
819
 
820
+ getStateMachineFields(typeName) {
821
+ this._ensureInitialized();
822
+ const typeInfo = this._types.get(typeName);
823
+ if (!typeInfo?.fields) return [];
824
+ return typeInfo.fields
825
+ .filter(f => f.extensions?.stateMachine === true)
826
+ .map(f => f.name);
827
+ }
828
+
829
+ getAvailableTransitions(typeName) {
830
+ this._ensureInitialized();
831
+ const transitions = [];
832
+ for (const info of this._mutations.values()) {
833
+ if (info.category === 'stateTransition' && info.typeName === typeName) {
834
+ transitions.push({
835
+ action: info.action,
836
+ mutationName: info.name,
837
+ });
838
+ }
839
+ }
840
+ return transitions;
841
+ }
842
+
588
843
  // -- Custom Mutations ----------------------------------------------------
589
844
 
590
845
  async customMutation(mutationName, args, fields) {
@@ -655,6 +910,409 @@ export default class SimfinityClient {
655
910
  return Object.fromEntries(this._mutations);
656
911
  }
657
912
 
913
+ // -- Selection Set Building ------------------------------------------------
914
+
915
+ buildSelectionSet(typeName) {
916
+ this._ensureInitialized();
917
+ const typeInfo = this._types.get(typeName);
918
+ if (!typeInfo?.fields) {
919
+ return { selection: 'id', columns: ['id'], sortFieldByColumn: {}, fieldTypeByColumn: {} };
920
+ }
921
+
922
+ const columns = [];
923
+ const sortFieldByColumn = {};
924
+ const fieldTypeByColumn = {};
925
+ const fieldSelections = [];
926
+
927
+ for (const field of typeInfo.fields) {
928
+ const { name, type: unwrapped, rawType, extensions } = field;
929
+
930
+ if (isListType(rawType)) continue;
931
+
932
+ if (isScalarOrEnum(unwrapped.kind)) {
933
+ fieldSelections.push(name);
934
+ if (name !== 'id') columns.push(name);
935
+ sortFieldByColumn[name] = name;
936
+ fieldTypeByColumn[name] = unwrapped.name || unwrapped.kind || 'SCALAR';
937
+ } else if (unwrapped.kind === 'OBJECT' && unwrapped.name) {
938
+ const objType = this._types.get(unwrapped.name);
939
+ const objFields = objType?.fields || [];
940
+ const objFieldNames = new Set(objFields.map(f => f.name));
941
+ const isEmbedded = extensions?.relation?.embedded === true;
942
+
943
+ let chosenDisplay = null;
944
+ const extDisplay = extensions?.relation?.displayField;
945
+ if (extDisplay && objFieldNames.has(extDisplay)) {
946
+ chosenDisplay = extDisplay;
947
+ } else if (objFieldNames.has('name')) {
948
+ chosenDisplay = 'name';
949
+ } else {
950
+ const firstScalar = objFields.find(f =>
951
+ isScalarOrEnum(f.type.kind) && !f.type.isList
952
+ );
953
+ chosenDisplay = firstScalar?.name || null;
954
+ }
955
+
956
+ const subFields = new Set();
957
+ if (chosenDisplay) subFields.add(chosenDisplay);
958
+ if (!isEmbedded && objFieldNames.has('id')) subFields.add('id');
959
+ if (subFields.size === 0) {
960
+ if (objFieldNames.has('id') && !isEmbedded) subFields.add('id');
961
+ else if (objFieldNames.has('name')) subFields.add('name');
962
+ else if (objFields[0]) subFields.add(objFields[0].name);
963
+ }
964
+
965
+ const subSelection = [...subFields].join(' ');
966
+ fieldSelections.push(`${name} { ${subSelection} }`);
967
+ columns.push(name);
968
+
969
+ if (chosenDisplay) {
970
+ sortFieldByColumn[name] = `${name}.${chosenDisplay}`;
971
+ const displayFieldInfo = objFields.find(f => f.name === chosenDisplay);
972
+ fieldTypeByColumn[name] = displayFieldInfo?.type?.name || 'OBJECT';
973
+ } else {
974
+ sortFieldByColumn[name] = name;
975
+ fieldTypeByColumn[name] = 'OBJECT';
976
+ }
977
+ }
978
+ }
979
+
980
+ const hasId = typeInfo.fields.some(f => f.name === 'id');
981
+ if (hasId && !fieldSelections.includes('id')) {
982
+ fieldSelections.unshift('id');
983
+ }
984
+
985
+ return {
986
+ selection: fieldSelections.join('\n'),
987
+ columns,
988
+ sortFieldByColumn,
989
+ fieldTypeByColumn,
990
+ };
991
+ }
992
+
993
+ // -- Schema Metadata Access -----------------------------------------------
994
+
995
+ getFieldExtensions(typeName, fieldName) {
996
+ this._ensureInitialized();
997
+ const typeInfo = this._types.get(typeName);
998
+ if (!typeInfo) return null;
999
+ const field = typeInfo.fields.find(f => f.name === fieldName);
1000
+ return field?.extensions || null;
1001
+ }
1002
+
1003
+ getDisplayField(typeName, fieldName) {
1004
+ this._ensureInitialized();
1005
+ const ext = this.getFieldExtensions(typeName, fieldName);
1006
+ const displayField = ext?.relation?.displayField;
1007
+ if (displayField) return displayField;
1008
+
1009
+ const typeInfo = this._types.get(typeName);
1010
+ const field = typeInfo?.fields.find(f => f.name === fieldName);
1011
+ if (!field || field.type.kind !== 'OBJECT') return null;
1012
+
1013
+ const objectType = this._types.get(field.type.name);
1014
+ if (!objectType?.fields) return null;
1015
+
1016
+ const objFieldNames = objectType.fields.map(f => f.name);
1017
+ if (objFieldNames.includes('name')) return 'name';
1018
+
1019
+ const firstScalar = objectType.fields.find(f =>
1020
+ isScalarOrEnum(f.type.kind) && !f.type.isList
1021
+ );
1022
+ return firstScalar?.name || null;
1023
+ }
1024
+
1025
+ isEmbeddedField(typeName, fieldName) {
1026
+ this._ensureInitialized();
1027
+ const ext = this.getFieldExtensions(typeName, fieldName);
1028
+ return ext?.relation?.embedded === true;
1029
+ }
1030
+
1031
+ getConnectionField(typeName, fieldName) {
1032
+ this._ensureInitialized();
1033
+ const ext = this.getFieldExtensions(typeName, fieldName);
1034
+ return ext?.relation?.connectionField || null;
1035
+ }
1036
+
1037
+ isStateMachineField(typeName, fieldName) {
1038
+ this._ensureInitialized();
1039
+ const ext = this.getFieldExtensions(typeName, fieldName);
1040
+ return ext?.stateMachine === true;
1041
+ }
1042
+
1043
+ isReadOnlyField(typeName, fieldName) {
1044
+ this._ensureInitialized();
1045
+ const ext = this.getFieldExtensions(typeName, fieldName);
1046
+ return ext?.readOnly === true;
1047
+ }
1048
+
1049
+ getEnumValues(typeName) {
1050
+ this._ensureInitialized();
1051
+ const typeInfo = this._types.get(typeName);
1052
+ if (!typeInfo || typeInfo.kind !== 'ENUM') return [];
1053
+ return typeInfo.enumValues || [];
1054
+ }
1055
+
1056
+ getFieldsOfType(typeName) {
1057
+ this._ensureInitialized();
1058
+ const typeInfo = this._types.get(typeName);
1059
+ if (!typeInfo) return [];
1060
+ return typeInfo.fields;
1061
+ }
1062
+
1063
+ getDescriptionFieldType(typeName, descriptionField) {
1064
+ this._ensureInitialized();
1065
+ const typeInfo = this._types.get(typeName);
1066
+ if (!typeInfo?.fields) return 'String';
1067
+ const field = typeInfo.fields.find(f => f.name === descriptionField);
1068
+ if (!field) return 'String';
1069
+ return field.type.name || 'String';
1070
+ }
1071
+
1072
+ // -- Entity & Query Name Resolution ----------------------------------------
1073
+
1074
+ getTypeNameForQuery(queryName) {
1075
+ this._ensureInitialized();
1076
+ const queryInfo = this._queries.get(queryName);
1077
+ return queryInfo?.returnType?.name || null;
1078
+ }
1079
+
1080
+ getPluralQueryName(typeName) {
1081
+ this._ensureInitialized();
1082
+ return this._typeNameToPlural.get(typeName) || null;
1083
+ }
1084
+
1085
+ getSingularQueryName(typeName) {
1086
+ this._ensureInitialized();
1087
+ return this._typeNameToSingular.get(typeName) || null;
1088
+ }
1089
+
1090
+ getListEntityNames() {
1091
+ this._ensureInitialized();
1092
+ const names = [];
1093
+ for (const [queryName, queryInfo] of this._queries) {
1094
+ if (queryInfo.returnType.isList && !queryName.endsWith('_aggregate')) {
1095
+ names.push(queryName);
1096
+ }
1097
+ }
1098
+ return names.sort();
1099
+ }
1100
+
1101
+ getListEntityNamesOfType(typeName) {
1102
+ this._ensureInitialized();
1103
+ const names = [];
1104
+ for (const [queryName, queryInfo] of this._queries) {
1105
+ if (queryInfo.returnType.isList
1106
+ && queryInfo.returnType.name === typeName
1107
+ && !queryName.endsWith('_aggregate')) {
1108
+ names.push(queryName);
1109
+ }
1110
+ }
1111
+ return names.sort();
1112
+ }
1113
+
1114
+ getQueryNamesForType(typeName) {
1115
+ this._ensureInitialized();
1116
+ return {
1117
+ pluralQueryName: this._typeNameToPlural.get(typeName) || null,
1118
+ singularQueryName: this._typeNameToSingular.get(typeName) || null,
1119
+ aggregateQueryName: this._typeNameToAggregate.get(typeName) || null,
1120
+ };
1121
+ }
1122
+
1123
+ // -- Scalar Type Utilities -----------------------------------------------
1124
+
1125
+ getActualScalarType(scalarName) { return getActualScalarType(scalarName); }
1126
+ isNumericScalar(scalarName) { return isNumericScalar(scalarName); }
1127
+ isBooleanScalar(scalarName) { return isBooleanScalar(scalarName); }
1128
+ isDateTimeScalar(scalarName) { return isDateTimeScalar(scalarName); }
1129
+
1130
+ // -- Mutation Input Transformation -----------------------------------------
1131
+
1132
+ transformInput(typeName, rawInput, options = {}) {
1133
+ this._ensureInitialized();
1134
+ const { skipFields = [], transientFields = [], mode = 'create' } = options;
1135
+ const typeInfo = this._types.get(typeName);
1136
+ if (!typeInfo) return { ...rawInput };
1137
+
1138
+ const transformed = {};
1139
+
1140
+ for (const field of typeInfo.fields) {
1141
+ const { name, type: unwrapped, rawType, extensions } = field;
1142
+
1143
+ if (rawInput[name] === undefined) continue;
1144
+ if (name === 'id') continue;
1145
+ if (skipFields.includes(name) || transientFields.includes(name)) continue;
1146
+ if (extensions?.stateMachine === true) continue;
1147
+ if (extensions?.readOnly === true) continue;
1148
+ if (isListType(rawType) && unwrapped.kind === 'OBJECT') continue;
1149
+
1150
+ let value = rawInput[name];
1151
+
1152
+ if (unwrapped.kind === 'OBJECT' && extensions?.relation?.embedded === true) {
1153
+ value = this._transformEmbeddedInput(unwrapped.name, value, options);
1154
+ } else if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1155
+ value = this._cleanObjectFieldForMutation(value);
1156
+ } else {
1157
+ value = this._coerceScalarValue(value, unwrapped.name);
1158
+ }
1159
+
1160
+ if (value !== undefined && value !== null && value !== '') {
1161
+ transformed[name] = value;
1162
+ }
1163
+ }
1164
+
1165
+ return this._deepRemoveTypename(transformed);
1166
+ }
1167
+
1168
+ _transformEmbeddedInput(objectTypeName, value, options = {}) {
1169
+ if (!value || typeof value !== 'object') return value;
1170
+ const typeInfo = this._types.get(objectTypeName);
1171
+ if (!typeInfo?.fields) return value;
1172
+
1173
+ const cleaned = {};
1174
+ for (const field of typeInfo.fields) {
1175
+ const { name, type: unwrapped, extensions } = field;
1176
+ if (value[name] === undefined) continue;
1177
+ if (extensions?.readOnly === true) continue;
1178
+
1179
+ let fieldValue = value[name];
1180
+ fieldValue = this._coerceScalarValue(fieldValue, unwrapped.name);
1181
+
1182
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
1183
+ cleaned[name] = fieldValue;
1184
+ }
1185
+ }
1186
+ return Object.keys(cleaned).length > 0 ? cleaned : undefined;
1187
+ }
1188
+
1189
+ _cleanObjectFieldForMutation(value) {
1190
+ if (!value) return value;
1191
+ if (typeof value === 'string') return { id: value };
1192
+ if (typeof value === 'object' && value.id) return { id: value.id };
1193
+ return value;
1194
+ }
1195
+
1196
+ _coerceScalarValue(value, scalarTypeName) {
1197
+ if (value === null || value === undefined) return value;
1198
+
1199
+ if (isNumericScalar(scalarTypeName) && typeof value === 'string') {
1200
+ return Number(value);
1201
+ }
1202
+
1203
+ if (isDateTimeScalar(scalarTypeName) && typeof value === 'string') {
1204
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1205
+ return `${value}T00:00:00.000Z`;
1206
+ }
1207
+ }
1208
+
1209
+ return value;
1210
+ }
1211
+
1212
+ _deepRemoveTypename(obj) {
1213
+ if (typeof obj !== 'object' || obj === null) return obj;
1214
+ if (Array.isArray(obj)) return obj.map(item => this._deepRemoveTypename(item));
1215
+
1216
+ const cleaned = {};
1217
+ for (const [key, value] of Object.entries(obj)) {
1218
+ if (key === '__typename') continue;
1219
+ cleaned[key] = this._deepRemoveTypename(value);
1220
+ }
1221
+ return cleaned;
1222
+ }
1223
+
1224
+ // -- Collection Delta Mutations --------------------------------------------
1225
+
1226
+ transformCollectionDelta(collectionTypeName, delta, options = {}) {
1227
+ this._ensureInitialized();
1228
+ const { connectionField = null } = options;
1229
+ const result = {};
1230
+
1231
+ if (delta.added && delta.added.length > 0) {
1232
+ result.added = delta.added.map(item => {
1233
+ let clean = { ...item };
1234
+
1235
+ delete clean.__status;
1236
+ delete clean.__originalData;
1237
+
1238
+ if (connectionField && clean[connectionField] !== undefined) {
1239
+ delete clean[connectionField];
1240
+ }
1241
+
1242
+ if (clean.id && typeof clean.id === 'string' && clean.id.startsWith('temp_')) {
1243
+ delete clean.id;
1244
+ }
1245
+
1246
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1247
+ clean = this._deepRemoveTypename(clean);
1248
+ return clean;
1249
+ });
1250
+ }
1251
+
1252
+ if (delta.updated && delta.updated.length > 0) {
1253
+ result.updated = delta.updated.map(item => {
1254
+ let clean = { ...item };
1255
+
1256
+ delete clean.__status;
1257
+ delete clean.__originalData;
1258
+
1259
+ if (connectionField && clean[connectionField] !== undefined) {
1260
+ delete clean[connectionField];
1261
+ }
1262
+
1263
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1264
+ clean = this._deepRemoveTypename(clean);
1265
+ return clean;
1266
+ });
1267
+ }
1268
+
1269
+ if (delta.deleted && delta.deleted.length > 0) {
1270
+ result.deleted = delta.deleted.map(item => {
1271
+ if (typeof item === 'string') return item;
1272
+ if (typeof item === 'object' && item.id) return item.id;
1273
+ return item;
1274
+ });
1275
+ }
1276
+
1277
+ return result;
1278
+ }
1279
+
1280
+ _cleanCollectionItem(collectionTypeName, item) {
1281
+ const typeInfo = this._types.get(collectionTypeName);
1282
+ if (!typeInfo?.fields) return item;
1283
+
1284
+ const cleaned = { ...item };
1285
+
1286
+ for (const field of typeInfo.fields) {
1287
+ const { name, type: unwrapped, rawType, extensions } = field;
1288
+ if (cleaned[name] === undefined) continue;
1289
+
1290
+ if (extensions?.stateMachine === true) {
1291
+ delete cleaned[name];
1292
+ continue;
1293
+ }
1294
+
1295
+ if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1296
+ const val = cleaned[name];
1297
+ if (val && typeof val === 'object' && 'id' in val) {
1298
+ cleaned[name] = { id: val.id };
1299
+ }
1300
+ continue;
1301
+ }
1302
+
1303
+ if (isNumericScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1304
+ cleaned[name] = Number(cleaned[name]);
1305
+ }
1306
+ if (isDateTimeScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1307
+ if (/^\d{4}-\d{2}-\d{2}$/.test(cleaned[name])) {
1308
+ cleaned[name] = `${cleaned[name]}T00:00:00.000Z`;
1309
+ }
1310
+ }
1311
+ }
1312
+
1313
+ return cleaned;
1314
+ }
1315
+
658
1316
  // -- Guard ---------------------------------------------------------------
659
1317
 
660
1318
  _ensureInitialized() {