diff --git a/integration-tests/api/__tests__/admin/customer-groups.js b/integration-tests/api/__tests__/admin/customer-groups.js index e628f962e8..4076f8e607 100644 --- a/integration-tests/api/__tests__/admin/customer-groups.js +++ b/integration-tests/api/__tests__/admin/customer-groups.js @@ -466,6 +466,66 @@ describe("/admin/customer-groups", () => { const db = useDb() await db.teardown() }) + + it("retreive a list of customer groups", async () => { + const api = useApi() + + const response = await api + .get( + `/admin/customer-groups?limit=5&offset=2&expand=customers&order=created_at`, + { + headers: { + Authorization: "Bearer test_token", + }, + } + ) + .catch(console.log) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(7) + expect(response.data.customer_groups.length).toEqual(5) + expect(response.data.customer_groups[0]).toEqual( + expect.objectContaining({ id: "customer-group-3" }) + ) + expect(response.data.customer_groups[0]).toHaveProperty("customers") + }) + + it("retreive a list of customer groups filtered by name using `q` param", async () => { + const api = useApi() + + const response = await api.get(`/admin/customer-groups?q=vip-customers`, { + headers: { + Authorization: "Bearer test_token", + }, + }) + + expect(response.status).toEqual(200) + expect(response.data.count).toEqual(1) + expect(response.data.customer_groups).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: "customer-group-1" }), + ]) + ) + expect(response.data.customer_groups[0]).not.toHaveProperty("customers") + }) + }) + + describe("GET /admin/customer-groups/:id", () => { + beforeEach(async () => { + try { + await adminSeeder(dbConnection) + await customerSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + it("gets customer group", async () => { const api = useApi() diff --git a/integration-tests/api/__tests__/admin/customer.js b/integration-tests/api/__tests__/admin/customer.js index 81a2a82358..f4bcb20b75 100644 --- a/integration-tests/api/__tests__/admin/customer.js +++ b/integration-tests/api/__tests__/admin/customer.js @@ -276,7 +276,10 @@ describe("/admin/customers", () => { expect(response.status).toEqual(200) expect(response.data.customer.groups).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: "test-group-4", name: "test-group-4" }), + expect.objectContaining({ + id: "test-group-4", + name: "test-group-4", + }), ]) ) @@ -323,8 +326,11 @@ describe("/admin/customers", () => { expect(response.data.customer.groups.length).toEqual(2) expect(response.data.customer.groups).toEqual( expect.arrayContaining([ - expect.objectContaining({ id: "test-group-4", name: "test-group-4" }), expect.objectContaining({ id: "test-group-5", name: "test-group-5" }), + expect.objectContaining({ + id: "test-group-4", + name: "test-group-4", + }), ]) ) }) diff --git a/integration-tests/api/helpers/customer-seeder.js b/integration-tests/api/helpers/customer-seeder.js index e70bcf9cf2..2878bdfc49 100644 --- a/integration-tests/api/helpers/customer-seeder.js +++ b/integration-tests/api/helpers/customer-seeder.js @@ -36,6 +36,21 @@ module.exports = async (connection, data = {}) => { has_account: true, }) + const customer5 = manager.create(Customer, { + id: "test-customer-5", + email: "test5@email.com", + }) + + const customer6 = manager.create(Customer, { + id: "test-customer-6", + email: "test6@email.com", + }) + + const customer7 = manager.create(Customer, { + id: "test-customer-7", + email: "test7@email.com", + }) + const deletionCustomer = await manager.create(Customer, { id: "test-customer-delete-cg", email: "test-deletetion-cg@email.com", @@ -55,7 +70,7 @@ module.exports = async (connection, data = {}) => { await manager.insert(CustomerGroup, { id: "customer-group-3", - name: "vest-group-3", + name: "test-group-3", }) await manager.insert(CustomerGroup, { @@ -63,24 +78,6 @@ module.exports = async (connection, data = {}) => { name: "test-group-4", }) - const customer5 = manager.create(Customer, { - id: "test-customer-5", - email: "test5@email.com", - }) - await manager.save(customer5) - - const customer6 = manager.create(Customer, { - id: "test-customer-6", - email: "test6@email.com", - }) - await manager.save(customer6) - - const customer7 = manager.create(Customer, { - id: "test-customer-7", - email: "test7@email.com", - }) - await manager.save(customer7) - const c_group_5 = manager.create(CustomerGroup, { id: "test-group-5", name: "test-group-5", diff --git a/integration-tests/api/package.json b/integration-tests/api/package.json index d512574b8b..b87084cc94 100644 --- a/integration-tests/api/package.json +++ b/integration-tests/api/package.json @@ -8,16 +8,16 @@ "build": "babel src -d dist --extensions \".ts,.js\"" }, "dependencies": { - "@medusajs/medusa": "1.1.64-dev-1645628651544", + "@medusajs/medusa": "1.2.0-dev-1646213107704", "faker": "^5.5.3", - "medusa-interfaces": "1.1.34-dev-1645628651544", + "medusa-interfaces": "1.2.0-dev-1646213107704", "typeorm": "^0.2.31" }, "devDependencies": { "@babel/cli": "^7.12.10", "@babel/core": "^7.12.10", "@babel/node": "^7.12.10", - "babel-preset-medusa-package": "1.1.19-dev-1645441522984", + "babel-preset-medusa-package": "1.1.19-dev-1646213107704", "jest": "^26.6.3" } } diff --git a/integration-tests/api/yarn.lock b/integration-tests/api/yarn.lock index 896242ce63..5262e28718 100644 --- a/integration-tests/api/yarn.lock +++ b/integration-tests/api/yarn.lock @@ -1256,10 +1256,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@medusajs/medusa-cli@1.1.27-dev-1645628651544": - version "1.1.27-dev-1645628651544" - resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.1.27-dev-1645628651544.tgz#0b5531d3c0df9a6a9ea01990b53f751692439458" - integrity sha512-3NrmJwIiUyLB/mYhVY0vUzrsFqHBCV+8LF+mrRwYWlsixXT8muSVUpObPJtvSRG4D/6xQrKcFk9wreDoT+Az9w== +"@medusajs/medusa-cli@1.2.0-dev-1646213107704": + version "1.2.0-dev-1646213107704" + resolved "http://localhost:4873/@medusajs%2fmedusa-cli/-/medusa-cli-1.2.0-dev-1646213107704.tgz#b84f9143450f3c03e732277b6c5bbeb0df761b2e" + integrity sha512-U1BqPe167vxpzf2YfopVpNTq0qcBkX3jkWFCNduHEjkBbuOXchlB/MheKtxIsxjHV1hzmjTr9uw3owu93pDYCQ== dependencies: "@babel/polyfill" "^7.8.7" "@babel/runtime" "^7.9.6" @@ -1277,8 +1277,8 @@ is-valid-path "^0.1.1" joi-objectid "^3.0.1" meant "^1.0.1" - medusa-core-utils "1.1.31-dev-1645628651544" - medusa-telemetry "0.0.11-dev-1645628651544" + medusa-core-utils "1.1.31-dev-1646213107704" + medusa-telemetry "0.0.11-dev-1646213107704" netrc-parser "^3.1.6" open "^8.0.6" ora "^5.4.1" @@ -1292,13 +1292,13 @@ winston "^3.3.3" yargs "^15.3.1" -"@medusajs/medusa@1.1.64-dev-1645628651544": - version "1.1.64-dev-1645628651544" - resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.1.64-dev-1645628651544.tgz#d4e314cc8511d337bf380cdf0d3714b3ab039298" - integrity sha512-h2A1V96bJzMn1pe0jhQ98J4b8tv1JUGUQV3vYNIjHLKrWQVVNoY52Rk8bQjaCyH9YoITbiLOSRTApTSUsqzpyQ== +"@medusajs/medusa@1.2.0-dev-1646213107704": + version "1.2.0-dev-1646213107704" + resolved "http://localhost:4873/@medusajs%2fmedusa/-/medusa-1.2.0-dev-1646213107704.tgz#40ed0efe4f8e242c620eb1a984f0904465576920" + integrity sha512-pZnoM/U6WF4IdKriX7lKaxlmt9nlK4WoQ4PtzLjvZemjNHKriLjsg8ugvuBOuuzZJe66O8XEAXcoPh2o/SmAeg== dependencies: "@hapi/joi" "^16.1.8" - "@medusajs/medusa-cli" "1.1.27-dev-1645628651544" + "@medusajs/medusa-cli" "1.2.0-dev-1646213107704" "@types/lodash" "^4.14.168" awilix "^4.2.3" body-parser "^1.19.0" @@ -1322,8 +1322,8 @@ joi "^17.3.0" joi-objectid "^3.0.1" jsonwebtoken "^8.5.1" - medusa-core-utils "1.1.31-dev-1645628651544" - medusa-test-utils "1.1.37-dev-1645628651544" + medusa-core-utils "1.1.31-dev-1646213107704" + medusa-test-utils "1.1.37-dev-1646213107704" morgan "^1.9.1" multer "^1.4.2" passport "^0.4.0" @@ -1947,10 +1947,10 @@ babel-preset-jest@^26.6.2: babel-plugin-jest-hoist "^26.6.2" babel-preset-current-node-syntax "^1.0.0" -babel-preset-medusa-package@1.1.19-dev-1645628651544: - version "1.1.19-dev-1645628651544" - resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1645628651544.tgz#506bd183276d30458da4e2857459c6f38435d4b0" - integrity sha512-0XBmV1OuUDVIQccYKfU3Yj2gMYIeLEV8I/ugyVfgQ4AiYU7to97wFzdD8dXUTXd1I94gw4jbyZeZwflj0NoMhw== +babel-preset-medusa-package@1.1.19-dev-1646213107704: + version "1.1.19-dev-1646213107704" + resolved "http://localhost:4873/babel-preset-medusa-package/-/babel-preset-medusa-package-1.1.19-dev-1646213107704.tgz#820201a6abfbdb95af623f1cae8981bb5ccddb8d" + integrity sha512-gTkFWvA/i+UmPSK8O0J4HBXE1tqQSD/D2p3ernvcvoaaXYhiLhNUrRRe9F8YXdkFT4tVXy2DKz/eQyUG1dsnnA== dependencies: "@babel/plugin-proposal-class-properties" "^7.12.1" "@babel/plugin-proposal-decorators" "^7.12.1" @@ -5140,25 +5140,25 @@ media-typer@0.3.0: resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" integrity sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g= -medusa-core-utils@1.1.31-dev-1645628651544: - version "1.1.31-dev-1645628651544" - resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1645628651544.tgz#bd6c5dc2aec5f9a109afef8eee22cbef1c77d63c" - integrity sha512-xkLY5QPcM1yjvhhSv39zJX9RimCMS7p8Y3j9ey6JrZYXBwzznzKsDPTlhZAdNKZuvGXVvnnmJXh563fvS8lGlg== +medusa-core-utils@1.1.31-dev-1646213107704: + version "1.1.31-dev-1646213107704" + resolved "http://localhost:4873/medusa-core-utils/-/medusa-core-utils-1.1.31-dev-1646213107704.tgz#40ca7c1a4b290b1af9cf855cfc3320c23bbf3c53" + integrity sha512-P3D/jVSnx0YYEU8hTVqEow4Xht/BAHnHyc/HSbU5YT7PtCwEd+MY7+bvSAkhbVXNDCVsxyGTDA2nuofPS/c4RQ== dependencies: joi "^17.3.0" joi-objectid "^3.0.1" -medusa-interfaces@1.1.34-dev-1645628651544: - version "1.1.34-dev-1645628651544" - resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.1.34-dev-1645628651544.tgz#6d19acdef4019813eaa7554721cbc4f02fb16083" - integrity sha512-QsP0CtriL0avWFtSWtFoLx1oAcMtgMKAyCIfkkRQuPLYtM0o7Rc1l4cMnvYw9I8XPw+/RFQnc0CL3vJqDVzEUQ== +medusa-interfaces@1.2.0-dev-1646213107704: + version "1.2.0-dev-1646213107704" + resolved "http://localhost:4873/medusa-interfaces/-/medusa-interfaces-1.2.0-dev-1646213107704.tgz#1a5310484afb5c1d127ba5775e08e3c0502e1488" + integrity sha512-3+Ve8f5a+dvdcRafOzAuisXk/vwY9fBXgshof8xYFgtDso2INwMHTNIEgUQrjMd8yXt2+EO8l9wlBeAr02eQ0A== dependencies: - medusa-core-utils "1.1.31-dev-1645628651544" + medusa-core-utils "1.1.31-dev-1646213107704" -medusa-telemetry@0.0.11-dev-1645628651544: - version "0.0.11-dev-1645628651544" - resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1645628651544.tgz#b1534c1e52d699f10cd66cdb270f04217d8aba56" - integrity sha512-+IkNVi75gogVyXTL4dDSYY+3g26B69yljQDH6XOqwNSb8OXU/XBxmv6EiHzjopESe82caXr8kUnyFWOyybp9VA== +medusa-telemetry@0.0.11-dev-1646213107704: + version "0.0.11-dev-1646213107704" + resolved "http://localhost:4873/medusa-telemetry/-/medusa-telemetry-0.0.11-dev-1646213107704.tgz#7dc191fd263774a31795c2696ffe015a2b5f7443" + integrity sha512-BLKdzaU1sE9Eqnt3lmE6wkocOuC7wMT0l2OWc0aW6YTRRWDOffxOuANku1bZCIdVtbJ1q25UpeY2dwuD7lXhZw== dependencies: axios "^0.21.1" axios-retry "^3.1.9" @@ -5170,13 +5170,13 @@ medusa-telemetry@0.0.11-dev-1645628651544: remove-trailing-slash "^0.1.1" uuid "^8.3.2" -medusa-test-utils@1.1.37-dev-1645628651544: - version "1.1.37-dev-1645628651544" - resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1645628651544.tgz#45103cf82131b41d073e17188f9b7f5985b4c083" - integrity sha512-56OByKGpTGnFQ6ThEadThovK5+h/24/CTBOvBCq/voOmwxsr89LBTwd9UB7wbubb6L/voMWSfmOsCb/7kUkq6w== +medusa-test-utils@1.1.37-dev-1646213107704: + version "1.1.37-dev-1646213107704" + resolved "http://localhost:4873/medusa-test-utils/-/medusa-test-utils-1.1.37-dev-1646213107704.tgz#5fdb8298ac482f1d89475115d7727dd5227ac866" + integrity sha512-S42x4NaioWXPwQluO+iOYDwV0LUmr7q58XClFJA8j7Nw07IFKJU2PPHAAPrbpWQKpa2pdCBYdR0FUID4/VOfXw== dependencies: "@babel/plugin-transform-classes" "^7.9.5" - medusa-core-utils "1.1.31-dev-1645628651544" + medusa-core-utils "1.1.31-dev-1646213107704" randomatic "^3.1.1" merge-descriptors@1.0.1: diff --git a/packages/medusa/src/api/routes/admin/collections/list-collections.ts b/packages/medusa/src/api/routes/admin/collections/list-collections.ts index 6ed382f6a2..a9dc443d03 100644 --- a/packages/medusa/src/api/routes/admin/collections/list-collections.ts +++ b/packages/medusa/src/api/routes/admin/collections/list-collections.ts @@ -3,7 +3,7 @@ import { IsNumber, IsOptional, IsString, ValidateNested } from "class-validator" import _, { identity } from "lodash" import { defaultAdminCollectionsFields, - defaultAdminCollectionsRelations + defaultAdminCollectionsRelations, } from "." import ProductCollectionService from "../../../../services/product-collection" import { DateComparisonOperator } from "../../../../types/common" diff --git a/packages/medusa/src/api/routes/admin/customer-groups/get-customer-group.ts b/packages/medusa/src/api/routes/admin/customer-groups/get-customer-group.ts index 2318f62d62..dd77de94e0 100644 --- a/packages/medusa/src/api/routes/admin/customer-groups/get-customer-group.ts +++ b/packages/medusa/src/api/routes/admin/customer-groups/get-customer-group.ts @@ -4,7 +4,7 @@ import { validator } from "../../../../utils/validator" import { defaultAdminCustomerGroupsRelations } from "." /** - * @oas [get] /customer-group/{id} + * @oas [get] /customer-groups/{id} * operationId: "GetCustomerGroupsGroup" * summary: "Retrieve a CustomerGroup" * description: "Retrieves a Customer Group." diff --git a/packages/medusa/src/api/routes/admin/customer-groups/index.ts b/packages/medusa/src/api/routes/admin/customer-groups/index.ts index eff71ef643..5d52632dca 100644 --- a/packages/medusa/src/api/routes/admin/customer-groups/index.ts +++ b/packages/medusa/src/api/routes/admin/customer-groups/index.ts @@ -8,6 +8,7 @@ const route = Router() export default (app) => { app.use("/customer-groups", route) + route.get("/", middlewares.wrap(require("./list-customer-groups").default)) route.get("/:id", middlewares.wrap(require("./get-customer-group").default)) route.post("/", middlewares.wrap(require("./create-customer-group").default)) route.post( diff --git a/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts b/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts new file mode 100644 index 0000000000..a38cd88b08 --- /dev/null +++ b/packages/medusa/src/api/routes/admin/customer-groups/list-customer-groups.ts @@ -0,0 +1,109 @@ +import { IsNumber, IsOptional, IsString } from "class-validator" +import { Type } from "class-transformer" +import omit from "lodash/omit" + +import { validator } from "../../../../utils/validator" +import { CustomerGroupService } from "../../../../services" +import { CustomerGroup } from "../../../../models/customer-group" +import { FindConfig } from "../../../../types/common" +import { defaultAdminCustomerGroupsRelations } from "." +import { FilterableCustomerGroupProps } from "../../../../types/customer-groups" + +/** + * @oas [get] /customer-groups + * operationId: "GetCustomerGroups" + * summary: "Retrieve a list of customer groups" + * description: "Retrieve a list of customer groups." + * x-authenticated: true + * parameters: + * - (query) q {string} Query used for searching user group names. + * - (query) offset {string} How many groups to skip in the result. + * - (query) id {string} Ids of the groups to search for. + * - (query) order {string} to retrieve customer groups in. + * - (query) created_at {DateComparisonOperator} Date comparison for when resulting customer group was created, i.e. less than, greater than etc. + * - (query) updated_at {DateComparisonOperator} Date comparison for when resulting ustomer group was updated, i.e. less than, greater than etc. + * - (query) offset {string} How many customer groups to skip in the result. + * - (query) limit {string} Limit the number of customer groups returned. + * - (query) expand {string} (Comma separated) Which fields should be expanded in each customer groups of the result. + + * tags: + * - CustomerGroup + * responses: + * 200: + * description: OK + * content: + * application/json: + * schema: + * properties: + * customerGroup: + * $ref: "#/components/schemas/customer_group" + */ +export default async (req, res) => { + const validated = await validator(AdminGetCustomerGroupsParams, req.query) + + const customerGroupService: CustomerGroupService = req.scope.resolve( + "customerGroupService" + ) + + let expandFields: string[] = [] + if (validated.expand) { + expandFields = validated.expand.split(",") + } + + const listConfig: FindConfig = { + relations: expandFields.length + ? expandFields + : defaultAdminCustomerGroupsRelations, + skip: validated.offset, + take: validated.limit, + order: { created_at: "DESC" } as { [k: string]: "DESC" }, + } + + if (typeof validated.order !== "undefined") { + if (validated.order.startsWith("-")) { + const [, field] = validated.order.split("-") + listConfig.order = { [field]: "DESC" } + } else { + listConfig.order = { [validated.order]: "ASC" } + } + } + + const filterableFields = omit(validated, [ + "limit", + "offset", + "expand", + "order", + ]) + + const [data, count] = await customerGroupService.listAndCount( + filterableFields, + listConfig + ) + + res.json({ + count, + customer_groups: data, + offset: validated.offset, + limit: validated.limit, + }) +} + +export class AdminGetCustomerGroupsParams extends FilterableCustomerGroupProps { + @IsString() + @IsOptional() + order?: string + + @IsNumber() + @IsOptional() + @Type(() => Number) + offset?: number = 0 + + @IsNumber() + @IsOptional() + @Type(() => Number) + limit?: number = 10 + + @IsString() + @IsOptional() + expand?: string +} diff --git a/packages/medusa/src/services/customer-group.ts b/packages/medusa/src/services/customer-group.ts index 656b40e819..9b46c071da 100644 --- a/packages/medusa/src/services/customer-group.ts +++ b/packages/medusa/src/services/customer-group.ts @@ -1,15 +1,15 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" -import { DeepPartial, EntityManager } from "typeorm" +import { DeepPartial, EntityManager, ILike, SelectQueryBuilder } from "typeorm" import { CustomerService } from "." import { CustomerGroup } from ".." import { CustomerGroupRepository } from "../repositories/customer-group" import { FindConfig } from "../types/common" -import { formatException } from "../utils/exception-formatter" import { CustomerGroupUpdate, FilterableCustomerGroupProps, } from "../types/customer-groups" +import { formatException } from "../utils/exception-formatter" type CustomerGroupConstructorProps = { manager: EntityManager @@ -226,6 +226,41 @@ class CustomerGroupService extends BaseService { return await cgRepo.find(query) } + /** + * Retrieve a list of customer groups and total count of records that match the query. + * + * @param {Object} selector - the query object for find + * @param {Object} config - the config to be used for find + * @return {Promise} the result of the find operation + */ + async listAndCount( + selector: FilterableCustomerGroupProps = {}, + config: FindConfig + ): Promise<[CustomerGroup[], number]> { + const cgRepo: CustomerGroupRepository = this.manager_.getCustomRepository( + this.customerGroupRepository_ + ) + + let q + if ("q" in selector) { + q = selector.q + delete selector.q + } + + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.name + + query.where = (qb: SelectQueryBuilder): void => { + qb.where(where).andWhere([{ name: ILike(`%${q}%`) }]) + } + } + return await cgRepo.findAndCount(query) + } + /** * Remove list of customers from a customergroup * diff --git a/packages/medusa/src/types/customer-groups.ts b/packages/medusa/src/types/customer-groups.ts index aa7d98ccdb..236cf468aa 100644 --- a/packages/medusa/src/types/customer-groups.ts +++ b/packages/medusa/src/types/customer-groups.ts @@ -1,12 +1,33 @@ -import { IsString, ValidateNested } from "class-validator" +import { Type } from "class-transformer" +import { IsArray, IsOptional, IsString, ValidateNested } from "class-validator" import { IsType } from "../utils/validators/is-type" -import { StringComparisonOperator } from "./common" +import { DateComparisonOperator, StringComparisonOperator } from "./common" export class FilterableCustomerGroupProps { + @IsOptional() @ValidateNested() @IsType([String, [String], StringComparisonOperator]) id?: string | string[] | StringComparisonOperator + + @IsString() + @IsOptional() + q?: string + + @IsOptional() + @IsArray() + @IsString({ each: true }) + name?: string[] + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + updated_at?: DateComparisonOperator + + @IsOptional() + @ValidateNested() + @Type(() => DateComparisonOperator) + created_at?: DateComparisonOperator } export class CustomerGroupsBatchCustomer {