graphql-compose-mongoose
graphql-compose-mongoose copied to clipboard
Sub documents.
Suppose I have subsets on a document like so,
friends : [ {userId: string, meta: {}},... ]
Where meta contains discriminator for the type of friend
How whould I create a relation on freinds that resolves to friends where meta.mutual = 1, and returning a list of friend Profile objects where the Profile object represents a subset user fields e.g., { username, avatar, userId, ....}?
One alternative would be to restructure the model
friends: [ userids....] mutual: [userIds...] likes: [ userId] likesMe: [userId...]
This is easier but I still would need the fields to resolve to objects of a type Friend {fields...}
You may do something like this:
UserTC.setFields({
// add new field to User type with name `mutualFriends`
mutualFriends: {
// require `friends` field from db, when you use just `mutualFriends` in query
projection: { friends: true },
...UserTC
// get standard resolver findMany which returns list of Users
.getResolver('findMany')
// wrap it with additional logic, which obtains list of mutual friends from `source.friends`
.wrapResolve(next => rp => {
const mutual = rp.source.friends.filter(o => o.meta.mutual).map(o => o.userId) || [];
if (mutual.length === 0) return [];
rp.rawQuery = {
_id: { $in: mutual }, // may be you'll need to convert stringId to MongoId
};
return next(rp);
})
// convert Resolver to FieldConfig { type:, args:, resolve: }
.getFieldConfig(),
});
I'm sorry perhaps I should have added more clarity. I believe I am asking about a more common use case. The example above is a bit misleading.
My user model looks like this
const UserPreference = new mongoose.Schema( {
typeOfPreference: { type: [String], enum: ['FOOD', 'BEVERAGE']
...
});
const UserSchema = new mongoose.Schema( {
username: String,
preferences: [UserPreference],
eventsAttended: [{ type: ObjectId, ref: 'Events' }],
});
I can actual use this to highlight 2 common use cases.
-
FILTERED RELATIONS: The events field contains an array of ObjectIds. I like for it to resolve to a subset of fields from the events collection (primary) and in an advance case add filters on a subset of the subset fields, e.g., { name, description, typeOfEvent } where type of event has a filter.
-
FILTER SUB DOCUMENTS: The preference field contains an array of subdocuments. I would like add a filter on the discriminator key (typeOfPreference). Do I need add a resolver (e.g., getUserPreferencesByType)
FILTERED RELATIONS
You may use standard resolver findMany
, connection
and extend filter for Events from User data in such manner:
UserTC.addRelation('myEventsWithFilter', () => ({
resolver: () =>
EventsTC.getResolver('findMany').wrapResolve(next => rp => {
// With object-path package set filter by one line
// objectPath.set(rp, 'args.filter._ids', rp.source.eventIds);
// or
if (!rp.args.filter) rp.args.filter = {}; // ensure that `filter` exists
rp.args.filter._ids = rp.source.eventIds; // set `_ids` from current User doc
// call standard `findMany` resolver with extended filter
return next(rp);
}),
projection: { eventIds: true },
}));
PS. Upgrade to [email protected] where added filter._ids
field for findMany
resolver.
FILTER SUB DOCUMENTS
In such case you need to extend resolver with your custom filters via addFilterArg
, addSortArg
. Something like this:
UserTC.setResolver(
'connection', // providing same name for replacing standard resolver `connection`, or you may set another name for keepeng standard resolver untoched
UserTC.getResolver('connection')
.addFilterArg({
name: 'region',
type: '[String]',
description: 'Region, Country, City',
query: (rawQuery, value) => {
if (value.length === 1) {
rawQuery['location.name'] = value[0];
} else {
rawQuery['location.name'] = { $in: value };
}
},
})
.addFilterArg({
name: 'salaryMax',
type: CvTC.get('[email protected]'),
description: 'Max salary',
query: (rawQuery, value) => {
if (value.total > 0) {
rawQuery['salary.total'] = { $gte: 1, $lte: value.total };
}
if (value.currency) {
rawQuery['salary.currency'] = value.currency;
}
},
})
.addFilterArg({
name: 'specializations',
type: '[String]',
description: 'Array of profession areas (any)',
query: (rawQuery, value) => {
rawQuery.specializations = { $in: value };
},
})
.addFilterArg({
name: 'employments',
type: '[String]',
description: 'Array of employment (any)',
query: (rawQuery, value) => {
rawQuery.employment = { $in: value };
},
})
.addFilterArg({
name: 'totalExperienceMin',
type: 'Int',
description: 'Min expirience in months',
query: (rawQuery, value) => {
rawQuery.totalExperience = { $gte: value };
},
})
.addFilterArg({
name: 'ageRange',
type: 'input AgeRange { min: Int, max: Int }',
description: 'Filter by age range (in years)',
query: (rawQuery, value) => {
const d = new Date();
const month = d.getMonth();
const day = d.getDate();
const year = d.getFullYear();
let minAge = value.min || 0;
let maxAge = value.max || 0;
if (!minAge && !maxAge) return;
if (minAge > maxAge && minAge && maxAge) [minAge, maxAge] = [maxAge, minAge];
rawQuery.birthday = {};
if (maxAge) {
rawQuery.birthday.$gte = new Date(year - maxAge - 1, month, day);
}
if (minAge) {
rawQuery.birthday.$lte = new Date(year - minAge, month, day);
}
},
})
.addFilterArg({
name: 'periodMaxH',
type: 'Int',
description: 'Filter by created date (in hours)',
query: (rawQuery, value) => {
if (value > 0 && value < 99999) {
const curDate = new Date();
const pastDate = new Date();
pastDate.setTime(pastDate.getTime() - value * 3600000);
rawQuery.createdAt = {
$gt: pastDate,
$lt: curDate,
};
}
},
})
.addFilterArg({
name: 'langs',
type: '[String]',
description: 'Language list (all)',
query: (rawQuery, value) => {
if (value.length === 1) {
rawQuery['languages.ln'] = value[0];
} else {
rawQuery['languages.ln'] = { $all: value };
}
},
})
.addFilterArg({
name: 'citizenships',
type: '[String]',
description: 'Citizenship list (any)',
query: (rawQuery, value) => {
if (value.length === 1) {
rawQuery.citizenship = value[0];
} else {
rawQuery.citizenship = { $in: value };
}
},
})
.addFilterArg({
name: 'hasPhoto',
type: 'Boolean',
query: (rawQuery, value) => {
rawQuery.avatarUrl = { $exists: value };
},
})
.addSortArg({
name: 'RELEVANCE',
description: 'Sort by text score or date',
value: resolveParams => {
if (resolveParams.rawQuery && resolveParams.rawQuery.$text) {
return { score: { $meta: 'textScore' } };
}
return { createdAt: -1 };
},
})
.addFilterArg({
name: 'q',
type: 'String',
description: 'Text search',
query: (rawQuery, value, resolveParams) => {
rawQuery.$text = { $search: value, $language: 'ru' };
resolveParams.projection.score = { $meta: 'textScore' };
},
})
.addSortArg({
name: 'SALARY_ASC',
value: resolveParams => {
if (!resolveParams.rawQuery) resolveParams.rawQuery = {};
resolveParams.rawQuery['salary.total'] = { $gt: 0 };
return { 'salary.total': 1 };
},
})
.addSortArg({
name: 'SALARY_DESC',
value: resolveParams => {
if (!resolveParams.rawQuery) resolveParams.rawQuery = {};
resolveParams.rawQuery['salary.total'] = { $gt: 0 };
return { 'salary.total': -1 };
},
})
.addSortArg({
name: 'DATE_ASC',
value: { createdAt: 1 },
})
.addSortArg({
name: 'DATE_DESC',
value: { createdAt: -1 },
})
);
@nodkz I'm trying to filter relations based on the above and not having any luck, see #96. Any suggestions? I notice my wrapResolve
and query
functions never run so I'm sure I'm missing something.