@simtlix/simfinity-js-client 1.0.6 → 1.1.0

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.
@@ -0,0 +1,1398 @@
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);
8
+
9
+ const INTROSPECTION_QUERY = `
10
+ query IntrospectionQuery {
11
+ __schema {
12
+ queryType { name }
13
+ mutationType { name }
14
+ types {
15
+ name
16
+ kind
17
+ fields {
18
+ name
19
+ type {
20
+ kind name
21
+ ofType {
22
+ kind name
23
+ ofType {
24
+ kind name
25
+ ofType {
26
+ kind name
27
+ ofType {
28
+ kind name
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ args {
35
+ name
36
+ type {
37
+ kind name
38
+ ofType {
39
+ kind name
40
+ ofType {
41
+ kind name
42
+ ofType {
43
+ kind name
44
+ ofType {
45
+ kind name
46
+ }
47
+ }
48
+ }
49
+ }
50
+ }
51
+ }
52
+ extensions {
53
+ relation {
54
+ displayField
55
+ embedded
56
+ connectionField
57
+ }
58
+ stateMachine
59
+ readOnly
60
+ }
61
+ }
62
+ enumValues { name }
63
+ }
64
+ }
65
+ }
66
+ `;
67
+
68
+ function unwrapType(typeRef) {
69
+ let isList = false;
70
+ let isNonNull = false;
71
+ let current = typeRef;
72
+ while (current && (current.kind === 'NON_NULL' || current.kind === 'LIST')) {
73
+ if (current.kind === 'NON_NULL') isNonNull = true;
74
+ if (current.kind === 'LIST') isList = true;
75
+ current = current.ofType;
76
+ }
77
+ return { name: current?.name, kind: current?.kind, isList, isNonNull };
78
+ }
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
+
122
+ function typeRefToString(typeRef) {
123
+ if (!typeRef) return '';
124
+ if (typeRef.kind === 'NON_NULL') return typeRefToString(typeRef.ofType) + '!';
125
+ if (typeRef.kind === 'LIST') return '[' + typeRefToString(typeRef.ofType) + ']';
126
+ return typeRef.name || '';
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // SelectionTree -- accumulates nested field selections and serializes them
131
+ // into a GraphQL selection set string.
132
+ // ---------------------------------------------------------------------------
133
+
134
+ class SelectionTree {
135
+ constructor() {
136
+ this.scalars = new Set();
137
+ this.children = new Map();
138
+ }
139
+
140
+ addScalars(fieldNames) {
141
+ for (const name of fieldNames.trim().split(/\s+/)) {
142
+ if (name) this.scalars.add(name);
143
+ }
144
+ }
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
+
173
+ addNested(dottedPath, fieldNames) {
174
+ const segments = dottedPath.split('.');
175
+ let node = this;
176
+ for (const segment of segments) {
177
+ if (!node.children.has(segment)) {
178
+ node.children.set(segment, new SelectionTree());
179
+ }
180
+ node = node.children.get(segment);
181
+ }
182
+ node.addScalars(fieldNames);
183
+ }
184
+
185
+ serialize() {
186
+ const parts = [...this.scalars];
187
+ for (const [name, child] of this.children) {
188
+ const childStr = child.serialize();
189
+ if (childStr) parts.push(`${name} { ${childStr} }`);
190
+ }
191
+ return parts.join(' ');
192
+ }
193
+
194
+ isEmpty() {
195
+ return this.scalars.size === 0 && this.children.size === 0;
196
+ }
197
+ }
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // QueryBuilder -- chainable builder returned by SimfinityClient.find()
201
+ // ---------------------------------------------------------------------------
202
+
203
+ class QueryBuilder {
204
+ constructor(client, typeName) {
205
+ this._client = client;
206
+ this._typeName = typeName;
207
+ this._variables = {};
208
+ this._selectionTree = new SelectionTree();
209
+ this._sortTerms = [];
210
+ this._hasExplicitFields = false;
211
+ }
212
+
213
+ // -- Filtering -----------------------------------------------------------
214
+
215
+ where(field, operatorOrTerms, value, value2) {
216
+ if (Array.isArray(operatorOrTerms)) {
217
+ if (this._variables[field]?.terms) {
218
+ this._variables[field].terms.push(...operatorOrTerms);
219
+ } else {
220
+ this._variables[field] = { terms: operatorOrTerms };
221
+ }
222
+ } else {
223
+ const filter = { operator: operatorOrTerms, value };
224
+ if (value2 !== undefined) filter.value2 = value2;
225
+ this._variables[field] = filter;
226
+ }
227
+ return this;
228
+ }
229
+
230
+ or(groups) {
231
+ if (!Array.isArray(groups)) {
232
+ throw new Error('or() expects an array of QLFilterGroup objects');
233
+ }
234
+ if (this._variables.OR) {
235
+ this._variables.OR.push(...groups);
236
+ } else {
237
+ this._variables.OR = [...groups];
238
+ }
239
+ return this;
240
+ }
241
+
242
+ and(groups) {
243
+ if (!Array.isArray(groups)) {
244
+ throw new Error('and() expects an array of QLFilterGroup objects');
245
+ }
246
+ if (this._variables.AND) {
247
+ this._variables.AND.push(...groups);
248
+ } else {
249
+ this._variables.AND = [...groups];
250
+ }
251
+ return this;
252
+ }
253
+
254
+ // -- Relation Field Selection --------------------------------------------
255
+
256
+ joinCollection(path, fields, filter) {
257
+ this._selectionTree.addNested(path, fields);
258
+ if (filter) {
259
+ const rootField = path.split('.')[0];
260
+ if (this._variables[rootField]?.terms) {
261
+ this._variables[rootField].terms.push(...filter);
262
+ } else {
263
+ this._variables[rootField] = { terms: filter };
264
+ }
265
+ }
266
+ return this;
267
+ }
268
+
269
+ joinObject(path, fields) {
270
+ this._selectionTree.addNested(path, fields);
271
+ return this;
272
+ }
273
+
274
+ // -- Result Shaping ------------------------------------------------------
275
+
276
+ fields(selectionString) {
277
+ this._selectionTree.addSelection(selectionString);
278
+ this._hasExplicitFields = true;
279
+ return this;
280
+ }
281
+
282
+ page(page, size, count) {
283
+ const pagination = { page, size };
284
+ if (count !== undefined) pagination.count = count;
285
+ this._variables.pagination = pagination;
286
+ return this;
287
+ }
288
+
289
+ sort(field, order) {
290
+ this._sortTerms.push({ field, order });
291
+ this._variables.sort = { terms: this._sortTerms };
292
+ return this;
293
+ }
294
+
295
+ clearSort() {
296
+ this._sortTerms = [];
297
+ delete this._variables.sort;
298
+ return this;
299
+ }
300
+
301
+ autoSelect() {
302
+ const result = this._client.buildSelectionSet(this._typeName);
303
+ this._hasExplicitFields = true;
304
+ this._autoSelection = result.selection;
305
+ this._selectionMeta = result;
306
+ return this;
307
+ }
308
+
309
+ // -- Execution -----------------------------------------------------------
310
+
311
+ async exec() {
312
+ const queryInfo = this._client._findPluralQuery(this._typeName);
313
+ if (!queryInfo) {
314
+ throw new Error(`No plural query found for type '${this._typeName}'`);
315
+ }
316
+
317
+ if (!this._hasExplicitFields) {
318
+ const autoFields = this._client._getScalarFields(this._typeName);
319
+ this._selectionTree.addScalars(autoFields);
320
+ }
321
+
322
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
323
+ if (!selectionSet) {
324
+ throw new Error(`No fields to select for type '${this._typeName}'`);
325
+ }
326
+
327
+ const varDefs = [];
328
+ const variables = {};
329
+ const argMappings = [];
330
+
331
+ for (const [key, val] of Object.entries(this._variables)) {
332
+ const arg = queryInfo.args.find(a => a.name === key);
333
+ if (arg) {
334
+ varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
335
+ variables[key] = val;
336
+ argMappings.push(`${key}: $${key}`);
337
+ }
338
+ }
339
+
340
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
341
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
342
+ const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
343
+
344
+ const response = await this._client._sendRequest(query, variables);
345
+ if (response.errors && !response.data?.[queryInfo.name]) {
346
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
347
+ error.graphQLErrors = response.errors;
348
+ throw error;
349
+ }
350
+
351
+ const result = response.data[queryInfo.name];
352
+
353
+ if (response.extensions) {
354
+ result.__extensions = response.extensions;
355
+ }
356
+
357
+ return result;
358
+ }
359
+
360
+ async execWithMeta() {
361
+ const queryInfo = this._client._findPluralQuery(this._typeName);
362
+ if (!queryInfo) {
363
+ throw new Error(`No plural query found for type '${this._typeName}'`);
364
+ }
365
+
366
+ if (!this._hasExplicitFields) {
367
+ const autoFields = this._client._getScalarFields(this._typeName);
368
+ this._selectionTree.addScalars(autoFields);
369
+ }
370
+
371
+ const selectionSet = this._autoSelection || this._selectionTree.serialize();
372
+ if (!selectionSet) {
373
+ throw new Error(`No fields to select for type '${this._typeName}'`);
374
+ }
375
+
376
+ const varDefs = [];
377
+ const variables = {};
378
+ const argMappings = [];
379
+
380
+ for (const [key, val] of Object.entries(this._variables)) {
381
+ const arg = queryInfo.args.find(a => a.name === key);
382
+ if (arg) {
383
+ varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
384
+ variables[key] = val;
385
+ argMappings.push(`${key}: $${key}`);
386
+ }
387
+ }
388
+
389
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
390
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
391
+ const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
392
+
393
+ const response = await this._client._sendRequest(query, variables);
394
+ if (response.errors && !response.data?.[queryInfo.name]) {
395
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
396
+ error.graphQLErrors = response.errors;
397
+ throw error;
398
+ }
399
+ return {
400
+ data: response.data[queryInfo.name],
401
+ extensions: response.extensions || null,
402
+ };
403
+ }
404
+ }
405
+
406
+ // ---------------------------------------------------------------------------
407
+ // AggregateBuilder -- chainable builder returned by SimfinityClient.aggregate()
408
+ // ---------------------------------------------------------------------------
409
+
410
+ class AggregateBuilder {
411
+ constructor(client, typeName) {
412
+ this._client = client;
413
+ this._typeName = typeName;
414
+ this._groupId = null;
415
+ this._facts = [];
416
+ this._variables = {};
417
+ this._sortTerms = [];
418
+ }
419
+
420
+ groupBy(field) {
421
+ this._groupId = field;
422
+ return this;
423
+ }
424
+
425
+ fact(operation, factName, path) {
426
+ this._facts.push({ operation, factName, path });
427
+ return this;
428
+ }
429
+
430
+ where(field, operatorOrTerms, value, value2) {
431
+ if (Array.isArray(operatorOrTerms)) {
432
+ if (this._variables[field]?.terms) {
433
+ this._variables[field].terms.push(...operatorOrTerms);
434
+ } else {
435
+ this._variables[field] = { terms: operatorOrTerms };
436
+ }
437
+ } else {
438
+ const filter = { operator: operatorOrTerms, value };
439
+ if (value2 !== undefined) filter.value2 = value2;
440
+ this._variables[field] = filter;
441
+ }
442
+ return this;
443
+ }
444
+
445
+ or(groups) {
446
+ if (!Array.isArray(groups)) {
447
+ throw new Error('or() expects an array of QLFilterGroup objects');
448
+ }
449
+ if (this._variables.OR) {
450
+ this._variables.OR.push(...groups);
451
+ } else {
452
+ this._variables.OR = [...groups];
453
+ }
454
+ return this;
455
+ }
456
+
457
+ and(groups) {
458
+ if (!Array.isArray(groups)) {
459
+ throw new Error('and() expects an array of QLFilterGroup objects');
460
+ }
461
+ if (this._variables.AND) {
462
+ this._variables.AND.push(...groups);
463
+ } else {
464
+ this._variables.AND = [...groups];
465
+ }
466
+ return this;
467
+ }
468
+
469
+ page(page, size, count) {
470
+ const pagination = { page, size };
471
+ if (count !== undefined) pagination.count = count;
472
+ this._variables.pagination = pagination;
473
+ return this;
474
+ }
475
+
476
+ sort(field, order) {
477
+ this._sortTerms.push({ field, order });
478
+ this._variables.sort = { terms: this._sortTerms };
479
+ return this;
480
+ }
481
+
482
+ async exec() {
483
+ if (!this._groupId) {
484
+ throw new Error('groupBy() is required for aggregate queries');
485
+ }
486
+ if (this._facts.length === 0) {
487
+ throw new Error('At least one fact() is required for aggregate queries');
488
+ }
489
+
490
+ const queryInfo = this._client._findAggregateQuery(this._typeName);
491
+ if (!queryInfo) {
492
+ throw new Error(`No aggregate query found for type '${this._typeName}'`);
493
+ }
494
+
495
+ this._variables.aggregation = {
496
+ groupId: this._groupId,
497
+ facts: this._facts,
498
+ };
499
+
500
+ const varDefs = [];
501
+ const variables = {};
502
+ const argMappings = [];
503
+
504
+ for (const [key, val] of Object.entries(this._variables)) {
505
+ const arg = queryInfo.args.find(a => a.name === key);
506
+ if (arg) {
507
+ varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
508
+ variables[key] = val;
509
+ argMappings.push(`${key}: $${key}`);
510
+ }
511
+ }
512
+
513
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
514
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
515
+ const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { groupId facts } }`;
516
+
517
+ const response = await this._client._sendRequest(query, variables);
518
+ if (response.errors) {
519
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
520
+ error.graphQLErrors = response.errors;
521
+ throw error;
522
+ }
523
+ return response.data[queryInfo.name];
524
+ }
525
+ }
526
+
527
+ // ---------------------------------------------------------------------------
528
+ // SimfinityClient -- introspection-driven GraphQL client
529
+ // ---------------------------------------------------------------------------
530
+
531
+ export default class SimfinityClient {
532
+ /**
533
+ * @param {string} endpoint GraphQL HTTP endpoint URL
534
+ * @param {{ prepareHeaders?: (headers: Record<string, string>) => void }} [options]
535
+ * Optional `prepareHeaders` runs before each request; mutate `headers` to add auth
536
+ * (e.g. `headers['Authorization'] = 'Bearer …'`). Login is not part of this client —
537
+ * perform auth in your app and set tokens inside this callback or a closure it sees.
538
+ */
539
+ constructor(endpoint, options = {}) {
540
+ this._endpoint = endpoint;
541
+ this._types = new Map();
542
+ this._queries = new Map();
543
+ this._mutations = new Map();
544
+ this._typeNameToPlural = new Map();
545
+ this._typeNameToSingular = new Map();
546
+ this._typeNameToAggregate = new Map();
547
+ this._queryNameToType = new Map();
548
+ this._initialized = false;
549
+ this._prepareHeaders =
550
+ typeof options.prepareHeaders === 'function' ? options.prepareHeaders : null;
551
+ }
552
+
553
+ // -- Initialization ------------------------------------------------------
554
+
555
+ async init() {
556
+ const response = await this._sendRequest(INTROSPECTION_QUERY);
557
+ if (response.errors) {
558
+ throw new Error(`Introspection failed: ${JSON.stringify(response.errors)}`);
559
+ }
560
+ this._parseIntrospection(response.data.__schema);
561
+ this._initialized = true;
562
+ }
563
+
564
+ _parseIntrospection(schema) {
565
+ const queryTypeName = schema.queryType?.name;
566
+ const mutationTypeName = schema.mutationType?.name;
567
+
568
+ this._buildTypesRegistry(schema.types);
569
+ this._buildQueriesRegistry(schema.types, queryTypeName);
570
+ this._buildMutationsRegistry(schema.types, mutationTypeName);
571
+ }
572
+
573
+ _buildTypesRegistry(types) {
574
+ for (const type of types) {
575
+ if (type.name.startsWith('__')) continue;
576
+ const validKinds = ['OBJECT', 'ENUM', 'SCALAR', 'INPUT_OBJECT'];
577
+ if (!validKinds.includes(type.kind)) continue;
578
+
579
+ const typeInfo = {
580
+ name: type.name,
581
+ kind: type.kind,
582
+ fields: (type.fields || []).map(f => ({
583
+ name: f.name,
584
+ type: unwrapType(f.type),
585
+ rawType: f.type,
586
+ args: (f.args || []).map(a => ({ name: a.name, type: a.type })),
587
+ extensions: f.extensions || null,
588
+ })),
589
+ enumValues: (type.enumValues || []).map(v => v.name),
590
+ };
591
+ this._types.set(type.name, typeInfo);
592
+ }
593
+ }
594
+
595
+ _buildQueriesRegistry(types, queryTypeName) {
596
+ const queryType = types.find(t => t.name === queryTypeName);
597
+ if (!queryType?.fields) return;
598
+
599
+ for (const field of queryType.fields) {
600
+ const returnType = unwrapType(field.type);
601
+ const queryInfo = {
602
+ name: field.name,
603
+ args: (field.args || []).map(a => ({ name: a.name, type: a.type })),
604
+ returnType,
605
+ rawReturnType: field.type,
606
+ };
607
+ this._queries.set(field.name, queryInfo);
608
+
609
+ if (!returnType.name) continue;
610
+ if (returnType.isList) {
611
+ this._typeNameToPlural.set(returnType.name, field.name);
612
+ } else if (queryInfo.args.some(a => a.name === 'id')) {
613
+ this._typeNameToSingular.set(returnType.name, field.name);
614
+ }
615
+ }
616
+
617
+ for (const [typeName, pluralName] of this._typeNameToPlural) {
618
+ const aggName = `${pluralName}_aggregate`;
619
+ if (this._queries.has(aggName)) {
620
+ this._typeNameToAggregate.set(typeName, aggName);
621
+ }
622
+ }
623
+
624
+ this._queryNameToType = new Map();
625
+ for (const [typeName, queryName] of this._typeNameToPlural) {
626
+ this._queryNameToType.set(queryName, typeName);
627
+ }
628
+ for (const [typeName, queryName] of this._typeNameToSingular) {
629
+ this._queryNameToType.set(queryName, typeName);
630
+ }
631
+ }
632
+
633
+ _buildMutationsRegistry(types, mutationTypeName) {
634
+ const mutationType = types.find(t => t.name === mutationTypeName);
635
+ if (!mutationType?.fields) return;
636
+
637
+ const knownTypeNames = [...this._typeNameToSingular.keys()];
638
+
639
+ for (const field of mutationType.fields) {
640
+ const returnType = unwrapType(field.type);
641
+ const mutationInfo = {
642
+ name: field.name,
643
+ args: (field.args || []).map(a => ({ name: a.name, type: a.type })),
644
+ returnType,
645
+ rawReturnType: field.type,
646
+ category: 'custom',
647
+ typeName: returnType.name,
648
+ };
649
+
650
+ this._classifyMutation(mutationInfo, knownTypeNames);
651
+ this._mutations.set(field.name, mutationInfo);
652
+ }
653
+ }
654
+
655
+ _classifyMutation(mutationInfo, knownTypeNames) {
656
+ const name = mutationInfo.name;
657
+ for (const typeName of knownTypeNames) {
658
+ if (name === `add${typeName}`) {
659
+ Object.assign(mutationInfo, { category: 'add', typeName });
660
+ return;
661
+ }
662
+ if (name === `update${typeName}`) {
663
+ Object.assign(mutationInfo, { category: 'update', typeName });
664
+ return;
665
+ }
666
+ if (name === `delete${typeName}`) {
667
+ Object.assign(mutationInfo, { category: 'delete', typeName });
668
+ return;
669
+ }
670
+ if (name.endsWith(typeName)) {
671
+ let prefix = name.slice(0, -typeName.length);
672
+ if (prefix.endsWith('_')) prefix = prefix.slice(0, -1);
673
+ if (prefix) {
674
+ Object.assign(mutationInfo, {
675
+ category: 'stateTransition',
676
+ typeName,
677
+ action: prefix,
678
+ });
679
+ return;
680
+ }
681
+ }
682
+ }
683
+ }
684
+
685
+ // -- Field Auto-Selection ------------------------------------------------
686
+
687
+ _getScalarFields(typeName) {
688
+ const typeInfo = this._types.get(typeName);
689
+ if (!typeInfo) return 'id';
690
+ const scalars = typeInfo.fields
691
+ .filter(f => f.type.kind === 'SCALAR' || f.type.kind === 'ENUM')
692
+ .map(f => f.name);
693
+ return scalars.length > 0 ? scalars.join(' ') : 'id';
694
+ }
695
+
696
+ _resolveFields(typeName, fields) {
697
+ return fields || this._getScalarFields(typeName);
698
+ }
699
+
700
+ // -- Query Lookup --------------------------------------------------------
701
+
702
+ _findPluralQuery(typeName) {
703
+ const queryName = this._typeNameToPlural.get(typeName);
704
+ return queryName ? this._queries.get(queryName) : null;
705
+ }
706
+
707
+ _findSingularQuery(typeName) {
708
+ const queryName = this._typeNameToSingular.get(typeName);
709
+ return queryName ? this._queries.get(queryName) : null;
710
+ }
711
+
712
+ _findAggregateQuery(typeName) {
713
+ const queryName = this._typeNameToAggregate.get(typeName);
714
+ return queryName ? this._queries.get(queryName) : null;
715
+ }
716
+
717
+ // -- HTTP ----------------------------------------------------------------
718
+
719
+ async _sendRequest(query, variables) {
720
+ const body = { query };
721
+ if (variables && Object.keys(variables).length > 0) {
722
+ body.variables = variables;
723
+ }
724
+ const headers = { 'Content-Type': 'application/json' };
725
+ if (this._prepareHeaders) {
726
+ this._prepareHeaders(headers);
727
+ }
728
+ const response = await _fetch(this._endpoint, {
729
+ method: 'POST',
730
+ headers,
731
+ body: JSON.stringify(body),
732
+ });
733
+ return response.json();
734
+ }
735
+
736
+ // -- Query Methods -------------------------------------------------------
737
+
738
+ find(typeName) {
739
+ this._ensureInitialized();
740
+ return new QueryBuilder(this, typeName);
741
+ }
742
+
743
+ aggregate(typeName) {
744
+ this._ensureInitialized();
745
+ return new AggregateBuilder(this, typeName);
746
+ }
747
+
748
+ findByParent(typeName, connectionField, parentId) {
749
+ this._ensureInitialized();
750
+ const builder = new QueryBuilder(this, typeName);
751
+ builder.where(connectionField, [{ path: 'id', operator: 'EQ', value: parentId }]);
752
+ builder.sort('id', 'ASC');
753
+ return builder;
754
+ }
755
+
756
+ async search(typeName, searchTerm, options = {}) {
757
+ this._ensureInitialized();
758
+ const { page = 1, size = 10, displayField = null } = options;
759
+
760
+ let searchField = displayField;
761
+ if (!searchField) {
762
+ const typeInfo = this._types.get(typeName);
763
+ if (typeInfo?.fields) {
764
+ const nameField = typeInfo.fields.find(f => f.name === 'name');
765
+ if (nameField) {
766
+ searchField = 'name';
767
+ } else {
768
+ const firstScalar = typeInfo.fields.find(f =>
769
+ isScalarOrEnum(f.type.kind) && f.name !== 'id'
770
+ );
771
+ searchField = firstScalar?.name || 'id';
772
+ }
773
+ }
774
+ }
775
+
776
+ const typeInfo = this._types.get(typeName);
777
+ const fieldInfo = typeInfo?.fields.find(f => f.name === searchField);
778
+ const fieldTypeName = fieldInfo?.type?.name || 'String';
779
+
780
+ const isString = !isNumericScalar(fieldTypeName) && !isBooleanScalar(fieldTypeName);
781
+ const operator = isString ? 'LIKE' : 'EQ';
782
+
783
+ let castedTerm = searchTerm;
784
+ if (isNumericScalar(fieldTypeName)) {
785
+ const actualType = getActualScalarType(fieldTypeName);
786
+ if (actualType?.toLowerCase() === 'int') {
787
+ const parsed = parseInt(searchTerm, 10);
788
+ if (!isNaN(parsed)) castedTerm = parsed;
789
+ } else {
790
+ const parsed = parseFloat(searchTerm);
791
+ if (!isNaN(parsed)) castedTerm = parsed;
792
+ }
793
+ } else if (isBooleanScalar(fieldTypeName)) {
794
+ if (searchTerm.toLowerCase() === 'true') castedTerm = true;
795
+ else if (searchTerm.toLowerCase() === 'false') castedTerm = false;
796
+ }
797
+
798
+ const builder = this.find(typeName)
799
+ .fields(`id ${searchField}`)
800
+ .page(page, size, false)
801
+ .where(searchField, operator, castedTerm);
802
+
803
+ return builder.exec();
804
+ }
805
+
806
+ async getById(typeName, id, fields) {
807
+ this._ensureInitialized();
808
+ const queryInfo = this._findSingularQuery(typeName);
809
+ if (!queryInfo) {
810
+ throw new Error(`No singular query found for type '${typeName}'`);
811
+ }
812
+
813
+ const selectionSet = this._resolveFields(typeName, fields);
814
+ const idArg = queryInfo.args.find(a => a.name === 'id');
815
+ const idType = idArg ? typeRefToString(idArg.type) : 'ID!';
816
+
817
+ const query = `query($id: ${idType}) { ${queryInfo.name}(id: $id) { ${selectionSet} } }`;
818
+ const response = await this._sendRequest(query, { id });
819
+
820
+ if (response.errors && !response.data?.[queryInfo.name]) {
821
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
822
+ error.graphQLErrors = response.errors;
823
+ throw error;
824
+ }
825
+ return response.data[queryInfo.name];
826
+ }
827
+
828
+ // -- CRUD Mutation Methods -----------------------------------------------
829
+
830
+ async add(typeName, input, fields, options = {}) {
831
+ this._ensureInitialized();
832
+ const transformedInput = options.transform
833
+ ? this.transformInput(typeName, input, { mode: 'create', ...options })
834
+ : input;
835
+ return this._executeMutation(`add${typeName}`, typeName, { input: transformedInput }, fields);
836
+ }
837
+
838
+ async update(typeName, id, input, fields, options = {}) {
839
+ this._ensureInitialized();
840
+ const transformedInput = options.transform
841
+ ? this.transformInput(typeName, input, { mode: 'update', ...options })
842
+ : input;
843
+ return this._executeMutation(
844
+ `update${typeName}`, typeName,
845
+ { input: { id, ...transformedInput } },
846
+ fields,
847
+ );
848
+ }
849
+
850
+ async delete(typeName, id, fields) {
851
+ this._ensureInitialized();
852
+ return this._executeMutation(`delete${typeName}`, typeName, { id }, fields);
853
+ }
854
+
855
+ // -- State Machine -------------------------------------------------------
856
+
857
+ async transition(typeName, action, id, inputOrFields, fields) {
858
+ this._ensureInitialized();
859
+ const mutationInfo = this._findTransitionMutation(typeName, action);
860
+ if (!mutationInfo) {
861
+ throw new Error(`No state machine transition '${action}' found for type '${typeName}'`);
862
+ }
863
+
864
+ let input, resolvedFields;
865
+ if (typeof inputOrFields === 'object' && inputOrFields !== null) {
866
+ input = inputOrFields;
867
+ resolvedFields = fields;
868
+ } else {
869
+ input = {};
870
+ resolvedFields = inputOrFields;
871
+ }
872
+
873
+ return this._executeMutation(
874
+ mutationInfo.name, typeName, { input: { id, ...input } }, resolvedFields,
875
+ );
876
+ }
877
+
878
+ _findTransitionMutation(typeName, action) {
879
+ for (const info of this._mutations.values()) {
880
+ if (info.category === 'stateTransition' && info.typeName === typeName && info.action === action) {
881
+ return info;
882
+ }
883
+ }
884
+ return null;
885
+ }
886
+
887
+ getStateMachineFields(typeName) {
888
+ this._ensureInitialized();
889
+ const typeInfo = this._types.get(typeName);
890
+ if (!typeInfo?.fields) return [];
891
+ return typeInfo.fields
892
+ .filter(f => f.extensions?.stateMachine === true)
893
+ .map(f => f.name);
894
+ }
895
+
896
+ getAvailableTransitions(typeName) {
897
+ this._ensureInitialized();
898
+ const transitions = [];
899
+ for (const info of this._mutations.values()) {
900
+ if (info.category === 'stateTransition' && info.typeName === typeName) {
901
+ transitions.push({
902
+ action: info.action,
903
+ mutationName: info.name,
904
+ });
905
+ }
906
+ }
907
+ return transitions;
908
+ }
909
+
910
+ // -- Custom Mutations ----------------------------------------------------
911
+
912
+ async customMutation(mutationName, args, fields) {
913
+ this._ensureInitialized();
914
+ const mutationInfo = this._mutations.get(mutationName);
915
+ if (!mutationInfo) {
916
+ throw new Error(`Mutation '${mutationName}' not found`);
917
+ }
918
+ return this._executeMutation(
919
+ mutationName,
920
+ mutationInfo.returnType.name,
921
+ args || {},
922
+ fields,
923
+ );
924
+ }
925
+
926
+ // -- Raw Execution -------------------------------------------------------
927
+
928
+ async execute(query, variables) {
929
+ return this._sendRequest(query, variables);
930
+ }
931
+
932
+ // -- Shared Mutation Executor --------------------------------------------
933
+
934
+ async _executeMutation(mutationName, typeName, providedArgs, fields) {
935
+ const mutationInfo = this._mutations.get(mutationName);
936
+ if (!mutationInfo) {
937
+ throw new Error(`Mutation '${mutationName}' not found`);
938
+ }
939
+
940
+ const selectionSet = this._resolveFields(typeName, fields);
941
+ const varDefs = [];
942
+ const variables = {};
943
+ const argMappings = [];
944
+
945
+ for (const arg of mutationInfo.args) {
946
+ if (providedArgs[arg.name] !== undefined) {
947
+ varDefs.push(`$${arg.name}: ${typeRefToString(arg.type)}`);
948
+ variables[arg.name] = providedArgs[arg.name];
949
+ argMappings.push(`${arg.name}: $${arg.name}`);
950
+ }
951
+ }
952
+
953
+ const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
954
+ const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
955
+ const query = `mutation${varDefsStr} { ${mutationName}${argsBlock} { ${selectionSet} } }`;
956
+
957
+ const response = await this._sendRequest(query, variables);
958
+ if (response.errors) {
959
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
960
+ error.graphQLErrors = response.errors;
961
+ throw error;
962
+ }
963
+ return response.data[mutationName];
964
+ }
965
+
966
+ // -- Schema Getters ------------------------------------------------------
967
+
968
+ getTypes() {
969
+ return Object.fromEntries(this._types);
970
+ }
971
+
972
+ getQueries() {
973
+ return Object.fromEntries(this._queries);
974
+ }
975
+
976
+ getMutations() {
977
+ return Object.fromEntries(this._mutations);
978
+ }
979
+
980
+ // -- Selection Set Building ------------------------------------------------
981
+
982
+ buildSelectionSet(typeName) {
983
+ this._ensureInitialized();
984
+ const typeInfo = this._types.get(typeName);
985
+ if (!typeInfo?.fields) {
986
+ return { selection: 'id', columns: ['id'], sortFieldByColumn: {}, fieldTypeByColumn: {} };
987
+ }
988
+
989
+ const columns = [];
990
+ const sortFieldByColumn = {};
991
+ const fieldTypeByColumn = {};
992
+ const fieldSelections = [];
993
+
994
+ for (const field of typeInfo.fields) {
995
+ const { name, type: unwrapped, rawType, extensions } = field;
996
+
997
+ if (isListType(rawType)) continue;
998
+
999
+ if (isScalarOrEnum(unwrapped.kind)) {
1000
+ fieldSelections.push(name);
1001
+ if (name !== 'id') columns.push(name);
1002
+ sortFieldByColumn[name] = name;
1003
+ fieldTypeByColumn[name] = unwrapped.name || unwrapped.kind || 'SCALAR';
1004
+ } else if (unwrapped.kind === 'OBJECT' && unwrapped.name) {
1005
+ const objType = this._types.get(unwrapped.name);
1006
+ const objFields = objType?.fields || [];
1007
+ const objFieldNames = new Set(objFields.map(f => f.name));
1008
+ const isEmbedded = extensions?.relation?.embedded === true;
1009
+
1010
+ let chosenDisplay = null;
1011
+ const extDisplay = extensions?.relation?.displayField;
1012
+ if (extDisplay && objFieldNames.has(extDisplay)) {
1013
+ chosenDisplay = extDisplay;
1014
+ } else if (objFieldNames.has('name')) {
1015
+ chosenDisplay = 'name';
1016
+ } else {
1017
+ const firstScalar = objFields.find(f =>
1018
+ isScalarOrEnum(f.type.kind) && !f.type.isList
1019
+ );
1020
+ chosenDisplay = firstScalar?.name || null;
1021
+ }
1022
+
1023
+ const subFields = new Set();
1024
+ if (chosenDisplay) subFields.add(chosenDisplay);
1025
+ if (!isEmbedded && objFieldNames.has('id')) subFields.add('id');
1026
+ if (subFields.size === 0) {
1027
+ if (objFieldNames.has('id') && !isEmbedded) subFields.add('id');
1028
+ else if (objFieldNames.has('name')) subFields.add('name');
1029
+ else if (objFields[0]) subFields.add(objFields[0].name);
1030
+ }
1031
+
1032
+ const subSelection = [...subFields].join(' ');
1033
+ fieldSelections.push(`${name} { ${subSelection} }`);
1034
+ columns.push(name);
1035
+
1036
+ if (chosenDisplay) {
1037
+ sortFieldByColumn[name] = `${name}.${chosenDisplay}`;
1038
+ const displayFieldInfo = objFields.find(f => f.name === chosenDisplay);
1039
+ fieldTypeByColumn[name] = displayFieldInfo?.type?.name || 'OBJECT';
1040
+ } else {
1041
+ sortFieldByColumn[name] = name;
1042
+ fieldTypeByColumn[name] = 'OBJECT';
1043
+ }
1044
+ }
1045
+ }
1046
+
1047
+ const hasId = typeInfo.fields.some(f => f.name === 'id');
1048
+ if (hasId && !fieldSelections.includes('id')) {
1049
+ fieldSelections.unshift('id');
1050
+ }
1051
+
1052
+ return {
1053
+ selection: fieldSelections.join('\n'),
1054
+ columns,
1055
+ sortFieldByColumn,
1056
+ fieldTypeByColumn,
1057
+ };
1058
+ }
1059
+
1060
+ // -- Schema Metadata Access -----------------------------------------------
1061
+
1062
+ getFieldExtensions(typeName, fieldName) {
1063
+ this._ensureInitialized();
1064
+ const typeInfo = this._types.get(typeName);
1065
+ if (!typeInfo) return null;
1066
+ const field = typeInfo.fields.find(f => f.name === fieldName);
1067
+ return field?.extensions || null;
1068
+ }
1069
+
1070
+ getDisplayField(typeName, fieldName) {
1071
+ this._ensureInitialized();
1072
+ const ext = this.getFieldExtensions(typeName, fieldName);
1073
+ const displayField = ext?.relation?.displayField;
1074
+ if (displayField) return displayField;
1075
+
1076
+ const typeInfo = this._types.get(typeName);
1077
+ const field = typeInfo?.fields.find(f => f.name === fieldName);
1078
+ if (!field || field.type.kind !== 'OBJECT') return null;
1079
+
1080
+ const objectType = this._types.get(field.type.name);
1081
+ if (!objectType?.fields) return null;
1082
+
1083
+ const objFieldNames = objectType.fields.map(f => f.name);
1084
+ if (objFieldNames.includes('name')) return 'name';
1085
+
1086
+ const firstScalar = objectType.fields.find(f =>
1087
+ isScalarOrEnum(f.type.kind) && !f.type.isList
1088
+ );
1089
+ return firstScalar?.name || null;
1090
+ }
1091
+
1092
+ isEmbeddedField(typeName, fieldName) {
1093
+ this._ensureInitialized();
1094
+ const ext = this.getFieldExtensions(typeName, fieldName);
1095
+ return ext?.relation?.embedded === true;
1096
+ }
1097
+
1098
+ getConnectionField(typeName, fieldName) {
1099
+ this._ensureInitialized();
1100
+ const ext = this.getFieldExtensions(typeName, fieldName);
1101
+ return ext?.relation?.connectionField || null;
1102
+ }
1103
+
1104
+ isStateMachineField(typeName, fieldName) {
1105
+ this._ensureInitialized();
1106
+ const ext = this.getFieldExtensions(typeName, fieldName);
1107
+ return ext?.stateMachine === true;
1108
+ }
1109
+
1110
+ isReadOnlyField(typeName, fieldName) {
1111
+ this._ensureInitialized();
1112
+ const ext = this.getFieldExtensions(typeName, fieldName);
1113
+ return ext?.readOnly === true;
1114
+ }
1115
+
1116
+ getEnumValues(typeName) {
1117
+ this._ensureInitialized();
1118
+ const typeInfo = this._types.get(typeName);
1119
+ if (!typeInfo || typeInfo.kind !== 'ENUM') return [];
1120
+ return typeInfo.enumValues || [];
1121
+ }
1122
+
1123
+ getFieldsOfType(typeName) {
1124
+ this._ensureInitialized();
1125
+ const typeInfo = this._types.get(typeName);
1126
+ if (!typeInfo) return [];
1127
+ return typeInfo.fields;
1128
+ }
1129
+
1130
+ getDescriptionFieldType(typeName, descriptionField) {
1131
+ this._ensureInitialized();
1132
+ const typeInfo = this._types.get(typeName);
1133
+ if (!typeInfo?.fields) return 'String';
1134
+ const field = typeInfo.fields.find(f => f.name === descriptionField);
1135
+ if (!field) return 'String';
1136
+ return field.type.name || 'String';
1137
+ }
1138
+
1139
+ // -- Entity & Query Name Resolution ----------------------------------------
1140
+
1141
+ getTypeNameForQuery(queryName) {
1142
+ this._ensureInitialized();
1143
+ const queryInfo = this._queries.get(queryName);
1144
+ return queryInfo?.returnType?.name || null;
1145
+ }
1146
+
1147
+ getPluralQueryName(typeName) {
1148
+ this._ensureInitialized();
1149
+ return this._typeNameToPlural.get(typeName) || null;
1150
+ }
1151
+
1152
+ getSingularQueryName(typeName) {
1153
+ this._ensureInitialized();
1154
+ return this._typeNameToSingular.get(typeName) || null;
1155
+ }
1156
+
1157
+ getListEntityNames() {
1158
+ this._ensureInitialized();
1159
+ const names = [];
1160
+ for (const [queryName, queryInfo] of this._queries) {
1161
+ if (queryInfo.returnType.isList && !queryName.endsWith('_aggregate')) {
1162
+ names.push(queryName);
1163
+ }
1164
+ }
1165
+ return names.sort();
1166
+ }
1167
+
1168
+ getListEntityNamesOfType(typeName) {
1169
+ this._ensureInitialized();
1170
+ const names = [];
1171
+ for (const [queryName, queryInfo] of this._queries) {
1172
+ if (queryInfo.returnType.isList
1173
+ && queryInfo.returnType.name === typeName
1174
+ && !queryName.endsWith('_aggregate')) {
1175
+ names.push(queryName);
1176
+ }
1177
+ }
1178
+ return names.sort();
1179
+ }
1180
+
1181
+ getQueryNamesForType(typeName) {
1182
+ this._ensureInitialized();
1183
+ return {
1184
+ pluralQueryName: this._typeNameToPlural.get(typeName) || null,
1185
+ singularQueryName: this._typeNameToSingular.get(typeName) || null,
1186
+ aggregateQueryName: this._typeNameToAggregate.get(typeName) || null,
1187
+ };
1188
+ }
1189
+
1190
+ // -- Scalar Type Utilities -----------------------------------------------
1191
+
1192
+ getActualScalarType(scalarName) { return getActualScalarType(scalarName); }
1193
+ isNumericScalar(scalarName) { return isNumericScalar(scalarName); }
1194
+ isBooleanScalar(scalarName) { return isBooleanScalar(scalarName); }
1195
+ isDateTimeScalar(scalarName) { return isDateTimeScalar(scalarName); }
1196
+
1197
+ // -- Mutation Input Transformation -----------------------------------------
1198
+
1199
+ transformInput(typeName, rawInput, options = {}) {
1200
+ this._ensureInitialized();
1201
+ const { skipFields = [], transientFields = [], mode = 'create' } = options;
1202
+ const typeInfo = this._types.get(typeName);
1203
+ if (!typeInfo) return { ...rawInput };
1204
+
1205
+ const transformed = {};
1206
+
1207
+ for (const field of typeInfo.fields) {
1208
+ const { name, type: unwrapped, rawType, extensions } = field;
1209
+
1210
+ if (rawInput[name] === undefined) continue;
1211
+ if (name === 'id') continue;
1212
+ if (skipFields.includes(name) || transientFields.includes(name)) continue;
1213
+ if (extensions?.stateMachine === true) continue;
1214
+ if (extensions?.readOnly === true) continue;
1215
+ if (isListType(rawType) && unwrapped.kind === 'OBJECT') continue;
1216
+
1217
+ let value = rawInput[name];
1218
+
1219
+ if (unwrapped.kind === 'OBJECT' && extensions?.relation?.embedded === true) {
1220
+ value = this._transformEmbeddedInput(unwrapped.name, value, options);
1221
+ } else if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1222
+ value = this._cleanObjectFieldForMutation(value);
1223
+ } else {
1224
+ value = this._coerceScalarValue(value, unwrapped.name);
1225
+ }
1226
+
1227
+ if (value !== undefined && value !== null && value !== '') {
1228
+ transformed[name] = value;
1229
+ }
1230
+ }
1231
+
1232
+ return this._deepRemoveTypename(transformed);
1233
+ }
1234
+
1235
+ _transformEmbeddedInput(objectTypeName, value, options = {}) {
1236
+ if (!value || typeof value !== 'object') return value;
1237
+ const typeInfo = this._types.get(objectTypeName);
1238
+ if (!typeInfo?.fields) return value;
1239
+
1240
+ const cleaned = {};
1241
+ for (const field of typeInfo.fields) {
1242
+ const { name, type: unwrapped, extensions } = field;
1243
+ if (value[name] === undefined) continue;
1244
+ if (extensions?.readOnly === true) continue;
1245
+
1246
+ let fieldValue = value[name];
1247
+ fieldValue = this._coerceScalarValue(fieldValue, unwrapped.name);
1248
+
1249
+ if (fieldValue !== undefined && fieldValue !== null && fieldValue !== '') {
1250
+ cleaned[name] = fieldValue;
1251
+ }
1252
+ }
1253
+ return Object.keys(cleaned).length > 0 ? cleaned : undefined;
1254
+ }
1255
+
1256
+ _cleanObjectFieldForMutation(value) {
1257
+ if (!value) return value;
1258
+ if (typeof value === 'string') return { id: value };
1259
+ if (typeof value === 'object' && value.id) return { id: value.id };
1260
+ return value;
1261
+ }
1262
+
1263
+ _coerceScalarValue(value, scalarTypeName) {
1264
+ if (value === null || value === undefined) return value;
1265
+
1266
+ if (isNumericScalar(scalarTypeName) && typeof value === 'string') {
1267
+ return Number(value);
1268
+ }
1269
+
1270
+ if (isDateTimeScalar(scalarTypeName) && typeof value === 'string') {
1271
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
1272
+ return `${value}T00:00:00.000Z`;
1273
+ }
1274
+ }
1275
+
1276
+ return value;
1277
+ }
1278
+
1279
+ _deepRemoveTypename(obj) {
1280
+ if (typeof obj !== 'object' || obj === null) return obj;
1281
+ if (Array.isArray(obj)) return obj.map(item => this._deepRemoveTypename(item));
1282
+
1283
+ const cleaned = {};
1284
+ for (const [key, value] of Object.entries(obj)) {
1285
+ if (key === '__typename') continue;
1286
+ cleaned[key] = this._deepRemoveTypename(value);
1287
+ }
1288
+ return cleaned;
1289
+ }
1290
+
1291
+ // -- Collection Delta Mutations --------------------------------------------
1292
+
1293
+ transformCollectionDelta(collectionTypeName, delta, options = {}) {
1294
+ this._ensureInitialized();
1295
+ const { connectionField = null } = options;
1296
+ const result = {};
1297
+
1298
+ if (delta.added && delta.added.length > 0) {
1299
+ result.added = delta.added.map(item => {
1300
+ let clean = { ...item };
1301
+
1302
+ delete clean.__status;
1303
+ delete clean.__originalData;
1304
+
1305
+ if (connectionField && clean[connectionField] !== undefined) {
1306
+ delete clean[connectionField];
1307
+ }
1308
+
1309
+ if (clean.id && typeof clean.id === 'string' && clean.id.startsWith('temp_')) {
1310
+ delete clean.id;
1311
+ }
1312
+
1313
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1314
+ clean = this._deepRemoveTypename(clean);
1315
+ return clean;
1316
+ });
1317
+ }
1318
+
1319
+ if (delta.updated && delta.updated.length > 0) {
1320
+ result.updated = delta.updated.map(item => {
1321
+ let clean = { ...item };
1322
+
1323
+ delete clean.__status;
1324
+ delete clean.__originalData;
1325
+
1326
+ if (connectionField && clean[connectionField] !== undefined) {
1327
+ delete clean[connectionField];
1328
+ }
1329
+
1330
+ clean = this._cleanCollectionItem(collectionTypeName, clean);
1331
+ clean = this._deepRemoveTypename(clean);
1332
+ return clean;
1333
+ });
1334
+ }
1335
+
1336
+ if (delta.deleted && delta.deleted.length > 0) {
1337
+ result.deleted = delta.deleted.map(item => {
1338
+ if (typeof item === 'string') return item;
1339
+ if (typeof item === 'object' && item.id) return item.id;
1340
+ return item;
1341
+ });
1342
+ }
1343
+
1344
+ return result;
1345
+ }
1346
+
1347
+ _cleanCollectionItem(collectionTypeName, item) {
1348
+ const typeInfo = this._types.get(collectionTypeName);
1349
+ if (!typeInfo?.fields) return item;
1350
+
1351
+ const cleaned = { ...item };
1352
+
1353
+ for (const field of typeInfo.fields) {
1354
+ const { name, type: unwrapped, rawType, extensions } = field;
1355
+ if (cleaned[name] === undefined) continue;
1356
+
1357
+ if (extensions?.stateMachine === true) {
1358
+ delete cleaned[name];
1359
+ continue;
1360
+ }
1361
+
1362
+ if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1363
+ const val = cleaned[name];
1364
+ if (val && typeof val === 'object' && 'id' in val) {
1365
+ cleaned[name] = { id: val.id };
1366
+ }
1367
+ continue;
1368
+ }
1369
+
1370
+ if (isNumericScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1371
+ cleaned[name] = Number(cleaned[name]);
1372
+ }
1373
+ if (isDateTimeScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1374
+ if (/^\d{4}-\d{2}-\d{2}$/.test(cleaned[name])) {
1375
+ cleaned[name] = `${cleaned[name]}T00:00:00.000Z`;
1376
+ }
1377
+ }
1378
+ }
1379
+
1380
+ return cleaned;
1381
+ }
1382
+
1383
+ // -- Guard ---------------------------------------------------------------
1384
+
1385
+ _ensureInitialized() {
1386
+ if (!this._initialized) {
1387
+ throw new Error('Client not initialized. Call init() first.');
1388
+ }
1389
+ }
1390
+
1391
+ // -- Static Helpers -------------------------------------------------------
1392
+
1393
+ static condition(field, operator, value, path) {
1394
+ const cond = { field, operator, value };
1395
+ if (path !== undefined) cond.path = path;
1396
+ return { conditions: [cond] };
1397
+ }
1398
+ }