@simtlix/simfinity-js-client 1.0.2 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -16,16 +16,29 @@ import SimfinityClient from '@simtlix/simfinity-js-client';
16
16
  const client = new SimfinityClient('http://localhost:3000/graphql');
17
17
  await client.init();
18
18
 
19
+ // Optional: attach auth headers (perform login in your app; this client does not include login)
20
+ const tokenStore = { accessToken: null };
21
+ const authedClient = new SimfinityClient('http://localhost:3000/graphql', {
22
+ prepareHeaders(headers) {
23
+ if (tokenStore.accessToken) {
24
+ headers['Authorization'] = `Bearer ${tokenStore.accessToken}`;
25
+ }
26
+ },
27
+ });
28
+ await authedClient.init();
29
+
19
30
  const series = await client.find('serie').exec();
20
31
  console.log(series);
21
32
  ```
22
33
 
23
34
  ## API Reference
24
35
 
25
- ### `new SimfinityClient(endpoint)`
36
+ ### `new SimfinityClient(endpoint [, options])`
26
37
 
27
38
  Creates a new client instance pointing at the given GraphQL endpoint.
28
39
 
40
+ - **`options.prepareHeaders(headers)`** (optional): invoked before every GraphQL HTTP request. Mutate the plain `headers` object to add credentials (for example `Authorization`). The client does not implement login — call your auth API separately, then set tokens in a closure or store that `prepareHeaders` reads.
41
+
29
42
  ### `client.init()`
30
43
 
31
44
  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.**
@@ -70,6 +83,106 @@ Filter by a collection field using an array of term objects:
70
83
  ])
71
84
  ```
72
85
 
86
+ #### `.or(groups)` / `.and(groups)`
87
+
88
+ Add AND/OR logical filter groups for complex boolean conditions. Each method accepts an array of `QLFilterGroup` objects and can be called multiple times (groups accumulate). These combine with existing flat `.where()` filters via AND at the top level.
89
+
90
+ A `QLFilterGroup` is a plain object with optional `AND`, `OR`, and `conditions` fields:
91
+
92
+ ```js
93
+ {
94
+ conditions: [{ field: 'category', operator: 'EQ', value: 'Drama' }],
95
+ AND: [/* nested QLFilterGroup objects */],
96
+ OR: [/* nested QLFilterGroup objects */],
97
+ }
98
+ ```
99
+
100
+ **Simple OR** -- return series in either Drama or Comedy:
101
+
102
+ ```js
103
+ const results = await client.find('serie')
104
+ .or([
105
+ SimfinityClient.condition('categories', 'EQ', 'Drama'),
106
+ SimfinityClient.condition('categories', 'EQ', 'Comedy'),
107
+ ])
108
+ .fields('id name categories')
109
+ .exec();
110
+ ```
111
+
112
+ **Flat filters combined with OR** -- flat `.where()` is ANDed with the OR group:
113
+
114
+ ```js
115
+ const results = await client.find('serie')
116
+ .where('categories', 'NE', 'Horror')
117
+ .or([
118
+ SimfinityClient.condition('name', 'LIKE', 'Breaking'),
119
+ SimfinityClient.condition('name', 'LIKE', 'Game'),
120
+ ])
121
+ .fields('id name categories')
122
+ .exec();
123
+ // Equivalent to: categories != "Horror" AND (name LIKE "Breaking" OR name LIKE "Game")
124
+ ```
125
+
126
+ **Nested AND inside OR**:
127
+
128
+ ```js
129
+ const results = await client.find('serie')
130
+ .or([
131
+ { AND: [
132
+ SimfinityClient.condition('categories', 'EQ', 'Drama'),
133
+ SimfinityClient.condition('year', 'GTE', 2020),
134
+ ]},
135
+ { AND: [
136
+ SimfinityClient.condition('categories', 'EQ', 'Comedy'),
137
+ SimfinityClient.condition('year', 'GTE', 2015),
138
+ ]},
139
+ ])
140
+ .exec();
141
+ // Equivalent to: (categories = "Drama" AND year >= 2020) OR (categories = "Comedy" AND year >= 2015)
142
+ ```
143
+
144
+ **AND with nested OR groups**:
145
+
146
+ ```js
147
+ const results = await client.find('serie')
148
+ .where('year', 'GTE', 2000)
149
+ .and([
150
+ { OR: [
151
+ SimfinityClient.condition('categories', 'EQ', 'Drama'),
152
+ SimfinityClient.condition('categories', 'EQ', 'Comedy'),
153
+ ]},
154
+ { OR: [
155
+ SimfinityClient.condition('director', 'EQ', 'UK', 'country'),
156
+ SimfinityClient.condition('director', 'EQ', 'US', 'country'),
157
+ ]},
158
+ ])
159
+ .exec();
160
+ // Equivalent to: year >= 2000 AND (Drama OR Comedy) AND (UK OR US director)
161
+ ```
162
+
163
+ **Relationship path filtering** -- use the `path` parameter for object field conditions:
164
+
165
+ ```js
166
+ .or([
167
+ SimfinityClient.condition('director', 'LIKE', 'Adams', 'name'),
168
+ SimfinityClient.condition('director', 'LIKE', 'Nolan', 'name'),
169
+ ])
170
+ ```
171
+
172
+ The Aggregate Builder also supports `.or()` and `.and()` with identical behavior. The server enforces a maximum nesting depth of 5 levels.
173
+
174
+ #### `SimfinityClient.condition(field, operator, value [, path])`
175
+
176
+ Static convenience factory for creating a filter group with a single condition. Returns a `QLFilterGroup` object:
177
+
178
+ ```js
179
+ SimfinityClient.condition('category', 'EQ', 'Drama')
180
+ // Equivalent to: { conditions: [{ field: 'category', operator: 'EQ', value: 'Drama' }] }
181
+
182
+ SimfinityClient.condition('author', 'LIKE', 'Adams', 'name')
183
+ // Equivalent to: { conditions: [{ field: 'author', operator: 'LIKE', value: 'Adams', path: 'name' }] }
184
+ ```
185
+
73
186
  #### `.joinObject(path, fields)`
74
187
 
75
188
  Include a related object's fields in the selection.
package/examples/demo.js CHANGED
@@ -344,6 +344,58 @@ const log = (label, data) => {
344
344
  totalCount: extensions?.count,
345
345
  });
346
346
 
347
+ // -----------------------------------------------------------------------
348
+ // 28. find() with OR filter
349
+ // -----------------------------------------------------------------------
350
+ const orResults = await client.find(serieType)
351
+ .or([
352
+ SimfinityClient.condition('categories', 'EQ', 'Drama'),
353
+ SimfinityClient.condition('categories', 'EQ', 'Comedy'),
354
+ ])
355
+ .fields('id name categories')
356
+ .exec();
357
+ log('28. find() -- OR filter (Drama OR Comedy)', orResults);
358
+
359
+ // -----------------------------------------------------------------------
360
+ // 29. find() with flat where + OR combined
361
+ // -----------------------------------------------------------------------
362
+ const combinedOrResults = await client.find(serieType)
363
+ .where('categories', 'NE', 'Horror')
364
+ .or([
365
+ SimfinityClient.condition('name', 'LIKE', 'Breaking'),
366
+ SimfinityClient.condition('name', 'LIKE', 'Game'),
367
+ ])
368
+ .fields('id name categories')
369
+ .exec();
370
+ log('29. find() -- flat where + OR (not Horror AND (name LIKE Breaking OR Game))', combinedOrResults);
371
+
372
+ // -----------------------------------------------------------------------
373
+ // 30. find() with AND containing nested OR
374
+ // -----------------------------------------------------------------------
375
+ const nestedAndOrResults = await client.find(serieType)
376
+ .and([
377
+ { OR: [
378
+ SimfinityClient.condition('categories', 'EQ', 'Drama'),
379
+ SimfinityClient.condition('categories', 'EQ', 'Comedy'),
380
+ ]},
381
+ ])
382
+ .fields('id name categories')
383
+ .exec();
384
+ log('30. find() -- AND with nested OR groups', nestedAndOrResults);
385
+
386
+ // -----------------------------------------------------------------------
387
+ // 31. aggregate() with OR filter
388
+ // -----------------------------------------------------------------------
389
+ const aggOrResults = await client.aggregate(seasonType)
390
+ .groupBy('serie')
391
+ .fact('COUNT', 'seasonCount', 'id')
392
+ .or([
393
+ SimfinityClient.condition('year', 'GTE', 2020),
394
+ SimfinityClient.condition('year', 'LTE', 2010),
395
+ ])
396
+ .exec();
397
+ log('31. aggregate() -- OR filter (year >= 2020 OR year <= 2010)', aggOrResults);
398
+
347
399
  console.log('\n' + '='.repeat(60));
348
400
  console.log('Demo complete.');
349
401
  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.2",
3
+ "version": "1.1.0",
4
4
  "description": "Introspection-driven GraphQL client for simfinity-js APIs",
5
5
  "type": "module",
6
6
  "main": "src/SimfinityClient.js",
@@ -227,6 +227,30 @@ class QueryBuilder {
227
227
  return this;
228
228
  }
229
229
 
230
+ or(groups) {
231
+ if (!Array.isArray(groups)) {
232
+ throw new Error('or() expects an array of QLFilterGroup objects');
233
+ }
234
+ if (this._variables.OR) {
235
+ this._variables.OR.push(...groups);
236
+ } else {
237
+ this._variables.OR = [...groups];
238
+ }
239
+ return this;
240
+ }
241
+
242
+ and(groups) {
243
+ if (!Array.isArray(groups)) {
244
+ throw new Error('and() expects an array of QLFilterGroup objects');
245
+ }
246
+ if (this._variables.AND) {
247
+ this._variables.AND.push(...groups);
248
+ } else {
249
+ this._variables.AND = [...groups];
250
+ }
251
+ return this;
252
+ }
253
+
230
254
  // -- Relation Field Selection --------------------------------------------
231
255
 
232
256
  joinCollection(path, fields, filter) {
@@ -418,6 +442,30 @@ class AggregateBuilder {
418
442
  return this;
419
443
  }
420
444
 
445
+ or(groups) {
446
+ if (!Array.isArray(groups)) {
447
+ throw new Error('or() expects an array of QLFilterGroup objects');
448
+ }
449
+ if (this._variables.OR) {
450
+ this._variables.OR.push(...groups);
451
+ } else {
452
+ this._variables.OR = [...groups];
453
+ }
454
+ return this;
455
+ }
456
+
457
+ and(groups) {
458
+ if (!Array.isArray(groups)) {
459
+ throw new Error('and() expects an array of QLFilterGroup objects');
460
+ }
461
+ if (this._variables.AND) {
462
+ this._variables.AND.push(...groups);
463
+ } else {
464
+ this._variables.AND = [...groups];
465
+ }
466
+ return this;
467
+ }
468
+
421
469
  page(page, size, count) {
422
470
  const pagination = { page, size };
423
471
  if (count !== undefined) pagination.count = count;
@@ -481,7 +529,14 @@ class AggregateBuilder {
481
529
  // ---------------------------------------------------------------------------
482
530
 
483
531
  export default class SimfinityClient {
484
- constructor(endpoint) {
532
+ /**
533
+ * @param {string} endpoint GraphQL HTTP endpoint URL
534
+ * @param {{ prepareHeaders?: (headers: Record<string, string>) => void }} [options]
535
+ * Optional `prepareHeaders` runs before each request; mutate `headers` to add auth
536
+ * (e.g. `headers['Authorization'] = 'Bearer …'`). Login is not part of this client —
537
+ * perform auth in your app and set tokens inside this callback or a closure it sees.
538
+ */
539
+ constructor(endpoint, options = {}) {
485
540
  this._endpoint = endpoint;
486
541
  this._types = new Map();
487
542
  this._queries = new Map();
@@ -491,6 +546,8 @@ export default class SimfinityClient {
491
546
  this._typeNameToAggregate = new Map();
492
547
  this._queryNameToType = new Map();
493
548
  this._initialized = false;
549
+ this._prepareHeaders =
550
+ typeof options.prepareHeaders === 'function' ? options.prepareHeaders : null;
494
551
  }
495
552
 
496
553
  // -- Initialization ------------------------------------------------------
@@ -664,9 +721,13 @@ export default class SimfinityClient {
664
721
  if (variables && Object.keys(variables).length > 0) {
665
722
  body.variables = variables;
666
723
  }
724
+ const headers = { 'Content-Type': 'application/json' };
725
+ if (this._prepareHeaders) {
726
+ this._prepareHeaders(headers);
727
+ }
667
728
  const response = await _fetch(this._endpoint, {
668
729
  method: 'POST',
669
- headers: { 'Content-Type': 'application/json' },
730
+ headers,
670
731
  body: JSON.stringify(body),
671
732
  });
672
733
  return response.json();
@@ -1326,4 +1387,12 @@ export default class SimfinityClient {
1326
1387
  throw new Error('Client not initialized. Call init() first.');
1327
1388
  }
1328
1389
  }
1390
+
1391
+ // -- Static Helpers -------------------------------------------------------
1392
+
1393
+ static condition(field, operator, value, path) {
1394
+ const cond = { field, operator, value };
1395
+ if (path !== undefined) cond.path = path;
1396
+ return { conditions: [cond] };
1397
+ }
1329
1398
  }