events.action: opened
events.author: smooJitter
events.masked_author: username_0
events.text: Suppose I have subsets that on a document like so,
  
  friends : [ {userId: string,  meta: {}},... ]
  
  Where meta contains discriminatory 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, ....}?
events.title: Sub documents.
events.type: issue
events.action: created
events.author: smooJitter
events.masked_author: username_0
events.text: 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...}
events.type: comment
events.action: created
events.author: nodkz
events.masked_author: username_1
events.text: You may do something like this:
  ```js
  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);
        }),
  });
  ```
events.type: comment
events.action: created
events.author: smooJitter
events.masked_author: username_0
events.text: 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)
events.type: comment
events.action: created
events.author: nodkz
events.masked_author: username_1
events.text: ### FILTERED RELATIONS
  
  You may use standard resolver `findMany`, `connection` and extend filter for Events from User data in such manner:
  ```js
  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 graphql-compose-mongoose@1.6.0 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:
  
  ```js
  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('$connection.@filter.salary'),
        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 },
      })
  );
  ```
events.type: comment
events.action: created
events.author: danielmahon
events.masked_author: username_2
events.text: @username_1 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.
events.type: comment