diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product-tag.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product-tag.js.snap index 9ae6dd7e62..91656cacc1 100644 --- a/integration-tests/api/__tests__/admin/__snapshots__/product-tag.js.snap +++ b/integration-tests/api/__tests__/admin/__snapshots__/product-tag.js.snap @@ -22,3 +22,26 @@ Array [ }, ] `; + +exports[`/admin/product-tags GET /admin/product-tags returns a list of product tags matching free text search param 1`] = ` +Array [ + Object { + "created_at": Any, + "id": "tag1", + "updated_at": Any, + "value": "123", + }, + Object { + "created_at": Any, + "id": "tag3", + "updated_at": Any, + "value": "123", + }, + Object { + "created_at": Any, + "id": "tag4", + "updated_at": Any, + "value": "123", + }, +] +`; diff --git a/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap b/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap new file mode 100644 index 0000000000..a2b42a03c1 --- /dev/null +++ b/integration-tests/api/__tests__/admin/__snapshots__/product-type.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`/admin/product-types GET /admin/product-types returns a list of product types 1`] = ` +Array [ + Object { + "created_at": Any, + "id": "test-type", + "updated_at": Any, + "value": "test-type", + }, + Object { + "created_at": Any, + "id": "test-type-new", + "updated_at": Any, + "value": "test-type-new", + }, +] +`; + +exports[`/admin/product-types GET /admin/product-types returns a list of product types matching free text search param 1`] = ` +Array [ + Object { + "created_at": Any, + "id": "test-type-new", + "updated_at": Any, + "value": "test-type-new", + }, +] +`; diff --git a/integration-tests/api/__tests__/admin/product-tag.js b/integration-tests/api/__tests__/admin/product-tag.js index 6110cdcd97..1c3de72149 100644 --- a/integration-tests/api/__tests__/admin/product-tag.js +++ b/integration-tests/api/__tests__/admin/product-tag.js @@ -68,5 +68,38 @@ describe("/admin/product-tags", () => { tagMatch, ]) }) + + it("returns a list of product tags matching free text search param", async () => { + const api = useApi() + + const res = await api + .get("/admin/product-tags?q=123", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(res.status).toEqual(200) + + const tagMatch = { + created_at: expect.any(String), + updated_at: expect.any(String), + } + + expect(res.data.product_tags.map((pt) => pt.value)).toEqual([ + "123", + "123", + "123", + ]) + + expect(res.data.product_tags).toMatchSnapshot([ + tagMatch, + tagMatch, + tagMatch, + ]) + }) }) }) diff --git a/integration-tests/api/__tests__/admin/product-type.js b/integration-tests/api/__tests__/admin/product-type.js new file mode 100644 index 0000000000..23dad55c81 --- /dev/null +++ b/integration-tests/api/__tests__/admin/product-type.js @@ -0,0 +1,102 @@ +const path = require("path") + +const setupServer = require("../../../helpers/setup-server") +const { useApi } = require("../../../helpers/use-api") +const { initDb, useDb } = require("../../../helpers/use-db") + +const adminSeeder = require("../../helpers/admin-seeder") +const productSeeder = require("../../helpers/product-seeder") + +jest.setTimeout(50000) + +describe("/admin/product-types", () => { + let medusaProcess + let dbConnection + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..")) + dbConnection = await initDb({ cwd }) + medusaProcess = await setupServer({ cwd }) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + + medusaProcess.kill() + }) + + describe("GET /admin/product-types", () => { + beforeEach(async () => { + try { + await productSeeder(dbConnection) + await adminSeeder(dbConnection) + } catch (err) { + console.log(err) + throw err + } + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("returns a list of product types", async () => { + const api = useApi() + + const res = await api + .get("/admin/product-types", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(res.status).toEqual(200) + + const typeMatch = { + created_at: expect.any(String), + updated_at: expect.any(String), + } + + expect(res.data.product_types).toMatchSnapshot([ + typeMatch, + typeMatch, + ]) + }) + + it("returns a list of product types matching free text search param", async () => { + const api = useApi() + + const res = await api + .get("/admin/product-types?q=test-type-new", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(res.status).toEqual(200) + + const typeMatch = { + created_at: expect.any(String), + updated_at: expect.any(String), + } + + // The value of the type should match the search param + expect(res.data.product_types.map((pt) => pt.value)).toEqual([ + "test-type-new" + ]) + + // Should only return one type as there is only one match to the search param + expect(res.data.product_types).toMatchSnapshot([ + typeMatch + ]) + }) + }) +}) diff --git a/integration-tests/api/helpers/product-seeder.js b/integration-tests/api/helpers/product-seeder.js index e593651071..a9ce2cb21f 100644 --- a/integration-tests/api/helpers/product-seeder.js +++ b/integration-tests/api/helpers/product-seeder.js @@ -69,6 +69,13 @@ module.exports = async (connection, data = {}) => { await manager.save(type) + const type2 = await manager.create(ProductType, { + id: "test-type-new", + value: "test-type-new", + }) + + await manager.save(type2) + const image = manager.create(Image, { id: "test-image", url: "test-image.png", diff --git a/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts b/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts index ae24764bf1..6638e8e93a 100644 --- a/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts +++ b/packages/medusa/src/api/routes/admin/product-tags/list-product-tags.ts @@ -1,7 +1,7 @@ import { Type } from "class-transformer" +import { IsNumber, IsOptional, IsString } from "class-validator" +import { identity, omit, pickBy } from "lodash" import { MedusaError } from "medusa-core-utils" -import { IsNumber, IsString, IsOptional, ValidateNested } from "class-validator" -import { omit, pickBy, identity } from "lodash" import { allowedAdminProductTagsFields, defaultAdminProductTagsFields, @@ -10,9 +10,9 @@ import { import { ProductTag } from "../../../../models/product-tag" import ProductTagService from "../../../../services/product-tag" import { - StringComparisonOperator, DateComparisonOperator, FindConfig, + StringComparisonOperator, } from "../../../../types/common" import { validator } from "../../../../utils/validator" import { IsType } from "../../../../utils/validators/is-type" @@ -100,12 +100,14 @@ export class AdminGetProductTagsPaginationParams { } export class AdminGetProductTagsParams extends AdminGetProductTagsPaginationParams { - @ValidateNested() @IsType([String, [String], StringComparisonOperator]) @IsOptional() id?: string | string[] | StringComparisonOperator - @ValidateNested() + @IsString() + @IsOptional() + q?: string + @IsType([String, [String], StringComparisonOperator]) @IsOptional() value?: string | string[] | StringComparisonOperator diff --git a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts index 215baed4b2..d04d1db731 100644 --- a/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts +++ b/packages/medusa/src/api/routes/admin/product-types/list-product-types.ts @@ -1,7 +1,7 @@ import { Type } from "class-transformer" +import { IsNumber, IsOptional, IsString } from "class-validator" +import { identity, omit, pickBy } from "lodash" import { MedusaError } from "medusa-core-utils" -import { IsNumber, IsString, IsOptional, ValidateNested } from "class-validator" -import { omit, pickBy, identity } from "lodash" import { allowedAdminProductTypeFields, defaultAdminProductTypeFields, @@ -10,9 +10,9 @@ import { import { ProductType } from "../../../../models/product-type" import ProductTypeService from "../../../../services/product-type" import { - StringComparisonOperator, DateComparisonOperator, FindConfig, + StringComparisonOperator, } from "../../../../types/common" import { validator } from "../../../../utils/validator" import { IsType } from "../../../../utils/validators/is-type" @@ -101,12 +101,14 @@ export class AdminGetProductTypesPaginationParams { } export class AdminGetProductTypesParams extends AdminGetProductTypesPaginationParams { - @ValidateNested() @IsType([String, [String], StringComparisonOperator]) @IsOptional() id?: string | string[] | StringComparisonOperator - @ValidateNested() + @IsString() + @IsOptional() + q?: string + @IsType([String, [String], StringComparisonOperator]) @IsOptional() value?: string | string[] | StringComparisonOperator diff --git a/packages/medusa/src/services/product-tag.ts b/packages/medusa/src/services/product-tag.ts index b76a99dc26..e9253539f2 100644 --- a/packages/medusa/src/services/product-tag.ts +++ b/packages/medusa/src/services/product-tag.ts @@ -1,8 +1,8 @@ -import { EntityManager } from "typeorm" -import { BaseService } from "medusa-interfaces" import { MedusaError } from "medusa-core-utils" -import { ProductTagRepository } from "../repositories/product-tag" +import { BaseService } from "medusa-interfaces" +import { EntityManager, ILike, SelectQueryBuilder } from "typeorm" import { ProductTag } from "../models/product-tag" +import { ProductTagRepository } from "../repositories/product-tag" import { FindConfig } from "../types/common" import { FilterableProductTagProps } from "../types/product" @@ -107,7 +107,24 @@ class ProductTagService extends BaseService { ): Promise<[ProductTag[], number]> { const tagRepo = this.manager_.getCustomRepository(this.tagRepo_) + let q: string | undefined = undefined + if ("q" in selector) { + q = selector.q + delete selector.q + } + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.value + + query.where = (qb: SelectQueryBuilder): void => { + qb.where(where).andWhere([{ value: ILike(`%${q}%`) }]) + } + } + return await tagRepo.findAndCount(query) } } diff --git a/packages/medusa/src/services/product-type.ts b/packages/medusa/src/services/product-type.ts index b62bed4f76..9eea109aca 100644 --- a/packages/medusa/src/services/product-type.ts +++ b/packages/medusa/src/services/product-type.ts @@ -1,10 +1,10 @@ import { MedusaError } from "medusa-core-utils" import { BaseService } from "medusa-interfaces" -import { EntityManager } from "typeorm" -import { FindConfig } from "../types/common" -import { FilterableProductTypeProps } from "../types/product" +import { EntityManager, ILike, SelectQueryBuilder } from "typeorm" import { ProductType } from "../models/product-type" import { ProductTypeRepository } from "../repositories/product-type" +import { FindConfig } from "../types/common" +import { FilterableProductTypeProps } from "../types/product" /** * Provides layer to manipulate products. @@ -91,7 +91,24 @@ class ProductTypeService extends BaseService { ): Promise<[ProductType[], number]> { const typeRepo = this.manager_.getCustomRepository(this.typeRepository_) + let q: string | undefined = undefined + if ("q" in selector) { + q = selector.q + delete selector.q + } + const query = this.buildQuery_(selector, config) + + if (q) { + const where = query.where + + delete where.value + + query.where = (qb: SelectQueryBuilder): void => { + qb.where(where).andWhere([{ value: ILike(`%${q}%`) }]) + } + } + return await typeRepo.findAndCount(query) } } diff --git a/packages/medusa/src/types/product.ts b/packages/medusa/src/types/product.ts index ac789d2574..5b27d0e2e5 100644 --- a/packages/medusa/src/types/product.ts +++ b/packages/medusa/src/types/product.ts @@ -81,33 +81,45 @@ export class FilterableProductProps { } export class FilterableProductTagProps { - @ValidateNested() + @IsOptional() @IsType([String, [String], StringComparisonOperator]) id?: string | string[] | StringComparisonOperator - @ValidateNested() + @IsOptional() @IsType([String, [String], StringComparisonOperator]) value?: string | string[] | StringComparisonOperator + @IsOptional() @IsType([DateComparisonOperator]) created_at?: DateComparisonOperator + @IsOptional() @IsType([DateComparisonOperator]) updated_at?: DateComparisonOperator + + @IsString() + @IsOptional() + q?: string } export class FilterableProductTypeProps { - @ValidateNested() + @IsOptional() @IsType([String, [String], StringComparisonOperator]) id?: string | string[] | StringComparisonOperator - @ValidateNested() + @IsOptional() @IsType([String, [String], StringComparisonOperator]) value?: string | string[] | StringComparisonOperator + @IsOptional() @IsType([DateComparisonOperator]) created_at?: DateComparisonOperator + @IsOptional() @IsType([DateComparisonOperator]) updated_at?: DateComparisonOperator + + @IsString() + @IsOptional() + q?: string }