fix(medusa): Implement listAndCount for UserService and update list endpoint (#6190)

This commit is contained in:
Kasper Fabricius Kristensen
2024-01-24 10:41:35 +01:00
committed by GitHub
parent eb498c500e
commit d68089b2aa
13 changed files with 633 additions and 198 deletions
@@ -21,8 +21,15 @@ describe("GET /admin/users", () => {
})
it("calls service retrieve", () => {
expect(UserServiceMock.list).toHaveBeenCalledTimes(1)
expect(UserServiceMock.list).toHaveBeenCalledWith({})
expect(UserServiceMock.listAndCount).toHaveBeenCalledTimes(1)
expect(UserServiceMock.listAndCount).toHaveBeenCalledWith(
{},
expect.objectContaining({
order: { created_at: "DESC" },
skip: 0,
take: 20,
})
)
})
})
})
@@ -1,7 +1,8 @@
import { Router } from "express"
import { User } from "../../../.."
import { User } from "../../../../models/user"
import { DeleteResponse } from "../../../../types/common"
import middlewares from "../../../middlewares"
import middlewares, { transformQuery } from "../../../middlewares"
import { AdminGetUsersParams } from "./list-users"
export const unauthenticatedUserRoutes = (app) => {
const route = Router()
@@ -30,11 +31,31 @@ export default (app) => {
route.delete("/:user_id", middlewares.wrap(require("./delete-user").default))
route.get("/", middlewares.wrap(require("./list-users").default))
route.get(
"/",
transformQuery(AdminGetUsersParams, {
defaultFields: defaultAdminUserFields,
isList: true,
}),
middlewares.wrap(require("./list-users").default)
)
return app
}
export const defaultAdminUserFields: (keyof User)[] = [
"id",
"email",
"first_name",
"last_name",
"role",
"api_token",
"created_at",
"updated_at",
"deleted_at",
"metadata",
]
/**
* @schema AdminUserRes
* type: object
@@ -1,13 +1,100 @@
import { Type } from "class-transformer"
import { IsEnum, IsOptional, IsString, ValidateNested } from "class-validator"
import { Request, Response } from "express"
import UserService from "../../../../services/user"
import {
DateComparisonOperator,
extendedFindParamsMixin,
} from "../../../../types/common"
import { UserRole } from "../../../../types/user"
import { IsType } from "../../../../utils"
/**
* @oas [get] /admin/users
* operationId: "GetUsers"
* summary: "List Users"
* description: "Retrieve all admin users."
* description: "Retrieves a list of users. The users can be filtered by fields such as `q` or `email`. The users can also be sorted or paginated."
* x-authenticated: true
* parameters:
* - (query) id {string} Filter by a user ID.
* - (query) email {string} Filter by email.
* - (query) first_name {string} Filter by first name.
* - (query) last_name {string} Filter by last name.
* - (query) q {string} term used to search users' first name, last name, and email.
* - (query) order {string} A user field to sort-order the retrieved users by.
* - in: query
* name: created_at
* description: Filter by a creation date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: updated_at
* description: Filter by an update date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - in: query
* name: deleted_at
* description: Filter by a deletion date range.
* schema:
* type: object
* properties:
* lt:
* type: string
* description: filter by dates less than this date
* format: date
* gt:
* type: string
* description: filter by dates greater than this date
* format: date
* lte:
* type: string
* description: filter by dates less than or equal to this date
* format: date
* gte:
* type: string
* description: filter by dates greater than or equal to this date
* format: date
* - (query) offset=0 {integer} The number of users to skip when retrieving the users.
* - (query) limit=20 {integer} Limit the number of users returned.
* - (query) expand {string} Comma-separated relations that should be expanded in the returned users.
* - (query) fields {string} Comma-separated fields that should be included in the returned users.
* x-codegen:
* method: list
* queryParams: AdminGetUsersParams
* x-codeSamples:
* - lang: JavaScript
* label: JS Client
@@ -16,7 +103,7 @@ import UserService from "../../../../services/user"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.users.list()
* .then(({ users }) => {
* .then(({ users, limit, offset, count }) => {
* console.log(users.length);
* })
* - lang: tsx
@@ -75,9 +162,96 @@ import UserService from "../../../../services/user"
* "500":
* $ref: "#/components/responses/500_error"
*/
export default async (req, res) => {
export default async (req: Request, res: Response) => {
const userService: UserService = req.scope.resolve("userService")
const users = await userService.list({})
res.status(200).json({ users })
const listConfig = req.listConfig
const filterableFields = req.filterableFields
const [users, count] = await userService.listAndCount(
filterableFields,
listConfig
)
res
.status(200)
.json({ users, count, offset: listConfig.skip, limit: listConfig.take })
}
/**
* Parameters used to filter and configure the pagination of the retrieved users.
*/
export class AdminGetUsersParams extends extendedFindParamsMixin() {
/**
* IDs to filter users by.
*/
@IsOptional()
@IsType([String, [String]])
id?: string | string[]
/**
* Search terms to search users' first name, last name, and email.
*/
@IsOptional()
@IsString()
q?: string
/**
* The field to sort the data by. By default, the sort order is ascending. To change the order to descending, prefix the field name with `-`.
*/
@IsString()
@IsOptional()
order?: string
/**
* Date filters to apply on the users' `update_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
updated_at?: DateComparisonOperator
/**
* Date filters to apply on the customer users' `created_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
created_at?: DateComparisonOperator
/**
* Date filters to apply on the users' `deleted_at` date.
*/
@IsOptional()
@ValidateNested()
@Type(() => DateComparisonOperator)
deleted_at?: DateComparisonOperator
/**
* Filter to apply on the users' `email` field.
*/
@IsOptional()
@IsString()
email?: string
/**
* Filter to apply on the users' `first_name` field.
*/
@IsOptional()
@IsString()
first_name?: string
/**
* Filter to apply on the users' `last_name` field.
*/
@IsOptional()
@IsString()
last_name?: string
/**
* Filter to apply on the users' `role` field.
*/
@IsOptional()
@IsEnum(UserRole, { each: true })
role?: UserRole
}
@@ -1,6 +1,5 @@
import Scrypt from "scrypt-kdf"
import { IdMap } from "medusa-test-utils"
import _ from "lodash"
import Scrypt from "scrypt-kdf"
export const users = {
testUser: {
@@ -29,7 +28,7 @@ export const UserServiceMock = {
withTransaction: function () {
return this
},
create: jest.fn().mockImplementation(data => {
create: jest.fn().mockImplementation((data) => {
if (data.email === "oliver@test.dk") {
return Promise.resolve(users.testUser)
}
@@ -37,7 +36,8 @@ export const UserServiceMock = {
}),
update: jest.fn().mockReturnValue(Promise.resolve()),
list: jest.fn().mockReturnValue(Promise.resolve([])),
delete: jest.fn().mockImplementation(data => {
listAndCount: jest.fn().mockReturnValue(Promise.resolve([[], 0])),
delete: jest.fn().mockImplementation((data) => {
if (data === IdMap.getId("delete-user")) {
return Promise.resolve({
id: IdMap.getId("delete-user"),
@@ -47,7 +47,7 @@ export const UserServiceMock = {
}
return Promise.resolve(undefined)
}),
retrieve: jest.fn().mockImplementation(userId => {
retrieve: jest.fn().mockImplementation((userId) => {
if (userId === IdMap.getId("test-user")) {
return Promise.resolve(users.testUser)
}
@@ -60,7 +60,7 @@ export const UserServiceMock = {
}
return Promise.resolve(undefined)
}),
setPassword_: jest.fn().mockImplementation(userId => {
setPassword_: jest.fn().mockImplementation((userId) => {
if (userId === IdMap.getId("test-user")) {
return Promise.resolve(users.testUser)
}
@@ -80,13 +80,13 @@ export const UserServiceMock = {
generateResetPasswordToken: jest
.fn()
.mockReturnValue(Promise.resolve("JSONWEBTOKEN")),
retrieveByApiToken: jest.fn().mockImplementation(token => {
retrieveByApiToken: jest.fn().mockImplementation((token) => {
if (token === "123456789") {
return Promise.resolve(users.user1)
}
return Promise.resolve(undefined)
}),
retrieveByEmail: jest.fn().mockImplementation(email => {
retrieveByEmail: jest.fn().mockImplementation((email) => {
if (email === "vandijk@test.dk") {
return Promise.resolve({
id: IdMap.getId("vandijk"),
@@ -95,7 +95,7 @@ export const UserServiceMock = {
})
}
if (email === "oliver@test.dk") {
return Scrypt.kdf("123456789", { logN: 1, r: 1, p: 1 }).then(hash => ({
return Scrypt.kdf("123456789", { logN: 1, r: 1, p: 1 }).then((hash) => ({
email,
password_hash: hash.toString("base64"),
}))
+84 -6
View File
@@ -1,17 +1,18 @@
import { Selector } from "@medusajs/types"
import { FlagRouter } from "@medusajs/utils"
import jwt from "jsonwebtoken"
import { isDefined, MedusaError } from "medusa-core-utils"
import Scrypt from "scrypt-kdf"
import { EntityManager } from "typeorm"
import { EntityManager, FindOptionsWhere, ILike } from "typeorm"
import { TransactionBaseService } from "../interfaces"
import AnalyticsFeatureFlag from "../loaders/feature-flags/analytics"
import { User } from "../models"
import { UserRepository } from "../repositories/user"
import { FindConfig } from "../types/common"
import {
CreateUserInput,
FilterableUserProps,
UpdateUserInput,
CreateUserInput,
FilterableUserProps,
UpdateUserInput,
} from "../types/user"
import { buildQuery, setMetadata } from "../utils"
import { validateEmail } from "../utils/is-email"
@@ -62,9 +63,86 @@ class UserService extends TransactionBaseService {
* @param {Object} config - the configuration object for the query
* @return {Promise} the result of the find operation
*/
async list(selector: FilterableUserProps, config = {}): Promise<User[]> {
async list(
selector: Selector<FilterableUserProps> & { q?: string } = {},
config: FindConfig<FilterableUserProps> = { skip: 0, take: 20 }
): Promise<User[]> {
const userRepo = this.activeManager_.withRepository(this.userRepository_)
return await userRepo.find(buildQuery(selector, config))
let q: string | undefined
if (selector.q) {
q = selector.q
delete selector.q
}
const query = buildQuery(selector, config)
if (q) {
const where = query.where as FindOptionsWhere<FilterableUserProps>
delete where.email
delete where.first_name
delete where.last_name
query.where = [
{
...where,
email: ILike(`%${q}%`),
},
{
...where,
first_name: ILike(`%${q}%`),
},
{
...where,
last_name: ILike(`%${q}%`),
},
]
}
return await userRepo.find(query)
}
async listAndCount(
selector: Selector<FilterableUserProps> & { q?: string } = {},
config: FindConfig<FilterableUserProps> = { skip: 0, take: 20 }
) {
const userRepo = this.activeManager_.withRepository(this.userRepository_)
let q: string | undefined
if (selector.q) {
q = selector.q
delete selector.q
}
const query = buildQuery(selector, config)
if (q) {
const where = query.where as FindOptionsWhere<FilterableUserProps>
delete where.email
delete where.first_name
delete where.last_name
query.where = [
{
...where,
email: ILike(`%${q}%`),
},
{
...where,
first_name: ILike(`%${q}%`),
},
{
...where,
last_name: ILike(`%${q}%`),
},
]
}
return await userRepo.findAndCount(query)
}
/**
+1
View File
@@ -32,6 +32,7 @@ export type FilterableUserProps = PartialPick<
| "email"
| "first_name"
| "last_name"
| "role"
| "created_at"
| "updated_at"
| "deleted_at"