diff --git a/packages/api/src/loaders/cartLoader.ts b/packages/api/src/loaders/cartLoader.ts new file mode 100644 index 000000000..5bd3b4032 --- /dev/null +++ b/packages/api/src/loaders/cartLoader.ts @@ -0,0 +1,25 @@ +import type { UnchainedCore } from '@unchainedshop/core'; +import type { Order } from '@unchainedshop/core-orders'; +import DataLoader from 'dataloader'; + +export default (unchainedAPI: UnchainedCore) => + new DataLoader<{ userId: string; countryCode?: string; orderNumber?: string }, Order | null>( + async (queries) => { + const userIds = [...new Set(queries.map((q) => q.userId).filter(Boolean))]; + + const carts = await unchainedAPI.modules.orders.findCarts({ userIds }); + + // findCarts returns carts sorted by `updated` descending, so the first + // match per query key is the most recently updated cart, mirroring the + // semantics of modules.orders.cart. + return queries.map( + (q) => + carts.find( + (cart) => + cart.userId === q.userId && + (!q.countryCode || cart.countryCode === q.countryCode) && + (!q.orderNumber || cart.orderNumber === q.orderNumber), + ) || null, + ); + }, + ); diff --git a/packages/api/src/loaders/index.ts b/packages/api/src/loaders/index.ts index 1c43b5d12..37ee92e89 100644 --- a/packages/api/src/loaders/index.ts +++ b/packages/api/src/loaders/index.ts @@ -27,6 +27,7 @@ import deliveryProviderLoader from './deliveryProviderLoader.ts'; import paymentProviderLoader from './paymentProviderLoader.ts'; import warehousingProviderLoader from './warehousingProviderLoader.ts'; import orderLoader from './orderLoader.ts'; +import cartLoader from './cartLoader.ts'; import quotationLoader from './quotationLoader.ts'; import tokenExportStatusLoader from './tokenExportStatusLoader.ts'; @@ -72,6 +73,8 @@ const loaders = (unchainedAPI: UnchainedCore) => { orderLoader: orderLoader(unchainedAPI), + cartLoader: cartLoader(unchainedAPI), + quotationLoader: quotationLoader(unchainedAPI), tokenExportStatusLoader: tokenExportStatusLoader(unchainedAPI), diff --git a/packages/api/src/resolvers/type/user-types.ts b/packages/api/src/resolvers/type/user-types.ts index d622f0410..c2fe88839 100755 --- a/packages/api/src/resolvers/type/user-types.ts +++ b/packages/api/src/resolvers/type/user-types.ts @@ -168,9 +168,9 @@ export const User: UserHelperTypes = { }, async cart(user, params, context) { - const { modules, countryCode } = context; + const { loaders, countryCode } = context; await checkAction(context, viewUserOrders, [user, params]); - return modules.orders.cart({ + return loaders.cartLoader.load({ countryCode, orderNumber: params.orderNumber, userId: user._id, diff --git a/packages/core-orders/src/module/configureOrdersModule-queries.ts b/packages/core-orders/src/module/configureOrdersModule-queries.ts index 62397da9a..93245c536 100644 --- a/packages/core-orders/src/module/configureOrdersModule-queries.ts +++ b/packages/core-orders/src/module/configureOrdersModule-queries.ts @@ -35,6 +35,36 @@ export interface DateRange { export type StatisticsDateField = 'created' | 'ordered' | 'rejected' | 'confirmed' | 'fulfilled'; +function buildCartSelector({ + countryCode, + orderNumber, + userId, + userIds, +}: { + countryCode?: string; + orderNumber?: string; + userId?: string; + userIds?: string[]; +}): mongodb.Filter { + const selector: mongodb.Filter = { + status: { $eq: null }, + }; + + if (userIds) { + selector.userId = { $in: userIds }; + } else if (userId) { + selector.userId = userId; + } + if (countryCode) { + selector.countryCode = countryCode; + } + if (orderNumber) { + selector.orderNumber = orderNumber; + } + + return selector; +} + function buildDateMatch(dateField: string, dateRange?: DateRange) { if (!dateRange?.start && !dateRange?.end) return { [dateField]: { $exists: true } }; @@ -70,16 +100,7 @@ export const configureOrdersModuleQueries = ({ Orders }: { Orders: mongodb.Colle orderNumber?: string; userId: string; }) => { - const selector: mongodb.Filter = { - countryCode, - status: { $eq: null }, - userId, - }; - - if (orderNumber) { - selector.orderNumber = orderNumber; - } - + const selector = buildCartSelector({ countryCode, orderNumber, userId }); const options: mongodb.FindOptions = { sort: { updated: -1, @@ -88,6 +109,29 @@ export const configureOrdersModuleQueries = ({ Orders }: { Orders: mongodb.Colle return Orders.findOne(selector, options); }, + // Batched variant of `cart`, used by the API cartLoader. Returns the open + // carts (status === null) for the given users, most recently updated first. + findCarts: async ( + { + countryCode, + orderNumber, + userId, + userIds, + }: { + countryCode?: string; + orderNumber?: string; + userId?: string; + userIds?: string[]; + }, + options?: mongodb.FindOptions, + ): Promise => { + const selector = buildCartSelector({ countryCode, orderNumber, userId, userIds }); + return Orders.find(selector, { + sort: { updated: -1 }, + ...options, + }).toArray(); + }, + count: async (query: OrderQuery): Promise => { const orderCount = await Orders.countDocuments(buildFindSelector(query)); return orderCount;