fix(medusa): Implement listAndCount for UserService and update list endpoint (#6190)
This commit is contained in:
@@ -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"),
|
||||
}))
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,6 +32,7 @@ export type FilterableUserProps = PartialPick<
|
||||
| "email"
|
||||
| "first_name"
|
||||
| "last_name"
|
||||
| "role"
|
||||
| "created_at"
|
||||
| "updated_at"
|
||||
| "deleted_at"
|
||||
|
||||
Reference in New Issue
Block a user