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
+9
View File
@@ -0,0 +1,9 @@
---
"@medusajs/medusa": patch
"@medusajs/medusa-js": patch
"medusa-react": patch
---
fix(medusa): Implements `listAndCount` method for UserService, and updates list endpoint to accept the expected params.
fix(medusa-js): Update `admin.users.list` to accept query params.
fix(medusa-react): Update `useAdminUsers` hook to accept query params.
@@ -1,90 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`/admin/users GET /admin/users lists users 1`] = `
Array [
Object {
"api_token": "test_token",
"created_at": Any<String>,
"deleted_at": null,
"email": "admin@medusa.js",
"first_name": null,
"id": "admin_user",
"last_name": null,
"metadata": null,
"role": "admin",
"updated_at": Any<String>,
},
Object {
"api_token": null,
"created_at": Any<String>,
"deleted_at": null,
"email": "member@test.com",
"first_name": "member",
"id": "member-user",
"last_name": "user",
"metadata": null,
"role": "member",
"updated_at": Any<String>,
},
]
`;
exports[`/admin/users GET /admin/users returns user by id 1`] = `
Object {
"api_token": "test_token",
"created_at": Any<String>,
"deleted_at": null,
"email": "admin@medusa.js",
"first_name": null,
"id": "admin_user",
"last_name": null,
"metadata": null,
"role": "admin",
"updated_at": Any<String>,
}
`;
exports[`/admin/users POST /admin/users creates a user 1`] = `
Object {
"api_token": null,
"created_at": Any<String>,
"deleted_at": null,
"email": "test@test123.com",
"first_name": null,
"id": StringMatching /\\^usr_\\*/,
"last_name": null,
"metadata": null,
"role": "member",
"updated_at": Any<String>,
}
`;
exports[`/admin/users POST /admin/users updates a user 1`] = `
Object {
"api_token": null,
"created_at": Any<String>,
"deleted_at": null,
"email": "member@test.com",
"first_name": "karl",
"id": "member-user",
"last_name": "user",
"metadata": null,
"password_hash": null,
"role": "member",
"updated_at": Any<String>,
}
`;
exports[`[MEDUSA_FF_ANALYTICS] /admin/analytics-config DELETE /admin/users Deletes a user and their analytics config 1`] = `
Array [
Object {
"anonymize": false,
"created_at": Any<Date>,
"deleted_at": Any<Date>,
"id": Any<String>,
"opt_out": false,
"updated_at": Any<Date>,
"user_id": "member-user",
},
]
`;
+109 -54
View File
@@ -54,14 +54,16 @@ describe("/admin/users", () => {
const response = await api.get("/admin/users/admin_user", adminReqConfig)
expect(response.status).toEqual(200)
expect(response.data.user).toMatchSnapshot({
id: "admin_user",
email: "admin@medusa.js",
api_token: "test_token",
role: "admin",
created_at: expect.any(String),
updated_at: expect.any(String),
})
expect(response.data.user).toEqual(
expect.objectContaining({
id: "admin_user",
email: "admin@medusa.js",
api_token: "test_token",
role: "admin",
created_at: expect.any(String),
updated_at: expect.any(String),
})
)
})
it("lists users", async () => {
@@ -75,25 +77,72 @@ describe("/admin/users", () => {
expect(response.status).toEqual(200)
expect(response.data.users).toMatchSnapshot([
{
id: "admin_user",
email: "admin@medusa.js",
api_token: "test_token",
role: "admin",
created_at: expect.any(String),
updated_at: expect.any(String),
},
{
id: "member-user",
role: "member",
email: "member@test.com",
first_name: "member",
last_name: "user",
created_at: expect.any(String),
updated_at: expect.any(String),
},
])
expect(response.data.users).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "admin_user",
email: "admin@medusa.js",
api_token: "test_token",
role: "admin",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
expect.objectContaining({
id: "member-user",
role: "member",
email: "member@test.com",
first_name: "member",
last_name: "user",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
])
)
})
it("lists users that match the free text search", async () => {
const api = useApi()
const response = await api.get("/admin/users?q=member", adminReqConfig)
expect(response.status).toEqual(200)
expect(response.data.users.length).toEqual(1)
expect(response.data.users).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "member-user",
role: "member",
email: "member@test.com",
first_name: "member",
last_name: "user",
created_at: expect.any(String),
updated_at: expect.any(String),
}),
])
)
})
it("orders users by created_at", async () => {
const api = useApi()
const response = await api.get(
"/admin/users?order=created_at",
adminReqConfig
)
expect(response.status).toEqual(200)
expect(response.data.users.length).toBeGreaterThan(0)
for (let i = 0; i < response.data.users.length - 1; i++) {
const user1 = response.data.users[i]
const user2 = response.data.users[i + 1]
const date1 = new Date(user1.created_at)
const date2 = new Date(user2.created_at)
expect(date1.getTime()).toBeLessThanOrEqual(date2.getTime())
}
})
})
@@ -138,13 +187,15 @@ describe("/admin/users", () => {
.catch((err) => console.log(err))
expect(response.status).toEqual(200)
expect(response.data.user).toMatchSnapshot({
id: expect.stringMatching(/^usr_*/),
created_at: expect.any(String),
updated_at: expect.any(String),
role: "member",
email: "test@test123.com",
})
expect(response.data.user).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^usr_*/),
created_at: expect.any(String),
updated_at: expect.any(String),
role: "member",
email: "test@test123.com",
})
)
})
it("updates a user", async () => {
@@ -159,15 +210,17 @@ describe("/admin/users", () => {
.catch((err) => console.log(err.response.data.message))
expect(updateResponse.status).toEqual(200)
expect(updateResponse.data.user).toMatchSnapshot({
id: "member-user",
created_at: expect.any(String),
updated_at: expect.any(String),
role: "member",
email: "member@test.com",
first_name: "karl",
last_name: "user",
})
expect(updateResponse.data.user).toEqual(
expect.objectContaining({
id: "member-user",
created_at: expect.any(String),
updated_at: expect.any(String),
role: "member",
email: "member@test.com",
first_name: "karl",
last_name: "user",
})
)
})
describe("Password reset", () => {
@@ -417,17 +470,19 @@ describe("[MEDUSA_FF_ANALYTICS] /admin/analytics-config", () => {
`SELECT * FROM public.analytics_config WHERE user_id = '${userId}'`
)
expect(configs).toMatchSnapshot([
{
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: expect.any(Date),
id: expect.any(String),
user_id: userId,
opt_out: false,
anonymize: false,
},
])
expect(configs).toEqual(
expect.arrayContaining([
expect.objectContaining({
created_at: expect.any(Date),
updated_at: expect.any(Date),
deleted_at: expect.any(Date),
id: expect.any(String),
user_id: userId,
opt_out: false,
anonymize: false,
}),
])
)
})
})
})
@@ -0,0 +1,110 @@
/* istanbul ignore file */
/* tslint:disable */
/* eslint-disable */
import { SetRelation, Merge } from "../core/ModelUtils"
export interface AdminGetUsersParams {
/**
* Filter by a user ID.
*/
id?: string
/**
* Filter by email.
*/
email?: string
/**
* Filter by first name.
*/
first_name?: string
/**
* Filter by last name.
*/
last_name?: string
/**
* term used to search users' first name, last name, and email.
*/
q?: string
/**
* A user field to sort-order the retrieved users by.
*/
order?: string
/**
* Filter by a creation date range.
*/
created_at?: {
/**
* filter by dates less than this date
*/
lt?: string
/**
* filter by dates greater than this date
*/
gt?: string
/**
* filter by dates less than or equal to this date
*/
lte?: string
/**
* filter by dates greater than or equal to this date
*/
gte?: string
}
/**
* Filter by an update date range.
*/
updated_at?: {
/**
* filter by dates less than this date
*/
lt?: string
/**
* filter by dates greater than this date
*/
gt?: string
/**
* filter by dates less than or equal to this date
*/
lte?: string
/**
* filter by dates greater than or equal to this date
*/
gte?: string
}
/**
* Filter by a deletion date range.
*/
deleted_at?: {
/**
* filter by dates less than this date
*/
lt?: string
/**
* filter by dates greater than this date
*/
gt?: string
/**
* filter by dates less than or equal to this date
*/
lte?: string
/**
* filter by dates greater than or equal to this date
*/
gte?: string
}
/**
* The number of users to skip when retrieving the users.
*/
offset?: number
/**
* Limit the number of users returned.
*/
limit?: number
/**
* Comma-separated relations that should be expanded in the returned users.
*/
expand?: string
/**
* Comma-separated fields that should be included in the returned users.
*/
fields?: string
}
@@ -92,6 +92,7 @@ export type { AdminGetStockLocationsParams } from "./AdminGetStockLocationsParam
export type { AdminGetSwapsParams } from "./AdminGetSwapsParams"
export type { AdminGetTaxRatesParams } from "./AdminGetTaxRatesParams"
export type { AdminGetTaxRatesTaxRateParams } from "./AdminGetTaxRatesTaxRateParams"
export type { AdminGetUsersParams } from "./AdminGetUsersParams"
export type { AdminGetVariantParams } from "./AdminGetVariantParams"
export type { AdminGetVariantsParams } from "./AdminGetVariantsParams"
export type { AdminGetVariantsVariantInventoryRes } from "./AdminGetVariantsVariantInventoryRes"
+39 -14
View File
@@ -1,37 +1,38 @@
import {
AdminDeleteUserRes,
AdminGetUsersParams,
AdminResetPasswordRequest,
AdminResetPasswordTokenRequest,
AdminUserRes,
AdminUsersListRes,
} from "@medusajs/medusa"
import qs from "qs"
import {
ResponsePromise,
AdminCreateUserPayload,
AdminUpdateUserPayload,
ResponsePromise,
} from "../.."
import BaseResource from "../base"
/**
* This class is used to send requests to [Admin User API Routes](https://docs.medusajs.com/api/admin#users). All its method
* are available in the JS Client under the `medusa.admin.users` property.
*
*
* All methods in this class require {@link AdminAuthResource.createSession | user authentication}.
*
*
* A store can have more than one user, each having the same privileges. Admins can manage users, their passwords, and more.
*
*
* Related Guide: [How to manage users](https://docs.medusajs.com/modules/users/admin/manage-users).
*/
class AdminUsersResource extends BaseResource {
/**
* Generate a password token for an admin user with a given email. This also triggers the `user.password_reset` event. So, if you have a Notification Service installed
* that can handle this event, a notification, such as an email, will be sent to the user. The token is triggered as part of the `user.password_reset` event's payload.
* that can handle this event, a notification, such as an email, will be sent to the user. The token is triggered as part of the `user.password_reset` event's payload.
* That token must be used later to reset the password using the {@link resetPassword} method.
* @param {AdminResetPasswordTokenRequest} payload - The user's reset details.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<void>} Resolves when the token is generated successfully.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -60,7 +61,7 @@ class AdminUsersResource extends BaseResource {
* @param {AdminResetPasswordRequest} payload - The reset details.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminUserRes>} Resolves to the user's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -86,7 +87,7 @@ class AdminUsersResource extends BaseResource {
* @param {string} id - The user's ID.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminUserRes>} Resolves to the user's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -109,7 +110,7 @@ class AdminUsersResource extends BaseResource {
* @param {AdminCreateUserPayload} payload - The user to create.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminUserRes>} Resolves to the user's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -136,7 +137,7 @@ class AdminUsersResource extends BaseResource {
* @param {AdminUpdateUserPayload} payload - The attributes to update in the user.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminUserRes>} Resolves to the user's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -162,7 +163,7 @@ class AdminUsersResource extends BaseResource {
* @param {string} id - The user's ID.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminDeleteUserRes>} Resolves to the deletion operation's details.
*
*
* @example
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
@@ -184,8 +185,10 @@ class AdminUsersResource extends BaseResource {
* Retrieve all admin users.
* @param {Record<string, any>} customHeaders - Custom headers to attach to the request.
* @returns {ResponsePromise<AdminUsersListRes>} Resolves to the list of users.
*
*
* @example
* To list users:
*
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
@@ -193,11 +196,33 @@ class AdminUsersResource extends BaseResource {
* .then(({ users }) => {
* console.log(users.length);
* })
*
* By default, only the first `20` users are returned. You can control pagination by specifying the `limit` and `offset` properties:
*
* ```ts
* import Medusa from "@medusajs/medusa-js"
* const medusa = new Medusa({ baseUrl: MEDUSA_BACKEND_URL, maxRetries: 3 })
* // must be previously logged in or use api token
* medusa.admin.users.list({
* limit,
* offset
* })
* .then(({ users, limit, offset, count }) => {
* console.log(users.length);
* })
* ```
*/
list(
query?: AdminGetUsersParams,
customHeaders: Record<string, any> = {}
): ResponsePromise<AdminUsersListRes> {
const path = `/admin/users`
let path = `/admin/users`
if (query) {
const queryString = qs.stringify(query)
path += `?${queryString}`
}
return this.client.request("GET", path, undefined, {}, customHeaders)
}
}
@@ -1,4 +1,8 @@
import { AdminUserRes, AdminUsersListRes } from "@medusajs/medusa"
import {
AdminGetUsersParams,
AdminUserRes,
AdminUsersListRes,
} from "@medusajs/medusa"
import { Response } from "@medusajs/medusa-js"
import { useQuery } from "@tanstack/react-query"
import { useMedusa } from "../../../contexts"
@@ -13,14 +17,17 @@ type UserQueryKeys = typeof adminUserKeys
/**
* This hook retrieves all admin users.
*
*
* @example
* To list users:
*
* ```tsx
* import React from "react"
* import { useAdminUsers } from "medusa-react"
*
*
* const Users = () => {
* const { users, isLoading } = useAdminUsers()
*
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
@@ -35,23 +42,60 @@ type UserQueryKeys = typeof adminUserKeys
* </div>
* )
* }
*
*
* export default Users
*
* ```
*
* By default, only the first `20` records are retrieved. You can control pagination by specifying the `limit` and `offset` properties:
*
* ```tsx
* import React from "react"
* import { useAdminUsers } from "medusa-react"
*
* const Users = () => {
* const {
* users,
* limit,
* offset,
* isLoading
* } = useAdminUsers({
* limit: 20,
* offset: 0
* })
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
* {users && !users.length && <span>No Users</span>}
* {users && users.length > 0 && (
* <ul>
* {users.map((user) => (
* <li key={user.id}>{user.email}</li>
* ))}
* </ul>
* )}
* </div>
* )
* }
*
* export default Users
* ```
*
* @customNamespace Hooks.Admin.Users
* @category Queries
*/
export const useAdminUsers = (
query?: AdminGetUsersParams,
options?: UseQueryOptionsWrapper<
Response<AdminUsersListRes>,
Error,
ReturnType<UserQueryKeys["lists"]>
ReturnType<UserQueryKeys["list"]>
>
) => {
const { client } = useMedusa()
const { data, ...rest } = useQuery(
adminUserKeys.lists(),
() => client.admin.users.list(),
adminUserKeys.list(query),
() => client.admin.users.list(query),
options
)
return { ...data, ...rest } as const
@@ -59,20 +103,20 @@ export const useAdminUsers = (
/**
* This hook retrieves an admin user's details.
*
*
* @example
* import React from "react"
* import { useAdminUser } from "medusa-react"
*
*
* type Props = {
* userId: string
* }
*
*
* const User = ({ userId }: Props) => {
* const { user, isLoading } = useAdminUser(
* userId
* )
*
*
* return (
* <div>
* {isLoading && <span>Loading...</span>}
@@ -80,9 +124,9 @@ export const useAdminUsers = (
* </div>
* )
* }
*
*
* export default User
*
*
* @customNamespace Hooks.Admin.Users
* @category Queries
*/
@@ -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"