Skip to content

Commit 913a731

Browse files
author
Claudio Gonzalez
committed
feat: Add collection field filtering support
- Add support for filtering collection fields (one-to-many relationships) using the same query format as main queries - Implement formatArgs function for proper GraphQL argument formatting - Add comprehensive documentation for collection field filtering feature - Update version to 2.0.0 This major version bump introduces the ability to filter related objects directly within GraphQL queries, providing more powerful and flexible querying capabilities.
1 parent 840f5d5 commit 913a731

4 files changed

Lines changed: 191 additions & 37 deletions

File tree

README.md

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,122 @@ query {
204204
- `NIN` - Not in array
205205
- `BTW` - Between two values
206206

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+
207323
## 🔧 Middlewares
208324

209325
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.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@simtlix/simfinity-js",
3-
"version": "1.9.1",
3+
"version": "2.0.0",
44
"description": "",
55
"main": "src/index.js",
66
"type": "module",

simtlix-simfinity-js-1.9.1.tgz

186 KB
Binary file not shown.

src/index.js

Lines changed: 74 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1516,37 +1516,7 @@ const buildRootQuery = (name, includedTypes) => {
15161516

15171517
const argTypes = type.gqltype.getFields();
15181518

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);
15501520

15511521
rootQueryArgs.fields[type.listEntitiesEndpointName] = {
15521522
type: new GraphQLList(type.gqltype),
@@ -1656,18 +1626,39 @@ const autoGenerateResolvers = (gqltype) => {
16561626
if (!relation.embedded) {
16571627
if (fieldEntry.type instanceof GraphQLList) {
16581628
// 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.
16591630
const relatedType = fieldEntry.type.ofType;
16601631
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+
16611638

1662-
fieldEntry.resolve = (parent) => {
1639+
const graphqlArgs = formatArgs(argsArray);
1640+
1641+
fieldEntry.args = graphqlArgs;
1642+
1643+
fieldEntry.resolve = async (parent, args) => {
16631644
// Lazy lookup of the related model
1664-
const relatedTypeInfo = typesDict.types[relatedType.name];
1645+
16651646
if (!relatedTypeInfo || !relatedTypeInfo.model) {
16661647
throw new Error(`Related type ${relatedType.name} not found or not connected. Make sure it's connected with simfinity.connect() or simfinity.addNoEndpointType().`);
16671648
}
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);
16711662
};
16721663
} else if (fieldEntry.type instanceof GraphQLObjectType
16731664
|| (fieldEntry.type instanceof GraphQLNonNull && fieldEntry.type.ofType instanceof GraphQLObjectType)) {
@@ -1741,3 +1732,50 @@ export const addNoEndpointType = (gqltype) => {
17411732
};
17421733

17431734
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 commit comments

Comments
 (0)