@simtlix/simfinity-js-client 1.1.0 → 3.19.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.
@@ -1,69 +1,6 @@
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
- `;
1
+ import { normalizeDescriptor, introspectionQuery } from './metadata.js';
2
+ import { SelectionTree } from './selection.js';
3
+ import { transformRootInput, transformDelta } from './inputs.js';
67
4
 
68
5
  function unwrapType(typeRef) {
69
6
  let isList = false;
@@ -106,6 +43,8 @@ function isDateTimeScalar(name) {
106
43
  || n === 'isodate' || n === 'graphqldate' || n === 'graphqldatetime';
107
44
  }
108
45
 
46
+ const canAutoSelect = (field) => !field.args?.some((arg) => arg.type.kind === 'NON_NULL' && !arg.hasDefault);
47
+
109
48
  function isScalarOrEnum(kind) {
110
49
  return kind === 'SCALAR' || kind === 'ENUM';
111
50
  }
@@ -131,77 +70,14 @@ function typeRefToString(typeRef) {
131
70
  // into a GraphQL selection set string.
132
71
  // ---------------------------------------------------------------------------
133
72
 
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
73
  // ---------------------------------------------------------------------------
200
74
  // QueryBuilder -- chainable builder returned by SimfinityClient.find()
201
75
  // ---------------------------------------------------------------------------
202
76
 
203
77
  class QueryBuilder {
204
- constructor(client, typeName) {
78
+ constructor(client, typeName, queryInfo = null) {
79
+ this._queryInfo = queryInfo;
80
+ this._collectionArguments = new Map();
205
81
  this._client = client;
206
82
  this._typeName = typeName;
207
83
  this._variables = {};
@@ -213,7 +89,9 @@ class QueryBuilder {
213
89
  // -- Filtering -----------------------------------------------------------
214
90
 
215
91
  where(field, operatorOrTerms, value, value2) {
216
- if (Array.isArray(operatorOrTerms)) {
92
+ if (operatorOrTerms && typeof operatorOrTerms === 'object' && !Array.isArray(operatorOrTerms)) {
93
+ this._variables[field] = { ...operatorOrTerms };
94
+ } else if (Array.isArray(operatorOrTerms)) {
217
95
  if (this._variables[field]?.terms) {
218
96
  this._variables[field].terms.push(...operatorOrTerms);
219
97
  } else {
@@ -227,6 +105,11 @@ class QueryBuilder {
227
105
  return this;
228
106
  }
229
107
 
108
+ filter(group) {
109
+ if (!group || typeof group !== 'object' || Array.isArray(group)) throw new Error('filter() expects a QLFilterGroup');
110
+ return this.and([group]);
111
+ }
112
+
230
113
  or(groups) {
231
114
  if (!Array.isArray(groups)) {
232
115
  throw new Error('or() expects an array of QLFilterGroup objects');
@@ -308,99 +191,28 @@ class QueryBuilder {
308
191
 
309
192
  // -- Execution -----------------------------------------------------------
310
193
 
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
- }
194
+ collectionArgs(path, args) {
195
+ if (!args || typeof args !== 'object' || Array.isArray(args)) throw new Error('collectionArgs expects an argument object');
196
+ this._collectionArguments.set(path, { ...args }); return this;
197
+ }
356
198
 
357
- return result;
199
+ async exec() {
200
+ const { data, extensions } = await this.execWithMeta();
201
+ if (data !== null && typeof data === 'object' && extensions) data.__extensions = extensions;
202
+ return data;
358
203
  }
359
204
 
360
205
  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
- };
206
+ const queryInfo = this._queryInfo || this._client._findPluralQuery(this._typeName);
207
+ if (!queryInfo) throw new Error(`No plural query found for type '${this._typeName}'`);
208
+ if (!this._hasExplicitFields) this._selectionTree.addScalars(this._client._getScalarFields(this._typeName));
209
+ const tree = this._autoSelection ? new SelectionTree() : this._selectionTree;
210
+ if (this._autoSelection) tree.addSelection(this._autoSelection);
211
+ const response = await this._client._requestOperation('query', queryInfo, this._variables, tree, this._collectionArguments);
212
+ this._client._checkErrors(response, queryInfo.name, true);
213
+ return { data: response.data?.[queryInfo.name], extensions: response.extensions || null };
403
214
  }
215
+
404
216
  }
405
217
 
406
218
  // ---------------------------------------------------------------------------
@@ -428,7 +240,9 @@ class AggregateBuilder {
428
240
  }
429
241
 
430
242
  where(field, operatorOrTerms, value, value2) {
431
- if (Array.isArray(operatorOrTerms)) {
243
+ if (operatorOrTerms && typeof operatorOrTerms === 'object' && !Array.isArray(operatorOrTerms)) {
244
+ this._variables[field] = { ...operatorOrTerms };
245
+ } else if (Array.isArray(operatorOrTerms)) {
432
246
  if (this._variables[field]?.terms) {
433
247
  this._variables[field].terms.push(...operatorOrTerms);
434
248
  } else {
@@ -442,6 +256,11 @@ class AggregateBuilder {
442
256
  return this;
443
257
  }
444
258
 
259
+ filter(group) {
260
+ if (!group || typeof group !== 'object' || Array.isArray(group)) throw new Error('filter() expects a QLFilterGroup');
261
+ return this.and([group]);
262
+ }
263
+
445
264
  or(groups) {
446
265
  if (!Array.isArray(groups)) {
447
266
  throw new Error('or() expects an array of QLFilterGroup objects');
@@ -497,29 +316,9 @@ class AggregateBuilder {
497
316
  facts: this._facts,
498
317
  };
499
318
 
500
- const varDefs = [];
501
- const variables = {};
502
- const argMappings = [];
319
+ const response = await this._client._requestOperation('query', queryInfo, this._variables, 'groupId facts');
320
+ this._client._checkErrors(response, queryInfo.name);
503
321
 
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
322
  return response.data[queryInfo.name];
524
323
  }
525
324
  }
@@ -537,6 +336,14 @@ export default class SimfinityClient {
537
336
  * perform auth in your app and set tokens inside this callback or a closure it sees.
538
337
  */
539
338
  constructor(endpoint, options = {}) {
339
+ if (options.descriptor !== undefined && options.descriptorEndpoint !== undefined) throw new Error('descriptor and descriptorEndpoint are mutually exclusive');
340
+ if (options.introspection !== undefined && !['extended', 'standard'].includes(options.introspection)) throw new Error('Invalid introspection mode');
341
+ this._options = options;
342
+ this._descriptor = null;
343
+ this._fetch = options.fetch || ((...args) => {
344
+ if (typeof globalThis.fetch !== 'function') throw new Error('SimfinityClient requires fetch or options.fetch');
345
+ return globalThis.fetch(...args);
346
+ });
540
347
  this._endpoint = endpoint;
541
348
  this._types = new Map();
542
349
  this._queries = new Map();
@@ -553,14 +360,49 @@ export default class SimfinityClient {
553
360
  // -- Initialization ------------------------------------------------------
554
361
 
555
362
  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);
363
+ let descriptor = this._options.descriptor;
364
+ if (this._options.descriptorEndpoint !== undefined) {
365
+ const headers = {};
366
+ if (this._prepareHeaders) await this._prepareHeaders(headers);
367
+ const response = await this._fetch(this._options.descriptorEndpoint, { method: 'GET', headers });
368
+ if (!response.ok) throw new Error(`Descriptor HTTP request failed (${response.status})`);
369
+ descriptor = await response.json();
370
+ }
371
+ let schema;
372
+ if (descriptor !== undefined) {
373
+ const normalized = normalizeDescriptor(descriptor);
374
+ schema = normalized.schema; descriptor = normalized.descriptor;
375
+ } else {
376
+ const response = await this._sendRequest(introspectionQuery(this._options.introspection !== 'standard'));
377
+ if (response.errors) { const error = new Error(`Introspection failed: ${JSON.stringify(response.errors)}`); error.graphQLErrors = response.errors; throw error; }
378
+ schema = response.data?.__schema;
379
+ if (!schema || !Array.isArray(schema.types)) throw new Error('Invalid introspection response');
380
+ }
381
+ const draft = new SimfinityClient(this._endpoint);
382
+ draft._parseIntrospection(schema);
383
+ draft._descriptor = descriptor || null;
384
+ if (descriptor) draft._applyDescriptor(descriptor);
385
+ for (const key of ['_types', '_queries', '_mutations', '_typeNameToPlural', '_typeNameToSingular', '_typeNameToAggregate', '_queryNameToType', '_descriptor']) this[key] = draft[key];
561
386
  this._initialized = true;
562
387
  }
563
388
 
389
+ getDescriptor() { return this._descriptor; }
390
+
391
+ _applyDescriptor(descriptor) {
392
+ this._typeNameToPlural.clear(); this._typeNameToSingular.clear(); this._typeNameToAggregate.clear();
393
+ for (const [typeName, type] of Object.entries(descriptor.types)) {
394
+ for (const [kind, map] of [['list', this._typeNameToPlural], ['get', this._typeNameToSingular], ['aggregate', this._typeNameToAggregate]]) {
395
+ if (type.endpoints?.[kind]) map.set(typeName, type.endpoints[kind]);
396
+ }
397
+ }
398
+ for (const [root, registry] of [['query', this._queries], ['mutation', this._mutations]]) {
399
+ for (const [name, operation] of Object.entries(descriptor.operations[root])) {
400
+ Object.assign(registry.get(name), operation, { returnType: registry.get(name).returnType, args: registry.get(name).args,
401
+ category: operation.operation === 'transition' ? 'stateTransition' : operation.operation, typeName: operation.type });
402
+ }
403
+ }
404
+ }
405
+
564
406
  _parseIntrospection(schema) {
565
407
  const queryTypeName = schema.queryType?.name;
566
408
  const mutationTypeName = schema.mutationType?.name;
@@ -573,20 +415,22 @@ export default class SimfinityClient {
573
415
  _buildTypesRegistry(types) {
574
416
  for (const type of types) {
575
417
  if (type.name.startsWith('__')) continue;
576
- const validKinds = ['OBJECT', 'ENUM', 'SCALAR', 'INPUT_OBJECT'];
418
+ const validKinds = ['OBJECT', 'ENUM', 'SCALAR', 'INPUT_OBJECT', 'INTERFACE', 'UNION'];
577
419
  if (!validKinds.includes(type.kind)) continue;
578
420
 
579
421
  const typeInfo = {
580
422
  name: type.name,
581
423
  kind: type.kind,
582
- fields: (type.fields || []).map(f => ({
424
+ fields: (type.fields || type.inputFields || []).map(f => ({
583
425
  name: f.name,
584
426
  type: unwrapType(f.type),
585
427
  rawType: f.type,
586
- args: (f.args || []).map(a => ({ name: a.name, type: a.type })),
428
+ args: (f.args || []).map(a => ({ ...a, hasDefault: a.hasDefault ?? (a.defaultValue != null) })),
587
429
  extensions: f.extensions || null,
430
+ hasDefault: f.hasDefault ?? (f.defaultValue != null), defaultValue: f.defaultValue,
588
431
  })),
589
432
  enumValues: (type.enumValues || []).map(v => v.name),
433
+ baseScalar: type.baseScalar, possibleTypes: type.possibleTypes?.map((value) => typeof value === 'string' ? value : value.name), interfaces: type.interfaces?.map((value) => typeof value === 'string' ? value : value.name), isOneOf: type.isOneOf,
590
434
  };
591
435
  this._types.set(type.name, typeInfo);
592
436
  }
@@ -600,7 +444,8 @@ export default class SimfinityClient {
600
444
  const returnType = unwrapType(field.type);
601
445
  const queryInfo = {
602
446
  name: field.name,
603
- args: (field.args || []).map(a => ({ name: a.name, type: a.type })),
447
+ description: field.description,
448
+ args: (field.args || []).map(a => ({ ...a, hasDefault: a.hasDefault ?? (a.defaultValue != null) })),
604
449
  returnType,
605
450
  rawReturnType: field.type,
606
451
  };
@@ -614,13 +459,19 @@ export default class SimfinityClient {
614
459
  }
615
460
  }
616
461
 
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
- }
462
+ for (const typeName of this._typeNameToPlural.keys()) {
463
+ const candidates = [...this._queries.values()].filter((query) => query.returnType.name === typeName && query.returnType.isList && !query.args.some((arg) => arg.name === 'aggregation'));
464
+ const canonical = candidates.filter((query) => this._queries.has(`${query.name}_aggregate`));
465
+ const selected = canonical.length === 1 ? canonical[0] : canonical.length === 0 && candidates.length === 1 ? candidates[0] : null;
466
+ if (selected) {
467
+ this._typeNameToPlural.set(typeName, selected.name);
468
+ if (canonical.length === 1) this._typeNameToAggregate.set(typeName, `${selected.name}_aggregate`);
469
+ } else this._typeNameToPlural.delete(typeName);
470
+ }
471
+ for (const typeName of this._typeNameToSingular.keys()) {
472
+ const candidates = [...this._queries.values()].filter((query) => query.returnType.name === typeName && !query.returnType.isList && query.args.some((arg) => arg.name === 'id'));
473
+ if (candidates.length !== 1) this._typeNameToSingular.delete(typeName);
622
474
  }
623
-
624
475
  this._queryNameToType = new Map();
625
476
  for (const [typeName, queryName] of this._typeNameToPlural) {
626
477
  this._queryNameToType.set(queryName, typeName);
@@ -640,7 +491,8 @@ export default class SimfinityClient {
640
491
  const returnType = unwrapType(field.type);
641
492
  const mutationInfo = {
642
493
  name: field.name,
643
- args: (field.args || []).map(a => ({ name: a.name, type: a.type })),
494
+ description: field.description,
495
+ args: (field.args || []).map(a => ({ ...a, hasDefault: a.hasDefault ?? (a.defaultValue != null) })),
644
496
  returnType,
645
497
  rawReturnType: field.type,
646
498
  category: 'custom',
@@ -654,7 +506,18 @@ export default class SimfinityClient {
654
506
 
655
507
  _classifyMutation(mutationInfo, knownTypeNames) {
656
508
  const name = mutationInfo.name;
509
+ for (const kind of ['add', 'update', 'delete']) {
510
+ if (mutationInfo.description === kind && name.startsWith(kind) && knownTypeNames.includes(mutationInfo.returnType.name)) {
511
+ Object.assign(mutationInfo, { category: kind, typeName: mutationInfo.returnType.name }); return;
512
+ }
513
+ }
657
514
  for (const typeName of knownTypeNames) {
515
+ const singular = this._typeNameToSingular.get(typeName);
516
+ const suffix = `_${singular}`;
517
+ if (mutationInfo.returnType.name === typeName && singular && name.endsWith(suffix)
518
+ && this._types.get(typeName)?.fields.some((field) => field.extensions?.stateMachine)) {
519
+ Object.assign(mutationInfo, { category: 'stateTransition', typeName, action: name.slice(0, -suffix.length) }); return;
520
+ }
658
521
  if (name === `add${typeName}`) {
659
522
  Object.assign(mutationInfo, { category: 'add', typeName });
660
523
  return;
@@ -688,7 +551,7 @@ export default class SimfinityClient {
688
551
  const typeInfo = this._types.get(typeName);
689
552
  if (!typeInfo) return 'id';
690
553
  const scalars = typeInfo.fields
691
- .filter(f => f.type.kind === 'SCALAR' || f.type.kind === 'ENUM')
554
+ .filter(f => (f.type.kind === 'SCALAR' || f.type.kind === 'ENUM') && canAutoSelect(f))
692
555
  .map(f => f.name);
693
556
  return scalars.length > 0 ? scalars.join(' ') : 'id';
694
557
  }
@@ -723,14 +586,71 @@ export default class SimfinityClient {
723
586
  }
724
587
  const headers = { 'Content-Type': 'application/json' };
725
588
  if (this._prepareHeaders) {
726
- this._prepareHeaders(headers);
589
+ await this._prepareHeaders(headers);
727
590
  }
728
- const response = await _fetch(this._endpoint, {
591
+ const response = await this._fetch(this._endpoint, {
729
592
  method: 'POST',
730
593
  headers,
731
594
  body: JSON.stringify(body),
732
595
  });
733
- return response.json();
596
+ let envelope;
597
+ try { envelope = await response.json(); } catch { throw new Error(`Invalid JSON response (HTTP ${response.status})`); }
598
+ if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
599
+ || (envelope.errors !== undefined && (!Array.isArray(envelope.errors) || envelope.errors.length === 0 || envelope.errors.some((error) => !error || typeof error.message !== 'string')))
600
+ || (!Object.hasOwn(envelope, 'data') && !envelope.errors)
601
+ || (envelope.data !== undefined && envelope.data !== null && (typeof envelope.data !== 'object' || Array.isArray(envelope.data)))) throw new Error('Invalid GraphQL response');
602
+ if (!response.ok && !envelope.errors) throw new Error(`HTTP request failed (${response.status})`);
603
+ return envelope;
604
+ }
605
+
606
+ _checkErrors(response, name, partial = false) {
607
+ if (response.errors && (!partial || !response.data?.[name])) {
608
+ const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
609
+ error.graphQLErrors = response.errors; throw error;
610
+ }
611
+ }
612
+
613
+ async _requestOperation(kind, info, provided = {}, selection, collectionArguments = new Map()) {
614
+ const definitions = []; const variables = {}; const mappings = [];
615
+ for (const [name, value] of Object.entries(provided)) {
616
+ if (value === undefined) continue;
617
+ const arg = info.args.find((item) => item.name === name);
618
+ if (!arg) throw new Error(`Unknown ${kind} argument ${info.name}.${name}`);
619
+ definitions.push(`$${name}: ${typeRefToString(arg.type)}`); variables[name] = value; mappings.push(`${name}: $${name}`);
620
+ }
621
+ const context = { types: this._types, arguments: collectionArguments, used: new Set(), variable: (ref, value) => {
622
+ let name = `c${definitions.length}`; while (Object.hasOwn(variables, name)) name += '_';
623
+ variables[name] = value; definitions.push(`$${name}: ${typeRefToString(ref)}`); return name;
624
+ } };
625
+ const composite = ['OBJECT', 'INTERFACE', 'UNION'].includes(info.returnType.kind);
626
+ let fields = '';
627
+ if (composite) {
628
+ const tree = selection instanceof SelectionTree ? selection : new SelectionTree();
629
+ if (!(selection instanceof SelectionTree)) tree.addSelection(selection || this._getScalarFields(info.returnType.name));
630
+ fields = tree.serialize(context, info.returnType.name);
631
+ if (!fields) throw new Error(`No fields to select for ${info.name}`);
632
+ }
633
+ for (const path of collectionArguments.keys()) if (!context.used.has(path)) throw new Error(`Collection path is not selected: ${path}`);
634
+ const document = `${kind}${definitions.length ? `(${definitions.join(', ')})` : ''} { ${info.name}${mappings.length ? `(${mappings.join(', ')})` : ''}${fields ? ` { ${fields} }` : ''} }`;
635
+ return this._sendRequest(document, variables);
636
+ }
637
+
638
+ async query(name, args = {}, fields) {
639
+ this._ensureInitialized(); const info = this._queries.get(name);
640
+ if (!info) throw new Error(`Query '${name}' not found`);
641
+ const response = await this._requestOperation('query', info, args, fields);
642
+ this._checkErrors(response, name); return response.data?.[name];
643
+ }
644
+
645
+ getViews(typeName) {
646
+ this._ensureInitialized();
647
+ return [...this._queries.values()].filter((info) => info.view && (!typeName || info.typeName === typeName));
648
+ }
649
+
650
+ findView(name, params = {}) {
651
+ this._ensureInitialized(); const info = this._queries.get(name);
652
+ if (!info?.returnType.isList || (this._descriptor && !info.view)) throw new Error(`View '${name}' not found`);
653
+ const builder = new QueryBuilder(this, info.returnType.name, info); builder._variables = { ...params }; return builder;
734
654
  }
735
655
 
736
656
  // -- Query Methods -------------------------------------------------------
@@ -766,7 +686,7 @@ export default class SimfinityClient {
766
686
  searchField = 'name';
767
687
  } else {
768
688
  const firstScalar = typeInfo.fields.find(f =>
769
- isScalarOrEnum(f.type.kind) && f.name !== 'id'
689
+ isScalarOrEnum(f.type.kind) && f.name !== 'id',
770
690
  );
771
691
  searchField = firstScalar?.name || 'id';
772
692
  }
@@ -832,7 +752,7 @@ export default class SimfinityClient {
832
752
  const transformedInput = options.transform
833
753
  ? this.transformInput(typeName, input, { mode: 'create', ...options })
834
754
  : input;
835
- return this._executeMutation(`add${typeName}`, typeName, { input: transformedInput }, fields);
755
+ return this._executeMutation(this._mutationName(typeName, 'add'), typeName, { input: transformedInput }, fields);
836
756
  }
837
757
 
838
758
  async update(typeName, id, input, fields, options = {}) {
@@ -841,15 +761,39 @@ export default class SimfinityClient {
841
761
  ? this.transformInput(typeName, input, { mode: 'update', ...options })
842
762
  : input;
843
763
  return this._executeMutation(
844
- `update${typeName}`, typeName,
764
+ this._mutationName(typeName, 'update'), typeName,
845
765
  { input: { id, ...transformedInput } },
846
766
  fields,
847
767
  );
848
768
  }
849
769
 
850
- async delete(typeName, id, fields) {
770
+ async delete(typeName, id, fields, options = {}) {
851
771
  this._ensureInitialized();
852
- return this._executeMutation(`delete${typeName}`, typeName, { id }, fields);
772
+ return this._executeMutation(this._mutationName(typeName, 'delete'), typeName, { id, ...(options.version !== undefined ? { version: options.version } : {}) }, fields);
773
+ }
774
+
775
+ _mutationName(typeName, kind) {
776
+ if (this._descriptor) {
777
+ const name = this._descriptor.types[typeName]?.endpoints?.[kind];
778
+ if (!name) throw new Error(`No ${kind} mutation for '${typeName}'`);
779
+ return name;
780
+ }
781
+ return [...this._mutations.values()].find((info) => info.category === kind && info.typeName === typeName)?.name || `${kind}${typeName}`;
782
+ }
783
+
784
+ _inputTypeName(typeName, kind) {
785
+ const descriptor = this._descriptor?.types[typeName]?.inputs?.[kind];
786
+ if (descriptor) return descriptor;
787
+ const info = this._mutations.get(this._mutationName(typeName, kind));
788
+ const input = info?.args.find((arg) => arg.name === 'input');
789
+ return input ? unwrapType(input.type).name : null;
790
+ }
791
+
792
+ async transaction(operations, fields = 'id', options = {}) {
793
+ this._ensureInitialized();
794
+ const info = options.name ? this._mutations.get(options.name) : [...this._mutations.values()].find((item) => item.category === 'transaction');
795
+ if (!info || (this._descriptor && info.category !== 'transaction') || (!this._descriptor && !options.name)) throw new Error('Generated transaction not found; introspection requires an explicit transaction name');
796
+ return this._executeMutation(info.name, info.returnType.name, { operations }, fields);
853
797
  }
854
798
 
855
799
  // -- State Machine -------------------------------------------------------
@@ -932,35 +876,10 @@ export default class SimfinityClient {
932
876
  // -- Shared Mutation Executor --------------------------------------------
933
877
 
934
878
  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];
879
+ const info = this._mutations.get(mutationName);
880
+ if (!info) throw new Error(`Mutation '${mutationName}' not found`);
881
+ const response = await this._requestOperation('mutation', info, providedArgs, fields);
882
+ this._checkErrors(response, mutationName); return response.data?.[mutationName];
964
883
  }
965
884
 
966
885
  // -- Schema Getters ------------------------------------------------------
@@ -994,7 +913,7 @@ export default class SimfinityClient {
994
913
  for (const field of typeInfo.fields) {
995
914
  const { name, type: unwrapped, rawType, extensions } = field;
996
915
 
997
- if (isListType(rawType)) continue;
916
+ if (isListType(rawType) || !canAutoSelect(field)) continue;
998
917
 
999
918
  if (isScalarOrEnum(unwrapped.kind)) {
1000
919
  fieldSelections.push(name);
@@ -1003,7 +922,7 @@ export default class SimfinityClient {
1003
922
  fieldTypeByColumn[name] = unwrapped.name || unwrapped.kind || 'SCALAR';
1004
923
  } else if (unwrapped.kind === 'OBJECT' && unwrapped.name) {
1005
924
  const objType = this._types.get(unwrapped.name);
1006
- const objFields = objType?.fields || [];
925
+ const objFields = (objType?.fields || []).filter((candidate) => isScalarOrEnum(candidate.type.kind) && !candidate.type.isList && canAutoSelect(candidate));
1007
926
  const objFieldNames = new Set(objFields.map(f => f.name));
1008
927
  const isEmbedded = extensions?.relation?.embedded === true;
1009
928
 
@@ -1015,7 +934,7 @@ export default class SimfinityClient {
1015
934
  chosenDisplay = 'name';
1016
935
  } else {
1017
936
  const firstScalar = objFields.find(f =>
1018
- isScalarOrEnum(f.type.kind) && !f.type.isList
937
+ isScalarOrEnum(f.type.kind) && !f.type.isList,
1019
938
  );
1020
939
  chosenDisplay = firstScalar?.name || null;
1021
940
  }
@@ -1029,6 +948,7 @@ export default class SimfinityClient {
1029
948
  else if (objFields[0]) subFields.add(objFields[0].name);
1030
949
  }
1031
950
 
951
+ if (subFields.size === 0) continue;
1032
952
  const subSelection = [...subFields].join(' ');
1033
953
  fieldSelections.push(`${name} { ${subSelection} }`);
1034
954
  columns.push(name);
@@ -1044,13 +964,13 @@ export default class SimfinityClient {
1044
964
  }
1045
965
  }
1046
966
 
1047
- const hasId = typeInfo.fields.some(f => f.name === 'id');
967
+ const hasId = typeInfo.fields.some(f => f.name === 'id' && canAutoSelect(f));
1048
968
  if (hasId && !fieldSelections.includes('id')) {
1049
969
  fieldSelections.unshift('id');
1050
970
  }
1051
971
 
1052
972
  return {
1053
- selection: fieldSelections.join('\n'),
973
+ selection: fieldSelections.join('\n') || '__typename',
1054
974
  columns,
1055
975
  sortFieldByColumn,
1056
976
  fieldTypeByColumn,
@@ -1084,7 +1004,7 @@ export default class SimfinityClient {
1084
1004
  if (objFieldNames.includes('name')) return 'name';
1085
1005
 
1086
1006
  const firstScalar = objectType.fields.find(f =>
1087
- isScalarOrEnum(f.type.kind) && !f.type.isList
1007
+ isScalarOrEnum(f.type.kind) && !f.type.isList,
1088
1008
  );
1089
1009
  return firstScalar?.name || null;
1090
1010
  }
@@ -1197,8 +1117,12 @@ export default class SimfinityClient {
1197
1117
  // -- Mutation Input Transformation -----------------------------------------
1198
1118
 
1199
1119
  transformInput(typeName, rawInput, options = {}) {
1120
+ this._ensureInitialized(); return transformRootInput(this, typeName, rawInput, options);
1121
+ }
1122
+
1123
+ _legacyTransformInput(typeName, rawInput, options = {}) {
1200
1124
  this._ensureInitialized();
1201
- const { skipFields = [], transientFields = [], mode = 'create' } = options;
1125
+ const { skipFields = [], transientFields = [] } = options;
1202
1126
  const typeInfo = this._types.get(typeName);
1203
1127
  if (!typeInfo) return { ...rawInput };
1204
1128
 
@@ -1232,7 +1156,7 @@ export default class SimfinityClient {
1232
1156
  return this._deepRemoveTypename(transformed);
1233
1157
  }
1234
1158
 
1235
- _transformEmbeddedInput(objectTypeName, value, options = {}) {
1159
+ _transformEmbeddedInput(objectTypeName, value) {
1236
1160
  if (!value || typeof value !== 'object') return value;
1237
1161
  const typeInfo = this._types.get(objectTypeName);
1238
1162
  if (!typeInfo?.fields) return value;
@@ -1291,93 +1215,7 @@ export default class SimfinityClient {
1291
1215
  // -- Collection Delta Mutations --------------------------------------------
1292
1216
 
1293
1217
  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;
1218
+ this._ensureInitialized(); return transformDelta(this, collectionTypeName, delta, options);
1381
1219
  }
1382
1220
 
1383
1221
  // -- Guard ---------------------------------------------------------------