@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.
- 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/simtlix-simfinity-js-client-1.0.1.tgz +0 -0
- package/src/SimfinityClient.js +683 -19
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,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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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() {
|