fix(medusa): Implement listAndCount for UserService and update list endpoint (#6190)
This commit is contained in:
committed by
GitHub
parent
eb498c500e
commit
d68089b2aa
@@ -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",
|
||||
},
|
||||
]
|
||||
`;
|
||||
@@ -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"
|
||||
|
||||
@@ -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"),
|
||||
}))
|
||||
|
||||
@@ -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