diff --git a/src/queries/groupsById.js b/src/queries/groupsById.js new file mode 100644 index 0000000..36cb1d0 --- /dev/null +++ b/src/queries/groupsById.js @@ -0,0 +1,34 @@ +import ReactionError from "@reactioncommerce/reaction-error"; + +/** + * @name groupsById + * @method + * @memberof Accounts/NoMeteorQueries + * @summary query the Groups collection and return a MongoDB cursor + * @param {Object} context - an object containing the per-request state + * @param {Array|String} groupIds - IDs of the groups to get + * @returns {Array|Object} Group objects + */ +export default async function groupsById(context, groupIds) { + const { collections } = context; + const { Groups } = collections; + + const groups = await Groups.find({ + _id: { + $in: groupIds + } + }).toArray(); + + + if (groups.length === 0) { + throw new ReactionError("not-found", "No groups matching the provided IDs were found"); + } + + if (groups.length !== groupIds.length) { + throw new ReactionError("not-found", `Could not find ${groupIds.length - groups.length} of ${groupIds.length} groups provided`); + } + + await Promise.all(groups.map((group) => context.validatePermissions("reaction:legacy:groups", "read", { shopId: group.shopId }))); + + return groups; +} diff --git a/src/queries/index.js b/src/queries/index.js index 5726f5c..37b3d7b 100644 --- a/src/queries/index.js +++ b/src/queries/index.js @@ -3,7 +3,8 @@ import accounts from "./accounts.js"; import group from "./group.js"; import groups from "./groups.js"; import groupsByAccount from "./groupsByAccount.js"; -import invitationsAggregate from "./invitationsAggregate.js"; +import groupsById from "./groupsById.js"; +import invitations from "./invitations.js"; import userAccount from "./userAccount.js"; export default { @@ -12,6 +13,7 @@ export default { group, groups, groupsByAccount, - invitationsAggregate, + groupsById, + invitations, userAccount }; diff --git a/src/queries/invitations.js b/src/queries/invitations.js new file mode 100644 index 0000000..9fff8b3 --- /dev/null +++ b/src/queries/invitations.js @@ -0,0 +1,28 @@ +/** + * @name accounts + * @method + * @memberof Accounts/NoMeteorQueries + * @summary Returns accounts optionally filtered by group IDs + * @param {Object} context - an object containing the per-request state + * @param {String} input - input for query + * @param {String} [input.shopIds] - Array of shop IDs to limit the results + * @returns {Promise} Mongo cursor + */ +export default async function accounts(context, { shopIds }) { + const { collections } = context; + const { AccountInvites } = collections; + + if (Array.isArray(shopIds) && shopIds.length > 0) { + await Promise.all(shopIds.map((shopId) => context.validatePermissions("reaction:legacy:groups", "manage:accounts", { shopId }))); + + return AccountInvites.find({ + shopId: { + $in: shopIds + } + }); + } + + await context.validatePermissions("reaction:legacy:invitations", "read"); + + return AccountInvites.find(); +} diff --git a/src/queries/invitationsAggregate.js b/src/queries/invitationsAggregate.js deleted file mode 100644 index 25b2bec..0000000 --- a/src/queries/invitationsAggregate.js +++ /dev/null @@ -1,78 +0,0 @@ -/** - * @name accounts - * @method - * @memberof Accounts/NoMeteorQueries - * @summary Returns accounts optionally filtered by group IDs - * @param {Object} context - an object containing the per-request state - * @param {String} input - input for query - * @param {String} [input.shopIds] - Array of shop IDs to limit the results - * @returns {Promise} Mongo cursor - */ -export default async function accounts(context, { shopIds }) { - const { collections } = context; - const { AccountInvites } = collections; - - const pipeline = []; - - if (Array.isArray(shopIds) && shopIds.length > 0) { - await Promise.all(shopIds.map((shopId) => context.validatePermissions("reaction:legacy:groups", "manage:accounts", { shopId }))); - - pipeline.push({ - $match: { - shopId: { - $in: shopIds - } - } - }) - } else { - await context.validatePermissions("reaction:legacy:invitations", "read"); - } - - pipeline.push( - { - $lookup: { - from: "Shops", - localField: "shopId", - foreignField: "_id", - as: "shop" - } - }, - { - $unwind: { - path: "$shop" - } - }, - { - $lookup: { - from: "Groups", - localField: "groupIds", - foreignField: "_id", - as: "groups" - } - }, - { - $lookup: { - from: "Accounts", - localField: "invitedByUserId", - foreignField: "userId", - as: "invitedBy" - } - }, - { - $unwind: { - path: "$invitedBy" - } - }, - { - $project: { - groupIds: 0, - invitedByUserId: 0 - } - } - ); - - return { - collection: AccountInvites, - pipeline - }; -} diff --git a/src/resolvers/Invitation/groups.js b/src/resolvers/Invitation/groups.js deleted file mode 100644 index 98b701f..0000000 --- a/src/resolvers/Invitation/groups.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Return group by ID - * @param parent - * @param _ - * @param context - * @returns {Promise<*[]>} - */ -export default async function groups(parent, _, context) { - const group = await context.queries.group(context, parent.groupId); - - return [group]; -} diff --git a/src/resolvers/Invitation/index.js b/src/resolvers/Invitation/index.js index e2be202..f3a5cee 100644 --- a/src/resolvers/Invitation/index.js +++ b/src/resolvers/Invitation/index.js @@ -1,11 +1,10 @@ import resolveShopFromShopId from "@reactioncommerce/api-utils/graphql/resolveShopFromShopId.js"; import { encodeInvitationOpaqueId } from "../../xforms/id.js"; -import groups from "./groups.js"; import invitedBy from "./invitedBy.js"; export default { _id: (node) => encodeInvitationOpaqueId(node._id), - groups, + groups: (parent, _, context) => context.queries.groupsById(context, parent.groupIds || [parent.groupId]), invitedBy, shop: resolveShopFromShopId }; diff --git a/src/resolvers/Query/invitations.js b/src/resolvers/Query/invitations.js index 96455d8..e47d9f8 100644 --- a/src/resolvers/Query/invitations.js +++ b/src/resolvers/Query/invitations.js @@ -1,4 +1,4 @@ -import getPaginatedResponseFromAggregate from "@reactioncommerce/api-utils/graphql/getPaginatedResponseFromAggregate.js"; +import getPaginatedResponse from "@reactioncommerce/api-utils/graphql/getPaginatedResponse.js"; import wasFieldRequested from "@reactioncommerce/api-utils/graphql/wasFieldRequested.js"; import { decodeShopOpaqueId } from "../../xforms/id.js"; @@ -23,9 +23,9 @@ export default async function invitations(_, args, context, info) { shopIds = encodedShopIds.map((shopId) => decodeShopOpaqueId(shopId)); } - const { collection, pipeline } = await context.queries.invitationsAggregate(context, { shopIds }); + const query = await context.queries.invitations(context, { shopIds }); - return getPaginatedResponseFromAggregate(collection, pipeline, connectionArgs, { + return getPaginatedResponse(query, connectionArgs, { includeHasNextPage: wasFieldRequested("pageInfo.hasNextPage", info), includeHasPreviousPage: wasFieldRequested("pageInfo.hasPreviousPage", info), includeTotalCount: wasFieldRequested("totalCount", info) diff --git a/src/schemas/inviteShopMember.graphql b/src/schemas/inviteShopMember.graphql index 88a6b8f..c974b42 100644 --- a/src/schemas/inviteShopMember.graphql +++ b/src/schemas/inviteShopMember.graphql @@ -91,6 +91,12 @@ extend type Query { "The shop IDs to get invitations for" shopIds: [ID], + "Return only results that come after this cursor. Use this with `first` to specify the number of results to return." + after: ConnectionCursor, + + "Return only results that come before this cursor. Use this with `last` to specify the number of results to return." + before: ConnectionCursor, + "Return at most this many results. This parameter may be used with either `after` or `offset` parameters." first: ConnectionLimitInt,