graphql-compose / graphql-compose-mongoose

Mongoose model converter to GraphQL types with resolvers for graphql-compose https://github.com/nodkz/graphql-compose
MIT License
708 stars 94 forks source link

Sort parent type by properties of relations #327

Closed dan-cooke closed 3 years ago

dan-cooke commented 3 years ago

Hi! First of all - thank you so much for graphql-compose and this amazing library.

I apologise if I'm being a bit nooby here -

But is there an easy way to sort by a nested property?

With the following schema:

const Quote = new Schema({
  symbol: { type: Schema.Types.ObjectId, ref: `Symbol` },
  change: 0.0,
  changePercent: 0.0,
  latestPrice: 0.0,
  latestUpdate: 0,
  extendedPrice: 0.0,
  extendedUpdate: 0,
});
const Symbol = new Schema({
  quote: { type: Schema.Types.ObjectId, ref: `Quote` },
});

const Position = new Schema({
  symbol: { type: Schema.Types.ObjectId, ref: `Symbol` },
});

There exists a nested structure of depth 3 : Position > Symbol > Quote

I have the following Type Composer setup for my Position model

  schemaComposer.Query.addFields({
    myPositions: getTypeComposer(`Position`)
      .mongooseResolvers.findMany()
  });

My GraphQL playground only allows me to sort via indexed fields.

Am I right in saying that these are my possible options:

  1. Use addSortArg to define a *_DESC/ASC for every field I wish to sort by on the Symbol + Quote types

OR

  1. Write a custom myPositions resolver to handle all these sort options

I'm leaning towards the first option as it requires less boilerplate -but I thought I would check here first incase there was a much simpler way that I have not found yet.

Thanks!

dan-cooke commented 3 years ago

Okay after scowering the forums for a bit - I have concluded that I need to write my own resolver for this use case.

here is what I arrived on - hope it helps someone in future,


export const addMyPositionsResolver = () => {
  getTypeComposer(`Position`).addResolver({
    name: `myPositions`,
    type: getTypeComposer(`Position`).mongooseResolvers.pagination().getType(),
    args: {
      filter: `
        input MyPositionsFilter {
            isEtf: Boolean
            exchange: String
            country: String
        }
      `,
      sort: `
        enum MyPositionsSort {
            SYMBOL_SYMBOL_ASC
            SYMBOL_SYMBOL_DESC
        }
      `,
      perPage: `Int`,
      page: `Int`,
    },
    resolve: async ({
      source,
      args: { owner, filter, sort, perPage, page },
    }: any) => {
      const limit = perPage || 25;
      const offset = (page ?? 0) * limit;
      const pipeline = [];

      // Lookup symbol
      pipeline.push({
        $lookup: {
          from: `symbols`,
          localField: `symbol`,
          foreignField: `_id`,
          as: `symbol`,
        },
      });

      // Unwind lookup result to a single symbol object
      pipeline.push({
        $unwind: {
          path: `$symbol`,
        },
      });

      if (sort) {
        const sortArray = sort?.split(`_`).map((s: string) => s.toLowerCase());
        // Last item in the array is the direction
        const sortDirection = sortArray.pop() === `asc` ? 1 : -1;
        // All remaining items are the object pth
        const path = sortArray.join(`.`);

        pipeline.push({
          $sort: {
            [path]: sortDirection,
          },
        });
      }

      pipeline.push({
        $facet: {
          items: [{ $limit: limit }, { $skip: offset }],
          count: [{ $count: `count` }],
        },
      });

      pipeline.push({
        $unwind: `$count`,
      });

      pipeline.push({
        $project: {
          items: 1,
          count: `$count.count`,
        },
      });
      const result = await M(`Position`)?.aggregate(pipeline).exec();

      return result[0];
    },
  });
};