diff --git a/integration-tests/api/__tests__/admin/product.js b/integration-tests/api/__tests__/admin/product.js index 68a80bdc8d..b90908500b 100644 --- a/integration-tests/api/__tests__/admin/product.js +++ b/integration-tests/api/__tests__/admin/product.js @@ -144,6 +144,9 @@ describe("/admin/products", () => { const notExpected = [ expect.objectContaining({ status: "draft" }), expect.objectContaining({ status: "rejected" }), + expect.objectContaining({ + id: "test-product_filtering_4", + }), ] const response = await api @@ -175,6 +178,48 @@ describe("/admin/products", () => { } }) + it("returns a list of deleted products with free text query", async () => { + const api = useApi() + + const response = await api + .get("/admin/products?deleted_at[gt]=01-26-1990&q=test", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: "test-product_filtering_4", + }), + ]) + }) + + it("returns a list of deleted products", async () => { + const api = useApi() + + const response = await api + .get("/admin/products?deleted_at[gt]=01-26-1990", { + headers: { + Authorization: "Bearer test_token", + }, + }) + .catch((err) => { + console.log(err) + }) + + expect(response.status).toEqual(200) + expect(response.data.products).toEqual([ + expect.objectContaining({ + id: "test-product_filtering_4", + }), + ]) + }) + it("returns a list of products in collection", async () => { const api = useApi() diff --git a/integration-tests/api/helpers/product-seeder.js b/integration-tests/api/helpers/product-seeder.js index 8cea9fadb6..aef5729fe7 100644 --- a/integration-tests/api/helpers/product-seeder.js +++ b/integration-tests/api/helpers/product-seeder.js @@ -271,4 +271,16 @@ module.exports = async (connection, data = {}) => { }) await manager.save(product3) + + const product4 = manager.create(Product, { + id: "test-product_filtering_4", + handle: "test-product_filtering_4", + title: "Test product filtering 4", + profile_id: defaultProfile.id, + description: "test-product-description", + status: "proposed", + deleted_at: new Date().toISOString(), + }) + + await manager.save(product4) } diff --git a/packages/medusa-interfaces/src/base-service.js b/packages/medusa-interfaces/src/base-service.js index 8f1238ce90..fa6807343c 100644 --- a/packages/medusa-interfaces/src/base-service.js +++ b/packages/medusa-interfaces/src/base-service.js @@ -1,5 +1,5 @@ import { MedusaError } from "medusa-core-utils" -import { In, FindOperator, Raw } from "typeorm" +import { FindOperator, In, Raw } from "typeorm" /** * Common functionality for Services @@ -63,13 +63,17 @@ class BaseService { return acc }, {}) - + return where } const query = { where: build(selector), } + + if ("deleted_at" in selector) { + query.withDeleted = true + } if ("skip" in config) { query.skip = config.skip diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index de987e6919..b7851f691e 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -1,8 +1,8 @@ import { flatten, groupBy, map, merge } from "lodash" import { - OrderByCondition, EntityRepository, FindManyOptions, + OrderByCondition, Repository, } from "typeorm" import { Product } from "../models/product" @@ -14,6 +14,7 @@ type CustomOptions = { order?: OrderByCondition skip?: number take?: number + withDeleted?: boolean } type FindWithRelationsOptions = CustomOptions @@ -31,7 +32,7 @@ export class ProductRepository extends Repository { private async queryProducts( optionsWithoutRelations: FindWithRelationsOptions, - shouldCount: boolean = false + shouldCount = false ): Promise<[Product[], number]> { const tags = optionsWithoutRelations.where.tags delete optionsWithoutRelations.where.tags @@ -48,6 +49,10 @@ export class ProductRepository extends Repository { .andWhere(`tags.id IN (:...ids)`, { ids: tags._value }) } + if (optionsWithoutRelations.withDeleted) { + qb = qb.withDeleted() + } + let entities: Product[] let count = null if (shouldCount) { @@ -79,7 +84,8 @@ export class ProductRepository extends Repository { private async queryProductsWithIds( entityIds: string[], - groupedRelations: { [toplevel: string]: string[] } + groupedRelations: { [toplevel: string]: string[] }, + withDeleted = false ): Promise { const entitiesIdsWithRelations = await Promise.all( Object.entries(groupedRelations).map(([toplevel, rels]) => { @@ -114,12 +120,22 @@ export class ProductRepository extends Repository { ) } - return querybuilder - .where( + if (withDeleted) { + querybuilder = querybuilder + .where("products.id IN (:...entitiesIds)", { + entitiesIds: entityIds, + }) + .withDeleted() + } else { + querybuilder = querybuilder.where( "products.deleted_at IS NULL AND products.id IN (:...entitiesIds)", - { entitiesIds: entityIds } + { + entitiesIds: entityIds, + } ) - .getMany() + } + + return querybuilder.getMany() }) ).then(flatten) @@ -133,7 +149,9 @@ export class ProductRepository extends Repository { let count: number let entities: Product[] if (Array.isArray(idsOrOptionsWithoutRelations)) { - entities = await this.findByIds(idsOrOptionsWithoutRelations) + entities = await this.findByIds(idsOrOptionsWithoutRelations, { + withDeleted: idsOrOptionsWithoutRelations.withDeleted ?? false, + }) count = entities.length } else { const result = await this.queryProducts( @@ -161,8 +179,10 @@ export class ProductRepository extends Repository { const groupedRelations = this.getGroupedRelations(relations) const entitiesIdsWithRelations = await this.queryProductsWithIds( entitiesIds, - groupedRelations + groupedRelations, + idsOrOptionsWithoutRelations.withDeleted ) + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) const entitiesToReturn = this.mergeEntitiesWithRelations(entitiesAndRelations) @@ -172,11 +192,14 @@ export class ProductRepository extends Repository { public async findWithRelations( relations: Array = [], - idsOrOptionsWithoutRelations: FindWithRelationsOptions = {} + idsOrOptionsWithoutRelations: FindWithRelationsOptions = {}, + withDeleted = false ): Promise { let entities: Product[] if (Array.isArray(idsOrOptionsWithoutRelations)) { - entities = await this.findByIds(idsOrOptionsWithoutRelations) + entities = await this.findByIds(idsOrOptionsWithoutRelations, { + withDeleted, + }) } else { const result = await this.queryProducts( idsOrOptionsWithoutRelations, @@ -198,8 +221,10 @@ export class ProductRepository extends Repository { const groupedRelations = this.getGroupedRelations(relations) const entitiesIdsWithRelations = await this.queryProductsWithIds( entitiesIds, - groupedRelations + groupedRelations, + withDeleted ) + const entitiesAndRelations = entitiesIdsWithRelations.concat(entities) const entitiesToReturn = this.mergeEntitiesWithRelations(entitiesAndRelations) diff --git a/packages/medusa/src/services/product.js b/packages/medusa/src/services/product.js index d6d9572e18..61db228ca3 100644 --- a/packages/medusa/src/services/product.js +++ b/packages/medusa/src/services/product.js @@ -106,7 +106,8 @@ class ProductService extends BaseService { const raw = await qb.getMany() return productRepo.findWithRelations( relations, - raw.map((i) => i.id) + raw.map((i) => i.id), + query.withDeleted ?? false ) } @@ -140,7 +141,8 @@ class ProductService extends BaseService { const products = await productRepo.findWithRelations( relations, - raw.map((i) => i.id) + raw.map((i) => i.id), + query.withDeleted ?? false ) return [products, count] } @@ -799,7 +801,7 @@ class ProductService extends BaseService { delete where.description delete where.title - return productRepo + let qb = productRepo .createQueryBuilder("product") .leftJoinAndSelect("product.variants", "variant") .leftJoinAndSelect("product.collection", "collection") @@ -814,6 +816,12 @@ class ProductService extends BaseService { .orWhere(`collection.title ILIKE :q`, { q: `%${q}%` }) }) ) + + if (query.withDeleted) { + qb = qb.withDeleted() + } + + return qb } }