@simtlix/simfinity-js 2.6.1 → 2.6.4
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 +23 -3
- package/package.json +1 -1
- package/src/index.js +155 -83
- package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +0 -23
- package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +0 -235
- package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +0 -567
- package/.claude/worktrees/agitated-kepler/LICENSE +0 -201
- package/.claude/worktrees/agitated-kepler/README.md +0 -3941
- package/.claude/worktrees/agitated-kepler/eslint.config.mjs +0 -71
- package/.claude/worktrees/agitated-kepler/package-lock.json +0 -4740
- package/.claude/worktrees/agitated-kepler/package.json +0 -41
- package/.claude/worktrees/agitated-kepler/src/auth/errors.js +0 -44
- package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +0 -273
- package/.claude/worktrees/agitated-kepler/src/auth/index.js +0 -391
- package/.claude/worktrees/agitated-kepler/src/auth/rules.js +0 -274
- package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +0 -39
- package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +0 -28
- package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +0 -39
- package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +0 -11
- package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +0 -15
- package/.claude/worktrees/agitated-kepler/src/index.js +0 -2412
- package/.claude/worktrees/agitated-kepler/src/plugins.js +0 -53
- package/.claude/worktrees/agitated-kepler/src/scalars.js +0 -188
- package/.claude/worktrees/agitated-kepler/src/validators.js +0 -250
- package/.claude/worktrees/agitated-kepler/yarn.lock +0 -1154
- package/.claude/worktrees/distracted-swirles-4601fe/.claude/settings.local.json +0 -10
- package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_CHANGES_SUMMARY.md +0 -235
- package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_EXAMPLE.md +0 -567
- package/.claude/worktrees/distracted-swirles-4601fe/LICENSE +0 -201
- package/.claude/worktrees/distracted-swirles-4601fe/README.md +0 -3941
- package/.claude/worktrees/distracted-swirles-4601fe/eslint.config.mjs +0 -71
- package/.claude/worktrees/distracted-swirles-4601fe/package-lock.json +0 -4740
- package/.claude/worktrees/distracted-swirles-4601fe/package.json +0 -41
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/errors.js +0 -44
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/expressions.js +0 -273
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/index.js +0 -391
- package/.claude/worktrees/distracted-swirles-4601fe/src/auth/rules.js +0 -274
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLOperator.js +0 -39
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLSort.js +0 -28
- package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLValue.js +0 -39
- package/.claude/worktrees/distracted-swirles-4601fe/src/errors/internal-server.error.js +0 -11
- package/.claude/worktrees/distracted-swirles-4601fe/src/errors/simfinity.error.js +0 -15
- package/.claude/worktrees/distracted-swirles-4601fe/src/index.js +0 -1925
- package/.claude/worktrees/distracted-swirles-4601fe/src/plugins.js +0 -53
- package/.claude/worktrees/distracted-swirles-4601fe/src/scalars.js +0 -188
- package/.claude/worktrees/distracted-swirles-4601fe/src/validators.js +0 -250
- package/.claude/worktrees/distracted-swirles-4601fe/yarn.lock +0 -1154
- package/.cursor/rules/simfinity-architecture.mdc +0 -71
- package/.cursor/rules/simfinity-auth-module.mdc +0 -100
- package/.cursor/rules/simfinity-coding-standards.mdc +0 -62
- package/.cursor/rules/simfinity-core-functions.mdc +0 -84
- package/.cursor/rules/simfinity-documentation.mdc +0 -59
- package/.cursor/rules/simfinity-extensions.mdc +0 -121
- package/.cursor/rules/simfinity-testing.mdc +0 -90
- package/eslint.config.mjs +0 -71
package/README.md
CHANGED
|
@@ -352,6 +352,25 @@ query {
|
|
|
352
352
|
}
|
|
353
353
|
```
|
|
354
354
|
|
|
355
|
+
As an equivalent shorthand, you can pass the full dotted path in `field` and omit `path`:
|
|
356
|
+
|
|
357
|
+
```graphql
|
|
358
|
+
query {
|
|
359
|
+
books(
|
|
360
|
+
OR: [
|
|
361
|
+
{ conditions: [{ field: "author.name", operator: LIKE, value: "Adams" }] }
|
|
362
|
+
{ conditions: [{ field: "author.name", operator: LIKE, value: "Pratchett" }] }
|
|
363
|
+
]
|
|
364
|
+
) {
|
|
365
|
+
id
|
|
366
|
+
title
|
|
367
|
+
author { name }
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
```
|
|
371
|
+
|
|
372
|
+
Both forms produce the same query. Multi-segment paths work too — e.g. `field: "author.country.code"` is equivalent to `field: "author", path: "country.code"`. Supplying both a dotted `field` and a separate `path` is rejected as ambiguous.
|
|
373
|
+
|
|
355
374
|
#### Mixing Flat Filters with AND/OR
|
|
356
375
|
|
|
357
376
|
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:
|
|
@@ -439,7 +458,8 @@ query {
|
|
|
439
458
|
AND: [
|
|
440
459
|
{ conditions: [{ field: "year", operator: GTE, value: 2022 }] }
|
|
441
460
|
{ conditions: [
|
|
442
|
-
{ field: "episodes", path: "name",
|
|
461
|
+
# Equivalent to: { field: "episodes", path: "name", ... }
|
|
462
|
+
{ field: "episodes.name", operator: LIKE, value: "Final" }
|
|
443
463
|
] }
|
|
444
464
|
]
|
|
445
465
|
}
|
|
@@ -462,8 +482,8 @@ This translates to: `number > 1 AND (year = 2020 OR (year >= 2022 AND episodes.n
|
|
|
462
482
|
| `QLFilterGroup` | `AND: [QLFilterGroup]`, `OR: [QLFilterGroup]`, `conditions: [QLFilterCondition]` | Recursive logical group |
|
|
463
483
|
| `QLFilterCondition` | `field: String!`, `operator: QLOperator`, `value: QLValue`, `path: String` | Individual filter condition |
|
|
464
484
|
|
|
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"`)
|
|
485
|
+
- `field` identifies the entity field by name (e.g., `"title"`, `"author"`). For object/relationship fields you can also pass the full dotted path here as a shorthand (e.g., `"author.name"`, `"author.country.name"`)
|
|
486
|
+
- `path` is required for object/relationship fields when `field` is just the top-level name (e.g., `"name"`, `"country.name"`). Omit `path` if you used the dotted-`field` shorthand. Supplying both is an error.
|
|
467
487
|
- Multiple `conditions` in the same group are combined with AND
|
|
468
488
|
- Maximum nesting depth: 5 levels
|
|
469
489
|
|
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -28,6 +28,7 @@ const GraphQLJSON = new GraphQLScalarType({
|
|
|
28
28
|
case Kind.BOOLEAN:
|
|
29
29
|
return ast.value;
|
|
30
30
|
case Kind.INT:
|
|
31
|
+
return parseInt(ast.value, 10);
|
|
31
32
|
case Kind.FLOAT:
|
|
32
33
|
return parseFloat(ast.value);
|
|
33
34
|
case Kind.OBJECT: {
|
|
@@ -300,6 +301,40 @@ const buildRelationLookup = ({
|
|
|
300
301
|
unwind: { $unwind: { path: `$${alias}`, preserveNullAndEmptyArrays: true } },
|
|
301
302
|
});
|
|
302
303
|
|
|
304
|
+
const escapeRegex = (s) => String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
305
|
+
|
|
306
|
+
const PATH_SEGMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
307
|
+
|
|
308
|
+
const assertValidFilterPath = (path) => {
|
|
309
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
310
|
+
throw new SimfinityError('Filter path must be a non-empty string', 'INVALID_FILTER_PATH', 400);
|
|
311
|
+
}
|
|
312
|
+
for (const segment of path.split('.')) {
|
|
313
|
+
if (!PATH_SEGMENT_RE.test(segment)) {
|
|
314
|
+
throw new SimfinityError(`Invalid filter path segment: "${segment}"`, 'INVALID_FILTER_PATH', 400);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
const normalizeFilterCondition = (condition) => {
|
|
320
|
+
const { field } = condition;
|
|
321
|
+
if (typeof field !== 'string') return condition;
|
|
322
|
+
const dotIdx = field.indexOf('.');
|
|
323
|
+
if (dotIdx < 0) return condition;
|
|
324
|
+
if (condition.path != null && condition.path !== '') {
|
|
325
|
+
throw new SimfinityError(
|
|
326
|
+
'Filter condition cannot use both dotted field syntax and a separate path',
|
|
327
|
+
'AMBIGUOUS_FILTER_FIELD',
|
|
328
|
+
400,
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
return {
|
|
332
|
+
...condition,
|
|
333
|
+
field: field.slice(0, dotIdx),
|
|
334
|
+
path: field.slice(dotIdx + 1),
|
|
335
|
+
};
|
|
336
|
+
};
|
|
337
|
+
|
|
303
338
|
const OP_TO_MONGO = {
|
|
304
339
|
LT: (v) => ({ $lt: v }),
|
|
305
340
|
GT: (v) => ({ $gt: v }),
|
|
@@ -307,7 +342,7 @@ const OP_TO_MONGO = {
|
|
|
307
342
|
GTE: (v) => ({ $gte: v }),
|
|
308
343
|
NE: (v) => ({ $ne: v }),
|
|
309
344
|
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
310
|
-
LIKE: (v) => ({ $regex: `.*${v}.*` }),
|
|
345
|
+
LIKE: (v) => ({ $regex: `.*${escapeRegex(v)}.*` }),
|
|
311
346
|
};
|
|
312
347
|
|
|
313
348
|
const AGG_OP_TO_MONGO = {
|
|
@@ -379,7 +414,13 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
379
414
|
const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
|
|
380
415
|
const originalFields = originalType.getFields();
|
|
381
416
|
const newFields = Object.fromEntries(
|
|
382
|
-
Object.entries(originalFields)
|
|
417
|
+
Object.entries(originalFields)
|
|
418
|
+
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
419
|
+
.map(([fieldName, field]) => [fieldName, {
|
|
420
|
+
type: field.type,
|
|
421
|
+
description: field.description,
|
|
422
|
+
defaultValue: field.defaultValue,
|
|
423
|
+
}]),
|
|
383
424
|
);
|
|
384
425
|
|
|
385
426
|
return new GraphQLInputObjectType({
|
|
@@ -446,9 +487,9 @@ const buildInputType = (gqltype) => {
|
|
|
446
487
|
|
|
447
488
|
if (!fieldEntry.extensions || !fieldEntry.extensions.readOnly) {
|
|
448
489
|
const hasStateMachine = !!typesDict.types[gqltype.name].stateMachine;
|
|
449
|
-
const
|
|
490
|
+
const stateFieldManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
|
|
450
491
|
|
|
451
|
-
if (!
|
|
492
|
+
if (!stateFieldManagedByStateMachine) {
|
|
452
493
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
453
494
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
454
495
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
@@ -484,8 +525,8 @@ const buildInputType = (gqltype) => {
|
|
|
484
525
|
if (fieldEntry.type.ofType === gqltype) {
|
|
485
526
|
selfReferenceCollections[fieldEntryName] = fieldEntry;
|
|
486
527
|
} else {
|
|
487
|
-
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, gqltype.name
|
|
488
|
-
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name
|
|
528
|
+
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, `${gqltype.name}A`, fieldEntry.extensions?.relation?.connectionField);
|
|
529
|
+
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, `${gqltype.name}U`, fieldEntry.extensions?.relation?.connectionField);
|
|
489
530
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
490
531
|
fieldArg.type = listInputTypeForAdd;
|
|
491
532
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
@@ -557,29 +598,35 @@ const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
|
557
598
|
export { getInputType };
|
|
558
599
|
|
|
559
600
|
const buildPendingInputTypes = (waitingForInputType) => {
|
|
560
|
-
|
|
561
|
-
let
|
|
562
|
-
|
|
563
|
-
Object.
|
|
564
|
-
const
|
|
601
|
+
let pending = waitingForInputType;
|
|
602
|
+
let previousPendingCount = Object.keys(pending).length + 1;
|
|
603
|
+
|
|
604
|
+
while (Object.keys(pending).length > 0) {
|
|
605
|
+
const currentCount = Object.keys(pending).length;
|
|
606
|
+
if (currentCount >= previousPendingCount) {
|
|
607
|
+
const unresolved = Object.keys(pending).join(', ');
|
|
608
|
+
throw new SimfinityError(
|
|
609
|
+
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
610
|
+
'INPUT_TYPE_UNRESOLVED',
|
|
611
|
+
500,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
previousPendingCount = currentCount;
|
|
565
615
|
|
|
566
|
-
|
|
567
|
-
|
|
616
|
+
const stillWaiting = {};
|
|
617
|
+
for (const [key, value] of Object.entries(pending)) {
|
|
618
|
+
const { gqltype } = value;
|
|
619
|
+
if (typesDict.types[gqltype.name].inputType) continue;
|
|
568
620
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
typesDict.types[gqltype.name].inputType =
|
|
572
|
-
typesDictForUpdate.types[gqltype.name].inputType =
|
|
573
|
-
.inputTypeBodyForUpdate;
|
|
621
|
+
const result = buildInputType(gqltype);
|
|
622
|
+
if (result && result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
623
|
+
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
624
|
+
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
574
625
|
} else {
|
|
575
|
-
|
|
576
|
-
isThereAtLeastOneWaiting = true;
|
|
626
|
+
stillWaiting[key] = value;
|
|
577
627
|
}
|
|
578
628
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (isThereAtLeastOneWaiting) {
|
|
582
|
-
buildPendingInputTypes(stillWaitingInputType);
|
|
629
|
+
pending = stillWaiting;
|
|
583
630
|
}
|
|
584
631
|
};
|
|
585
632
|
|
|
@@ -622,9 +669,11 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
622
669
|
null, operation, session)).modelArgs;
|
|
623
670
|
}
|
|
624
671
|
} else {
|
|
625
|
-
|
|
626
|
-
.
|
|
627
|
-
|
|
672
|
+
throw new SimfinityError(
|
|
673
|
+
`Field ${gqltype.name}.${fieldEntryName} is an object type but does not define extensions.relation`,
|
|
674
|
+
'MISSING_RELATION_EXTENSION',
|
|
675
|
+
500,
|
|
676
|
+
);
|
|
628
677
|
}
|
|
629
678
|
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
630
679
|
const { ofType } = fieldEntry.type;
|
|
@@ -668,34 +717,38 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
668
717
|
const MAX_TRANSIENT_RETRIES = 5;
|
|
669
718
|
|
|
670
719
|
const withTransaction = async (session, body) => {
|
|
720
|
+
const ownsSession = !session;
|
|
671
721
|
const mySession = session || await mongoose.startSession();
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
722
|
+
try {
|
|
723
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
724
|
+
await mySession.startTransaction();
|
|
725
|
+
try {
|
|
726
|
+
const result = await body(mySession);
|
|
727
|
+
await mySession.commitTransaction();
|
|
728
|
+
return result;
|
|
729
|
+
} catch (error) {
|
|
730
|
+
await mySession.abortTransaction();
|
|
731
|
+
const isTransient = error.errorLabels?.includes('TransientTransactionError');
|
|
732
|
+
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
733
|
+
continue;
|
|
734
|
+
}
|
|
735
|
+
throw error;
|
|
684
736
|
}
|
|
737
|
+
}
|
|
738
|
+
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
739
|
+
} finally {
|
|
740
|
+
if (ownsSession) {
|
|
685
741
|
mySession.endSession();
|
|
686
|
-
throw error;
|
|
687
742
|
}
|
|
688
743
|
}
|
|
689
|
-
mySession.endSession();
|
|
690
|
-
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
691
744
|
};
|
|
692
745
|
|
|
693
|
-
const executeRegisteredMutation = (args, callback, session) => withTransaction(
|
|
746
|
+
const executeRegisteredMutation = (args, callback, context, session) => withTransaction(
|
|
694
747
|
session,
|
|
695
|
-
(mySession) => callback(args, mySession),
|
|
748
|
+
(mySession) => callback(args, mySession, context),
|
|
696
749
|
);
|
|
697
750
|
|
|
698
|
-
const
|
|
751
|
+
const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
699
752
|
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
700
753
|
if (collectionField.added) {
|
|
701
754
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
@@ -741,7 +794,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
741
794
|
|
|
742
795
|
if (embeddedFieldNames.length > 0) {
|
|
743
796
|
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
744
|
-
const currentObject = await Model.findById(objectId, projection).lean();
|
|
797
|
+
const currentObject = await Model.findById(objectId, projection).session(session).lean();
|
|
745
798
|
if (currentObject) {
|
|
746
799
|
for (const fieldEntryName of embeddedFieldNames) {
|
|
747
800
|
const oldObjectData = currentObject[fieldEntryName];
|
|
@@ -775,7 +828,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
775
828
|
).session(session);
|
|
776
829
|
|
|
777
830
|
if (materializedModel.collectionFields) {
|
|
778
|
-
await
|
|
831
|
+
await iterateOnCollectionFields(materializedModel, gqltype, objectId, session, context);
|
|
779
832
|
}
|
|
780
833
|
|
|
781
834
|
if (controller && controller.onUpdated) {
|
|
@@ -786,7 +839,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
786
839
|
};
|
|
787
840
|
|
|
788
841
|
const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
|
|
789
|
-
const storedModel = await Model.findById(args.id);
|
|
842
|
+
const storedModel = await Model.findById(args.id).session(session);
|
|
790
843
|
if (!storedModel) {
|
|
791
844
|
throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
|
|
792
845
|
}
|
|
@@ -822,10 +875,9 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
|
|
|
822
875
|
result = result.toObject();
|
|
823
876
|
|
|
824
877
|
if (materializedModel.collectionFields) {
|
|
825
|
-
await
|
|
878
|
+
await iterateOnCollectionFields(materializedModel, gqltype, newObject._id, session, context);
|
|
826
879
|
}
|
|
827
880
|
|
|
828
|
-
|
|
829
881
|
if (controller && controller.onSaved) {
|
|
830
882
|
await controller.onSaved(result, args, session, context);
|
|
831
883
|
}
|
|
@@ -898,27 +950,25 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
898
950
|
const shouldNotBeIncludedInSchema = (includedTypes,
|
|
899
951
|
type) => includedTypes && !includedTypes.includes(type);
|
|
900
952
|
|
|
901
|
-
const
|
|
953
|
+
const executeMiddleware = async (context) => {
|
|
902
954
|
const buildNext = (middlewaresParam) => {
|
|
903
955
|
if (!middlewaresParam) {
|
|
904
|
-
return () => {};
|
|
956
|
+
return async () => {};
|
|
905
957
|
}
|
|
906
|
-
|
|
958
|
+
return async () => {
|
|
907
959
|
const middleware = middlewaresParam[0];
|
|
908
960
|
if (middleware) {
|
|
909
|
-
middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
961
|
+
await middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
910
962
|
}
|
|
911
963
|
};
|
|
912
|
-
return next;
|
|
913
964
|
};
|
|
914
965
|
|
|
915
|
-
|
|
916
|
-
middleware();
|
|
966
|
+
await buildNext(middlewares)();
|
|
917
967
|
};
|
|
918
968
|
|
|
919
969
|
const executeScope = async (params) => {
|
|
920
970
|
const { type, args, operation, context } = params;
|
|
921
|
-
|
|
971
|
+
|
|
922
972
|
if (!type || !type.gqltype || !type.gqltype.extensions) {
|
|
923
973
|
return null;
|
|
924
974
|
}
|
|
@@ -960,7 +1010,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
960
1010
|
context,
|
|
961
1011
|
};
|
|
962
1012
|
|
|
963
|
-
|
|
1013
|
+
await executeMiddleware(params);
|
|
964
1014
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
965
1015
|
args.input, operations.SAVE, null, null, context);
|
|
966
1016
|
},
|
|
@@ -977,7 +1027,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
977
1027
|
context,
|
|
978
1028
|
};
|
|
979
1029
|
|
|
980
|
-
|
|
1030
|
+
await executeMiddleware(params);
|
|
981
1031
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
982
1032
|
args.id, operations.DELETE, null, null, context);
|
|
983
1033
|
},
|
|
@@ -1002,7 +1052,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1002
1052
|
context,
|
|
1003
1053
|
};
|
|
1004
1054
|
|
|
1005
|
-
|
|
1055
|
+
await executeMiddleware(params);
|
|
1006
1056
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1007
1057
|
args.input, operations.UPDATE, null, null, context);
|
|
1008
1058
|
},
|
|
@@ -1024,7 +1074,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1024
1074
|
context,
|
|
1025
1075
|
};
|
|
1026
1076
|
|
|
1027
|
-
|
|
1077
|
+
await executeMiddleware(params);
|
|
1028
1078
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1029
1079
|
args.input, operations.STATE_CHANGED, actionField, null, context);
|
|
1030
1080
|
},
|
|
@@ -1051,8 +1101,8 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1051
1101
|
entry,
|
|
1052
1102
|
context,
|
|
1053
1103
|
};
|
|
1054
|
-
|
|
1055
|
-
return executeRegisteredMutation(args.input, registeredMutation.callback);
|
|
1104
|
+
await executeMiddleware(params);
|
|
1105
|
+
return executeRegisteredMutation(args.input, registeredMutation.callback, context);
|
|
1056
1106
|
},
|
|
1057
1107
|
};
|
|
1058
1108
|
}
|
|
@@ -1128,10 +1178,10 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
1128
1178
|
|
|
1129
1179
|
const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
1130
1180
|
const objectIdFields = [];
|
|
1131
|
-
|
|
1181
|
+
|
|
1132
1182
|
for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
|
|
1133
1183
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1134
|
-
|
|
1184
|
+
|
|
1135
1185
|
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1136
1186
|
objectIdFields.push(currentPath);
|
|
1137
1187
|
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
@@ -1147,7 +1197,7 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1147
1197
|
}
|
|
1148
1198
|
}
|
|
1149
1199
|
}
|
|
1150
|
-
|
|
1200
|
+
|
|
1151
1201
|
return objectIdFields;
|
|
1152
1202
|
};
|
|
1153
1203
|
|
|
@@ -1172,7 +1222,12 @@ const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}
|
|
|
1172
1222
|
return model;
|
|
1173
1223
|
};
|
|
1174
1224
|
|
|
1175
|
-
const coerceIdArray = (value) => value.map((element) =>
|
|
1225
|
+
const coerceIdArray = (value) => value.map((element) => {
|
|
1226
|
+
if (element === null || element === undefined) {
|
|
1227
|
+
throw new SimfinityError('ID value cannot be null in collection filter', 'INVALID_FILTER_VALUE', 400);
|
|
1228
|
+
}
|
|
1229
|
+
return new mongoose.Types.ObjectId(element);
|
|
1230
|
+
});
|
|
1176
1231
|
|
|
1177
1232
|
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1178
1233
|
const isIdField = fieldname.endsWith('_id');
|
|
@@ -1181,11 +1236,12 @@ const buildMatchesClause = (fieldname, operator, value) => {
|
|
|
1181
1236
|
if (op === 'EQ') {
|
|
1182
1237
|
return { [fieldname]: isIdField ? new mongoose.Types.ObjectId(value) : value };
|
|
1183
1238
|
}
|
|
1184
|
-
if (op === 'IN') {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1239
|
+
if (op === 'IN' || op === 'NIN') {
|
|
1240
|
+
if (!Array.isArray(value)) {
|
|
1241
|
+
throw new SimfinityError(`${op} requires an array value for ${fieldname}`, 'INVALID_FILTER_VALUE', 400);
|
|
1242
|
+
}
|
|
1243
|
+
const coerced = isIdField ? coerceIdArray(value) : value;
|
|
1244
|
+
return { [fieldname]: { [op === 'IN' ? '$in' : '$nin']: coerced } };
|
|
1189
1245
|
}
|
|
1190
1246
|
const builder = OP_TO_MONGO[op];
|
|
1191
1247
|
return builder ? { [fieldname]: builder(value) } : {};
|
|
@@ -1223,9 +1279,14 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
1223
1279
|
return aggregateClauses;
|
|
1224
1280
|
}
|
|
1225
1281
|
|
|
1282
|
+
if (!Array.isArray(filterField?.terms) || filterField.terms.length === 0) {
|
|
1283
|
+
return aggregateClauses;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1226
1286
|
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1227
1287
|
|
|
1228
1288
|
filterField.terms.forEach((term) => {
|
|
1289
|
+
assertValidFilterPath(term.path);
|
|
1229
1290
|
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1230
1291
|
&& !aggregateClauses[fieldName]) {
|
|
1231
1292
|
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
@@ -1267,6 +1328,10 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1267
1328
|
const matchesClauses = {};
|
|
1268
1329
|
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1269
1330
|
|
|
1331
|
+
if (filterField == null) {
|
|
1332
|
+
return { aggregateClauses, matchesClauses };
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1270
1335
|
if (fieldType instanceof GraphQLScalarType
|
|
1271
1336
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1272
1337
|
|| fieldType instanceof GraphQLEnumType
|
|
@@ -1280,9 +1345,14 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1280
1345
|
return { aggregateClauses, matchesClauses };
|
|
1281
1346
|
}
|
|
1282
1347
|
|
|
1348
|
+
if (!Array.isArray(filterField.terms) || filterField.terms.length === 0) {
|
|
1349
|
+
return { aggregateClauses, matchesClauses };
|
|
1350
|
+
}
|
|
1351
|
+
|
|
1283
1352
|
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1284
1353
|
|
|
1285
1354
|
filterField.terms.forEach((term) => {
|
|
1355
|
+
assertValidFilterPath(term.path);
|
|
1286
1356
|
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1287
1357
|
&& !aggregateClauses[fieldName]) {
|
|
1288
1358
|
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
@@ -1346,7 +1416,8 @@ const buildFilterGroupMatch = async (filterGroup, gqltype, aggregateClauses, agg
|
|
|
1346
1416
|
const fields = gqltype.getFields();
|
|
1347
1417
|
|
|
1348
1418
|
if (filterGroup.conditions?.length > 0) {
|
|
1349
|
-
for (const
|
|
1419
|
+
for (const rawCondition of filterGroup.conditions) {
|
|
1420
|
+
const condition = normalizeFilterCondition(rawCondition);
|
|
1350
1421
|
const qlField = fields[condition.field];
|
|
1351
1422
|
if (!qlField) {
|
|
1352
1423
|
throw new SimfinityError(`Unknown filter field: ${condition.field}`, 'INVALID_FILTER_FIELD', 400);
|
|
@@ -1467,6 +1538,7 @@ const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsInclu
|
|
|
1467
1538
|
const fields = gqltype.getFields();
|
|
1468
1539
|
|
|
1469
1540
|
sortTerms.forEach((sort) => {
|
|
1541
|
+
assertValidFilterPath(sort.field);
|
|
1470
1542
|
let fixedSortField = sort.field;
|
|
1471
1543
|
if (sort.field.indexOf('.') >= 0) {
|
|
1472
1544
|
const sortParts = sort.field.split('.');
|
|
@@ -1506,19 +1578,19 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1506
1578
|
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1507
1579
|
}
|
|
1508
1580
|
|
|
1509
|
-
let limitClause = { $limit: 100 };
|
|
1510
1581
|
let skipClause = { $skip: 0 };
|
|
1582
|
+
let limitClause = { $limit: 100 };
|
|
1511
1583
|
if (input.pagination?.page && input.pagination?.size) {
|
|
1512
|
-
|
|
1513
|
-
limitClause = { $limit: input.pagination.size
|
|
1514
|
-
skipClause = { $skip: skip };
|
|
1584
|
+
skipClause = { $skip: input.pagination.size * (input.pagination.page - 1) };
|
|
1585
|
+
limitClause = { $limit: input.pagination.size };
|
|
1515
1586
|
}
|
|
1516
|
-
aggregateClauses.push(
|
|
1587
|
+
aggregateClauses.push(skipClause, limitClause);
|
|
1517
1588
|
|
|
1518
1589
|
return aggregateClauses;
|
|
1519
1590
|
};
|
|
1520
1591
|
|
|
1521
1592
|
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1593
|
+
assertValidFilterPath(fieldPath);
|
|
1522
1594
|
const pathParts = fieldPath.split('.');
|
|
1523
1595
|
const lookupPairs = [];
|
|
1524
1596
|
let currentPath = '';
|
|
@@ -1617,7 +1689,7 @@ const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
|
1617
1689
|
|
|
1618
1690
|
if (input.pagination?.page && input.pagination?.size) {
|
|
1619
1691
|
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1620
|
-
aggregateClauses.push({ $
|
|
1692
|
+
aggregateClauses.push({ $skip: skip }, { $limit: input.pagination.size });
|
|
1621
1693
|
}
|
|
1622
1694
|
|
|
1623
1695
|
return aggregateClauses;
|
|
@@ -1643,7 +1715,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1643
1715
|
const params = {
|
|
1644
1716
|
type, args, operation: 'get_by_id', context,
|
|
1645
1717
|
};
|
|
1646
|
-
|
|
1718
|
+
await executeMiddleware(params);
|
|
1647
1719
|
|
|
1648
1720
|
const hasScope = type.gqltype.extensions?.scope?.get_by_id;
|
|
1649
1721
|
if (!hasScope) {
|
|
@@ -1677,7 +1749,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1677
1749
|
operation: 'find',
|
|
1678
1750
|
context,
|
|
1679
1751
|
};
|
|
1680
|
-
|
|
1752
|
+
await executeMiddleware(params);
|
|
1681
1753
|
await executeScope(params);
|
|
1682
1754
|
const aggregateClauses = await buildQuery(args, type.gqltype);
|
|
1683
1755
|
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
@@ -1712,7 +1784,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1712
1784
|
operation: 'aggregate',
|
|
1713
1785
|
context,
|
|
1714
1786
|
};
|
|
1715
|
-
|
|
1787
|
+
await executeMiddleware(params);
|
|
1716
1788
|
await executeScope(params);
|
|
1717
1789
|
const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
|
|
1718
1790
|
const result = await type.model.aggregate(aggregateClauses);
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"permissions": {
|
|
3
|
-
"allow": [
|
|
4
|
-
"Bash(npm test:*)",
|
|
5
|
-
"Bash(/usr/local/bin/node ./node_modules/.bin/vitest run)",
|
|
6
|
-
"Bash(/opt/homebrew/bin/npx vitest:*)",
|
|
7
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" npm test)",
|
|
8
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" npm run lint)",
|
|
9
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" npx eslint tests/and-or-filters.test.js src/index.js)",
|
|
10
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" npx eslint tests/and-or-filters.test.js)",
|
|
11
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git status)",
|
|
12
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git diff --stat)",
|
|
13
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git log --oneline -5)",
|
|
14
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git diff README.md src/index.js)",
|
|
15
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git add src/index.js tests/and-or-filters.test.js README.md)",
|
|
16
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" git checkout master)",
|
|
17
|
-
"Bash(git merge:*)",
|
|
18
|
-
"Bash(curl -s http://localhost:3000/graphql -H \"Content-Type: application/json\" -d '{\"\"\"\"query\"\"\"\":\"\"\"\"{ __schema { queryType { fields { name args { name type { name kind ofType { name } } } } } } }\"\"\"\"}')",
|
|
19
|
-
"Bash(curl -s http://localhost:3000/graphql -H \"Content-Type: application/json\" -d '{\"\"\"\"query\"\"\"\":\"\"\"\"{ barbershops\\(pagination: { page: 1, size: 3 }\\) { id name state averageRating reviewCount } }\"\"\"\"}')",
|
|
20
|
-
"Bash(PATH=\"/opt/homebrew/bin:$PATH\" node -p \"JSON.stringify\\(JSON.parse\\(require\\(''''fs''''\\).readFileSync\\(''''/dev/stdin'''',''''utf8''''\\)\\),null,2\\)\")"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|