@simtlix/simfinity-js 2.1.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "2.1.0",
3
+ "version": "2.3.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/index.js CHANGED
@@ -614,53 +614,53 @@ const executeRegisteredMutation = async (args, callback, session) => {
614
614
  }
615
615
  };
616
616
 
617
- const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session) => {
617
+ const iterateonCollectionFields = async (materializedModel, gqltype, objectId, session, context) => {
618
618
  for (const [collectionFieldKey, collectionField] of
619
619
  Object.entries(materializedModel.collectionFields)) {
620
620
  if (collectionField.added) {
621
621
 
622
622
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
623
- collectionField.added, operations.SAVE);
623
+ collectionField.added, operations.SAVE, context);
624
624
  }
625
625
  if (collectionField.updated) {
626
626
 
627
627
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
628
- collectionField.updated, operations.UPDATE);
628
+ collectionField.updated, operations.UPDATE, context);
629
629
  }
630
630
  if (collectionField.deleted) {
631
631
 
632
632
  await executeItemFunction(gqltype, collectionFieldKey, objectId, session,
633
- collectionField.deleted, operations.DELETE);
633
+ collectionField.deleted, operations.DELETE, context);
634
634
  }
635
635
  }
636
636
  };
637
637
 
638
- const onDeleteObject = async (Model, gqltype, controller, args, session) => {
638
+ const onDeleteObject = async (Model, gqltype, controller, args, session, context) => {
639
639
  const deletedObject = await Model.findById({ _id: args }).session(session).lean();
640
640
 
641
641
  if (controller && controller.onDelete) {
642
- await controller.onDelete(deletedObject, session);
642
+ await controller.onDelete(deletedObject, session, context);
643
643
  }
644
644
 
645
645
  return Model.findByIdAndDelete({ _id: args }).session(session);
646
646
  };
647
647
 
648
- const onDeleteSubject = async (Model, controller, id, session) => {
648
+ const onDeleteSubject = async (Model, controller, id, session, context) => {
649
649
  const currentObject = await Model.findById({ _id: id }).session(session).lean();
650
650
 
651
651
  if (controller && controller.onDelete) {
652
- await controller.onDelete(currentObject, session);
652
+ await controller.onDelete(currentObject, session, context);
653
653
  }
654
654
 
655
655
  return Model.findByIdAndDelete({ _id: id }).session(session);
656
656
  };
657
657
 
658
- const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent) => {
658
+ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
659
659
  const materializedModel = await materializeModel(args, gqltype, linkToParent, 'UPDATE', session);
660
660
  const objectId = args.id;
661
661
 
662
662
  if (materializedModel.collectionFields) {
663
- await iterateonCollectionFields(materializedModel, gqltype, objectId, session);
663
+ await iterateonCollectionFields(materializedModel, gqltype, objectId, session, context);
664
664
  }
665
665
 
666
666
  const currentObject = await Model.findById({ _id: objectId }).lean();
@@ -688,7 +688,7 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
688
688
  });
689
689
 
690
690
  if (controller && controller.onUpdating) {
691
- await controller.onUpdating(objectId, materializedModel.modelArgs, session);
691
+ await controller.onUpdating(objectId, materializedModel.modelArgs, session, context);
692
692
  }
693
693
 
694
694
  const result = Model.findByIdAndUpdate(
@@ -696,13 +696,13 @@ const onUpdateSubject = async (Model, gqltype, controller, args, session, linkTo
696
696
  ).session(session);
697
697
 
698
698
  if (controller && controller.onUpdated) {
699
- await controller.onUpdated(result, session);
699
+ await controller.onUpdated(result, session, context);
700
700
  }
701
701
 
702
702
  return result;
703
703
  };
704
704
 
705
- const onStateChanged = async (Model, gqltype, controller, args, session, actionField) => {
705
+ const onStateChanged = async (Model, gqltype, controller, args, session, actionField, context) => {
706
706
  const storedModel = await Model.findById(args.id);
707
707
  if (!storedModel) {
708
708
  throw new SimfinityError(`${gqltype.name} ${args.id} is not valid`, 'NOT_VALID_ID', 404);
@@ -713,7 +713,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
713
713
  }
714
714
 
715
715
  args.state = actionField.to.name;
716
- let result = await onUpdateSubject(Model, gqltype, controller, args, session);
716
+ let result = await onUpdateSubject(Model, gqltype, controller, args, session, null, context);
717
717
  result = result.toObject();
718
718
  result.state = actionField.to.value;
719
719
  return result;
@@ -721,7 +721,7 @@ const onStateChanged = async (Model, gqltype, controller, args, session, actionF
721
721
  throw new SimfinityError(`Action is not allowed from state ${storedModel.state}`, 'BAD_REQUEST', 400);
722
722
  };
723
723
 
724
- const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent) => {
724
+ const onSaveObject = async (Model, gqltype, controller, args, session, linkToParent, context) => {
725
725
  const materializedModel = await materializeModel(args, gqltype, linkToParent, 'CREATE', session);
726
726
  if (typesDict.types[gqltype.name].stateMachine) {
727
727
  materializedModel.modelArgs.state = typesDict.types[gqltype.name]
@@ -732,17 +732,17 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
732
732
  newObject.$session(session);
733
733
 
734
734
  if (controller && controller.onSaving) {
735
- await controller.onSaving(newObject, args, session);
735
+ await controller.onSaving(newObject, args, session, context);
736
736
  }
737
737
 
738
738
  if (materializedModel.collectionFields) {
739
- await iterateonCollectionFields(materializedModel, gqltype, newObject._id, session);
739
+ await iterateonCollectionFields(materializedModel, gqltype, newObject._id, session, context);
740
740
  }
741
741
 
742
742
  let result = await newObject.save();
743
743
  result = result.toObject();
744
744
  if (controller && controller.onSaved) {
745
- await controller.onSaved(result, args, session);
745
+ await controller.onSaved(result, args, session, context);
746
746
  }
747
747
  if (typesDict.types[gqltype.name].stateMachine) {
748
748
  result.state = typesDict.types[gqltype.name].stateMachine.initialState.value;
@@ -750,29 +750,29 @@ const onSaveObject = async (Model, gqltype, controller, args, session, linkToPar
750
750
  return result;
751
751
  };
752
752
 
753
- export const saveObject = async (typeName, args, session) => {
753
+ export const saveObject = async (typeName, args, session, context) => {
754
754
  const type = typesDict.types[typeName];
755
- return onSaveObject(type.model, type.gqltype, type.controller, args, session);
755
+ return onSaveObject(type.model, type.gqltype, type.controller, args, session, null, context);
756
756
  };
757
757
 
758
758
  const executeOperation = async (Model, gqltype, controller,
759
- args, operation, actionField, session) => {
759
+ args, operation, actionField, session, context) => {
760
760
  const mySession = session || await mongoose.startSession();
761
761
  await mySession.startTransaction();
762
762
  try {
763
763
  let newObject = null;
764
764
  switch (operation) {
765
765
  case operations.SAVE:
766
- newObject = await onSaveObject(Model, gqltype, controller, args, mySession);
766
+ newObject = await onSaveObject(Model, gqltype, controller, args, mySession, null, context);
767
767
  break;
768
768
  case operations.UPDATE:
769
- newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession);
769
+ newObject = await onUpdateSubject(Model, gqltype, controller, args, mySession, null, context);
770
770
  break;
771
771
  case operations.DELETE:
772
- newObject = await onDeleteObject(Model, gqltype, controller, args, mySession);
772
+ newObject = await onDeleteObject(Model, gqltype, controller, args, mySession, context);
773
773
  break;
774
774
  case operations.STATE_CHANGED:
775
- newObject = await onStateChanged(Model, gqltype, controller, args, mySession, actionField);
775
+ newObject = await onStateChanged(Model, gqltype, controller, args, mySession, actionField, context);
776
776
  break;
777
777
  }
778
778
  await mySession.commitTransaction();
@@ -781,7 +781,7 @@ const executeOperation = async (Model, gqltype, controller,
781
781
  } catch (error) {
782
782
  await mySession.abortTransaction();
783
783
  if (error.errorLabels && error.errorLabels.includes('TransientTransactionError')) {
784
- return executeOperation(Model, gqltype, controller, args, operation, actionField, mySession);
784
+ return executeOperation(Model, gqltype, controller, args, operation, actionField, mySession, context);
785
785
  }
786
786
  mySession.endSession();
787
787
  throw error;
@@ -789,7 +789,7 @@ const executeOperation = async (Model, gqltype, controller,
789
789
  };
790
790
 
791
791
  const executeItemFunction = async (gqltype, collectionField, objectId, session,
792
- collectionFieldsList, operationType) => {
792
+ collectionFieldsList, operationType, context) => {
793
793
  const argTypes = gqltype.getFields();
794
794
  const collectionGQLType = argTypes[collectionField].type.ofType;
795
795
  const { connectionField } = argTypes[collectionField].extensions.relation;
@@ -802,7 +802,7 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
802
802
  await onSaveObject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
803
803
  typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
804
804
  item[connectionField] = objectId;
805
- });
805
+ }, context);
806
806
  };
807
807
  break;
808
808
  case operations.UPDATE:
@@ -810,13 +810,13 @@ const executeItemFunction = async (gqltype, collectionField, objectId, session,
810
810
  await onUpdateSubject(typesDict.types[collectionGQLType.name].model, collectionGQLType,
811
811
  typesDict.types[collectionGQLType.name].controller, collectionItem, session, (item) => {
812
812
  item[connectionField] = objectId;
813
- });
813
+ }, context);
814
814
  };
815
815
  break;
816
816
  case operations.DELETE:
817
817
  operationFunction = async (collectionItem) => {
818
818
  await onDeleteSubject(typesDict.types[collectionGQLType.name].model,
819
- typesDict.types[collectionGQLType.name].controller, collectionItem, session);
819
+ typesDict.types[collectionGQLType.name].controller, collectionItem, session, context);
820
820
  };
821
821
  }
822
822
 
@@ -846,6 +846,31 @@ const excecuteMiddleware = (context) => {
846
846
  middleware();
847
847
  };
848
848
 
849
+ const executeScope = async (params) => {
850
+ const { type, args, operation, context } = params;
851
+
852
+ if (!type || !type.gqltype || !type.gqltype.extensions) {
853
+ return null;
854
+ }
855
+
856
+ const extensions = type.gqltype.extensions;
857
+ if (!extensions.scope || !extensions.scope[operation]) {
858
+ return null;
859
+ }
860
+
861
+ const scopeFunction = extensions.scope[operation];
862
+ if (typeof scopeFunction !== 'function') {
863
+ return null;
864
+ }
865
+
866
+ // Call the scope function with the same params as middleware
867
+ const result = await scopeFunction({ type, args, operation, context });
868
+
869
+ // For get_by_id, the scope function returns additional filters to merge
870
+ // For find and aggregate, it modifies args in place
871
+ return result;
872
+ };
873
+
849
874
  const buildMutation = (name, includedMutationTypes, includedCustomMutations) => {
850
875
  const rootQueryArgs = {};
851
876
  rootQueryArgs.name = name;
@@ -872,7 +897,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
872
897
 
873
898
  excecuteMiddleware(params);
874
899
  return executeOperation(type.model, type.gqltype, type.controller,
875
- args.input, operations.SAVE);
900
+ args.input, operations.SAVE, null, null, context);
876
901
  },
877
902
  };
878
903
  rootQueryArgs.fields[`delete${type.simpleEntityEndpointName}`] = {
@@ -889,7 +914,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
889
914
 
890
915
  excecuteMiddleware(params);
891
916
  return executeOperation(type.model, type.gqltype, type.controller,
892
- args.id, operations.DELETE);
917
+ args.id, operations.DELETE, null, null, context);
893
918
  },
894
919
  };
895
920
  }
@@ -914,7 +939,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
914
939
 
915
940
  excecuteMiddleware(params);
916
941
  return executeOperation(type.model, type.gqltype, type.controller,
917
- args.input, operations.UPDATE);
942
+ args.input, operations.UPDATE, null, null, context);
918
943
  },
919
944
  };
920
945
  if (type.stateMachine) {
@@ -936,7 +961,7 @@ const buildMutation = (name, includedMutationTypes, includedCustomMutations) =>
936
961
 
937
962
  excecuteMiddleware(params);
938
963
  return executeOperation(type.model, type.gqltype, type.controller,
939
- args.input, operations.STATE_CHANGED, actionField);
964
+ args.input, operations.STATE_CHANGED, actionField, null, context);
940
965
  },
941
966
  };
942
967
  }
@@ -1829,7 +1854,44 @@ const buildRootQuery = (name, includedTypes) => {
1829
1854
  context,
1830
1855
  };
1831
1856
  excecuteMiddleware(params);
1832
- return await type.model.findById(args.id);
1857
+
1858
+ // Check if scope is defined for get_by_id
1859
+ const hasScope = type.gqltype.extensions && type.gqltype.extensions.scope && type.gqltype.extensions.scope.get_by_id;
1860
+
1861
+ if (hasScope) {
1862
+ // Build query args with id filter - scope function will modify this
1863
+ const queryArgs = {
1864
+ id: { operator: 'EQ', value: args.id },
1865
+ };
1866
+
1867
+ // Create temporary params with queryArgs for scope function
1868
+ const scopeParams = {
1869
+ type,
1870
+ args: queryArgs,
1871
+ operation: 'get_by_id',
1872
+ context,
1873
+ };
1874
+
1875
+ // Execute scope which will modify queryArgs in place
1876
+ await executeScope(scopeParams);
1877
+
1878
+ // Build aggregation pipeline from the combined filters
1879
+ const aggregateClauses = await buildQuery(queryArgs, type.gqltype);
1880
+
1881
+ // Execute the query and get the first result
1882
+ let result;
1883
+ if (aggregateClauses.length === 0) {
1884
+ result = await type.model.findOne({ _id: args.id });
1885
+ } else {
1886
+ const results = await type.model.aggregate(aggregateClauses);
1887
+ result = results.length > 0 ? results[0] : null;
1888
+ }
1889
+
1890
+ return result;
1891
+ } else {
1892
+ // No scope defined, use the original findById
1893
+ return await type.model.findById(args.id);
1894
+ }
1833
1895
  },
1834
1896
  };
1835
1897
 
@@ -1848,6 +1910,7 @@ const buildRootQuery = (name, includedTypes) => {
1848
1910
  context,
1849
1911
  };
1850
1912
  excecuteMiddleware(params);
1913
+ await executeScope(params);
1851
1914
  const aggregateClauses = await buildQuery(args, type.gqltype);
1852
1915
  if (args.pagination && args.pagination.count) {
1853
1916
  const aggregateClausesForCount = await buildQuery(args, type.gqltype, true);
@@ -1882,6 +1945,7 @@ const buildRootQuery = (name, includedTypes) => {
1882
1945
  context,
1883
1946
  };
1884
1947
  excecuteMiddleware(params);
1948
+ await executeScope(params);
1885
1949
  const aggregateClauses = await buildAggregationQuery(args, type.gqltype, args.aggregation);
1886
1950
  const result = await type.model.aggregate(aggregateClauses);
1887
1951
  return result;
@@ -2075,6 +2139,9 @@ export const addNoEndpointType = (gqltype) => {
2075
2139
 
2076
2140
  export { createValidatedScalar };
2077
2141
 
2142
+ export { default as validators } from './validators.js';
2143
+ export { default as scalars } from './scalars.js';
2144
+
2078
2145
  const createArgsForQuery = (argTypes) => {
2079
2146
  const argsObject = {};
2080
2147
 
package/src/scalars.js ADDED
@@ -0,0 +1,188 @@
1
+ import {
2
+ GraphQLString, GraphQLInt, GraphQLFloat,
3
+ } from 'graphql';
4
+ import { createValidatedScalar } from './index.js';
5
+
6
+ /**
7
+ * Email scalar - validates email format
8
+ * Type name: Email_String
9
+ */
10
+ export const EmailScalar = createValidatedScalar(
11
+ 'Email',
12
+ 'A valid email address',
13
+ GraphQLString,
14
+ (value) => {
15
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
16
+ if (!emailRegex.test(value)) {
17
+ throw new Error('Invalid email format');
18
+ }
19
+ },
20
+ );
21
+
22
+ /**
23
+ * URL scalar - validates URL format
24
+ * Type name: URL_String
25
+ */
26
+ export const URLScalar = createValidatedScalar(
27
+ 'URL',
28
+ 'A valid URL',
29
+ GraphQLString,
30
+ (value) => {
31
+ try {
32
+ new URL(value);
33
+ } catch {
34
+ throw new Error('Invalid URL format');
35
+ }
36
+ },
37
+ );
38
+
39
+ /**
40
+ * PositiveInt scalar - validates positive integers
41
+ * Type name: PositiveInt_Int
42
+ */
43
+ export const PositiveIntScalar = createValidatedScalar(
44
+ 'PositiveInt',
45
+ 'A positive integer',
46
+ GraphQLInt,
47
+ (value) => {
48
+ if (value <= 0) {
49
+ throw new Error('Value must be positive');
50
+ }
51
+ },
52
+ );
53
+
54
+ /**
55
+ * PositiveFloat scalar - validates positive floats
56
+ * Type name: PositiveFloat_Float
57
+ */
58
+ export const PositiveFloatScalar = createValidatedScalar(
59
+ 'PositiveFloat',
60
+ 'A positive float',
61
+ GraphQLFloat,
62
+ (value) => {
63
+ if (value <= 0) {
64
+ throw new Error('Value must be positive');
65
+ }
66
+ },
67
+ );
68
+
69
+ /**
70
+ * Factory function to create a bounded string scalar
71
+ * @param {string} name - Name for the scalar
72
+ * @param {number} min - Minimum length
73
+ * @param {number} max - Maximum length
74
+ * @returns {GraphQLScalarType} A scalar type with length validation
75
+ */
76
+ export const createBoundedStringScalar = (name, min, max) => {
77
+ return createValidatedScalar(
78
+ name,
79
+ `A string with length between ${min} and ${max} characters`,
80
+ GraphQLString,
81
+ (value) => {
82
+ if (typeof value !== 'string') {
83
+ throw new Error('Value must be a string');
84
+ }
85
+ if (min !== undefined && value.length < min) {
86
+ throw new Error(`String must be at least ${min} characters`);
87
+ }
88
+ if (max !== undefined && value.length > max) {
89
+ throw new Error(`String must be at most ${max} characters`);
90
+ }
91
+ },
92
+ );
93
+ };
94
+
95
+ /**
96
+ * Factory function to create a bounded integer scalar
97
+ * @param {string} name - Name for the scalar
98
+ * @param {number} min - Minimum value
99
+ * @param {number} max - Maximum value
100
+ * @returns {GraphQLScalarType} A scalar type with range validation
101
+ */
102
+ export const createBoundedIntScalar = (name, min, max) => {
103
+ return createValidatedScalar(
104
+ name,
105
+ `An integer between ${min} and ${max}`,
106
+ GraphQLInt,
107
+ (value) => {
108
+ if (typeof value !== 'number' || isNaN(value)) {
109
+ throw new Error('Value must be a number');
110
+ }
111
+ if (min !== undefined && value < min) {
112
+ throw new Error(`Value must be at least ${min}`);
113
+ }
114
+ if (max !== undefined && value > max) {
115
+ throw new Error(`Value must be at most ${max}`);
116
+ }
117
+ },
118
+ );
119
+ };
120
+
121
+ /**
122
+ * Factory function to create a bounded float scalar
123
+ * @param {string} name - Name for the scalar
124
+ * @param {number} min - Minimum value
125
+ * @param {number} max - Maximum value
126
+ * @returns {GraphQLScalarType} A scalar type with range validation
127
+ */
128
+ export const createBoundedFloatScalar = (name, min, max) => {
129
+ return createValidatedScalar(
130
+ name,
131
+ `A float between ${min} and ${max}`,
132
+ GraphQLFloat,
133
+ (value) => {
134
+ if (typeof value !== 'number' || isNaN(value)) {
135
+ throw new Error('Value must be a number');
136
+ }
137
+ if (min !== undefined && value < min) {
138
+ throw new Error(`Value must be at least ${min}`);
139
+ }
140
+ if (max !== undefined && value > max) {
141
+ throw new Error(`Value must be at most ${max}`);
142
+ }
143
+ },
144
+ );
145
+ };
146
+
147
+ /**
148
+ * Factory function to create a regex pattern string scalar
149
+ * @param {string} name - Name for the scalar
150
+ * @param {RegExp|string} pattern - Regex pattern to validate against
151
+ * @param {string} message - Error message if validation fails
152
+ * @returns {GraphQLScalarType} A scalar type with pattern validation
153
+ */
154
+ export const createPatternStringScalar = (name, pattern, message) => {
155
+ const regex = typeof pattern === 'string' ? new RegExp(pattern) : pattern;
156
+ const errorMessage = message || 'Value does not match required pattern';
157
+
158
+ return createValidatedScalar(
159
+ name,
160
+ `A string matching the pattern: ${pattern}`,
161
+ GraphQLString,
162
+ (value) => {
163
+ if (typeof value !== 'string') {
164
+ throw new Error('Value must be a string');
165
+ }
166
+ if (!regex.test(value)) {
167
+ throw new Error(errorMessage);
168
+ }
169
+ },
170
+ );
171
+ };
172
+
173
+ // Export all scalars as an object for convenience
174
+ const scalars = {
175
+ // Pre-built scalars
176
+ EmailScalar,
177
+ URLScalar,
178
+ PositiveIntScalar,
179
+ PositiveFloatScalar,
180
+ // Factory functions
181
+ createBoundedStringScalar,
182
+ createBoundedIntScalar,
183
+ createBoundedFloatScalar,
184
+ createPatternStringScalar,
185
+ };
186
+
187
+ export default scalars;
188
+