@simtlix/simfinity-js 1.9.0 → 2.0.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/README.md CHANGED
@@ -204,6 +204,122 @@ query {
204
204
  - `NIN` - Not in array
205
205
  - `BTW` - Between two values
206
206
 
207
+ ### Collection Field Filtering
208
+
209
+ Simfinity.js now supports filtering collection fields (one-to-many relationships) using the same powerful query format. This allows you to filter related objects directly within your GraphQL queries.
210
+
211
+ #### Basic Collection Filtering
212
+
213
+ Filter collection fields using the same operators and format as main queries:
214
+
215
+ ```graphql
216
+ query {
217
+ series {
218
+ seasons(number: { operator: EQ, value: 1 }) {
219
+ number
220
+ id
221
+ year
222
+ }
223
+ }
224
+ }
225
+ ```
226
+
227
+ #### Advanced Collection Filtering
228
+
229
+ You can use complex filtering with nested object properties:
230
+
231
+ ```graphql
232
+ query {
233
+ series {
234
+ seasons(
235
+ year: { operator: GTE, value: 2020 }
236
+ episodes: {
237
+ terms: [
238
+ {
239
+ path: "name",
240
+ operator: LIKE,
241
+ value: "Pilot"
242
+ }
243
+ ]
244
+ }
245
+ ) {
246
+ number
247
+ year
248
+ episodes {
249
+ name
250
+ date
251
+ }
252
+ }
253
+ }
254
+ }
255
+ ```
256
+
257
+ #### Collection Filtering with Multiple Conditions
258
+
259
+ Combine multiple filter conditions for collection fields:
260
+
261
+ ```graphql
262
+ query {
263
+ series {
264
+ seasons(
265
+ number: { operator: GT, value: 1 }
266
+ year: { operator: BTW, value: [2015, 2023] }
267
+ ) {
268
+ number
269
+ year
270
+ state
271
+ }
272
+ }
273
+ }
274
+ ```
275
+
276
+ #### Nested Collection Filtering
277
+
278
+ Filter deeply nested collections using dot notation:
279
+
280
+ ```graphql
281
+ query {
282
+ series {
283
+ seasons(
284
+ episodes: {
285
+ terms: [
286
+ {
287
+ path: "name",
288
+ operator: LIKE,
289
+ value: "Final"
290
+ }
291
+ ]
292
+ }
293
+ ) {
294
+ number
295
+ episodes {
296
+ name
297
+ date
298
+ }
299
+ }
300
+ }
301
+ }
302
+ ```
303
+
304
+ #### Collection Filtering with Array Operations
305
+
306
+ Use array operations for collection fields:
307
+
308
+ ```graphql
309
+ query {
310
+ series {
311
+ seasons(
312
+ categories: { operator: IN, value: ["Drama", "Crime"] }
313
+ ) {
314
+ number
315
+ categories
316
+ }
317
+ }
318
+ }
319
+ ```
320
+
321
+ **Note**: Collection field filtering uses the exact same format as main query filtering, ensuring consistency across your GraphQL API. All available operators (`EQ`, `NE`, `GT`, `LT`, `GTE`, `LTE`, `LIKE`, `IN`, `NIN`, `BTW`) work with collection fields.
322
+
207
323
  ## 🔧 Middlewares
208
324
 
209
325
  Middlewares provide a powerful way to intercept and process all GraphQL operations before they execute. Use them for cross-cutting concerns like authentication, logging, validation, and performance monitoring.
@@ -1072,13 +1188,13 @@ const OrderType = new GraphQLObjectType({
1072
1188
 
1073
1189
  ### Custom Validated Scalar Types
1074
1190
 
1075
- Create custom scalar types with built-in validation:
1191
+ Create custom scalar types with built-in validation. The generated type names follow the pattern `{name}_{baseScalarTypeName}`:
1076
1192
 
1077
1193
  ```javascript
1078
1194
  const { GraphQLString, GraphQLInt } = require('graphql');
1079
1195
  const { createValidatedScalar } = require('@simtlix/simfinity-js');
1080
1196
 
1081
- // Email scalar with validation
1197
+ // Email scalar with validation (generates type name: Email_String)
1082
1198
  const EmailScalar = createValidatedScalar(
1083
1199
  'Email',
1084
1200
  'A valid email address',
@@ -1091,7 +1207,7 @@ const EmailScalar = createValidatedScalar(
1091
1207
  }
1092
1208
  );
1093
1209
 
1094
- // Positive integer scalar
1210
+ // Positive integer scalar (generates type name: PositiveInt_Int)
1095
1211
  const PositiveIntScalar = createValidatedScalar(
1096
1212
  'PositiveInt',
1097
1213
  'A positive integer',
@@ -1108,8 +1224,8 @@ const UserType = new GraphQLObjectType({
1108
1224
  name: 'User',
1109
1225
  fields: () => ({
1110
1226
  id: { type: GraphQLID },
1111
- email: { type: EmailScalar },
1112
- age: { type: PositiveIntScalar },
1227
+ email: { type: EmailScalar }, // Type name: Email_String
1228
+ age: { type: PositiveIntScalar }, // Type name: PositiveInt_Int
1113
1229
  }),
1114
1230
  });
1115
1231
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@simtlix/simfinity-js",
3
- "version": "1.9.0",
3
+ "version": "2.0.0",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
@@ -18,7 +18,7 @@
18
18
  "license": "Apache-2.0",
19
19
  "repository": {
20
20
  "type": "git",
21
- "url": "https://github.com/simtlix/simfinity.git"
21
+ "url": "git+https://github.com/simtlix/simfinity.git"
22
22
  },
23
23
  "peerDependencies": {
24
24
  "graphql": "^16.11.0",
Binary file
package/src/index.js CHANGED
@@ -202,7 +202,7 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
202
202
  const baseKind = kindMap[baseScalarType.name] || Kind.STRING;
203
203
 
204
204
  const scalar = new GraphQLScalarType({
205
- name,
205
+ name: `${name}_${baseScalarType.name}`,
206
206
  description,
207
207
  serialize(value) {
208
208
  validate(value);
@@ -214,7 +214,7 @@ function createValidatedScalar(name, description, baseScalarType, validate) {
214
214
  },
215
215
  parseLiteral(ast, variables) {
216
216
  if (ast.kind !== baseKind) {
217
- throw new Error(`${name} must be a ${baseScalarType.name}`);
217
+ throw new Error(`${name}_${baseScalarType.name} must be a ${baseScalarType.name}`);
218
218
  }
219
219
  const value = baseScalarType.parseLiteral(ast, variables);
220
220
  validate(value);
@@ -1516,37 +1516,7 @@ const buildRootQuery = (name, includedTypes) => {
1516
1516
 
1517
1517
  const argTypes = type.gqltype.getFields();
1518
1518
 
1519
- const argsObject = {};
1520
-
1521
- for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
1522
- argsObject[fieldEntryName] = {};
1523
-
1524
- if (fieldEntry.type instanceof GraphQLScalarType
1525
- || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
1526
- || fieldEntry.type instanceof GraphQLEnumType
1527
- || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
1528
- argsObject[fieldEntryName].type = QLFilter;
1529
- } else if (fieldEntry.type instanceof GraphQLObjectType
1530
- || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
1531
- argsObject[fieldEntryName].type = QLTypeFilterExpression;
1532
- } else if (fieldEntry.type instanceof GraphQLList) {
1533
- const listOfType = fieldEntry.type.ofType;
1534
- if (listOfType instanceof GraphQLScalarType
1535
- || isNonNullOfType(listOfType, GraphQLScalarType)
1536
- || listOfType instanceof GraphQLEnumType
1537
- || isNonNullOfType(listOfType, GraphQLEnumType)) {
1538
- argsObject[fieldEntryName].type = QLFilter;
1539
- } else {
1540
- argsObject[fieldEntryName].type = QLTypeFilterExpression;
1541
- }
1542
- }
1543
- }
1544
-
1545
- argsObject.pagination = {};
1546
- argsObject.pagination.type = QLPagination;
1547
-
1548
- argsObject.sort = {};
1549
- argsObject.sort.type = QLSortExpression;
1519
+ const argsObject = createArgsForQuery(argTypes);
1550
1520
 
1551
1521
  rootQueryArgs.fields[type.listEntitiesEndpointName] = {
1552
1522
  type: new GraphQLList(type.gqltype),
@@ -1656,18 +1626,39 @@ const autoGenerateResolvers = (gqltype) => {
1656
1626
  if (!relation.embedded) {
1657
1627
  if (fieldEntry.type instanceof GraphQLList) {
1658
1628
  // Collection field - generate resolve for one-to-many relationship
1629
+ //This is a one-to-many resolver that will return a list of related objects. Also this one allows to filter the related objects as is in the find endpoint.
1659
1630
  const relatedType = fieldEntry.type.ofType;
1660
1631
  const connectionField = relation.connectionField || fieldName;
1632
+ const relatedTypeInfo = typesDict.types[relatedType.name];
1633
+ const argsObject = createArgsForQuery(relatedTypeInfo.gqltype.getFields());
1634
+
1635
+ delete argsObject[connectionField];
1636
+ const argsArray = Object.entries(argsObject);
1637
+
1661
1638
 
1662
- fieldEntry.resolve = (parent) => {
1639
+ const graphqlArgs = formatArgs(argsArray);
1640
+
1641
+ fieldEntry.args = graphqlArgs;
1642
+
1643
+ fieldEntry.resolve = async (parent, args) => {
1663
1644
  // Lazy lookup of the related model
1664
- const relatedTypeInfo = typesDict.types[relatedType.name];
1645
+
1665
1646
  if (!relatedTypeInfo || !relatedTypeInfo.model) {
1666
1647
  throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
1667
1648
  }
1668
- const query = {};
1669
- query[connectionField] = parent.id || parent._id;
1670
- return relatedTypeInfo.model.find(query);
1649
+
1650
+ args[connectionField] = {
1651
+ terms: [{
1652
+ path: 'id',
1653
+ operator: 'EQ',
1654
+ value: parent.id || parent._id,
1655
+ }],
1656
+ };
1657
+
1658
+
1659
+ const aggregateClauses = await buildQuery(args, relatedTypeInfo.gqltype);
1660
+
1661
+ return await relatedTypeInfo.model.aggregate(aggregateClauses);
1671
1662
  };
1672
1663
  } else if (fieldEntry.type instanceof GraphQLObjectType
1673
1664
  || (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
@@ -1741,3 +1732,50 @@ export const addNoEndpointType = (gqltype) => {
1741
1732
  };
1742
1733
 
1743
1734
  export { createValidatedScalar };
1735
+
1736
+ const createArgsForQuery = (argTypes) => {
1737
+ const argsObject = {};
1738
+
1739
+ for (const [fieldEntryName, fieldEntry] of Object.entries(argTypes)) {
1740
+ argsObject[fieldEntryName] = {};
1741
+
1742
+ if (fieldEntry.type instanceof GraphQLScalarType
1743
+ || isNonNullOfType(fieldEntry.type, GraphQLScalarType)
1744
+ || fieldEntry.type instanceof GraphQLEnumType
1745
+ || isNonNullOfType(fieldEntry.type, GraphQLEnumType)) {
1746
+ argsObject[fieldEntryName].type = QLFilter;
1747
+ } else if (fieldEntry.type instanceof GraphQLObjectType
1748
+ || isNonNullOfType(fieldEntry.type, GraphQLObjectType)) {
1749
+ argsObject[fieldEntryName].type = QLTypeFilterExpression;
1750
+ } else if (fieldEntry.type instanceof GraphQLList) {
1751
+ const listOfType = fieldEntry.type.ofType;
1752
+ if (listOfType instanceof GraphQLScalarType
1753
+ || isNonNullOfType(listOfType, GraphQLScalarType)
1754
+ || listOfType instanceof GraphQLEnumType
1755
+ || isNonNullOfType(listOfType, GraphQLEnumType)) {
1756
+ argsObject[fieldEntryName].type = QLFilter;
1757
+ } else {
1758
+ argsObject[fieldEntryName].type = QLTypeFilterExpression;
1759
+ }
1760
+ }
1761
+ }
1762
+
1763
+ argsObject.pagination = {};
1764
+ argsObject.pagination.type = QLPagination;
1765
+
1766
+ argsObject.sort = {};
1767
+ argsObject.sort.type = QLSortExpression;
1768
+ return argsObject;
1769
+ };
1770
+
1771
+ function formatArgs(argsArray) {
1772
+ const graphqlArgs = [];
1773
+ for (const [key, value] of argsArray) {
1774
+ const item = {
1775
+ name: key,
1776
+ type: value.type,
1777
+ };
1778
+ graphqlArgs.push(item);
1779
+ }
1780
+ return graphqlArgs;
1781
+ }
@@ -0,0 +1,125 @@
1
+ import {
2
+ describe, test, expect,
3
+ } from 'vitest';
4
+ import { GraphQLString, GraphQLInt, GraphQLFloat, GraphQLBoolean, GraphQLID } from 'graphql';
5
+ import { createValidatedScalar } from '../src/index.js';
6
+
7
+ describe('Validated Scalar Naming Convention', () => {
8
+ test('should generate correct type names with base scalar type suffix', () => {
9
+ const EmailScalar = createValidatedScalar(
10
+ 'Email',
11
+ 'A valid email address',
12
+ GraphQLString,
13
+ (value) => {
14
+ if (!value.includes('@')) {
15
+ throw new Error('Invalid email format');
16
+ }
17
+ },
18
+ );
19
+
20
+ const EpisodeNumberScalar = createValidatedScalar(
21
+ 'EpisodeNumber',
22
+ 'A valid episode number',
23
+ GraphQLInt,
24
+ (value) => {
25
+ if (value <= 0) {
26
+ throw new Error('Episode number must be positive');
27
+ }
28
+ },
29
+ );
30
+
31
+ const RatingScalar = createValidatedScalar(
32
+ 'Rating',
33
+ 'A valid rating between 0 and 10',
34
+ GraphQLFloat,
35
+ (value) => {
36
+ if (value < 0 || value > 10) {
37
+ throw new Error('Rating must be between 0 and 10');
38
+ }
39
+ },
40
+ );
41
+
42
+ const IsActiveScalar = createValidatedScalar(
43
+ 'IsActive',
44
+ 'A boolean indicating if something is active',
45
+ GraphQLBoolean,
46
+ (value) => {
47
+ // Boolean validation is usually not needed, but this is for testing
48
+ if (typeof value !== 'boolean') {
49
+ throw new Error('Must be a boolean value');
50
+ }
51
+ },
52
+ );
53
+
54
+ const CustomIdScalar = createValidatedScalar(
55
+ 'CustomId',
56
+ 'A custom ID with specific format',
57
+ GraphQLID,
58
+ (value) => {
59
+ if (!value.startsWith('CUST_')) {
60
+ throw new Error('Custom ID must start with CUST_');
61
+ }
62
+ },
63
+ );
64
+
65
+ // Test the naming convention
66
+ expect(EmailScalar.name).toBe('Email_String');
67
+ expect(EpisodeNumberScalar.name).toBe('EpisodeNumber_Int');
68
+ expect(RatingScalar.name).toBe('Rating_Float');
69
+ expect(IsActiveScalar.name).toBe('IsActive_Boolean');
70
+ expect(CustomIdScalar.name).toBe('CustomId_ID');
71
+ });
72
+
73
+ test('should maintain baseScalarType property', () => {
74
+ const EmailScalar = createValidatedScalar(
75
+ 'Email',
76
+ 'A valid email address',
77
+ GraphQLString,
78
+ (value) => {
79
+ if (!value.includes('@')) {
80
+ throw new Error('Invalid email format');
81
+ }
82
+ },
83
+ );
84
+
85
+ expect(EmailScalar.baseScalarType).toBe(GraphQLString);
86
+ expect(EmailScalar.name).toBe('Email_String');
87
+ });
88
+
89
+ test('should work with validation functions', () => {
90
+ const EpisodeNumberScalar = createValidatedScalar(
91
+ 'EpisodeNumber',
92
+ 'A valid episode number',
93
+ GraphQLInt,
94
+ (value) => {
95
+ if (value <= 0) {
96
+ throw new Error('Episode number must be positive');
97
+ }
98
+ },
99
+ );
100
+
101
+ // Test valid value
102
+ expect(() => EpisodeNumberScalar.serialize(5)).not.toThrow();
103
+ expect(EpisodeNumberScalar.serialize(5)).toBe(5);
104
+
105
+ // Test invalid value
106
+ expect(() => EpisodeNumberScalar.serialize(0)).toThrow('Episode number must be positive');
107
+ expect(() => EpisodeNumberScalar.serialize(-1)).toThrow('Episode number must be positive');
108
+ });
109
+
110
+ test('should generate error messages with correct type names', () => {
111
+ const EpisodeNumberScalar = createValidatedScalar(
112
+ 'EpisodeNumber',
113
+ 'A valid episode number',
114
+ GraphQLInt,
115
+ (value) => {
116
+ if (value <= 0) {
117
+ throw new Error('Episode number must be positive');
118
+ }
119
+ },
120
+ );
121
+
122
+ // The error message should include the full type name
123
+ expect(() => EpisodeNumberScalar.serialize(0)).toThrow('Episode number must be positive');
124
+ });
125
+ });
@@ -70,7 +70,7 @@ describe('Custom Validated Scalar Types', () => {
70
70
  describe('createValidatedScalar function', () => {
71
71
  test('should create a valid scalar type with baseScalarType property', () => {
72
72
  expect(EmailScalar).toBeDefined();
73
- expect(EmailScalar.name).toBe('Email');
73
+ expect(EmailScalar.name).toBe('Email_String');
74
74
  expect(EmailScalar.baseScalarType).toBe(GraphQLString);
75
75
  expect(EmailScalar.serialize).toBeDefined();
76
76
  expect(EmailScalar.parseValue).toBeDefined();