@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.
Files changed (54) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +134 -82
  3. package/.claude/worktrees/agitated-kepler/.claude/settings.local.json +0 -23
  4. package/.claude/worktrees/agitated-kepler/AGGREGATION_CHANGES_SUMMARY.md +0 -235
  5. package/.claude/worktrees/agitated-kepler/AGGREGATION_EXAMPLE.md +0 -567
  6. package/.claude/worktrees/agitated-kepler/LICENSE +0 -201
  7. package/.claude/worktrees/agitated-kepler/README.md +0 -3941
  8. package/.claude/worktrees/agitated-kepler/eslint.config.mjs +0 -71
  9. package/.claude/worktrees/agitated-kepler/package-lock.json +0 -4740
  10. package/.claude/worktrees/agitated-kepler/package.json +0 -41
  11. package/.claude/worktrees/agitated-kepler/src/auth/errors.js +0 -44
  12. package/.claude/worktrees/agitated-kepler/src/auth/expressions.js +0 -273
  13. package/.claude/worktrees/agitated-kepler/src/auth/index.js +0 -391
  14. package/.claude/worktrees/agitated-kepler/src/auth/rules.js +0 -274
  15. package/.claude/worktrees/agitated-kepler/src/const/QLOperator.js +0 -39
  16. package/.claude/worktrees/agitated-kepler/src/const/QLSort.js +0 -28
  17. package/.claude/worktrees/agitated-kepler/src/const/QLValue.js +0 -39
  18. package/.claude/worktrees/agitated-kepler/src/errors/internal-server.error.js +0 -11
  19. package/.claude/worktrees/agitated-kepler/src/errors/simfinity.error.js +0 -15
  20. package/.claude/worktrees/agitated-kepler/src/index.js +0 -2412
  21. package/.claude/worktrees/agitated-kepler/src/plugins.js +0 -53
  22. package/.claude/worktrees/agitated-kepler/src/scalars.js +0 -188
  23. package/.claude/worktrees/agitated-kepler/src/validators.js +0 -250
  24. package/.claude/worktrees/agitated-kepler/yarn.lock +0 -1154
  25. package/.claude/worktrees/distracted-swirles-4601fe/.claude/settings.local.json +0 -10
  26. package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_CHANGES_SUMMARY.md +0 -235
  27. package/.claude/worktrees/distracted-swirles-4601fe/AGGREGATION_EXAMPLE.md +0 -567
  28. package/.claude/worktrees/distracted-swirles-4601fe/LICENSE +0 -201
  29. package/.claude/worktrees/distracted-swirles-4601fe/README.md +0 -3941
  30. package/.claude/worktrees/distracted-swirles-4601fe/eslint.config.mjs +0 -71
  31. package/.claude/worktrees/distracted-swirles-4601fe/package-lock.json +0 -4740
  32. package/.claude/worktrees/distracted-swirles-4601fe/package.json +0 -41
  33. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/errors.js +0 -44
  34. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/expressions.js +0 -273
  35. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/index.js +0 -391
  36. package/.claude/worktrees/distracted-swirles-4601fe/src/auth/rules.js +0 -274
  37. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLOperator.js +0 -39
  38. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLSort.js +0 -28
  39. package/.claude/worktrees/distracted-swirles-4601fe/src/const/QLValue.js +0 -39
  40. package/.claude/worktrees/distracted-swirles-4601fe/src/errors/internal-server.error.js +0 -11
  41. package/.claude/worktrees/distracted-swirles-4601fe/src/errors/simfinity.error.js +0 -15
  42. package/.claude/worktrees/distracted-swirles-4601fe/src/index.js +0 -1925
  43. package/.claude/worktrees/distracted-swirles-4601fe/src/plugins.js +0 -53
  44. package/.claude/worktrees/distracted-swirles-4601fe/src/scalars.js +0 -188
  45. package/.claude/worktrees/distracted-swirles-4601fe/src/validators.js +0 -250
  46. package/.claude/worktrees/distracted-swirles-4601fe/yarn.lock +0 -1154
  47. package/.cursor/rules/simfinity-architecture.mdc +0 -71
  48. package/.cursor/rules/simfinity-auth-module.mdc +0 -100
  49. package/.cursor/rules/simfinity-coding-standards.mdc +0 -62
  50. package/.cursor/rules/simfinity-core-functions.mdc +0 -84
  51. package/.cursor/rules/simfinity-documentation.mdc +0 -59
  52. package/.cursor/rules/simfinity-extensions.mdc +0 -121
  53. package/.cursor/rules/simfinity-testing.mdc +0 -90
  54. package/eslint.config.mjs +0 -71
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.6.1",
3
+ "version": "2.6.3",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
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).filter(([fieldName]) => fieldName !== fieldToExclude),
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 doesEstateFieldExistButIsManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
471
+ const stateFieldManagedByStateMachine = !!(fieldEntryName === 'state' && hasStateMachine);
450
472
 
451
- if (!doesEstateFieldExistButIsManagedByStateMachine) {
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 + 'A', fieldEntry.extensions?.relation?.connectionField);
488
- const listInputTypeForUpdate = graphQLListInputType(typesDictForUpdate, fieldEntry, fieldEntryName, gqltype.name +'U', fieldEntry.extensions?.relation?.connectionField);
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
- const stillWaitingInputType = {};
561
- let isThereAtLeastOneWaiting = false;
562
-
563
- Object.entries(waitingForInputType).forEach(([key, value]) => {
564
- const { gqltype } = value;
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
- if (!typesDict.types[gqltype.name].inputType) {
567
- const buildInputTypeResult = buildInputType(gqltype);
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
- if (buildInputTypeResult && buildInputTypeResult.inputTypeBody
570
- && buildInputTypeResult.inputTypeBodyForUpdate) {
571
- typesDict.types[gqltype.name].inputType = buildInputTypeResult.inputTypeBody;
572
- typesDictForUpdate.types[gqltype.name].inputType = buildInputTypeResult
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
- stillWaitingInputType[key] = value;
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
- modelArgs[fieldEntry.name] = new mongoose.Types
626
- .ObjectId(args[fieldEntryName].id);
627
- console.warn(`Configuration issue: Field ${fieldEntryName} does not define extensions.relation`);
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
- for (let attempt = 0; attempt <= MAX_TRANSIENT_RETRIES; attempt++) {
673
- await mySession.startTransaction();
674
- try {
675
- const result = await body(mySession);
676
- await mySession.commitTransaction();
677
- mySession.endSession();
678
- return result;
679
- } catch (error) {
680
- await mySession.abortTransaction();
681
- const isTransient = error.errorLabels?.includes('TransientTransactionError');
682
- if (isTransient && attempt < MAX_TRANSIENT_RETRIES) {
683
- continue;
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 iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
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 iterateonCollectionFields(materializedModel, gqltype, objectId, session, context);
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 iterateonCollectionFields(materializedModel, gqltype, newObject._id, session, context);
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 excecuteMiddleware = (context) => {
934
+ const executeMiddleware = async (context) => {
902
935
  const buildNext = (middlewaresParam) => {
903
936
  if (!middlewaresParam) {
904
- return () => {};
937
+ return async () => {};
905
938
  }
906
- const next = () => {
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
- const middleware = buildNext(middlewares);
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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) => new mongoose.Types.ObjectId(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
- return { [fieldname]: { $in: value && isIdField ? coerceIdArray(value) : value } };
1186
- }
1187
- if (op === 'NIN') {
1188
- return { [fieldname]: { $nin: value && isIdField ? coerceIdArray(value) : value } };
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
- const skip = input.pagination.size * (input.pagination.page - 1);
1513
- limitClause = { $limit: input.pagination.size + skip };
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(limitClause, skipClause);
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({ $limit: input.pagination.size + skip }, { $skip: skip });
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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
- excecuteMiddleware(params);
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
- }