feat(medusa): Add proper pagination (#4517)

* update method for listing regions

* add changeset

* fix unit tests

* listAndCount swaps

* add count calculation to list-returns

* swap integration test

* notes pagination

* pagination props for notifications

* listAndCount store regions

* fix nit

* fix note unit test

* update list-regions store unit test

* cleanup integration test

* rename introduced tests

---------

Co-authored-by: Oliver Windall Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Philip KorsholmandOliver Windall Juhl authored and GitHub committed 2023-07-14 16:14:51 +02:00
1 parent 55db914151
commit 5b91a3503a
21 files changed
+401 -192

No files matched your search

@@ -7,4 +7,16 @@ import type { Notification } from "./Notification"
export interface AdminNotificationsListRes {
notifications: Array<SetRelation<Notification, "resends">>
/**
* The total number of notifications
*/
count?: number
/**
* The number of notifications skipped before these notifications
*/
offset?: number
/**
* The number of notifications per page
*/
limit?: number
}
@@ -12,4 +12,16 @@ export interface StoreRegionsListRes {
"countries" | "payment_providers" | "fulfillment_providers"
>
>
/**
* The total number of items available
*/
count?: number
/**
* The number of items skipped before these items
*/
offset?: number
/**
* The number of items per page
*/
limit?: number
}
@@ -69,7 +69,7 @@ export default async (req, res) => {
}
const noteService: NoteService = req.scope.resolve("noteService")
const notes = await noteService.list(selector, {
const [notes, count] = await noteService.listAndCount(selector, {
take: validated.limit,
skip: validated.offset,
relations: ["author"],
@@ -77,7 +77,7 @@ export default async (req, res) => {
res.status(200).json({
notes,
count: notes.length,
count,
offset: validated.offset,
limit: validated.limit,
})
@@ -1,5 +1,6 @@
import { Router } from "express"
import { Notification } from "./../../../../"
import { PaginatedResponse } from "@medusajs/types"
import { Router } from "express"
import middlewares from "../../../middlewares"
const route = Router()
@@ -50,8 +51,17 @@ export const defaultAdminNotificationsFields = [
* type: array
* items:
* $ref: "#/components/schemas/Notification"
* count:
* type: integer
* description: The total number of notifications
* offset:
* type: integer
* description: The number of notifications skipped before these notifications
* limit:
* type: integer
* description: The number of notifications per page
*/
export type AdminNotificationsListRes = {
export type AdminNotificationsListRes = PaginatedResponse & {
notifications: Notification[]
}
@@ -3,9 +3,9 @@ import {
defaultAdminNotificationsFields,
defaultAdminNotificationsRelations,
} from "./"
import { Notification } from "../../../../models"
import { FindConfig } from "../../../../types/common"
import { FindConfig } from "../../../../types/common"
import { Notification } from "../../../../models"
import { NotificationService } from "../../../../services"
import { Type } from "class-transformer"
import { pick } from "lodash"
@@ -135,7 +135,10 @@ export default async (req, res) => {
order: { created_at: "DESC" },
} as FindConfig<Notification>
const notifications = await notificationService.list(selector, listConfig)
const [notifications, count] = await notificationService.listAndCount(
selector,
listConfig
)
const resultFields = [
...(listConfig.select ?? []),
@@ -143,7 +146,7 @@ export default async (req, res) => {
]
const data = notifications.map((o) => pick(o, resultFields))
res.json({ notifications: data })
res.json({ notifications: data, count, limit, offset })
}
export class AdminGetNotificationsParams {
@@ -1,11 +1,11 @@
import { IsNumber, IsOptional } from "class-validator"
import { ReturnService } from "../../../../services"
import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator"
import { FindConfig } from "../../../../types/common"
import { Return } from "../../../../models"
import { ReturnService } from "../../../../services"
import { Type } from "class-transformer"
import { defaultRelationsList } from "."
import { validator } from "../../../../utils/validator"
/**
* @oas [get] /admin/returns
@@ -73,11 +73,13 @@ export default async (req, res) => {
order: { created_at: "DESC" },
} as FindConfig<Return>
const returns = await returnService.list(selector, { ...listConfig })
const [returns, count] = await returnService.listAndCount(selector, {
...listConfig,
})
res.json({
returns,
count: returns.length,
count,
offset: validated.offset,
limit: validated.limit,
})
@@ -1,6 +1,6 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { SwapServiceMock } from "../../../../../services/__mocks__/swap"
import { request } from "../../../../../helpers/test-request"
const defaultListOptions = {
take: 50,
@@ -27,8 +27,8 @@ describe("GET /admin/swaps/", () => {
})
it("calls swapService list with default pagination and sorting options", () => {
expect(SwapServiceMock.list).toHaveBeenCalledTimes(1)
expect(SwapServiceMock.list).toHaveBeenCalledWith(
expect(SwapServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(SwapServiceMock.listAndCount).toHaveBeenCalledWith(
{},
{
...defaultListOptions,
@@ -1,10 +1,10 @@
import { Type } from "class-transformer"
import { IsInt, IsOptional } from "class-validator"
import { SwapService } from "../../../../services"
import { validator } from "../../../../utils/validator"
import { FindConfig } from "../../../../types/common"
import { Swap } from "../../../../models"
import { SwapService } from "../../../../services"
import { Type } from "class-transformer"
import { validator } from "../../../../utils/validator"
/**
* @oas [get] /admin/swaps
@@ -72,9 +72,11 @@ export default async (req, res) => {
order: { created_at: "DESC" },
}
const swaps = await swapService.list(selector, { ...listConfig })
const [swaps, count] = await swapService.listAndCount(selector, {
...listConfig,
})
res.json({ swaps, count: swaps.length, offset, limit })
res.json({ swaps, count, offset, limit })
}
export class AdminGetSwapsParams {
@@ -1,6 +1,6 @@
import { IdMap } from "medusa-test-utils"
import { request } from "../../../../../helpers/test-request"
import { RegionServiceMock } from "../../../../../services/__mocks__/region"
import { request } from "../../../../../helpers/test-request"
describe("List regions", () => {
describe("list regions", () => {
@@ -14,8 +14,8 @@ describe("List regions", () => {
})
it("calls list from region service", () => {
expect(RegionServiceMock.list).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.list).toHaveBeenCalledWith(
expect(RegionServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(RegionServiceMock.listAndCount).toHaveBeenCalledWith(
{},
{
relations: [
@@ -1,6 +1,7 @@
import { PaginatedResponse } from "@medusajs/types"
import { Region } from "./../../../../"
import { Router } from "express"
import middlewares from "../../../middlewares"
import { Region } from "./../../../../"
const route = Router()
@@ -38,8 +39,17 @@ export const defaultRelations = [
* type: array
* items:
* $ref: "#/components/schemas/Region"
* count:
* type: integer
* description: The total number of items available
* offset:
* type: integer
* description: The number of items skipped before these items
* limit:
* type: integer
* description: The number of items per page
*/
export type StoreRegionsListRes = {
export type StoreRegionsListRes = PaginatedResponse & {
regions: Region[]
}
@@ -3,9 +3,9 @@ import { IsInt, IsOptional, ValidateNested } from "class-validator"
import { DateComparisonOperator } from "../../../../types/common"
import RegionService from "../../../../services/region"
import { Type } from "class-transformer"
import { defaultRelations } from "."
import { omit } from "lodash"
import { validator } from "../../../../utils/validator"
import { defaultRelations } from "."
/**
* @oas [get] /store/regions
@@ -110,9 +110,12 @@ export default async (req, res) => {
take: limit,
}
const regions = await regionService.list(filterableFields, listConfig)
const [regions, count] = await regionService.listAndCount(
filterableFields,
listConfig
)
res.json({ regions })
res.json({ regions, count, limit, offset })
}
export class StoreGetRegionsParams {
+12 -6
View File
@@ -1,16 +1,16 @@
import { IdMap } from "medusa-test-utils"
export const SwapServiceMock = {
withTransaction: function() {
withTransaction: function () {
return this
},
registerCartCompletion: jest.fn().mockImplementation(data => {
registerCartCompletion: jest.fn().mockImplementation((data) => {
return Promise.resolve({ id: "test-swap" })
}),
create: jest.fn().mockImplementation(data => {
create: jest.fn().mockImplementation((data) => {
return Promise.resolve()
}),
retrieve: jest.fn().mockImplementation(data => {
retrieve: jest.fn().mockImplementation((data) => {
switch (data) {
case IdMap.getId("test-swap"):
return Promise.resolve({
@@ -21,11 +21,11 @@ export const SwapServiceMock = {
return Promise.resolve({ id: "test-swap" })
}
}),
cancel: jest.fn().mockImplementation(f => {
cancel: jest.fn().mockImplementation((f) => {
return Promise.resolve({ f })
}),
cancelFulfillment: jest.fn().mockImplementation(f => {
cancelFulfillment: jest.fn().mockImplementation((f) => {
return Promise.resolve({ id: IdMap.getId("test-swap") })
}),
list: jest.fn().mockImplementation((...args) => {
@@ -34,6 +34,12 @@ export const SwapServiceMock = {
{ id: IdMap.getId("test-swap-1") },
])
}),
listAndCount: jest.fn().mockImplementation((...args) => {
return Promise.resolve([
[{ id: IdMap.getId("test-swap") }, { id: IdMap.getId("test-swap-1") }],
2,
])
}),
}
const mock = jest.fn().mockImplementation(() => {
+10 -3
View File
@@ -1,6 +1,7 @@
import NoteService from "../note"
import { IdMap, MockManager, MockRepository } from "medusa-test-utils"
import { EventBusServiceMock } from "../__mocks__/event-bus"
import NoteService from "../note"
describe("NoteService", () => {
describe("list", () => {
@@ -10,6 +11,12 @@ describe("NoteService", () => {
{ id: IdMap.getId("note"), value: "some note" },
])
},
findAndCount: (q) => {
return Promise.resolve([
[{ id: IdMap.getId("note"), value: "some note" }],
1,
])
},
})
const noteService = new NoteService({
@@ -28,8 +35,8 @@ describe("NoteService", () => {
relations: ["author"],
}
)
expect(noteRepo.find).toHaveBeenCalledTimes(1)
expect(noteRepo.find).toHaveBeenCalledWith({
expect(noteRepo.findAndCount).toHaveBeenCalledTimes(1)
expect(noteRepo.findAndCount).toHaveBeenCalledWith({
where: {
resource_id: IdMap.getId("note"),
},
+28 -6
View File
@@ -1,12 +1,13 @@
import { isDefined, MedusaError } from "medusa-core-utils"
import { FindConfig, Selector } from "../types/common"
import { MedusaError, isDefined } from "medusa-core-utils"
import { CreateNoteInput } from "../types/note"
import { EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import EventBusService from "./event-bus"
import { Note } from "../models"
import { NoteRepository } from "../repositories/note"
import { FindConfig, Selector } from "../types/common"
import { CreateNoteInput } from "../types/note"
import { TransactionBaseService } from "../interfaces"
import { buildQuery } from "../utils"
import EventBusService from "./event-bus"
type InjectedDependencies = {
manager: EntityManager
@@ -81,11 +82,32 @@ class NoteService extends TransactionBaseService {
relations: [],
}
): Promise<Note[]> {
const [result] = await this.listAndCount(selector, config)
return result
}
/** Fetches all notes related to the given selector
* @param selector - the query object for find
* @param config - the configuration used to find the objects. contains relations, skip, and take.
* @param config.relations - Which relations to include in the resulting list of Notes.
* @param config.take - How many Notes to take in the resulting list of Notes.
* @param config.skip - How many Notes to skip in the resulting list of Notes.
* @return notes related to the given search.
*/
async listAndCount(
selector: Selector<Note>,
config: FindConfig<Note> = {
skip: 0,
take: 50,
relations: [],
}
): Promise<[Note[], number]> {
const noteRepo = this.activeManager_.withRepository(this.noteRepository_)
const query = buildQuery(selector, config)
return noteRepo.find(query)
return noteRepo.findAndCount(query)
}
/**
+26 -6
View File
@@ -1,15 +1,16 @@
import { MedusaError } from "medusa-core-utils"
import {
AbstractNotificationService,
TransactionBaseService,
} from "../interfaces"
import { FindConfig, Selector } from "../types/common"
import { EntityManager } from "typeorm"
import { Logger } from "../types/global"
import { NotificationRepository } from "../repositories/notification"
import { NotificationProviderRepository } from "../repositories/notification-provider"
import { FindConfig, Selector } from "../types/common"
import { buildQuery } from "../utils"
import { MedusaError } from "medusa-core-utils"
import { Notification } from "../models"
import { NotificationProviderRepository } from "../repositories/notification-provider"
import { NotificationRepository } from "../repositories/notification"
import { buildQuery } from "../utils"
type InjectedDependencies = {
manager: EntityManager
@@ -82,11 +83,30 @@ class NotificationService extends TransactionBaseService {
order: { created_at: "DESC" },
}
): Promise<Notification[]> {
const [notifications] = await this.listAndCount(selector, config)
return notifications
}
/**
* Retrieves a list of notifications and total count.
* @param selector - the params to select the notifications by.
* @param config - the configuration to apply to the query
* @return the notifications that satisfy the query as well as the count.
*/
async listAndCount(
selector: Selector<Notification>,
config: FindConfig<Notification> = {
skip: 0,
take: 50,
order: { created_at: "DESC" },
}
): Promise<[Notification[], number]> {
const notiRepo = this.activeManager_.withRepository(
this.notificationRepository_
)
const query = buildQuery(selector, config)
return await notiRepo.find(query)
return await notiRepo.findAndCount(query)
}
/**
+36 -19
View File
@@ -1,22 +1,6 @@
import { isDefined, MedusaError } from "medusa-core-utils"
import { DeepPartial, EntityManager } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import {
FulfillmentStatus,
LineItem,
Order,
PaymentStatus,
Return,
ReturnItem,
ReturnStatus,
} from "../models"
import { ReturnRepository } from "../repositories/return"
import { ReturnItemRepository } from "../repositories/return-item"
import { FindConfig, Selector } from "../types/common"
import { OrdersReturnItem } from "../types/orders"
import { CreateReturnInput, UpdateReturnInput } from "../types/return"
import { buildQuery, setMetadata } from "../utils"
import { DeepPartial, EntityManager } from "typeorm"
import { FindConfig, Selector } from "../types/common"
import {
FulfillmentProviderService,
LineItemService,
@@ -27,6 +11,22 @@ import {
TaxProviderService,
TotalsService,
} from "."
import {
FulfillmentStatus,
LineItem,
Order,
PaymentStatus,
Return,
ReturnItem,
ReturnStatus,
} from "../models"
import { MedusaError, isDefined } from "medusa-core-utils"
import { buildQuery, setMetadata } from "../utils"
import { OrdersReturnItem } from "../types/orders"
import { ReturnItemRepository } from "../repositories/return-item"
import { ReturnRepository } from "../repositories/return"
import { TransactionBaseService } from "../interfaces"
type InjectedDependencies = {
manager: EntityManager
@@ -146,11 +146,28 @@ class ReturnService extends TransactionBaseService {
order: { created_at: "DESC" },
}
): Promise<Return[]> {
const [returns] = await this.listAndCount(selector, config)
return returns
}
/**
* @param selector - the query object for find
* @param config - the config object for find
* @return the result of the find operation
*/
async listAndCount(
selector: Selector<Return>,
config: FindConfig<Return> = {
skip: 0,
take: 50,
order: { created_at: "DESC" },
}
): Promise<[Return[], number]> {
const returnRepo = this.activeManager_.withRepository(
this.returnRepository_
)
const query = buildQuery(selector, config)
return returnRepo.find(query)
return returnRepo.findAndCount(query)
}
/**
+22 -2
View File
@@ -29,7 +29,7 @@ import {
} from "./index"
import { EntityManager, In } from "typeorm"
import { FindConfig, Selector, WithRequiredProperty } from "../types/common"
import { isDefined, MedusaError } from "medusa-core-utils"
import { MedusaError, isDefined } from "medusa-core-utils"
import { buildQuery, setMetadata, validateId } from "../utils"
import { CreateShipmentConfig } from "../types/fulfillment"
@@ -278,11 +278,31 @@ class SwapService extends TransactionBaseService {
order: { created_at: "DESC" },
}
): Promise<Swap[]> {
const [swaps] = await this.listAndCount(selector, config)
return swaps
}
/**
* List swaps.
*
* @param selector - the query object for find
* @param config - the configuration used to find the objects. contains relations, skip, and take.
* @return the result of the find operation
*/
async listAndCount(
selector: Selector<Swap>,
config: FindConfig<Swap> = {
skip: 0,
take: 50,
order: { created_at: "DESC" },
}
): Promise<[Swap[], number]> {
const swapRepo = this.activeManager_.withRepository(this.swapRepository_)
const query = buildQuery(selector, config)
query.relationLoadStrategy = "query"
return await swapRepo.find(query)
return await swapRepo.findAndCount(query)
}
/**