diff --git a/integration-tests/http/__tests__/product/admin/product.spec.ts b/integration-tests/http/__tests__/product/admin/product.spec.ts index f92456b01c..11322c4a9f 100644 --- a/integration-tests/http/__tests__/product/admin/product.spec.ts +++ b/integration-tests/http/__tests__/product/admin/product.spec.ts @@ -449,7 +449,7 @@ medusaIntegrationTestRunner({ // BREAKING: Comparison operators changed, so eg. `gt` became `$gt` const response = await api .get( - `/admin/products?deleted_at[$gt]=01-26-1990&q=test`, + `/admin/products?deleted_at[$gt]=01-26-1990&with_deleted=true&q=test`, adminHeaders ) .catch((err) => { @@ -519,7 +519,10 @@ medusaIntegrationTestRunner({ it("returns a list of deleted products", async () => { const response = await api - .get(`/admin/products?deleted_at[$gt]=01-26-1990`, adminHeaders) + .get( + `/admin/products?deleted_at[$gt]=01-26-1990&with_deleted=true`, + adminHeaders + ) .catch((err) => { console.log(err) }) diff --git a/packages/core/framework/src/http/types.ts b/packages/core/framework/src/http/types.ts index 990c81812a..64be1afd67 100644 --- a/packages/core/framework/src/http/types.ts +++ b/packages/core/framework/src/http/types.ts @@ -141,6 +141,7 @@ export interface MedusaRequest< queryConfig: { fields: string[] pagination: { order?: Record; skip: number; take?: number } + withDeleted?: boolean } /** diff --git a/packages/core/framework/src/http/utils/get-query-config.ts b/packages/core/framework/src/http/utils/get-query-config.ts index 993082d390..574a3afc4b 100644 --- a/packages/core/framework/src/http/utils/get-query-config.ts +++ b/packages/core/framework/src/http/utils/get-query-config.ts @@ -101,7 +101,13 @@ export function prepareListQuery( defaultLimit = 50, isList, } = queryConfig - const { order, fields, limit = defaultLimit, offset = 0 } = validated + const { + order, + fields, + limit = defaultLimit, + offset = 0, + with_deleted, + } = validated // e.g *product.variants meaning that we want all fields from the product.variants // in that case it wont be part of the select but it will be part of the relations. @@ -220,6 +226,7 @@ export function prepareListQuery( skip: offset, take: limit, order: finalOrder, + withDeleted: with_deleted, }, remoteQueryConfig: { // Add starFields that are relations only on which we want all properties with a dedicated format to the remote query @@ -234,6 +241,7 @@ export function prepareListQuery( order: finalOrder, } : {}, + withDeleted: with_deleted, }, } } @@ -255,6 +263,7 @@ export function prepareRetrieveQuery( remoteQueryConfig: { fields: remoteQueryConfig.fields, pagination: {}, + withDeleted: remoteQueryConfig.withDeleted, }, } } diff --git a/packages/core/framework/src/http/utils/refetch-entities.ts b/packages/core/framework/src/http/utils/refetch-entities.ts index d277addae5..2e92cf9aa4 100644 --- a/packages/core/framework/src/http/utils/refetch-entities.ts +++ b/packages/core/framework/src/http/utils/refetch-entities.ts @@ -11,7 +11,8 @@ export const refetchEntities = async ( idOrFilter: string | object, scope: MedusaContainer, fields: string[], - pagination?: MedusaRequest["queryConfig"]["pagination"] + pagination?: MedusaRequest["queryConfig"]["pagination"], + withDeleted?: boolean ) => { const remoteQuery = scope.resolve(ContainerRegistrationKeys.REMOTE_QUERY) const filters = isString(idOrFilter) ? { id: idOrFilter } : idOrFilter @@ -25,7 +26,7 @@ export const refetchEntities = async ( delete filters.context } - const variables = { filters, ...context, ...pagination } + const variables = { filters, ...context, ...pagination, withDeleted } const queryObject = remoteQueryObjectFromString({ entryPoint, diff --git a/packages/core/framework/src/http/utils/validate-query.ts b/packages/core/framework/src/http/utils/validate-query.ts index 726e88eaf4..a82dcb856c 100644 --- a/packages/core/framework/src/http/utils/validate-query.ts +++ b/packages/core/framework/src/http/utils/validate-query.ts @@ -77,9 +77,10 @@ export function validateAndTransformQuery( } delete req.allowed - const query = normalizeQuery(req) + const query = normalizeQuery(req) as Record const validated = await zodValidator(zodSchema, query) + const cnf = queryConfig.isList ? prepareListQuery(validated, { ...queryConfig, allowed, restricted }) : prepareRetrieveQuery(validated, { @@ -88,7 +89,8 @@ export function validateAndTransformQuery( restricted, }) - req.validatedQuery = validated + const { with_deleted, ...validatedQueryFilters } = validated + req.validatedQuery = validatedQueryFilters req.filterableFields = getFilterableFields(req.validatedQuery) req.queryConfig = cnf.remoteQueryConfig as any req.remoteQueryConfig = req.queryConfig diff --git a/packages/core/types/src/common/common.ts b/packages/core/types/src/common/common.ts index 6aaa36f4ee..16a1185d7f 100644 --- a/packages/core/types/src/common/common.ts +++ b/packages/core/types/src/common/common.ts @@ -80,7 +80,7 @@ export interface FindConfig { /** * An array of strings, each being relation names of the entity to retrieve in the result. - * + * * You can only retrieve data models defined in the same module. To retrieve linked data models * from other modules, use [Query](https://docs.medusajs.com/learn/fundamentals/module-links/query) instead. */ @@ -150,6 +150,11 @@ export type RequestQueryFields = { * 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 `-`. */ order?: string + + /** + * Whether to include deleted records in the result. + */ + with_deleted?: boolean } /** diff --git a/packages/core/types/src/product/common.ts b/packages/core/types/src/product/common.ts index edad5a1cba..3552df4c30 100644 --- a/packages/core/types/src/product/common.ts +++ b/packages/core/types/src/product/common.ts @@ -688,27 +688,30 @@ export interface FilterableProductProps /** * The status to filter products by */ - status?: ProductStatus | ProductStatus[] + status?: + | ProductStatus + | ProductStatus[] + | OperatorMap /** * The titles to filter products by. */ - title?: string | string[] + title?: string | string[] | OperatorMap /** * The handles to filter products by. */ - handle?: string | string[] + handle?: string | string[] | OperatorMap /** * The skus to filter products by. */ - sku?: string | string[] + sku?: string | string[] | OperatorMap /** * The IDs to filter products by. */ - id?: string | string[] + id?: string | string[] | OperatorMap /** * The external IDs to filter products by. */ - external_id?: string | string[] + external_id?: string | string[] | OperatorMap /** * Filters only or excluding gift card products */ @@ -736,27 +739,27 @@ export interface FilterableProductProps /** * Filter a product by the ID of the associated type */ - type_id?: string | string[] + type_id?: string | string[] | OperatorMap /** * Filter a product by the IDs of their associated categories. */ - categories?: { id: OperatorMap } | { id: OperatorMap } + categories?: { id: string | string[] | OperatorMap } /** * Filters a product by the IDs of their associated collections. */ - collection_id?: string | string[] | OperatorMap + collection_id?: string | string[] | OperatorMap /** * Filters a product based on when it was created */ - created_at?: OperatorMap + created_at?: string | OperatorMap /** * Filters a product based on when it was updated */ - updated_at?: OperatorMap + updated_at?: string | OperatorMap /** * Filters soft-deleted products based on the date they were deleted at. */ - deleted_at?: OperatorMap + deleted_at?: string | OperatorMap } /** diff --git a/packages/core/utils/src/modules-sdk/__tests__/build-query.spec.ts b/packages/core/utils/src/modules-sdk/__tests__/build-query.spec.ts index b30c3c994f..17f601337b 100644 --- a/packages/core/utils/src/modules-sdk/__tests__/build-query.spec.ts +++ b/packages/core/utils/src/modules-sdk/__tests__/build-query.spec.ts @@ -78,7 +78,7 @@ describe("buildQuery", () => { test("should handle withDeleted flag", () => { const filters = { deleted_at: "some-value" } - const result = buildQuery(filters) + const result = buildQuery(filters, { withDeleted: true }) expect(result.options.filters).toHaveProperty(SoftDeletableFilterKey) expect(result.options.filters?.[SoftDeletableFilterKey]).toEqual({ withDeleted: true, diff --git a/packages/core/utils/src/modules-sdk/build-query.ts b/packages/core/utils/src/modules-sdk/build-query.ts index 315ede012e..88afe1f761 100644 --- a/packages/core/utils/src/modules-sdk/build-query.ts +++ b/packages/core/utils/src/modules-sdk/build-query.ts @@ -3,13 +3,6 @@ import { deduplicate, isObject } from "../common" import { SoftDeletableFilterKey } from "../dal/mikro-orm/mikro-orm-soft-deletable-filter" -// Following convention here is fine, we can make it configurable if needed. -const DELETED_AT_FIELD_NAME = "deleted_at" - -type FilterFlags = { - withDeleted?: boolean -} - export function buildQuery( filters: Record = {}, config: FindConfig> & { @@ -17,8 +10,7 @@ export function buildQuery( } = {} ): Required> { const where = {} as DAL.FilterQuery - const filterFlags: FilterFlags = {} - buildWhere(filters, where, filterFlags) + buildWhere(filters, where) delete config.primaryKeyFields @@ -41,7 +33,7 @@ export function buildQuery( >["options"]["orderBy"] } - if (config.withDeleted || filterFlags.withDeleted) { + if (config.withDeleted) { findOptions.filters ??= {} findOptions.filters[SoftDeletableFilterKey] = { withDeleted: true, @@ -63,16 +55,8 @@ export function buildQuery( return { where, options: findOptions } as Required> } -function buildWhere( - filters: Record = {}, - where = {}, - flags: FilterFlags = {} -) { +function buildWhere(filters: Record = {}, where = {}) { for (let [prop, value] of Object.entries(filters)) { - if (prop === DELETED_AT_FIELD_NAME) { - flags.withDeleted = true - } - if (["$or", "$and"].includes(prop)) { if (!Array.isArray(value)) { throw new Error(`Expected array for ${prop} but got ${value}`) @@ -80,7 +64,7 @@ function buildWhere( where[prop] = value.map((val) => { const deepWhere = {} - buildWhere(val, deepWhere, flags) + buildWhere(val, deepWhere) return deepWhere }) continue @@ -93,7 +77,7 @@ function buildWhere( if (isObject(value)) { where[prop] = {} - buildWhere(value, where[prop], flags) + buildWhere(value, where[prop]) continue } diff --git a/packages/medusa/src/api/admin/products/route.ts b/packages/medusa/src/api/admin/products/route.ts index fe68f9c81f..49a3a534bd 100644 --- a/packages/medusa/src/api/admin/products/route.ts +++ b/packages/medusa/src/api/admin/products/route.ts @@ -43,7 +43,8 @@ async function getProducts( req.filterableFields, req.scope, selectFields, - req.queryConfig.pagination + req.queryConfig.pagination, + req.queryConfig.withDeleted ) res.json({ @@ -75,6 +76,7 @@ async function getProductsWithIndexEngine( fields: req.queryConfig.fields ?? [], filters: filters, pagination: req.queryConfig.pagination, + withDeleted: req.queryConfig.withDeleted, }) res.json({ diff --git a/packages/medusa/src/api/utils/validators.ts b/packages/medusa/src/api/utils/validators.ts index fdccc57c3f..b26c7a0e2d 100644 --- a/packages/medusa/src/api/utils/validators.ts +++ b/packages/medusa/src/api/utils/validators.ts @@ -92,6 +92,12 @@ export const createFindParams = ({ order: order ? z.string().optional().default(order) : z.string().optional(), + with_deleted: z.preprocess((val) => { + if (val && typeof val === "string") { + return val === "true" ? true : val === "false" ? false : val + } + return val + }, z.boolean().optional()), }) ) } diff --git a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts index e28d441ed0..ed05dd058d 100644 --- a/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts +++ b/packages/modules/product/integration-tests/__tests__/product-module-service/products.spec.ts @@ -1277,9 +1277,14 @@ moduleIntegrationTestRunner({ await service.softDeleteProducts([products[0].id]) - const softDeleted = await service.listProducts({ - deleted_at: { $gt: "01-01-2022" }, - }) + const softDeleted = await service.listProducts( + { + deleted_at: { $gt: "01-01-2022" }, + }, + { + withDeleted: true, + } + ) expect(softDeleted).toHaveLength(1) })