@simtlix/simfinity-js-client 1.0.6 → 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,35 @@ 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
+
113
+ or(groups) {
114
+ if (!Array.isArray(groups)) {
115
+ throw new Error('or() expects an array of QLFilterGroup objects');
116
+ }
117
+ if (this._variables.OR) {
118
+ this._variables.OR.push(...groups);
119
+ } else {
120
+ this._variables.OR = [...groups];
121
+ }
122
+ return this;
123
+ }
124
+
125
+ and(groups) {
126
+ if (!Array.isArray(groups)) {
127
+ throw new Error('and() expects an array of QLFilterGroup objects');
128
+ }
129
+ if (this._variables.AND) {
130
+ this._variables.AND.push(...groups);
131
+ } else {
132
+ this._variables.AND = [...groups];
133
+ }
134
+ return this;
135
+ }
136
+
230
137
  // -- Relation Field Selection --------------------------------------------
231
138
 
232
139
  joinCollection(path, fields, filter) {
@@ -284,99 +191,28 @@ class QueryBuilder {
284
191
 
285
192
  // -- Execution -----------------------------------------------------------
286
193
 
287
- async exec() {
288
- const queryInfo = this._client._findPluralQuery(this._typeName);
289
- if (!queryInfo) {
290
- throw new Error(`No plural query found for type '${this._typeName}'`);
291
- }
292
-
293
- if (!this._hasExplicitFields) {
294
- const autoFields = this._client._getScalarFields(this._typeName);
295
- this._selectionTree.addScalars(autoFields);
296
- }
297
-
298
- const selectionSet = this._autoSelection || this._selectionTree.serialize();
299
- if (!selectionSet) {
300
- throw new Error(`No fields to select for type '${this._typeName}'`);
301
- }
302
-
303
- const varDefs = [];
304
- const variables = {};
305
- const argMappings = [];
306
-
307
- for (const [key, val] of Object.entries(this._variables)) {
308
- const arg = queryInfo.args.find(a => a.name === key);
309
- if (arg) {
310
- varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
311
- variables[key] = val;
312
- argMappings.push(`${key}: $${key}`);
313
- }
314
- }
315
-
316
- const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
317
- const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
318
- const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { ${selectionSet} } }`;
319
-
320
- const response = await this._client._sendRequest(query, variables);
321
- if (response.errors && !response.data?.[queryInfo.name]) {
322
- const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
323
- error.graphQLErrors = response.errors;
324
- throw error;
325
- }
326
-
327
- const result = response.data[queryInfo.name];
328
-
329
- if (response.extensions) {
330
- result.__extensions = response.extensions;
331
- }
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
+ }
332
198
 
333
- 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;
334
203
  }
335
204
 
336
205
  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
- };
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 };
379
214
  }
215
+
380
216
  }
381
217
 
382
218
  // ---------------------------------------------------------------------------
@@ -404,7 +240,9 @@ class AggregateBuilder {
404
240
  }
405
241
 
406
242
  where(field, operatorOrTerms, value, value2) {
407
- if (Array.isArray(operatorOrTerms)) {
243
+ if (operatorOrTerms && typeof operatorOrTerms === 'object' && !Array.isArray(operatorOrTerms)) {
244
+ this._variables[field] = { ...operatorOrTerms };
245
+ } else if (Array.isArray(operatorOrTerms)) {
408
246
  if (this._variables[field]?.terms) {
409
247
  this._variables[field].terms.push(...operatorOrTerms);
410
248
  } else {
@@ -418,6 +256,35 @@ class AggregateBuilder {
418
256
  return this;
419
257
  }
420
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
+
264
+ or(groups) {
265
+ if (!Array.isArray(groups)) {
266
+ throw new Error('or() expects an array of QLFilterGroup objects');
267
+ }
268
+ if (this._variables.OR) {
269
+ this._variables.OR.push(...groups);
270
+ } else {
271
+ this._variables.OR = [...groups];
272
+ }
273
+ return this;
274
+ }
275
+
276
+ and(groups) {
277
+ if (!Array.isArray(groups)) {
278
+ throw new Error('and() expects an array of QLFilterGroup objects');
279
+ }
280
+ if (this._variables.AND) {
281
+ this._variables.AND.push(...groups);
282
+ } else {
283
+ this._variables.AND = [...groups];
284
+ }
285
+ return this;
286
+ }
287
+
421
288
  page(page, size, count) {
422
289
  const pagination = { page, size };
423
290
  if (count !== undefined) pagination.count = count;
@@ -449,29 +316,9 @@ class AggregateBuilder {
449
316
  facts: this._facts,
450
317
  };
451
318
 
452
- const varDefs = [];
453
- const variables = {};
454
- const argMappings = [];
319
+ const response = await this._client._requestOperation('query', queryInfo, this._variables, 'groupId facts');
320
+ this._client._checkErrors(response, queryInfo.name);
455
321
 
456
- for (const [key, val] of Object.entries(this._variables)) {
457
- const arg = queryInfo.args.find(a => a.name === key);
458
- if (arg) {
459
- varDefs.push(`$${key}: ${typeRefToString(arg.type)}`);
460
- variables[key] = val;
461
- argMappings.push(`${key}: $${key}`);
462
- }
463
- }
464
-
465
- const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
466
- const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
467
- const query = `query${varDefsStr} { ${queryInfo.name}${argsBlock} { groupId facts } }`;
468
-
469
- const response = await this._client._sendRequest(query, variables);
470
- if (response.errors) {
471
- const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
472
- error.graphQLErrors = response.errors;
473
- throw error;
474
- }
475
322
  return response.data[queryInfo.name];
476
323
  }
477
324
  }
@@ -489,6 +336,14 @@ export default class SimfinityClient {
489
336
  * perform auth in your app and set tokens inside this callback or a closure it sees.
490
337
  */
491
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
+ });
492
347
  this._endpoint = endpoint;
493
348
  this._types = new Map();
494
349
  this._queries = new Map();
@@ -505,14 +360,49 @@ export default class SimfinityClient {
505
360
  // -- Initialization ------------------------------------------------------
506
361
 
507
362
  async init() {
508
- const response = await this._sendRequest(INTROSPECTION_QUERY);
509
- if (response.errors) {
510
- throw new Error(`Introspection failed: ${JSON.stringify(response.errors)}`);
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');
511
380
  }
512
- this._parseIntrospection(response.data.__schema);
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];
513
386
  this._initialized = true;
514
387
  }
515
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
+
516
406
  _parseIntrospection(schema) {
517
407
  const queryTypeName = schema.queryType?.name;
518
408
  const mutationTypeName = schema.mutationType?.name;
@@ -525,20 +415,22 @@ export default class SimfinityClient {
525
415
  _buildTypesRegistry(types) {
526
416
  for (const type of types) {
527
417
  if (type.name.startsWith('__')) continue;
528
- const validKinds = ['OBJECT', 'ENUM', 'SCALAR', 'INPUT_OBJECT'];
418
+ const validKinds = ['OBJECT', 'ENUM', 'SCALAR', 'INPUT_OBJECT', 'INTERFACE', 'UNION'];
529
419
  if (!validKinds.includes(type.kind)) continue;
530
420
 
531
421
  const typeInfo = {
532
422
  name: type.name,
533
423
  kind: type.kind,
534
- fields: (type.fields || []).map(f => ({
424
+ fields: (type.fields || type.inputFields || []).map(f => ({
535
425
  name: f.name,
536
426
  type: unwrapType(f.type),
537
427
  rawType: f.type,
538
- args: (f.args || []).map(a => ({ name: a.name, type: a.type })),
428
+ args: (f.args || []).map(a => ({ ...a, hasDefault: a.hasDefault ?? (a.defaultValue != null) })),
539
429
  extensions: f.extensions || null,
430
+ hasDefault: f.hasDefault ?? (f.defaultValue != null), defaultValue: f.defaultValue,
540
431
  })),
541
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,
542
434
  };
543
435
  this._types.set(type.name, typeInfo);
544
436
  }
@@ -552,7 +444,8 @@ export default class SimfinityClient {
552
444
  const returnType = unwrapType(field.type);
553
445
  const queryInfo = {
554
446
  name: field.name,
555
- 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) })),
556
449
  returnType,
557
450
  rawReturnType: field.type,
558
451
  };
@@ -566,13 +459,19 @@ export default class SimfinityClient {
566
459
  }
567
460
  }
568
461
 
569
- for (const [typeName, pluralName] of this._typeNameToPlural) {
570
- const aggName = `${pluralName}_aggregate`;
571
- if (this._queries.has(aggName)) {
572
- this._typeNameToAggregate.set(typeName, aggName);
573
- }
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);
574
474
  }
575
-
576
475
  this._queryNameToType = new Map();
577
476
  for (const [typeName, queryName] of this._typeNameToPlural) {
578
477
  this._queryNameToType.set(queryName, typeName);
@@ -592,7 +491,8 @@ export default class SimfinityClient {
592
491
  const returnType = unwrapType(field.type);
593
492
  const mutationInfo = {
594
493
  name: field.name,
595
- 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) })),
596
496
  returnType,
597
497
  rawReturnType: field.type,
598
498
  category: 'custom',
@@ -606,7 +506,18 @@ export default class SimfinityClient {
606
506
 
607
507
  _classifyMutation(mutationInfo, knownTypeNames) {
608
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
+ }
609
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
+ }
610
521
  if (name === `add${typeName}`) {
611
522
  Object.assign(mutationInfo, { category: 'add', typeName });
612
523
  return;
@@ -640,7 +551,7 @@ export default class SimfinityClient {
640
551
  const typeInfo = this._types.get(typeName);
641
552
  if (!typeInfo) return 'id';
642
553
  const scalars = typeInfo.fields
643
- .filter(f => f.type.kind === 'SCALAR' || f.type.kind === 'ENUM')
554
+ .filter(f => (f.type.kind === 'SCALAR' || f.type.kind === 'ENUM') && canAutoSelect(f))
644
555
  .map(f => f.name);
645
556
  return scalars.length > 0 ? scalars.join(' ') : 'id';
646
557
  }
@@ -675,14 +586,71 @@ export default class SimfinityClient {
675
586
  }
676
587
  const headers = { 'Content-Type': 'application/json' };
677
588
  if (this._prepareHeaders) {
678
- this._prepareHeaders(headers);
589
+ await this._prepareHeaders(headers);
679
590
  }
680
- const response = await _fetch(this._endpoint, {
591
+ const response = await this._fetch(this._endpoint, {
681
592
  method: 'POST',
682
593
  headers,
683
594
  body: JSON.stringify(body),
684
595
  });
685
- 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;
686
654
  }
687
655
 
688
656
  // -- Query Methods -------------------------------------------------------
@@ -718,7 +686,7 @@ export default class SimfinityClient {
718
686
  searchField = 'name';
719
687
  } else {
720
688
  const firstScalar = typeInfo.fields.find(f =>
721
- isScalarOrEnum(f.type.kind) && f.name !== 'id'
689
+ isScalarOrEnum(f.type.kind) && f.name !== 'id',
722
690
  );
723
691
  searchField = firstScalar?.name || 'id';
724
692
  }
@@ -784,7 +752,7 @@ export default class SimfinityClient {
784
752
  const transformedInput = options.transform
785
753
  ? this.transformInput(typeName, input, { mode: 'create', ...options })
786
754
  : input;
787
- return this._executeMutation(`add${typeName}`, typeName, { input: transformedInput }, fields);
755
+ return this._executeMutation(this._mutationName(typeName, 'add'), typeName, { input: transformedInput }, fields);
788
756
  }
789
757
 
790
758
  async update(typeName, id, input, fields, options = {}) {
@@ -793,15 +761,39 @@ export default class SimfinityClient {
793
761
  ? this.transformInput(typeName, input, { mode: 'update', ...options })
794
762
  : input;
795
763
  return this._executeMutation(
796
- `update${typeName}`, typeName,
764
+ this._mutationName(typeName, 'update'), typeName,
797
765
  { input: { id, ...transformedInput } },
798
766
  fields,
799
767
  );
800
768
  }
801
769
 
802
- async delete(typeName, id, fields) {
770
+ async delete(typeName, id, fields, options = {}) {
803
771
  this._ensureInitialized();
804
- 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);
805
797
  }
806
798
 
807
799
  // -- State Machine -------------------------------------------------------
@@ -884,35 +876,10 @@ export default class SimfinityClient {
884
876
  // -- Shared Mutation Executor --------------------------------------------
885
877
 
886
878
  async _executeMutation(mutationName, typeName, providedArgs, fields) {
887
- const mutationInfo = this._mutations.get(mutationName);
888
- if (!mutationInfo) {
889
- throw new Error(`Mutation '${mutationName}' not found`);
890
- }
891
-
892
- const selectionSet = this._resolveFields(typeName, fields);
893
- const varDefs = [];
894
- const variables = {};
895
- const argMappings = [];
896
-
897
- for (const arg of mutationInfo.args) {
898
- if (providedArgs[arg.name] !== undefined) {
899
- varDefs.push(`$${arg.name}: ${typeRefToString(arg.type)}`);
900
- variables[arg.name] = providedArgs[arg.name];
901
- argMappings.push(`${arg.name}: $${arg.name}`);
902
- }
903
- }
904
-
905
- const varDefsStr = varDefs.length > 0 ? `(${varDefs.join(', ')})` : '';
906
- const argsBlock = argMappings.length > 0 ? `(${argMappings.join(', ')})` : '';
907
- const query = `mutation${varDefsStr} { ${mutationName}${argsBlock} { ${selectionSet} } }`;
908
-
909
- const response = await this._sendRequest(query, variables);
910
- if (response.errors) {
911
- const error = new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
912
- error.graphQLErrors = response.errors;
913
- throw error;
914
- }
915
- 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];
916
883
  }
917
884
 
918
885
  // -- Schema Getters ------------------------------------------------------
@@ -946,7 +913,7 @@ export default class SimfinityClient {
946
913
  for (const field of typeInfo.fields) {
947
914
  const { name, type: unwrapped, rawType, extensions } = field;
948
915
 
949
- if (isListType(rawType)) continue;
916
+ if (isListType(rawType) || !canAutoSelect(field)) continue;
950
917
 
951
918
  if (isScalarOrEnum(unwrapped.kind)) {
952
919
  fieldSelections.push(name);
@@ -955,7 +922,7 @@ export default class SimfinityClient {
955
922
  fieldTypeByColumn[name] = unwrapped.name || unwrapped.kind || 'SCALAR';
956
923
  } else if (unwrapped.kind === 'OBJECT' && unwrapped.name) {
957
924
  const objType = this._types.get(unwrapped.name);
958
- const objFields = objType?.fields || [];
925
+ const objFields = (objType?.fields || []).filter((candidate) => isScalarOrEnum(candidate.type.kind) && !candidate.type.isList && canAutoSelect(candidate));
959
926
  const objFieldNames = new Set(objFields.map(f => f.name));
960
927
  const isEmbedded = extensions?.relation?.embedded === true;
961
928
 
@@ -967,7 +934,7 @@ export default class SimfinityClient {
967
934
  chosenDisplay = 'name';
968
935
  } else {
969
936
  const firstScalar = objFields.find(f =>
970
- isScalarOrEnum(f.type.kind) && !f.type.isList
937
+ isScalarOrEnum(f.type.kind) && !f.type.isList,
971
938
  );
972
939
  chosenDisplay = firstScalar?.name || null;
973
940
  }
@@ -981,6 +948,7 @@ export default class SimfinityClient {
981
948
  else if (objFields[0]) subFields.add(objFields[0].name);
982
949
  }
983
950
 
951
+ if (subFields.size === 0) continue;
984
952
  const subSelection = [...subFields].join(' ');
985
953
  fieldSelections.push(`${name} { ${subSelection} }`);
986
954
  columns.push(name);
@@ -996,13 +964,13 @@ export default class SimfinityClient {
996
964
  }
997
965
  }
998
966
 
999
- const hasId = typeInfo.fields.some(f => f.name === 'id');
967
+ const hasId = typeInfo.fields.some(f => f.name === 'id' && canAutoSelect(f));
1000
968
  if (hasId && !fieldSelections.includes('id')) {
1001
969
  fieldSelections.unshift('id');
1002
970
  }
1003
971
 
1004
972
  return {
1005
- selection: fieldSelections.join('\n'),
973
+ selection: fieldSelections.join('\n') || '__typename',
1006
974
  columns,
1007
975
  sortFieldByColumn,
1008
976
  fieldTypeByColumn,
@@ -1036,7 +1004,7 @@ export default class SimfinityClient {
1036
1004
  if (objFieldNames.includes('name')) return 'name';
1037
1005
 
1038
1006
  const firstScalar = objectType.fields.find(f =>
1039
- isScalarOrEnum(f.type.kind) && !f.type.isList
1007
+ isScalarOrEnum(f.type.kind) && !f.type.isList,
1040
1008
  );
1041
1009
  return firstScalar?.name || null;
1042
1010
  }
@@ -1149,8 +1117,12 @@ export default class SimfinityClient {
1149
1117
  // -- Mutation Input Transformation -----------------------------------------
1150
1118
 
1151
1119
  transformInput(typeName, rawInput, options = {}) {
1120
+ this._ensureInitialized(); return transformRootInput(this, typeName, rawInput, options);
1121
+ }
1122
+
1123
+ _legacyTransformInput(typeName, rawInput, options = {}) {
1152
1124
  this._ensureInitialized();
1153
- const { skipFields = [], transientFields = [], mode = 'create' } = options;
1125
+ const { skipFields = [], transientFields = [] } = options;
1154
1126
  const typeInfo = this._types.get(typeName);
1155
1127
  if (!typeInfo) return { ...rawInput };
1156
1128
 
@@ -1184,7 +1156,7 @@ export default class SimfinityClient {
1184
1156
  return this._deepRemoveTypename(transformed);
1185
1157
  }
1186
1158
 
1187
- _transformEmbeddedInput(objectTypeName, value, options = {}) {
1159
+ _transformEmbeddedInput(objectTypeName, value) {
1188
1160
  if (!value || typeof value !== 'object') return value;
1189
1161
  const typeInfo = this._types.get(objectTypeName);
1190
1162
  if (!typeInfo?.fields) return value;
@@ -1243,93 +1215,7 @@ export default class SimfinityClient {
1243
1215
  // -- Collection Delta Mutations --------------------------------------------
1244
1216
 
1245
1217
  transformCollectionDelta(collectionTypeName, delta, options = {}) {
1246
- this._ensureInitialized();
1247
- const { connectionField = null } = options;
1248
- const result = {};
1249
-
1250
- if (delta.added && delta.added.length > 0) {
1251
- result.added = delta.added.map(item => {
1252
- let clean = { ...item };
1253
-
1254
- delete clean.__status;
1255
- delete clean.__originalData;
1256
-
1257
- if (connectionField && clean[connectionField] !== undefined) {
1258
- delete clean[connectionField];
1259
- }
1260
-
1261
- if (clean.id && typeof clean.id === 'string' && clean.id.startsWith('temp_')) {
1262
- delete clean.id;
1263
- }
1264
-
1265
- clean = this._cleanCollectionItem(collectionTypeName, clean);
1266
- clean = this._deepRemoveTypename(clean);
1267
- return clean;
1268
- });
1269
- }
1270
-
1271
- if (delta.updated && delta.updated.length > 0) {
1272
- result.updated = delta.updated.map(item => {
1273
- let clean = { ...item };
1274
-
1275
- delete clean.__status;
1276
- delete clean.__originalData;
1277
-
1278
- if (connectionField && clean[connectionField] !== undefined) {
1279
- delete clean[connectionField];
1280
- }
1281
-
1282
- clean = this._cleanCollectionItem(collectionTypeName, clean);
1283
- clean = this._deepRemoveTypename(clean);
1284
- return clean;
1285
- });
1286
- }
1287
-
1288
- if (delta.deleted && delta.deleted.length > 0) {
1289
- result.deleted = delta.deleted.map(item => {
1290
- if (typeof item === 'string') return item;
1291
- if (typeof item === 'object' && item.id) return item.id;
1292
- return item;
1293
- });
1294
- }
1295
-
1296
- return result;
1297
- }
1298
-
1299
- _cleanCollectionItem(collectionTypeName, item) {
1300
- const typeInfo = this._types.get(collectionTypeName);
1301
- if (!typeInfo?.fields) return item;
1302
-
1303
- const cleaned = { ...item };
1304
-
1305
- for (const field of typeInfo.fields) {
1306
- const { name, type: unwrapped, rawType, extensions } = field;
1307
- if (cleaned[name] === undefined) continue;
1308
-
1309
- if (extensions?.stateMachine === true) {
1310
- delete cleaned[name];
1311
- continue;
1312
- }
1313
-
1314
- if (unwrapped.kind === 'OBJECT' && !extensions?.relation?.embedded) {
1315
- const val = cleaned[name];
1316
- if (val && typeof val === 'object' && 'id' in val) {
1317
- cleaned[name] = { id: val.id };
1318
- }
1319
- continue;
1320
- }
1321
-
1322
- if (isNumericScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1323
- cleaned[name] = Number(cleaned[name]);
1324
- }
1325
- if (isDateTimeScalar(unwrapped.name) && typeof cleaned[name] === 'string') {
1326
- if (/^\d{4}-\d{2}-\d{2}$/.test(cleaned[name])) {
1327
- cleaned[name] = `${cleaned[name]}T00:00:00.000Z`;
1328
- }
1329
- }
1330
- }
1331
-
1332
- return cleaned;
1218
+ this._ensureInitialized(); return transformDelta(this, collectionTypeName, delta, options);
1333
1219
  }
1334
1220
 
1335
1221
  // -- Guard ---------------------------------------------------------------
@@ -1339,4 +1225,12 @@ export default class SimfinityClient {
1339
1225
  throw new Error('Client not initialized. Call init() first.');
1340
1226
  }
1341
1227
  }
1228
+
1229
+ // -- Static Helpers -------------------------------------------------------
1230
+
1231
+ static condition(field, operator, value, path) {
1232
+ const cond = { field, operator, value };
1233
+ if (path !== undefined) cond.path = path;
1234
+ return { conditions: [cond] };
1235
+ }
1342
1236
  }