@simtlix/simfinity-js-client 1.0.0 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @simtlix/simfinity-js-client
2
2
 
3
- Introspection-driven GraphQL client for [simfinity-js](https://github.com/simtlix/simfinity.js) APIs. It discovers your schema at runtime via introspection and exposes a chainable query builder, CRUD helpers, aggregate queries, and state-machine transitions — no code generation required.
3
+ Introspection-driven GraphQL client for [simfinity-js](https://github.com/simtlix/simfinity.js) APIs. It discovers your schema at runtime via introspection and exposes a chainable query builder, CRUD helpers, aggregate queries, state-machine transitions, schema metadata access, mutation input transformation, and more — no code generation required.
4
4
 
5
5
  ## Installation
6
6
 
@@ -28,7 +28,7 @@ Creates a new client instance pointing at the given GraphQL endpoint.
28
28
 
29
29
  ### `client.init()`
30
30
 
31
- Runs an introspection query against the endpoint and builds internal registries of types, queries, and mutations. **Must be called before any other method.**
31
+ Runs an introspection query against the endpoint and builds internal registries of types, queries, and mutations — including Simfinity-specific field extensions (relation metadata, state machine flags, read-only markers). **Must be called before any other method.**
32
32
 
33
33
  ### Schema Inspection
34
34
 
@@ -58,7 +58,7 @@ const results = await client.find('serie')
58
58
 
59
59
  #### `.where(field, operator, value [, value2])`
60
60
 
61
- Filter by a scalar field. Supported operators depend on the server schema (e.g. `EQ`, `NE`, `GT`, `LT`, `GTE`, `LTE`, `BETWEEN`, `LIKE`).
61
+ Filter by a scalar field. Supported operators depend on the server schema (e.g. `EQ`, `NE`, `GT`, `LT`, `GTE`, `LTE`, `BETWEEN`, `LIKE`, `NIN`).
62
62
 
63
63
  #### `.where(field, terms)`
64
64
 
@@ -82,14 +82,42 @@ Include a related collection's fields. Supports dot-notation for nested joins (e
82
82
 
83
83
  Space-separated list of scalar fields to select on the root type. If omitted, all scalar and enum fields are selected automatically.
84
84
 
85
+ #### `.autoSelect()`
86
+
87
+ Automatically builds a full selection set using schema metadata, including nested object fields with display field resolution. Returns the builder for chaining. The selection metadata is available via `builder._selectionMeta`.
88
+
89
+ ```js
90
+ const results = await client.find('serie')
91
+ .autoSelect()
92
+ .page(1, 10)
93
+ .exec();
94
+ ```
95
+
85
96
  #### `.page(page, size [, count])`
86
97
 
87
- Paginate results. `page` is 1-based.
98
+ Paginate results. `page` is 1-based. Set `count` to `true` to include total count in response extensions.
88
99
 
89
100
  #### `.sort(field, order)`
90
101
 
91
102
  Sort results. `order` is `'ASC'` or `'DESC'`. Can be called multiple times for multi-field sorting.
92
103
 
104
+ #### `.exec()`
105
+
106
+ Executes the query and returns the result array. If the server returns extensions (e.g. pagination count), they are attached as `result.__extensions`.
107
+
108
+ #### `.execWithMeta()`
109
+
110
+ Executes the query and returns a `{ data, extensions }` wrapper for clean access to response metadata:
111
+
112
+ ```js
113
+ const { data: series, extensions } = await client.find('serie')
114
+ .page(1, 10, true)
115
+ .execWithMeta();
116
+
117
+ console.log(series); // the result array
118
+ console.log(extensions?.count); // total count from server
119
+ ```
120
+
93
121
  ---
94
122
 
95
123
  ### Aggregate Builder — `client.aggregate(typeName)`
@@ -130,6 +158,44 @@ If `fields` is omitted, all scalar fields are returned.
130
158
 
131
159
  ---
132
160
 
161
+ ### Collection Item Queries — `client.findByParent()`
162
+
163
+ Query collection items filtered by their parent entity:
164
+
165
+ ```js
166
+ const seasons = await client.findByParent('season', 'serie', parentSerieId)
167
+ .page(1, 10, true)
168
+ .fields('id number year state')
169
+ .exec();
170
+ ```
171
+
172
+ Returns a `QueryBuilder` pre-configured with a connection field filter and default sort by `id ASC`. Chain additional `.where()`, `.page()`, `.fields()`, `.sort()` as needed.
173
+
174
+ ---
175
+
176
+ ### FK/Relation Search — `client.search()`
177
+
178
+ Search-as-you-type for FK reference fields:
179
+
180
+ ```js
181
+ const results = await client.search('Genre', 'com', {
182
+ displayField: 'name',
183
+ page: 1,
184
+ size: 10,
185
+ });
186
+ // [{ id: '1', name: 'Comedy' }, { id: '2', name: 'Romantic Comedy' }]
187
+ ```
188
+
189
+ Automatically selects the appropriate operator (`LIKE` for strings, `EQ` for numeric/boolean) and casts the search term to the field's scalar type.
190
+
191
+ | Option | Default | Description |
192
+ |---|---|---|
193
+ | `displayField` | auto-resolved (`name` or first scalar) | Field to search by |
194
+ | `page` | `1` | Page number |
195
+ | `size` | `10` | Page size |
196
+
197
+ ---
198
+
133
199
  ### CRUD Mutations
134
200
 
135
201
  ```js
@@ -140,6 +206,56 @@ const updated = await client.update('star', createdId, { name: 'Jane Doe' }, 'id
140
206
  const deleted = await client.delete('star', createdId, 'id name');
141
207
  ```
142
208
 
209
+ #### Automatic Input Transformation
210
+
211
+ Pass `{ transform: true }` as the last argument to have the client automatically clean mutation input based on schema metadata — stripping `__typename`, coercing scalar types, extracting FK IDs, handling embedded objects, and skipping read-only/state-machine fields:
212
+
213
+ ```js
214
+ const created = await client.add('serie', rawFormData, 'id name', { transform: true });
215
+
216
+ const updated = await client.update('serie', id, rawFormData, 'id name', {
217
+ transform: true,
218
+ skipFields: ['temporaryField'],
219
+ });
220
+ ```
221
+
222
+ ---
223
+
224
+ ### Mutation Input Transformation — `client.transformInput()`
225
+
226
+ Schema-aware mutation input cleaning, usable standalone:
227
+
228
+ ```js
229
+ const cleaned = client.transformInput('serie', rawInput, {
230
+ mode: 'create', // or 'update'
231
+ skipFields: [], // fields to exclude
232
+ transientFields: [], // transient fields to exclude
233
+ });
234
+ ```
235
+
236
+ Handles: `__typename` removal, numeric/date coercion, embedded object cleaning, FK reference extraction (`{ id }` only), and skipping read-only, state-machine, and collection fields.
237
+
238
+ ---
239
+
240
+ ### Collection Delta Mutations — `client.transformCollectionDelta()`
241
+
242
+ Build the `{ added, updated, deleted }` structure for collection field mutations:
243
+
244
+ ```js
245
+ const delta = {
246
+ added: [{ id: 'temp_1', number: '3', year: '2025', __status: 'added' }],
247
+ updated: [{ id: 'real-id', number: '2', year: '2024', __status: 'modified' }],
248
+ deleted: [{ id: 'del-id' }],
249
+ };
250
+
251
+ const cleaned = client.transformCollectionDelta('season', delta, {
252
+ connectionField: 'serie',
253
+ });
254
+ // { added: [{ number: 3, year: 2025 }], updated: [{ id: 'real-id', number: 2, year: 2024 }], deleted: ['del-id'] }
255
+ ```
256
+
257
+ Strips metadata (`__status`, `__originalData`, `__typename`), removes temporary IDs and connection fields, coerces scalar types, and extracts FK object IDs.
258
+
143
259
  ---
144
260
 
145
261
  ### State Machine Transitions
@@ -175,9 +291,83 @@ console.log(response.data);
175
291
 
176
292
  ---
177
293
 
294
+ ### Selection Set Building — `client.buildSelectionSet()`
295
+
296
+ Generate a complete GraphQL selection set string with metadata from the schema, useful for building dynamic UIs:
297
+
298
+ ```js
299
+ const { selection, columns, sortFieldByColumn, fieldTypeByColumn } =
300
+ client.buildSelectionSet('serie');
301
+ ```
302
+
303
+ Returns:
304
+
305
+ | Property | Type | Description |
306
+ |---|---|---|
307
+ | `selection` | `string` | GraphQL selection set string (includes nested object sub-selections) |
308
+ | `columns` | `string[]` | Display column names (excludes `id`) |
309
+ | `sortFieldByColumn` | `object` | Maps column name to sort field path (e.g. `director` -> `director.name`) |
310
+ | `fieldTypeByColumn` | `object` | Maps column name to scalar type name |
311
+
312
+ ---
313
+
314
+ ### Schema Metadata Access
315
+
316
+ #### Field Extensions
317
+
318
+ ```js
319
+ const ext = client.getFieldExtensions('serie', 'director');
320
+ // { relation: { displayField: 'name', embedded: true, connectionField: null }, stateMachine: null, readOnly: null }
321
+ ```
322
+
323
+ | Method | Signature | Description |
324
+ |---|---|---|
325
+ | `getFieldExtensions` | `(typeName, fieldName)` | Full extensions object for a field |
326
+ | `getDisplayField` | `(typeName, fieldName)` | Display field with fallback chain (extension -> `name` -> first scalar) |
327
+ | `isEmbeddedField` | `(typeName, fieldName)` | Whether the field is an embedded object |
328
+ | `getConnectionField` | `(typeName, fieldName)` | Back-reference field name for collections |
329
+ | `isStateMachineField` | `(typeName, fieldName)` | Whether the field is state-machine managed |
330
+ | `isReadOnlyField` | `(typeName, fieldName)` | Whether the field is read-only |
331
+ | `getEnumValues` | `(typeName)` | Enum values for an enum type |
332
+ | `getFieldsOfType` | `(typeName)` | All fields with full metadata |
333
+ | `getDescriptionFieldType` | `(typeName, fieldName)` | Scalar type name of a display field |
334
+
335
+ #### Entity & Query Name Resolution
336
+
337
+ ```js
338
+ client.getTypeNameForQuery('series'); // 'serie'
339
+ client.getPluralQueryName('serie'); // 'series'
340
+ client.getSingularQueryName('serie'); // 'serie'
341
+ client.getListEntityNames(); // ['episodes', 'seasons', 'series', 'stars', ...]
342
+ client.getListEntityNamesOfType('serie'); // ['series']
343
+ client.getQueryNamesForType('serie');
344
+ // { pluralQueryName: 'series', singularQueryName: 'serie', aggregateQueryName: 'series_aggregate' }
345
+ ```
346
+
347
+ #### Scalar Type Utilities
348
+
349
+ Simfinity uses validated scalar names like `SeasonNumber_Int` where the suffix after `_` indicates the base type:
350
+
351
+ ```js
352
+ client.getActualScalarType('SeasonNumber_Int'); // 'Int'
353
+ client.isNumericScalar('SeasonNumber_Int'); // true
354
+ client.isBooleanScalar('Boolean'); // true
355
+ client.isDateTimeScalar('StartDate_DateTime'); // true
356
+ ```
357
+
358
+ #### State Machine Metadata
359
+
360
+ ```js
361
+ client.getStateMachineFields('season'); // ['state']
362
+ client.getAvailableTransitions('season');
363
+ // [{ action: 'activate', mutationName: 'activate_season' }, { action: 'finalize', mutationName: 'finalize_season' }]
364
+ ```
365
+
366
+ ---
367
+
178
368
  ## Examples
179
369
 
180
- See [`examples/demo.js`](examples/demo.js) for a comprehensive walkthrough covering all features: schema discovery, filtering, joins, pagination, sorting, CRUD, state machine transitions, aggregates, and raw queries.
370
+ See [`examples/demo.js`](examples/demo.js) for a comprehensive walkthrough covering all features: schema discovery, filtering, joins, pagination, sorting, CRUD, state machine transitions, aggregates, raw queries, schema metadata, selection set building, entity resolution, input transformation, collection deltas, parent queries, FK search, and response extensions.
181
371
 
182
372
  ```bash
183
373
  GRAPHQL_ENDPOINT=http://localhost:3000/graphql npm run demo
package/examples/demo.js CHANGED
@@ -198,6 +198,152 @@ const log = (label, data) => {
198
198
  .exec();
199
199
  log('17. aggregate("episode") -- count episodes per season', aggFiltered);
200
200
 
201
+ // -----------------------------------------------------------------------
202
+ // 18. Scalar type utilities (Feature 2)
203
+ // -----------------------------------------------------------------------
204
+ log('18. Scalar type utilities', {
205
+ 'getActualScalarType("SeasonNumber_Int")': client.getActualScalarType('SeasonNumber_Int'),
206
+ 'getActualScalarType("StartDate_Date")': client.getActualScalarType('StartDate_Date'),
207
+ 'isNumericScalar("SeasonNumber_Int")': client.isNumericScalar('SeasonNumber_Int'),
208
+ 'isNumericScalar("String")': client.isNumericScalar('String'),
209
+ 'isBooleanScalar("Boolean")': client.isBooleanScalar('Boolean'),
210
+ 'isDateTimeScalar("EpisodeDate_DateTime")': client.isDateTimeScalar('EpisodeDate_DateTime'),
211
+ 'isDateTimeScalar("Int")': client.isDateTimeScalar('Int'),
212
+ });
213
+
214
+ // -----------------------------------------------------------------------
215
+ // 19. Schema metadata access (Feature 3)
216
+ // -----------------------------------------------------------------------
217
+ const serieType = client.getTypes()['serie'] ? 'serie' : 'Serie';
218
+ const serieFields = client.getFieldsOfType(serieType);
219
+ log('19. Schema metadata -- getFieldsOfType(' + serieType + ')', serieFields.map(f => ({
220
+ name: f.name,
221
+ kind: f.type.kind,
222
+ typeName: f.type.name,
223
+ extensions: f.extensions,
224
+ })));
225
+
226
+ const objectFields = serieFields.filter(f => f.type.kind === 'OBJECT');
227
+ if (objectFields.length > 0) {
228
+ const objField = objectFields[0];
229
+ log('19b. Schema metadata -- extensions for ' + serieType + '.' + objField.name, {
230
+ extensions: client.getFieldExtensions(serieType, objField.name),
231
+ displayField: client.getDisplayField(serieType, objField.name),
232
+ isEmbedded: client.isEmbeddedField(serieType, objField.name),
233
+ connectionField: client.getConnectionField(serieType, objField.name),
234
+ isStateMachine: client.isStateMachineField(serieType, objField.name),
235
+ isReadOnly: client.isReadOnlyField(serieType, objField.name),
236
+ });
237
+ }
238
+
239
+ // -----------------------------------------------------------------------
240
+ // 20. buildSelectionSet / autoSelect (Feature 4)
241
+ // -----------------------------------------------------------------------
242
+ const selSet = client.buildSelectionSet(serieType);
243
+ log('20. buildSelectionSet(' + serieType + ')', selSet);
244
+
245
+ const autoSelectResult = await client.find(serieType)
246
+ .autoSelect()
247
+ .page(1, 2)
248
+ .exec();
249
+ log('20b. find().autoSelect().page(1,2).exec()', autoSelectResult);
250
+
251
+ // -----------------------------------------------------------------------
252
+ // 21. Entity & query name resolution (Feature 5)
253
+ // -----------------------------------------------------------------------
254
+ log('21. Entity & query name resolution', {
255
+ listEntityNames: client.getListEntityNames(),
256
+ 'getQueryNamesForType(serieType)': client.getQueryNamesForType(serieType),
257
+ 'getPluralQueryName(serieType)': client.getPluralQueryName(serieType),
258
+ 'getSingularQueryName(serieType)': client.getSingularQueryName(serieType),
259
+ });
260
+
261
+ const pluralName = client.getPluralQueryName(serieType);
262
+ if (pluralName) {
263
+ log('21b. getTypeNameForQuery("' + pluralName + '")',
264
+ client.getTypeNameForQuery(pluralName));
265
+ }
266
+
267
+ // -----------------------------------------------------------------------
268
+ // 22. transformInput (Feature 6)
269
+ // -----------------------------------------------------------------------
270
+ const rawInput = {
271
+ name: 'Test Serie',
272
+ categories: 'Drama',
273
+ year: '2025',
274
+ __typename: 'Serie',
275
+ director: { id: 'dir-123', name: 'Some Director', __typename: 'Director' },
276
+ };
277
+ const transformed = client.transformInput(serieType, rawInput, { mode: 'create' });
278
+ log('22. transformInput -- raw vs transformed', { rawInput, transformed });
279
+
280
+ // -----------------------------------------------------------------------
281
+ // 23. transformCollectionDelta (Feature 7)
282
+ // -----------------------------------------------------------------------
283
+ const seasonType = client.getTypes()['season'] ? 'season' : 'Season';
284
+ const delta = {
285
+ added: [
286
+ { id: 'temp_1', number: '3', year: '2025', __typename: 'Season', __status: 'added' },
287
+ ],
288
+ updated: [
289
+ { id: 'real-id-1', number: '2', year: '2024', __status: 'modified' },
290
+ ],
291
+ deleted: [
292
+ { id: 'del-id-1' },
293
+ ],
294
+ };
295
+ const connectionField = client.getConnectionField(serieType, 'seasons');
296
+ const transformedDelta = client.transformCollectionDelta(seasonType, delta, {
297
+ connectionField: connectionField || 'serie',
298
+ });
299
+ log('23. transformCollectionDelta', { delta, transformedDelta });
300
+
301
+ // -----------------------------------------------------------------------
302
+ // 24. findByParent (Feature 8)
303
+ // -----------------------------------------------------------------------
304
+ if (allSeries.length > 0) {
305
+ const parentId = allSeries[0].id;
306
+ const childSeasons = await client
307
+ .findByParent(seasonType, 'serie', parentId)
308
+ .page(1, 5, true)
309
+ .fields('id number year')
310
+ .exec();
311
+ log('24. findByParent(' + seasonType + ', "serie", "' + parentId + '")', childSeasons);
312
+ } else {
313
+ log('24. findByParent -- skipped (no series)', '');
314
+ }
315
+
316
+ // -----------------------------------------------------------------------
317
+ // 25. search (Feature 9)
318
+ // -----------------------------------------------------------------------
319
+ const searchResults = await client.search('star', 'a', {
320
+ displayField: 'name',
321
+ page: 1,
322
+ size: 5,
323
+ });
324
+ log('25. search("star", "a", { displayField: "name" })', searchResults);
325
+
326
+ // -----------------------------------------------------------------------
327
+ // 26. State machine metadata (Feature 10)
328
+ // -----------------------------------------------------------------------
329
+ log('26. State machine metadata', {
330
+ 'getStateMachineFields(seasonType)': client.getStateMachineFields(seasonType),
331
+ 'getAvailableTransitions(seasonType)': client.getAvailableTransitions(seasonType),
332
+ });
333
+
334
+ // -----------------------------------------------------------------------
335
+ // 27. execWithMeta -- pagination count (Feature 11)
336
+ // -----------------------------------------------------------------------
337
+ const { data: pagedSeries, extensions } = await client
338
+ .find(serieType)
339
+ .page(1, 2, true)
340
+ .execWithMeta();
341
+ log('27. execWithMeta -- pagination count', {
342
+ data: pagedSeries,
343
+ extensions,
344
+ totalCount: extensions?.count,
345
+ });
346
+
201
347
  console.log('\n' + '='.repeat(60));
202
348
  console.log('Demo complete.');
203
349
  console.log('='.repeat(60));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js-client",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Introspection-driven GraphQL client for simfinity-js APIs",
5
5
  "type": "module",
6
6
  "main": "src/SimfinityClient.js",
@@ -23,7 +23,7 @@
23
23
  "type": "git",
24
24
  "url": "https://github.com/simtlix/simfinity-js-client"
25
25
  },
26
- "dependencies": {
27
- "node-fetch": "^3.3.2"
26
+ "engines": {
27
+ "node": ">=18.0.0"
28
28
  }
29
29
  }