@simtlix/simfinity-js 2.6.1 → 2.6.3
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/package.json +1 -1
- package/src/index.js +134 -82
- 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/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,21 @@ 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
|
+
|
|
303
319
|
const OP_TO_MONGO = {
|
|
304
320
|
LT: (v) => ({ $lt: v }),
|
|
305
321
|
GT: (v) => ({ $gt: v }),
|
|
@@ -307,7 +323,7 @@ const OP_TO_MONGO = {
|
|
|
307
323
|
GTE: (v) => ({ $gte: v }),
|
|
308
324
|
NE: (v) => ({ $ne: v }),
|
|
309
325
|
BTW: (v) => ({ $gte: v[0], $lte: v[1] }),
|
|
310
|
-
LIKE: (v) => ({ $regex: `.*${v}.*` }),
|
|
326
|
+
LIKE: (v) => ({ $regex: `.*${escapeRegex(v)}.*` }),
|
|
311
327
|
};
|
|
312
328
|
|
|
313
329
|
const AGG_OP_TO_MONGO = {
|
|
@@ -379,7 +395,13 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
|
|
|
379
395
|
const createTypeWithExcludedField = (inputNamePrefix, originalType, fieldToExclude) => {
|
|
380
396
|
const originalFields = originalType.getFields();
|
|
381
397
|
const newFields = Object.fromEntries(
|
|
382
|
-
Object.entries(originalFields)
|
|
398
|
+
Object.entries(originalFields)
|
|
399
|
+
.filter(([fieldName]) => fieldName !== fieldToExclude)
|
|
400
|
+
.map(([fieldName, field]) => [fieldName, {
|
|
401
|
+
type: field.type,
|
|
402
|
+
description: field.description,
|
|
403
|
+
defaultValue: field.defaultValue,
|
|
404
|
+
}]),
|
|
383
405
|
);
|
|
384
406
|
|
|
385
407
|
return new GraphQLInputObjectType({
|
|
@@ -446,9 +468,9 @@ const buildInputType = (gqltype) => {
|
|
|
446
468
|
|
|
447
469
|
if (!fieldEntry.extensions || !fieldEntry.extensions.readOnly) {
|
|
448
470
|
const hasStateMachine = !!typesDict.types[gqltype.name].stateMachine;
|
|
449
|
-
const
|
|
471
|
+
const stateFieldManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
|
|
450
472
|
|
|
451
|
-
if (!
|
|
473
|
+
if (!stateFieldManagedByStateMachine) {
|
|
452
474
|
if (fieldEntry.type instanceof GraphQLScalarType
|
|
453
475
|
|| fieldEntry.type instanceof GraphQLEnumType
|
|
454
476
|
|| isNonNullOfType(fieldEntry.type, GraphQLScalarType)
|
|
@@ -484,8 +506,8 @@ const buildInputType = (gqltype) => {
|
|
|
484
506
|
if (fieldEntry.type.ofType === gqltype) {
|
|
485
507
|
selfReferenceCollections[fieldEntryName] = fieldEntry;
|
|
486
508
|
} else {
|
|
487
|
-
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, gqltype.name
|
|
488
|
-
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name
|
|
509
|
+
const listInputTypeForAdd = graphQLListInputType(typesDict, fieldEntry, fieldEntryName, `${gqltype.name}A`, fieldEntry.extensions?.relation?.connectionField);
|
|
510
|
+
const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, `${gqltype.name}U`, fieldEntry.extensions?.relation?.connectionField);
|
|
489
511
|
if (listInputTypeForAdd && listInputTypeForUpdate) {
|
|
490
512
|
fieldArg.type = listInputTypeForAdd;
|
|
491
513
|
fieldArgForUpdate.type = listInputTypeForUpdate;
|
|
@@ -557,29 +579,35 @@ const getInputType = (type) => typesDict.types[type.name].inputType;
|
|
|
557
579
|
export { getInputType };
|
|
558
580
|
|
|
559
581
|
const buildPendingInputTypes = (waitingForInputType) => {
|
|
560
|
-
|
|
561
|
-
let
|
|
562
|
-
|
|
563
|
-
Object.
|
|
564
|
-
const
|
|
582
|
+
let pending = waitingForInputType;
|
|
583
|
+
let previousPendingCount = Object.keys(pending).length + 1;
|
|
584
|
+
|
|
585
|
+
while (Object.keys(pending).length > 0) {
|
|
586
|
+
const currentCount = Object.keys(pending).length;
|
|
587
|
+
if (currentCount >= previousPendingCount) {
|
|
588
|
+
const unresolved = Object.keys(pending).join(', ');
|
|
589
|
+
throw new SimfinityError(
|
|
590
|
+
`Could not build input types for: ${unresolved}. Check for circular or misconfigured relations.`,
|
|
591
|
+
'INPUT_TYPE_UNRESOLVED',
|
|
592
|
+
500,
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
previousPendingCount = currentCount;
|
|
565
596
|
|
|
566
|
-
|
|
567
|
-
|
|
597
|
+
const stillWaiting = {};
|
|
598
|
+
for (const [key, value] of Object.entries(pending)) {
|
|
599
|
+
const { gqltype } = value;
|
|
600
|
+
if (typesDict.types[gqltype.name].inputType) continue;
|
|
568
601
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
typesDict.types[gqltype.name].inputType =
|
|
572
|
-
typesDictForUpdate.types[gqltype.name].inputType =
|
|
573
|
-
.inputTypeBodyForUpdate;
|
|
602
|
+
const result = buildInputType(gqltype);
|
|
603
|
+
if (result && result.inputTypeBody && result.inputTypeBodyForUpdate) {
|
|
604
|
+
typesDict.types[gqltype.name].inputType = result.inputTypeBody;
|
|
605
|
+
typesDictForUpdate.types[gqltype.name].inputType = result.inputTypeBodyForUpdate;
|
|
574
606
|
} else {
|
|
575
|
-
|
|
576
|
-
isThereAtLeastOneWaiting = true;
|
|
607
|
+
stillWaiting[key] = value;
|
|
577
608
|
}
|
|
578
609
|
}
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
if (isThereAtLeastOneWaiting) {
|
|
582
|
-
buildPendingInputTypes(stillWaitingInputType);
|
|
610
|
+
pending = stillWaiting;
|
|
583
611
|
}
|
|
584
612
|
};
|
|
585
613
|
|
|
@@ -622,9 +650,11 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
622
650
|
null, operation, session)).modelArgs;
|
|
623
651
|
}
|
|
624
652
|
} else {
|
|
625
|
-
|
|
626
|
-
.
|
|
627
|
-
|
|
653
|
+
throw new SimfinityError(
|
|
654
|
+
`Field ${gqltype.name}.${fieldEntryName} is an object type but does not define extensions.relation`,
|
|
655
|
+
'MISSING_RELATION_EXTENSION',
|
|
656
|
+
500,
|
|
657
|
+
);
|
|
628
658
|
}
|
|
629
659
|
} else if (fieldEntry.type instanceof GraphQLList) {
|
|
630
660
|
const { ofType } = fieldEntry.type;
|
|
@@ -668,34 +698,38 @@ const materializeModel = async (args, gqltype, linkToParent, operation, session)
|
|
|
668
698
|
const MAX_TRANSIENT_RETRIES = 5;
|
|
669
699
|
|
|
670
700
|
const withTransaction = async (session, body) => {
|
|
701
|
+
const ownsSession = !session;
|
|
671
702
|
const mySession = session || await mongoose.startSession();
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
703
|
+
try {
|
|
704
|
+
for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
|
|
705
|
+
await mySession.startTransaction();
|
|
706
|
+
try {
|
|
707
|
+
const result = await body(mySession);
|
|
708
|
+
await mySession.commitTransaction();
|
|
709
|
+
return result;
|
|
710
|
+
} catch (error) {
|
|
711
|
+
await mySession.abortTransaction();
|
|
712
|
+
const isTransient = error.errorLabels?.includes('TransientTransactionError');
|
|
713
|
+
if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
throw error;
|
|
684
717
|
}
|
|
718
|
+
}
|
|
719
|
+
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
720
|
+
} finally {
|
|
721
|
+
if (ownsSession) {
|
|
685
722
|
mySession.endSession();
|
|
686
|
-
throw error;
|
|
687
723
|
}
|
|
688
724
|
}
|
|
689
|
-
mySession.endSession();
|
|
690
|
-
throw new SimfinityError('Transaction exceeded retry limit', 'TRANSACTION_RETRY_EXCEEDED', 500);
|
|
691
725
|
};
|
|
692
726
|
|
|
693
|
-
const executeRegisteredMutation = (args, callback, session) => withTransaction(
|
|
727
|
+
const executeRegisteredMutation = (args, callback, context, session) => withTransaction(
|
|
694
728
|
session,
|
|
695
|
-
(mySession) => callback(args, mySession),
|
|
729
|
+
(mySession) => callback(args, mySession, context),
|
|
696
730
|
);
|
|
697
731
|
|
|
698
|
-
const
|
|
732
|
+
const iterateOnCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
|
|
699
733
|
for (const [collectionFieldKey, collectionField] of Object.entries(materializedModel.collectionFields)) {
|
|
700
734
|
if (collectionField.added) {
|
|
701
735
|
await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
|
|
@@ -741,7 +775,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
741
775
|
|
|
742
776
|
if (embeddedFieldNames.length > 0) {
|
|
743
777
|
const projection = Object.fromEntries(embeddedFieldNames.map((name) => [name, 1]));
|
|
744
|
-
const currentObject = await Model.findById(objectId, projection).lean();
|
|
778
|
+
const currentObject = await Model.findById(objectId, projection).session(session).lean();
|
|
745
779
|
if (currentObject) {
|
|
746
780
|
for (const fieldEntryName of embeddedFieldNames) {
|
|
747
781
|
const oldObjectData = currentObject[fieldEntryName];
|
|
@@ -775,7 +809,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
775
809
|
).session(session);
|
|
776
810
|
|
|
777
811
|
if (materializedModel.collectionFields) {
|
|
778
|
-
await
|
|
812
|
+
await iterateOnCollectionFields(materializedModel, gqltype, objectId, session, context);
|
|
779
813
|
}
|
|
780
814
|
|
|
781
815
|
if (controller && controller.onUpdated) {
|
|
@@ -786,7 +820,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
|
|
|
786
820
|
};
|
|
787
821
|
|
|
788
822
|
const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
|
|
789
|
-
const storedModel = await Model.findById(args.id);
|
|
823
|
+
const storedModel = await Model.findById(args.id).session(session);
|
|
790
824
|
if (!storedModel) {
|
|
791
825
|
throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
|
|
792
826
|
}
|
|
@@ -822,10 +856,9 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
|
|
|
822
856
|
result = result.toObject();
|
|
823
857
|
|
|
824
858
|
if (materializedModel.collectionFields) {
|
|
825
|
-
await
|
|
859
|
+
await iterateOnCollectionFields(materializedModel, gqltype, newObject._id, session, context);
|
|
826
860
|
}
|
|
827
861
|
|
|
828
|
-
|
|
829
862
|
if (controller && controller.onSaved) {
|
|
830
863
|
await controller.onSaved(result, args, session, context);
|
|
831
864
|
}
|
|
@@ -898,27 +931,25 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
|
|
|
898
931
|
const shouldNotBeIncludedInSchema = (includedTypes,
|
|
899
932
|
type) => includedTypes && !includedTypes.includes(type);
|
|
900
933
|
|
|
901
|
-
const
|
|
934
|
+
const executeMiddleware = async (context) => {
|
|
902
935
|
const buildNext = (middlewaresParam) => {
|
|
903
936
|
if (!middlewaresParam) {
|
|
904
|
-
return () => {};
|
|
937
|
+
return async () => {};
|
|
905
938
|
}
|
|
906
|
-
|
|
939
|
+
return async () => {
|
|
907
940
|
const middleware = middlewaresParam[0];
|
|
908
941
|
if (middleware) {
|
|
909
|
-
middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
942
|
+
await middleware(context, buildNext(middlewaresParam.slice(1)));
|
|
910
943
|
}
|
|
911
944
|
};
|
|
912
|
-
return next;
|
|
913
945
|
};
|
|
914
946
|
|
|
915
|
-
|
|
916
|
-
middleware();
|
|
947
|
+
await buildNext(middlewares)();
|
|
917
948
|
};
|
|
918
949
|
|
|
919
950
|
const executeScope = async (params) => {
|
|
920
951
|
const { type, args, operation, context } = params;
|
|
921
|
-
|
|
952
|
+
|
|
922
953
|
if (!type || !type.gqltype || !type.gqltype.extensions) {
|
|
923
954
|
return null;
|
|
924
955
|
}
|
|
@@ -960,7 +991,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
960
991
|
context,
|
|
961
992
|
};
|
|
962
993
|
|
|
963
|
-
|
|
994
|
+
await executeMiddleware(params);
|
|
964
995
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
965
996
|
args.input, operations.SAVE, null, null, context);
|
|
966
997
|
},
|
|
@@ -977,7 +1008,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
977
1008
|
context,
|
|
978
1009
|
};
|
|
979
1010
|
|
|
980
|
-
|
|
1011
|
+
await executeMiddleware(params);
|
|
981
1012
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
982
1013
|
args.id, operations.DELETE, null, null, context);
|
|
983
1014
|
},
|
|
@@ -1002,7 +1033,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1002
1033
|
context,
|
|
1003
1034
|
};
|
|
1004
1035
|
|
|
1005
|
-
|
|
1036
|
+
await executeMiddleware(params);
|
|
1006
1037
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1007
1038
|
args.input, operations.UPDATE, null, null, context);
|
|
1008
1039
|
},
|
|
@@ -1024,7 +1055,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1024
1055
|
context,
|
|
1025
1056
|
};
|
|
1026
1057
|
|
|
1027
|
-
|
|
1058
|
+
await executeMiddleware(params);
|
|
1028
1059
|
return executeOperation(type.model, type.gqltype, type.controller,
|
|
1029
1060
|
args.input, operations.STATE_CHANGED, actionField, null, context);
|
|
1030
1061
|
},
|
|
@@ -1051,8 +1082,8 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
|
|
|
1051
1082
|
entry,
|
|
1052
1083
|
context,
|
|
1053
1084
|
};
|
|
1054
|
-
|
|
1055
|
-
return executeRegisteredMutation(args.input, registeredMutation.callback);
|
|
1085
|
+
await executeMiddleware(params);
|
|
1086
|
+
return executeRegisteredMutation(args.input, registeredMutation.callback, context);
|
|
1056
1087
|
},
|
|
1057
1088
|
};
|
|
1058
1089
|
}
|
|
@@ -1128,10 +1159,10 @@ const generateSchemaDefinition = (gqlType) => {
|
|
|
1128
1159
|
|
|
1129
1160
|
const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
1130
1161
|
const objectIdFields = [];
|
|
1131
|
-
|
|
1162
|
+
|
|
1132
1163
|
for (const [fieldName, fieldDefinition] of Object.entries(schemaDefinition)) {
|
|
1133
1164
|
const currentPath = parentPath ? `${parentPath}.${fieldName}` : fieldName;
|
|
1134
|
-
|
|
1165
|
+
|
|
1135
1166
|
if (fieldDefinition === mongoose.Schema.Types.ObjectId) {
|
|
1136
1167
|
objectIdFields.push(currentPath);
|
|
1137
1168
|
} else if (typeof fieldDefinition === 'object' && fieldDefinition !== null) {
|
|
@@ -1147,7 +1178,7 @@ const findObjectIdFields = (schemaDefinition, parentPath = '') => {
|
|
|
1147
1178
|
}
|
|
1148
1179
|
}
|
|
1149
1180
|
}
|
|
1150
|
-
|
|
1181
|
+
|
|
1151
1182
|
return objectIdFields;
|
|
1152
1183
|
};
|
|
1153
1184
|
|
|
@@ -1172,7 +1203,12 @@ const generateModel = (gqlType, onModelCreated, { createCollection = true } = {}
|
|
|
1172
1203
|
return model;
|
|
1173
1204
|
};
|
|
1174
1205
|
|
|
1175
|
-
const coerceIdArray = (value) => value.map((element) =>
|
|
1206
|
+
const coerceIdArray = (value) => value.map((element) => {
|
|
1207
|
+
if (element === null || element === undefined) {
|
|
1208
|
+
throw new SimfinityError('ID value cannot be null in collection filter', 'INVALID_FILTER_VALUE', 400);
|
|
1209
|
+
}
|
|
1210
|
+
return new mongoose.Types.ObjectId(element);
|
|
1211
|
+
});
|
|
1176
1212
|
|
|
1177
1213
|
const buildMatchesClause = (fieldname, operator, value) => {
|
|
1178
1214
|
const isIdField = fieldname.endsWith('_id');
|
|
@@ -1181,11 +1217,12 @@ const buildMatchesClause = (fieldname, operator, value) => {
|
|
|
1181
1217
|
if (op === 'EQ') {
|
|
1182
1218
|
return { [fieldname]: isIdField ? new mongoose.Types.ObjectId(value) : value };
|
|
1183
1219
|
}
|
|
1184
|
-
if (op === 'IN') {
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1220
|
+
if (op === 'IN' || op === 'NIN') {
|
|
1221
|
+
if (!Array.isArray(value)) {
|
|
1222
|
+
throw new SimfinityError(`${op} requires an array value for ${fieldname}`, 'INVALID_FILTER_VALUE', 400);
|
|
1223
|
+
}
|
|
1224
|
+
const coerced = isIdField ? coerceIdArray(value) : value;
|
|
1225
|
+
return { [fieldname]: { [op === 'IN' ? '$in' : '$nin']: coerced } };
|
|
1189
1226
|
}
|
|
1190
1227
|
const builder = OP_TO_MONGO[op];
|
|
1191
1228
|
return builder ? { [fieldname]: builder(value) } : {};
|
|
@@ -1223,9 +1260,14 @@ const buildAggregationsForSort = (filterField, qlField, fieldName) => {
|
|
|
1223
1260
|
return aggregateClauses;
|
|
1224
1261
|
}
|
|
1225
1262
|
|
|
1263
|
+
if (!Array.isArray(filterField?.terms) || filterField.terms.length === 0) {
|
|
1264
|
+
return aggregateClauses;
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1226
1267
|
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1227
1268
|
|
|
1228
1269
|
filterField.terms.forEach((term) => {
|
|
1270
|
+
assertValidFilterPath(term.path);
|
|
1229
1271
|
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1230
1272
|
&& !aggregateClauses[fieldName]) {
|
|
1231
1273
|
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
@@ -1267,6 +1309,10 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1267
1309
|
const matchesClauses = {};
|
|
1268
1310
|
const fieldType = unwrapListAndNonNull(qlField.type);
|
|
1269
1311
|
|
|
1312
|
+
if (filterField == null) {
|
|
1313
|
+
return { aggregateClauses, matchesClauses };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1270
1316
|
if (fieldType instanceof GraphQLScalarType
|
|
1271
1317
|
|| isNonNullOfType(fieldType, GraphQLScalarType)
|
|
1272
1318
|
|| fieldType instanceof GraphQLEnumType
|
|
@@ -1280,9 +1326,14 @@ const buildQueryTerms = async (filterField, qlField, fieldName) => {
|
|
|
1280
1326
|
return { aggregateClauses, matchesClauses };
|
|
1281
1327
|
}
|
|
1282
1328
|
|
|
1329
|
+
if (!Array.isArray(filterField.terms) || filterField.terms.length === 0) {
|
|
1330
|
+
return { aggregateClauses, matchesClauses };
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1283
1333
|
const resolvedFieldType = unwrapNonNull(fieldType);
|
|
1284
1334
|
|
|
1285
1335
|
filterField.terms.forEach((term) => {
|
|
1336
|
+
assertValidFilterPath(term.path);
|
|
1286
1337
|
if (qlField.extensions?.relation && !qlField.extensions.relation.embedded
|
|
1287
1338
|
&& !aggregateClauses[fieldName]) {
|
|
1288
1339
|
aggregateClauses[fieldName] = topLevelRelationLookup(qlField, resolvedFieldType, fieldName);
|
|
@@ -1467,6 +1518,7 @@ const buildSortClause = (sortTerms, gqltype, aggregateClauses, aggregationsInclu
|
|
|
1467
1518
|
const fields = gqltype.getFields();
|
|
1468
1519
|
|
|
1469
1520
|
sortTerms.forEach((sort) => {
|
|
1521
|
+
assertValidFilterPath(sort.field);
|
|
1470
1522
|
let fixedSortField = sort.field;
|
|
1471
1523
|
if (sort.field.indexOf('.') >= 0) {
|
|
1472
1524
|
const sortParts = sort.field.split('.');
|
|
@@ -1506,19 +1558,19 @@ const buildQuery = async (input, gqltype, isCount) => {
|
|
|
1506
1558
|
aggregateClauses.push(buildSortClause(input.sort.terms, gqltype, aggregateClauses, aggregationsIncluded));
|
|
1507
1559
|
}
|
|
1508
1560
|
|
|
1509
|
-
let limitClause = { $limit: 100 };
|
|
1510
1561
|
let skipClause = { $skip: 0 };
|
|
1562
|
+
let limitClause = { $limit: 100 };
|
|
1511
1563
|
if (input.pagination?.page && input.pagination?.size) {
|
|
1512
|
-
|
|
1513
|
-
limitClause = { $limit: input.pagination.size
|
|
1514
|
-
skipClause = { $skip: skip };
|
|
1564
|
+
skipClause = { $skip: input.pagination.size * (input.pagination.page - 1) };
|
|
1565
|
+
limitClause = { $limit: input.pagination.size };
|
|
1515
1566
|
}
|
|
1516
|
-
aggregateClauses.push(
|
|
1567
|
+
aggregateClauses.push(skipClause, limitClause);
|
|
1517
1568
|
|
|
1518
1569
|
return aggregateClauses;
|
|
1519
1570
|
};
|
|
1520
1571
|
|
|
1521
1572
|
const buildFieldPath = (gqltype, fieldPath) => {
|
|
1573
|
+
assertValidFilterPath(fieldPath);
|
|
1522
1574
|
const pathParts = fieldPath.split('.');
|
|
1523
1575
|
const lookupPairs = [];
|
|
1524
1576
|
let currentPath = '';
|
|
@@ -1617,7 +1669,7 @@ const buildAggregationQuery = async (input, gqltype, aggregationExpression) => {
|
|
|
1617
1669
|
|
|
1618
1670
|
if (input.pagination?.page && input.pagination?.size) {
|
|
1619
1671
|
const skip = input.pagination.size * (input.pagination.page - 1);
|
|
1620
|
-
aggregateClauses.push({ $
|
|
1672
|
+
aggregateClauses.push({ $skip: skip }, { $limit: input.pagination.size });
|
|
1621
1673
|
}
|
|
1622
1674
|
|
|
1623
1675
|
return aggregateClauses;
|
|
@@ -1643,7 +1695,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1643
1695
|
const params = {
|
|
1644
1696
|
type, args, operation: 'get_by_id', context,
|
|
1645
1697
|
};
|
|
1646
|
-
|
|
1698
|
+
await executeMiddleware(params);
|
|
1647
1699
|
|
|
1648
1700
|
const hasScope = type.gqltype.extensions?.scope?.get_by_id;
|
|
1649
1701
|
if (!hasScope) {
|
|
@@ -1677,7 +1729,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1677
1729
|
operation: 'find',
|
|
1678
1730
|
context,
|
|
1679
1731
|
};
|
|
1680
|
-
|
|
1732
|
+
await executeMiddleware(params);
|
|
1681
1733
|
await executeScope(params);
|
|
1682
1734
|
const aggregateClauses = await buildQuery(args, type.gqltype);
|
|
1683
1735
|
const wantsCount = !!(args.pagination && args.pagination.count);
|
|
@@ -1712,7 +1764,7 @@ const buildRootQuery = (name, includedTypes) => {
|
|
|
1712
1764
|
operation: 'aggregate',
|
|
1713
1765
|
context,
|
|
1714
1766
|
};
|
|
1715
|
-
|
|
1767
|
+
await executeMiddleware(params);
|
|
1716
1768
|
await executeScope(params);
|
|
1717
1769
|
const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
|
|
1718
1770
|
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
|
-
}
|