@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.
- package/IMPLEMENTATION_PROMPT.md +1137 -0
- package/README.md +195 -5
- package/examples/demo.js +146 -0
- package/package.json +3 -3
- package/simtlix-simfinity-js-client-1.0.0.tgz +0 -0
- package/src/SimfinityClient.js +675 -17
package/src/SimfinityClient.js
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
|
|
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
|
|
20
|
+
kind name
|
|
15
21
|
ofType {
|
|
16
|
-
name
|
|
22
|
+
kind name
|
|
17
23
|
ofType {
|
|
18
|
-
name
|
|
19
|
-
ofType {
|
|
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
|
|
37
|
+
kind name
|
|
27
38
|
ofType {
|
|
28
|
-
name
|
|
39
|
+
kind name
|
|
29
40
|
ofType {
|
|
30
|
-
name
|
|
31
|
-
ofType {
|
|
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.
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|