graphql-compose-mongoose icon indicating copy to clipboard operation
graphql-compose-mongoose copied to clipboard

Sub documents.

Open smooJitter opened this issue 7 years ago • 5 comments

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, ....}?

smooJitter avatar Jul 08 '17 00:07 smooJitter

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

smooJitter avatar Jul 08 '17 00:07 smooJitter

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(),
});

nodkz avatar Jul 10 '17 04:07 nodkz

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.

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

  2. 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)

smooJitter avatar Jul 18 '17 15:07 smooJitter

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 avatar Jul 23 '17 11:07 nodkz

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

danielmahon avatar Mar 17 '18 15:03 danielmahon