@simtlix/simfinity-js 2.4.4 → 2.5.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.
Files changed (27) hide show
  1. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +23 -0
  2. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +235 -0
  3. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +567 -0
  4. package/.claude/worktrees/agitated-kepler/LICENSE +201 -0
  5. package/.claude/worktrees/agitated-kepler/README.md +3941 -0
  6. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +71 -0
  7. package/.claude/worktrees/agitated-kepler/package-lock.json +4740 -0
  8. package/.claude/worktrees/agitated-kepler/package.json +41 -0
  9. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +44 -0
  10. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +273 -0
  11. package/.claude/worktrees/agitated-kepler/src/auth/index.js +391 -0
  12. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +274 -0
  13. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +39 -0
  14. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +28 -0
  15. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +39 -0
  16. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +11 -0
  17. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +15 -0
  18. package/.claude/worktrees/agitated-kepler/src/index.js +2412 -0
  19. package/.claude/worktrees/agitated-kepler/src/plugins.js +53 -0
  20. package/.claude/worktrees/agitated-kepler/src/scalars.js +188 -0
  21. package/.claude/worktrees/agitated-kepler/src/validators.js +250 -0
  22. package/.claude/worktrees/agitated-kepler/yarn.lock +1154 -0
  23. package/.cursor/rules/simfinity-core-functions.mdc +3 -1
  24. package/README.md +202 -0
  25. package/git-report.js +224 -0
  26. package/package.json +1 -1
  27. package/src/index.js +237 -23
@@ -55,11 +55,13 @@ Handles: scalars, enums, relation fields (`IdInputType` for refs, nested input f
55
55
  `buildQuery(args, gqltype)` translates GraphQL filter arguments into a MongoDB aggregation pipeline:
56
56
 
57
57
  - `buildQueryTerms()` -- converts scalar/enum filters to `$match`, object/relation filters to `$lookup` + `$unwind` + `$match`
58
+ - `buildFilterGroupMatch()` -- recursively processes `QLFilterGroup` (AND/OR) into nested MongoDB `$and`/`$or` match expressions. Reuses `buildQueryTerms()` for each leaf condition. Deduplicates `$lookup` stages via shared `aggregationsIncluded` dict. Max depth: 5.
58
59
  - `buildAggregationsForSort()` -- adds `$lookup`/`$unwind` for sort on related fields
59
60
  - Pagination: `$skip` + `$limit` from `QLPagination` input
60
61
  - Sort: `$sort` from `QLSortExpression` input
62
+ - AND/OR: optional `AND`/`OR` arguments containing `QLFilterGroup` trees. Flat field filters and scope-injected filters are always ANDed at the top level (cannot be bypassed by user OR).
61
63
 
62
- `buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX).
64
+ `buildAggregationQuery()` extends this with `$group` for aggregation operations (SUM, COUNT, AVG, MIN, MAX). Supports the same AND/OR filter pattern.
63
65
 
64
66
  ## Mutation Pipeline
65
67
 
package/README.md CHANGED
@@ -265,6 +265,208 @@ query {
265
265
  - `NIN` - Not in array
266
266
  - `BTW` - Between two values
267
267
 
268
+ ### Logical Filters (AND / OR)
269
+
270
+ By default, all field-level filters are combined with implicit AND logic. For complex conditions requiring OR logic or nested combinations, use the `AND` and `OR` query arguments.
271
+
272
+ #### Simple OR
273
+
274
+ Return books in either the Sci-Fi or Fantasy category:
275
+
276
+ ```graphql
277
+ query {
278
+ books(
279
+ OR: [
280
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
281
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
282
+ ]
283
+ ) {
284
+ id
285
+ title
286
+ category
287
+ }
288
+ }
289
+ ```
290
+
291
+ #### Flat Filters Combined with OR
292
+
293
+ Flat field filters are always ANDed at the top level, making them ideal for scope/security conditions that cannot be bypassed by user OR logic:
294
+
295
+ ```graphql
296
+ query {
297
+ books(
298
+ rating: { operator: GTE, value: 7.0 }
299
+ OR: [
300
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
301
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
302
+ ]
303
+ ) {
304
+ id
305
+ title
306
+ }
307
+ }
308
+ ```
309
+
310
+ This translates to: `rating >= 7.0 AND (category = "Sci-Fi" OR category = "Fantasy")`.
311
+
312
+ #### Nested AND inside OR
313
+
314
+ ```graphql
315
+ query {
316
+ books(
317
+ OR: [
318
+ { AND: [
319
+ { conditions: [{ field: "rating", operator: GTE, value: 9.0 }] }
320
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
321
+ ]}
322
+ { AND: [
323
+ { conditions: [{ field: "rating", operator: GTE, value: 8.0 }] }
324
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
325
+ ]}
326
+ ]
327
+ ) {
328
+ id
329
+ title
330
+ }
331
+ }
332
+ ```
333
+
334
+ This translates to: `(rating >= 9.0 AND category = "Sci-Fi") OR (rating >= 8.0 AND category = "Fantasy")`.
335
+
336
+ #### Filtering on Relationships within AND/OR
337
+
338
+ Use the `path` parameter to filter on related entity fields:
339
+
340
+ ```graphql
341
+ query {
342
+ books(
343
+ OR: [
344
+ { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Adams" }] }
345
+ { conditions: [{ field: "author", path: "name", operator: LIKE, value: "Pratchett" }] }
346
+ ]
347
+ ) {
348
+ id
349
+ title
350
+ author { name }
351
+ }
352
+ }
353
+ ```
354
+
355
+ #### Mixing Flat Filters with AND/OR
356
+
357
+ You can freely combine the existing flat filter syntax with AND/OR groups. Flat filters and AND groups are all ANDed together at the top level:
358
+
359
+ ```graphql
360
+ query {
361
+ books(
362
+ rating: { operator: GTE, value: 7.0 }
363
+ author: { terms: [{ path: "country", operator: EQ, value: "UK" }] }
364
+ OR: [
365
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
366
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
367
+ ]
368
+ ) {
369
+ id
370
+ title
371
+ rating
372
+ category
373
+ author { name country }
374
+ }
375
+ }
376
+ ```
377
+
378
+ This translates to: `rating >= 7.0 AND author.country = "UK" AND (category = "Sci-Fi" OR category = "Fantasy")`. The flat field filters (`rating`, `author`) use the existing syntax while the OR group uses the new `QLFilterGroup` syntax.
379
+
380
+ You can also combine flat filters with explicit AND groups for more complex logic:
381
+
382
+ ```graphql
383
+ query {
384
+ books(
385
+ rating: { operator: GTE, value: 5.0 }
386
+ AND: [
387
+ {
388
+ OR: [
389
+ { conditions: [{ field: "category", operator: EQ, value: "Sci-Fi" }] }
390
+ { conditions: [{ field: "category", operator: EQ, value: "Fantasy" }] }
391
+ ]
392
+ }
393
+ {
394
+ OR: [
395
+ { conditions: [{ field: "author", path: "country", operator: EQ, value: "UK" }] }
396
+ { conditions: [{ field: "author", path: "country", operator: EQ, value: "US" }] }
397
+ ]
398
+ }
399
+ ]
400
+ ) {
401
+ id
402
+ title
403
+ }
404
+ }
405
+ ```
406
+
407
+ This translates to: `rating >= 5.0 AND (category = "Sci-Fi" OR category = "Fantasy") AND (author.country = "UK" OR author.country = "US")`.
408
+
409
+ #### Collection Filtering with AND/OR
410
+
411
+ AND/OR filters are also available on collection fields (one-to-many relationships). The auto-generated resolvers for collection fields support the same `AND` and `OR` arguments:
412
+
413
+ ```graphql
414
+ query {
415
+ series {
416
+ seasons(
417
+ OR: [
418
+ { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
419
+ { conditions: [{ field: "year", operator: EQ, value: 2021 }] }
420
+ ]
421
+ ) {
422
+ number
423
+ year
424
+ }
425
+ }
426
+ }
427
+ ```
428
+
429
+ You can mix flat collection filters with AND/OR:
430
+
431
+ ```graphql
432
+ query {
433
+ series {
434
+ seasons(
435
+ number: { operator: GT, value: 1 }
436
+ OR: [
437
+ { conditions: [{ field: "year", operator: EQ, value: 2020 }] }
438
+ {
439
+ AND: [
440
+ { conditions: [{ field: "year", operator: GTE, value: 2022 }] }
441
+ { conditions: [
442
+ { field: "episodes", path: "name", operator: LIKE, value: "Final" }
443
+ ] }
444
+ ]
445
+ }
446
+ ]
447
+ ) {
448
+ number
449
+ year
450
+ episodes { name }
451
+ }
452
+ }
453
+ }
454
+ ```
455
+
456
+ This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.name LIKE "Final"))`.
457
+
458
+ #### Filter Types Reference
459
+
460
+ | Type | Fields | Description |
461
+ |------|--------|-------------|
462
+ | `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
463
+ | `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |
464
+
465
+ - `field` identifies the entity field by name (e.g., `"title"`, `"author"`)
466
+ - `path` is required for object/relationship fields (e.g., `"name"`, `"country.name"`)
467
+ - Multiple `conditions` in the same group are combined with AND
468
+ - Maximum nesting depth: 5 levels
469
+
268
470
  ### Collection Field Filtering
269
471
 
270
472
  Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
package/git-report.js ADDED
@@ -0,0 +1,224 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { execFileSync } from 'node:child_process';
4
+ import { writeFileSync } from 'node:fs';
5
+ import { resolve } from 'node:path';
6
+
7
+ const CHANGE_TYPE_LABELS = {
8
+ A: 'Added',
9
+ M: 'Modified',
10
+ D: 'Deleted',
11
+ R: 'Renamed',
12
+ C: 'Copied',
13
+ T: 'TypeChanged',
14
+ };
15
+
16
+ const COMMIT_DELIMITER = '---GIT_REPORT_COMMIT---';
17
+ const FIELD_DELIMITER = '---GIT_REPORT_FIELD---';
18
+
19
+ function printHelp() {
20
+ const help = `
21
+ Usage: node git-report.js --branch <branch> --author <author> --months <N> [--output <file.csv>]
22
+
23
+ Generate a CSV report of git commits for a given author on a branch within a time period.
24
+
25
+ Required arguments:
26
+ --branch <branch> Git branch name to analyze
27
+ --author <author> Git author name or email to filter by
28
+ --months <N> Number of months to look back from today
29
+
30
+ Optional arguments:
31
+ --output <file> Output CSV file path (default: git-report.csv)
32
+ --help, -h Show this help message and exit
33
+
34
+ Output:
35
+ A CSV file with one row per affected file per commit, containing:
36
+ commit_hash, author, date, message, change_type, file_path
37
+
38
+ Examples:
39
+ node git-report.js --branch master --author "John Doe" --months 3
40
+ node git-report.js --branch main --author john@example.com --months 6 --output report.csv
41
+ `;
42
+ console.log(help.trim());
43
+ }
44
+
45
+ function parseArgs(argv) {
46
+ const args = argv.slice(2);
47
+
48
+ if (args.includes('--help') || args.includes('-h')) {
49
+ printHelp();
50
+ process.exit(0);
51
+ }
52
+
53
+ const opts = {};
54
+ for (let i = 0; i < args.length; i++) {
55
+ switch (args[i]) {
56
+ case '--branch':
57
+ opts.branch = args[++i];
58
+ break;
59
+ case '--author':
60
+ opts.author = args[++i];
61
+ break;
62
+ case '--months':
63
+ opts.months = args[++i];
64
+ break;
65
+ case '--output':
66
+ opts.output = args[++i];
67
+ break;
68
+ default:
69
+ console.error(`Unknown argument: ${args[i]}`);
70
+ printHelp();
71
+ process.exit(1);
72
+ }
73
+ }
74
+
75
+ const missing = [];
76
+ if (!opts.branch) missing.push('--branch');
77
+ if (!opts.author) missing.push('--author');
78
+ if (!opts.months) missing.push('--months');
79
+
80
+ if (missing.length > 0) {
81
+ console.error(`Missing required arguments: ${missing.join(', ')}`);
82
+ printHelp();
83
+ process.exit(1);
84
+ }
85
+
86
+ const months = parseInt(opts.months, 10);
87
+ if (Number.isNaN(months) || months <= 0) {
88
+ console.error('--months must be a positive integer');
89
+ process.exit(1);
90
+ }
91
+ opts.months = months;
92
+ opts.output = opts.output || 'git-report.csv';
93
+
94
+ return opts;
95
+ }
96
+
97
+ function validateGitEnvironment() {
98
+ try {
99
+ execFileSync('git', ['--version'], { stdio: 'pipe' });
100
+ } catch {
101
+ console.error('Error: git is not installed or not found in PATH.');
102
+ process.exit(1);
103
+ }
104
+
105
+ try {
106
+ execFileSync('git', ['rev-parse', '--is-inside-work-tree'], { stdio: 'pipe' });
107
+ } catch {
108
+ console.error('Error: current directory is not inside a git repository.');
109
+ process.exit(1);
110
+ }
111
+ }
112
+
113
+ function computeSinceDate(months) {
114
+ const date = new Date();
115
+ date.setMonth(date.getMonth() - months);
116
+ return date.toISOString().slice(0, 10);
117
+ }
118
+
119
+ function escapeCsvField(value) {
120
+ const str = String(value).replace(/\r?\n/g, ' ');
121
+ if (str.includes(',') || str.includes('"') || str.includes('\n')) {
122
+ return `"${str.replace(/"/g, '""')}"`;
123
+ }
124
+ return str;
125
+ }
126
+
127
+ function getGitLog(branch, author, sinceDate) {
128
+ const format = [
129
+ `${COMMIT_DELIMITER}`,
130
+ `%h${FIELD_DELIMITER}%an${FIELD_DELIMITER}%ai${FIELD_DELIMITER}%s`,
131
+ ].join('');
132
+
133
+ const args = [
134
+ 'log',
135
+ `--format=${format}`,
136
+ '--name-status',
137
+ `--author=${author}`,
138
+ `--since=${sinceDate}`,
139
+ branch,
140
+ ];
141
+
142
+ try {
143
+ return execFileSync('git', args, { encoding: 'utf-8', maxBuffer: 50 * 1024 * 1024 });
144
+ } catch (err) {
145
+ console.error(`Error running git log: ${err.message}`);
146
+ process.exit(1);
147
+ }
148
+ }
149
+
150
+ function parseGitLog(rawOutput) {
151
+ const rows = [];
152
+ const commits = rawOutput.split(COMMIT_DELIMITER).filter((s) => s.trim());
153
+
154
+ for (const block of commits) {
155
+ const lines = block.trim().split('\n');
156
+ if (lines.length === 0) continue;
157
+
158
+ const headerLine = lines[0];
159
+ const parts = headerLine.split(FIELD_DELIMITER);
160
+ if (parts.length < 4) continue;
161
+
162
+ const [hash, author, dateRaw, message] = parts;
163
+ const date = dateRaw.slice(0, 10);
164
+
165
+ for (let i = 1; i < lines.length; i++) {
166
+ const line = lines[i].trim();
167
+ if (!line) continue;
168
+
169
+ const match = line.match(/^([AMDRTC])\d*\t(.+)$/);
170
+ if (!match) continue;
171
+
172
+ const statusCode = match[1];
173
+ const filePath = match[2].includes('\t') ? match[2].split('\t').pop() : match[2];
174
+ const changeType = CHANGE_TYPE_LABELS[statusCode] || statusCode;
175
+
176
+ rows.push({
177
+ commit_hash: hash,
178
+ author,
179
+ date,
180
+ message,
181
+ change_type: changeType,
182
+ file_path: filePath,
183
+ });
184
+ }
185
+ }
186
+
187
+ return rows;
188
+ }
189
+
190
+ function writeCsv(rows, outputPath) {
191
+ const columns = ['commit_hash', 'author', 'date', 'message', 'change_type', 'file_path'];
192
+ const header = columns.join(',');
193
+ const lines = rows.map((row) => columns.map((col) => escapeCsvField(row[col])).join(','));
194
+
195
+ const csv = [header, ...lines, ''].join('\n');
196
+ const absPath = resolve(outputPath);
197
+ writeFileSync(absPath, csv, 'utf-8');
198
+ return absPath;
199
+ }
200
+
201
+ function main() {
202
+ const opts = parseArgs(process.argv);
203
+ validateGitEnvironment();
204
+
205
+ const sinceDate = computeSinceDate(opts.months);
206
+ console.error(`Searching commits on branch "${opts.branch}" by "${opts.author}" since ${sinceDate}...`);
207
+
208
+ const rawLog = getGitLog(opts.branch, opts.author, sinceDate);
209
+ const rows = parseGitLog(rawLog);
210
+
211
+ const uniqueCommits = new Set(rows.map((r) => r.commit_hash)).size;
212
+
213
+ if (rows.length === 0) {
214
+ console.error('No commits found matching the criteria.');
215
+ process.exit(0);
216
+ }
217
+
218
+ const absPath = writeCsv(rows, opts.output);
219
+
220
+ console.error(`Done. ${uniqueCommits} commit(s), ${rows.length} file change(s).`);
221
+ console.error(`Report written to: ${absPath}`);
222
+ }
223
+
224
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.4.4",
3
+ "version": "2.5.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",