feat(medusa): List products with Remote Query (#4969)

**What**
- includes some type fixes in the DAL layer
- List products including their prices and filtered by the sales channel as well as q parameter and category scope and all other filters
- Assign shipping profile
- ordering
- Add missing columns in the product module
- update product module migrations

**Comment**
-  In regards to the fields, we can pass whatever we want the module will only return the one that exists (default behavior), but on the other hand, that is not possible for the relations.

**question**
- To simplify usage, should we expose the fields/relations available from the module to simplify building a query for the user and be aware of what the module provides

**todo**
- Add back the support for the user to ask for fields/relations
This commit is contained in:
Adrien de Peretti
2023-09-12 15:55:05 +00:00
committed by GitHub
parent afd4e72cdf
commit 30863fee52
34 changed files with 1066 additions and 124 deletions
+11
View File
@@ -0,0 +1,11 @@
---
"@medusajs/medusa": patch
"@medusajs/link-modules": patch
"@medusajs/modules-sdk": patch
"@medusajs/orchestration": patch
"@medusajs/pricing": patch
"@medusajs/product": patch
"@medusajs/types": patch
---
feat: store List products remote query with product isolation
@@ -33,6 +33,9 @@ export const ProductShippingProfile: ModuleJoinerConfig = {
extends: [ extends: [
{ {
serviceName: Modules.PRODUCT, serviceName: Modules.PRODUCT,
fieldAlias: {
profile: "shipping_profile.profile",
},
relationship: { relationship: {
serviceName: LINKS.ProductShippingProfile, serviceName: LINKS.ProductShippingProfile,
isInternalService: true, isInternalService: true,
@@ -2,6 +2,7 @@ import {
CartService, CartService,
ProductService, ProductService,
ProductVariantInventoryService, ProductVariantInventoryService,
SalesChannelService,
} from "../../../../services" } from "../../../../services"
import { import {
IsArray, IsArray,
@@ -22,6 +23,8 @@ import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-cha
import { cleanResponseData } from "../../../../utils/clean-response-data" import { cleanResponseData } from "../../../../utils/clean-response-data"
import { defaultStoreCategoryScope } from "../product-categories" import { defaultStoreCategoryScope } from "../product-categories"
import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean"
import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain"
import { defaultStoreProductsFields } from "./index"
/** /**
* @oas [get] /store/products * @oas [get] /store/products
@@ -216,6 +219,8 @@ export default async (req, res) => {
const pricingService: PricingService = req.scope.resolve("pricingService") const pricingService: PricingService = req.scope.resolve("pricingService")
const cartService: CartService = req.scope.resolve("cartService") const cartService: CartService = req.scope.resolve("cartService")
const featureFlagRouter = req.scope.resolve("featureFlagRouter")
const validated = req.validatedQuery as StoreGetProductsParams const validated = req.validatedQuery as StoreGetProductsParams
let { let {
@@ -224,7 +229,6 @@ export default async (req, res) => {
currency_code: currencyCode, currency_code: currencyCode,
...filterableFields ...filterableFields
} = req.filterableFields } = req.filterableFields
const listConfig = req.listConfig const listConfig = req.listConfig
// get only published products for store endpoint // get only published products for store endpoint
@@ -246,9 +250,23 @@ export default async (req, res) => {
} }
} }
const isIsolateProductDomain = featureFlagRouter.isFeatureEnabled(
IsolateProductDomain.key
)
const promises: Promise<any>[] = [] const promises: Promise<any>[] = []
promises.push(productService.listAndCount(filterableFields, listConfig)) if (isIsolateProductDomain) {
promises.push(
listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
listConfig
)
)
} else {
promises.push(productService.listAndCount(filterableFields, listConfig))
}
if (validated.cart_id) { if (validated.cart_id) {
promises.push( promises.push(
@@ -312,6 +330,197 @@ export default async (req, res) => {
}) })
} }
async function listAndCountProductWithIsolatedProductModule(
req,
filterableFields,
listConfig
) {
// TODO: Add support for fields/expands
const remoteQuery = req.scope.resolve("remoteQuery")
let salesChannelIdFilter = filterableFields.sales_channel_id
if (req.publishableApiKeyScopes?.sales_channel_ids.length) {
salesChannelIdFilter ??= req.publishableApiKeyScopes.sales_channel_ids
}
delete filterableFields.sales_channel_id
filterableFields["categories"] = {
$or: [
{
id: null,
},
{
...(filterableFields.categories || {}),
// Store APIs are only allowed to query active and public categories
...defaultStoreCategoryScope,
},
],
}
// This is not the best way of handling cross filtering but for now I would say it is fine
if (salesChannelIdFilter) {
const salesChannelService = req.scope.resolve(
"salesChannelService"
) as SalesChannelService
const productIdsInSalesChannel =
await salesChannelService.listProductIdsBySalesChannelIds(
salesChannelIdFilter
)
let filteredProductIds = productIdsInSalesChannel[salesChannelIdFilter]
if (filterableFields.id) {
filterableFields.id = Array.isArray(filterableFields.id)
? filterableFields.id
: [filterableFields.id]
const salesChannelProductIdsSet = new Set(filteredProductIds)
filteredProductIds = filterableFields.id.filter((productId) =>
salesChannelProductIdsSet.has(productId)
)
}
filterableFields.id = filteredProductIds
}
const variables = {
filters: filterableFields,
order: listConfig.order,
skip: listConfig.skip,
take: listConfig.take,
}
// prettier-ignore
const args = `
filters: $filters,
order: $order,
skip: $skip,
take: $take
`
const query = `
query ($filters: any, $order: any, $skip: Int, $take: Int) {
product (${args}) {
${defaultStoreProductsFields.join("\n")}
images {
id
created_at
updated_at
deleted_at
url
metadata
}
tags {
id
created_at
updated_at
deleted_at
value
}
type {
id
created_at
updated_at
deleted_at
value
}
collection {
title
handle
id
created_at
updated_at
deleted_at
}
options {
id
created_at
updated_at
deleted_at
title
product_id
metadata
values {
id
created_at
updated_at
deleted_at
value
option_id
variant_id
metadata
}
}
variants {
id
created_at
updated_at
deleted_at
title
product_id
sku
barcode
ean
upc
variant_rank
inventory_quantity
allow_backorder
manage_inventory
hs_code
origin_country
mid_code
material
weight
length
height
width
metadata
options {
id
created_at
updated_at
deleted_at
value
option_id
variant_id
metadata
}
}
profile {
id
created_at
updated_at
deleted_at
name
type
}
}
}
`
const {
rows: products,
metadata: { count },
} = await remoteQuery(query, variables)
products.forEach((product) => {
product.profile_id = product.profile?.id
})
return [products, count]
}
export class StoreGetProductsPaginationParams extends PriceSelectionParams { export class StoreGetProductsPaginationParams extends PriceSelectionParams {
@IsNumber() @IsNumber()
@IsOptional() @IsOptional()
@@ -0,0 +1,22 @@
import { MigrationInterface, QueryRunner } from "typeorm"
import IsolateProductDomain from "../loaders/feature-flags/isolate-product-domain"
export const featureFlag = IsolateProductDomain.key
export class LineItemProductId1692870898424 implements MigrationInterface {
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "product_shipping_profile" ADD COLUMN IF NOT EXISTS "created_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now();
ALTER TABLE "product_shipping_profile" ADD COLUMN IF NOT EXISTS "updated_at" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now();
ALTER TABLE "product_shipping_profile" ADD COLUMN IF NOT EXISTS "deleted_at" TIMESTAMP WITH TIME ZONE;
`)
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
ALTER TABLE "product_shipping_profile" DROP COLUMN IF NOT EXISTS "created_at";
ALTER TABLE "product_shipping_profile" DROP COLUMN IF NOT EXISTS "updated_at";
ALTER TABLE "product_shipping_profile" DROP COLUMN IF NOT EXISTS "deleted_at";
`)
}
}
+2 -2
View File
@@ -178,7 +178,7 @@ export class LineItem extends BaseEntity {
} }
} }
@FeatureFlagDecorators(IsolateProductDomain.key, [BeforeUpdate]) @FeatureFlagDecorators(IsolateProductDomain.key, [BeforeUpdate()])
beforeUpdate(): void { beforeUpdate(): void {
if ( if (
this.variant && this.variant &&
@@ -189,7 +189,7 @@ export class LineItem extends BaseEntity {
} }
} }
@FeatureFlagDecorators(IsolateProductDomain.key, [AfterLoad, AfterUpdate]) @FeatureFlagDecorators(IsolateProductDomain.key, [AfterLoad(), AfterUpdate()])
afterUpdateOrLoad(): void { afterUpdateOrLoad(): void {
if (this.variant) { if (this.variant) {
return return
@@ -1,4 +1,4 @@
import { Brackets, DeleteResult, FindOptionsWhere, In, ILike } from "typeorm" import { DeleteResult, FindOptionsWhere, ILike, In } from "typeorm"
import { SalesChannel } from "../models" import { SalesChannel } from "../models"
import { ExtendedFindConfig } from "../types/common" import { ExtendedFindConfig } from "../types/common"
import { dataSource } from "../loaders/database" import { dataSource } from "../loaders/database"
@@ -76,6 +76,27 @@ export const SalesChannelRepository = dataSource
.orIgnore() .orIgnore()
.execute() .execute()
}, },
async listProductIdsBySalesChannelIds(
salesChannelIds: string | string[]
): Promise<{ [salesChannelId: string]: string[] }> {
salesChannelIds = Array.isArray(salesChannelIds)
? salesChannelIds
: [salesChannelIds]
const result = await this.createQueryBuilder()
.select(["sales_channel_id", "product_id"])
.from(productSalesChannelTable, "psc")
.where({ sales_channel_id: In(salesChannelIds) })
.execute()
return result.reduce((acc, curr) => {
acc[curr.sales_channel_id] ??= []
acc[curr.sales_channel_id].push(curr.product_id)
return acc
}, {})
},
}) })
export default SalesChannelRepository export default SalesChannelRepository
@@ -1,6 +1,25 @@
import { ShippingProfile } from "../models" import { ShippingProfile } from "../models"
import { dataSource } from "../loaders/database" import { dataSource } from "../loaders/database"
export const ShippingProfileRepository = export const ShippingProfileRepository = dataSource
dataSource.getRepository(ShippingProfile) .getRepository(ShippingProfile)
.extend({
async findByProducts(
productIds: string | string[]
): Promise<{ [product_id: string]: ShippingProfile[] }> {
productIds = Array.isArray(productIds) ? productIds : [productIds]
const shippingProfiles = await this.createQueryBuilder("sp")
.select("*")
.innerJoin("product_shipping_profile", "psp", "psp.profile_id = sp.id")
.where("psp.product_id IN (:...productIds)", { productIds })
.execute()
return shippingProfiles.reduce((acc, productShippingProfile) => {
acc[productShippingProfile.product_id] ??= []
acc[productShippingProfile.product_id].push(productShippingProfile)
return acc
}, {})
},
})
export default ShippingProfileRepository export default ShippingProfileRepository
@@ -302,6 +302,22 @@ class SalesChannelService extends TransactionBaseService {
return store.default_sales_channel return store.default_sales_channel
} }
/**
* List all product ids that belongs to the sales channels ids
*
* @param salesChannelIds
*/
async listProductIdsBySalesChannelIds(
salesChannelIds: string | string[]
): Promise<{ [salesChannelId: string]: string[] }> {
const salesChannelRepo = this.activeManager_.withRepository(
this.salesChannelRepository_
)
return await salesChannelRepo.listProductIdsBySalesChannelIds(
salesChannelIds
)
}
/** /**
* Remove a batch of product from a sales channel * Remove a batch of product from a sales channel
* @param salesChannelId - The id of the sales channel on which to remove the products * @param salesChannelId - The id of the sales channel on which to remove the products
@@ -156,6 +156,36 @@ class ShippingProfileService extends TransactionBaseService {
return profile return profile
} }
async retrieveForProducts(
productIds: string | string[]
): Promise<{ [product_id: string]: ShippingProfile[] }> {
if (!isDefined(productIds)) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`"productIds" must be defined`
)
}
productIds = isString(productIds) ? [productIds] : productIds
const profileRepository = this.activeManager_.withRepository(
this.shippingProfileRepository_
)
const productProfilesMap = await profileRepository.findByProducts(
productIds
)
if (!Object.keys(productProfilesMap)?.length) {
throw new MedusaError(
MedusaError.Types.NOT_FOUND,
`No Profile found for products with id: ${productIds.join(", ")}`
)
}
return productProfilesMap
}
async retrieveDefault(): Promise<ShippingProfile | null> { async retrieveDefault(): Promise<ShippingProfile | null> {
const profileRepository = this.activeManager_.withRepository( const profileRepository = this.activeManager_.withRepository(
this.shippingProfileRepository_ this.shippingProfileRepository_
+1
View File
@@ -157,6 +157,7 @@ export class RemoteQuery {
"skip", "skip",
"take", "take",
"limit", "limit",
"order",
"offset", "offset",
"cursor", "cursor",
"sort", "sort",
@@ -573,7 +573,7 @@ export class RemoteJoiner {
const alias = fieldAlias[prop] as any const alias = fieldAlias[prop] as any
const path = isString(alias) ? alias : alias.path const path = isString(alias) ? alias : alias.path
const fullPath = currentPath.concat(path.split(".")) const fullPath = [...new Set(currentPath.concat(path.split(".")))]
forwardArgumentsOnPath = forwardArgumentsOnPath.concat( forwardArgumentsOnPath = forwardArgumentsOnPath.concat(
(alias?.forwardArgumentsOnPath || []).map( (alias?.forwardArgumentsOnPath || []).map(
@@ -24,7 +24,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
}, },
}, },
@@ -48,7 +48,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -78,7 +78,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -116,7 +116,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -154,7 +154,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
withDeleted: undefined, withDeleted: undefined,
populate: ["tags"], populate: ["tags"],
}, },
@@ -192,7 +192,7 @@ describe("Currency service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
withDeleted: undefined, withDeleted: undefined,
populate: ["tags"], populate: ["tags"],
}, },
+1 -1
View File
@@ -8,7 +8,7 @@ module.exports = {
"^.+\\.[jt]s?$": [ "^.+\\.[jt]s?$": [
"ts-jest", "ts-jest",
{ {
tsConfig: "tsconfig.spec.json", tsconfig: "tsconfig.spec.json",
isolatedModules: true, isolatedModules: true,
}, },
], ],
@@ -1,5 +1,7 @@
{ {
"namespaces": ["public"], "namespaces": [
"public"
],
"name": "public", "name": "public",
"tables": [ "tables": [
{ {
@@ -97,6 +99,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
}, },
"updated_at": { "updated_at": {
@@ -107,6 +110,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
} }
}, },
@@ -115,21 +119,27 @@
"indexes": [ "indexes": [
{ {
"keyName": "IDX_product_category_path", "keyName": "IDX_product_category_path",
"columnNames": ["mpath"], "columnNames": [
"mpath"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"keyName": "IDX_product_category_handle", "keyName": "IDX_product_category_handle",
"columnNames": ["handle"], "columnNames": [
"handle"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "product_category_pkey", "keyName": "product_category_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -139,9 +149,13 @@
"foreignKeys": { "foreignKeys": {
"product_category_parent_category_id_foreign": { "product_category_parent_category_id_foreign": {
"constraintName": "product_category_parent_category_id_foreign", "constraintName": "product_category_parent_category_id_foreign",
"columnNames": ["parent_category_id"], "columnNames": [
"parent_category_id"
],
"localTableName": "public.product_category", "localTableName": "public.product_category",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_category", "referencedTableName": "public.product_category",
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
@@ -186,6 +200,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -201,7 +237,9 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_collection_deleted_at", "keyName": "IDX_product_collection_deleted_at",
"primary": false, "primary": false,
@@ -209,14 +247,18 @@
}, },
{ {
"keyName": "IDX_product_collection_handle_unique", "keyName": "IDX_product_collection_handle_unique",
"columnNames": ["handle"], "columnNames": [
"handle"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "product_collection_pkey", "keyName": "product_collection_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -254,6 +296,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -269,14 +333,18 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["url"], "columnNames": [
"url"
],
"composite": false, "composite": false,
"keyName": "IDX_product_image_url", "keyName": "IDX_product_image_url",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_image_deleted_at", "keyName": "IDX_product_image_deleted_at",
"primary": false, "primary": false,
@@ -284,7 +352,9 @@
}, },
{ {
"keyName": "image_pkey", "keyName": "image_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -322,6 +392,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -337,7 +429,9 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_tag_deleted_at", "keyName": "IDX_product_tag_deleted_at",
"primary": false, "primary": false,
@@ -345,7 +439,9 @@
}, },
{ {
"keyName": "product_tag_pkey", "keyName": "product_tag_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -383,6 +479,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -398,7 +516,9 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_type_deleted_at", "keyName": "IDX_product_type_deleted_at",
"primary": false, "primary": false,
@@ -406,7 +526,9 @@
}, },
{ {
"keyName": "product_type_pkey", "keyName": "product_type_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -479,7 +601,12 @@
"autoincrement": false, "autoincrement": false,
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"enumItems": ["draft", "proposed", "published", "rejected"], "enumItems": [
"draft",
"proposed",
"published",
"rejected"
],
"mappedType": "enum" "mappedType": "enum"
}, },
"thumbnail": { "thumbnail": {
@@ -608,6 +735,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
}, },
"updated_at": { "updated_at": {
@@ -618,6 +746,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
}, },
"deleted_at": { "deleted_at": {
@@ -644,14 +773,18 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["type_id"], "columnNames": [
"type_id"
],
"composite": false, "composite": false,
"keyName": "IDX_product_type_id", "keyName": "IDX_product_type_id",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_deleted_at", "keyName": "IDX_product_deleted_at",
"primary": false, "primary": false,
@@ -659,14 +792,18 @@
}, },
{ {
"keyName": "IDX_product_handle_unique", "keyName": "IDX_product_handle_unique",
"columnNames": ["handle"], "columnNames": [
"handle"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "product_pkey", "keyName": "product_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -676,18 +813,26 @@
"foreignKeys": { "foreignKeys": {
"product_collection_id_foreign": { "product_collection_id_foreign": {
"constraintName": "product_collection_id_foreign", "constraintName": "product_collection_id_foreign",
"columnNames": ["collection_id"], "columnNames": [
"collection_id"
],
"localTableName": "public.product", "localTableName": "public.product",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_collection", "referencedTableName": "public.product_collection",
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"product_type_id_foreign": { "product_type_id_foreign": {
"constraintName": "product_type_id_foreign", "constraintName": "product_type_id_foreign",
"columnNames": ["type_id"], "columnNames": [
"type_id"
],
"localTableName": "public.product", "localTableName": "public.product",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_type", "referencedTableName": "public.product_type",
"deleteRule": "set null", "deleteRule": "set null",
"updateRule": "cascade" "updateRule": "cascade"
@@ -732,6 +877,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -747,14 +914,18 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_product_id", "keyName": "IDX_product_option_product_id",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_deleted_at", "keyName": "IDX_product_option_deleted_at",
"primary": false, "primary": false,
@@ -762,7 +933,9 @@
}, },
{ {
"keyName": "product_option_pkey", "keyName": "product_option_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -772,9 +945,13 @@
"foreignKeys": { "foreignKeys": {
"product_option_product_id_foreign": { "product_option_product_id_foreign": {
"constraintName": "product_option_product_id_foreign", "constraintName": "product_option_product_id_foreign",
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"localTableName": "public.product_option", "localTableName": "public.product_option",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product", "referencedTableName": "public.product",
"updateRule": "cascade" "updateRule": "cascade"
} }
@@ -806,7 +983,10 @@
"indexes": [ "indexes": [
{ {
"keyName": "product_tags_pkey", "keyName": "product_tags_pkey",
"columnNames": ["product_id", "product_tag_id"], "columnNames": [
"product_id",
"product_tag_id"
],
"composite": true, "composite": true,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -816,18 +996,26 @@
"foreignKeys": { "foreignKeys": {
"product_tags_product_id_foreign": { "product_tags_product_id_foreign": {
"constraintName": "product_tags_product_id_foreign", "constraintName": "product_tags_product_id_foreign",
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"localTableName": "public.product_tags", "localTableName": "public.product_tags",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product", "referencedTableName": "public.product",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"product_tags_product_tag_id_foreign": { "product_tags_product_tag_id_foreign": {
"constraintName": "product_tags_product_tag_id_foreign", "constraintName": "product_tags_product_tag_id_foreign",
"columnNames": ["product_tag_id"], "columnNames": [
"product_tag_id"
],
"localTableName": "public.product_tags", "localTableName": "public.product_tags",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_tag", "referencedTableName": "public.product_tag",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -873,9 +1061,13 @@
"foreignKeys": { "foreignKeys": {
"product_images_product_id_foreign": { "product_images_product_id_foreign": {
"constraintName": "product_images_product_id_foreign", "constraintName": "product_images_product_id_foreign",
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"localTableName": "public.product_images", "localTableName": "public.product_images",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product", "referencedTableName": "public.product",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -886,7 +1078,9 @@
"image_id" "image_id"
], ],
"localTableName": "public.product_images", "localTableName": "public.product_images",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.image", "referencedTableName": "public.image",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -919,7 +1113,10 @@
"indexes": [ "indexes": [
{ {
"keyName": "product_category_product_pkey", "keyName": "product_category_product_pkey",
"columnNames": ["product_id", "product_category_id"], "columnNames": [
"product_id",
"product_category_id"
],
"composite": true, "composite": true,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -929,18 +1126,26 @@
"foreignKeys": { "foreignKeys": {
"product_category_product_product_id_foreign": { "product_category_product_product_id_foreign": {
"constraintName": "product_category_product_product_id_foreign", "constraintName": "product_category_product_product_id_foreign",
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"localTableName": "public.product_category_product", "localTableName": "public.product_category_product",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product", "referencedTableName": "public.product",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"product_category_product_product_category_id_foreign": { "product_category_product_product_category_id_foreign": {
"constraintName": "product_category_product_product_category_id_foreign", "constraintName": "product_category_product_product_category_id_foreign",
"columnNames": ["product_category_id"], "columnNames": [
"product_category_id"
],
"localTableName": "public.product_category_product", "localTableName": "public.product_category_product",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_category", "referencedTableName": "public.product_category",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -1141,6 +1346,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
}, },
"updated_at": { "updated_at": {
@@ -1151,6 +1357,7 @@
"primary": false, "primary": false,
"nullable": false, "nullable": false,
"length": 6, "length": 6,
"default": "now()",
"mappedType": "datetime" "mappedType": "datetime"
}, },
"deleted_at": { "deleted_at": {
@@ -1168,14 +1375,18 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_variant_deleted_at", "keyName": "IDX_product_variant_deleted_at",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"composite": false, "composite": false,
"keyName": "IDX_product_variant_product_id", "keyName": "IDX_product_variant_product_id",
"primary": false, "primary": false,
@@ -1183,35 +1394,45 @@
}, },
{ {
"keyName": "IDX_product_variant_sku_unique", "keyName": "IDX_product_variant_sku_unique",
"columnNames": ["sku"], "columnNames": [
"sku"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "IDX_product_variant_barcode_unique", "keyName": "IDX_product_variant_barcode_unique",
"columnNames": ["barcode"], "columnNames": [
"barcode"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "IDX_product_variant_ean_unique", "keyName": "IDX_product_variant_ean_unique",
"columnNames": ["ean"], "columnNames": [
"ean"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "IDX_product_variant_upc_unique", "keyName": "IDX_product_variant_upc_unique",
"columnNames": ["upc"], "columnNames": [
"upc"
],
"composite": false, "composite": false,
"primary": false, "primary": false,
"unique": true "unique": true
}, },
{ {
"keyName": "product_variant_pkey", "keyName": "product_variant_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -1221,9 +1442,13 @@
"foreignKeys": { "foreignKeys": {
"product_variant_product_id_foreign": { "product_variant_product_id_foreign": {
"constraintName": "product_variant_product_id_foreign", "constraintName": "product_variant_product_id_foreign",
"columnNames": ["product_id"], "columnNames": [
"product_id"
],
"localTableName": "public.product_variant", "localTableName": "public.product_variant",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product", "referencedTableName": "public.product",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -1277,6 +1502,28 @@
"nullable": true, "nullable": true,
"mappedType": "json" "mappedType": "json"
}, },
"created_at": {
"name": "created_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"updated_at": {
"name": "updated_at",
"type": "timestamptz",
"unsigned": false,
"autoincrement": false,
"primary": false,
"nullable": false,
"length": 6,
"default": "now()",
"mappedType": "datetime"
},
"deleted_at": { "deleted_at": {
"name": "deleted_at", "name": "deleted_at",
"type": "timestamptz", "type": "timestamptz",
@@ -1292,21 +1539,27 @@
"schema": "public", "schema": "public",
"indexes": [ "indexes": [
{ {
"columnNames": ["option_id"], "columnNames": [
"option_id"
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_value_option_id", "keyName": "IDX_product_option_value_option_id",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["variant_id"], "columnNames": [
"variant_id"
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_value_variant_id", "keyName": "IDX_product_option_value_variant_id",
"primary": false, "primary": false,
"unique": false "unique": false
}, },
{ {
"columnNames": ["deleted_at"], "columnNames": [
"deleted_at"
],
"composite": false, "composite": false,
"keyName": "IDX_product_option_value_deleted_at", "keyName": "IDX_product_option_value_deleted_at",
"primary": false, "primary": false,
@@ -1314,7 +1567,9 @@
}, },
{ {
"keyName": "product_option_value_pkey", "keyName": "product_option_value_pkey",
"columnNames": ["id"], "columnNames": [
"id"
],
"composite": false, "composite": false,
"primary": true, "primary": true,
"unique": true "unique": true
@@ -1324,17 +1579,25 @@
"foreignKeys": { "foreignKeys": {
"product_option_value_option_id_foreign": { "product_option_value_option_id_foreign": {
"constraintName": "product_option_value_option_id_foreign", "constraintName": "product_option_value_option_id_foreign",
"columnNames": ["option_id"], "columnNames": [
"option_id"
],
"localTableName": "public.product_option_value", "localTableName": "public.product_option_value",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_option", "referencedTableName": "public.product_option",
"updateRule": "cascade" "updateRule": "cascade"
}, },
"product_option_value_variant_id_foreign": { "product_option_value_variant_id_foreign": {
"constraintName": "product_option_value_variant_id_foreign", "constraintName": "product_option_value_variant_id_foreign",
"columnNames": ["variant_id"], "columnNames": [
"variant_id"
],
"localTableName": "public.product_option_value", "localTableName": "public.product_option_value",
"referencedColumnNames": ["id"], "referencedColumnNames": [
"id"
],
"referencedTableName": "public.product_variant", "referencedTableName": "public.product_variant",
"deleteRule": "cascade", "deleteRule": "cascade",
"updateRule": "cascade" "updateRule": "cascade"
@@ -0,0 +1,69 @@
import { Migration } from '@mikro-orm/migrations';
export class Migration20230908084537 extends Migration {
async up(): Promise<void> {
this.addSql('alter table "product_category" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product_category" alter column "created_at" set default now();');
this.addSql('alter table "product_category" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product_category" alter column "updated_at" set default now();');
this.addSql('alter table "product_collection" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
this.addSql('alter table "image" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
this.addSql('alter table "product_tag" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
this.addSql('alter table "product_type" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
this.addSql('alter table "product" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product" alter column "created_at" set default now();');
this.addSql('alter table "product" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product" alter column "updated_at" set default now();');
this.addSql('alter table "product_option" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
this.addSql('alter table "product_variant" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product_variant" alter column "created_at" set default now();');
this.addSql('alter table "product_variant" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product_variant" alter column "updated_at" set default now();');
this.addSql('alter table "product_option_value" add column "created_at" timestamptz not null default now(), add column "updated_at" timestamptz not null default now();');
}
async down(): Promise<void> {
this.addSql('alter table "product_category" alter column "created_at" drop default;');
this.addSql('alter table "product_category" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product_category" alter column "updated_at" drop default;');
this.addSql('alter table "product_category" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product_collection" drop column "created_at";');
this.addSql('alter table "product_collection" drop column "updated_at";');
this.addSql('alter table "image" drop column "created_at";');
this.addSql('alter table "image" drop column "updated_at";');
this.addSql('alter table "product_tag" drop column "created_at";');
this.addSql('alter table "product_tag" drop column "updated_at";');
this.addSql('alter table "product_type" drop column "created_at";');
this.addSql('alter table "product_type" drop column "updated_at";');
this.addSql('alter table "product" alter column "created_at" drop default;');
this.addSql('alter table "product" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product" alter column "updated_at" drop default;');
this.addSql('alter table "product" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product_option" drop column "created_at";');
this.addSql('alter table "product_option" drop column "updated_at";');
this.addSql('alter table "product_variant" alter column "created_at" drop default;');
this.addSql('alter table "product_variant" alter column "created_at" type timestamptz using ("created_at"::timestamptz);');
this.addSql('alter table "product_variant" alter column "updated_at" drop default;');
this.addSql('alter table "product_variant" alter column "updated_at" type timestamptz using ("updated_at"::timestamptz);');
this.addSql('alter table "product_option_value" drop column "created_at";');
this.addSql('alter table "product_option_value" drop column "updated_at";');
}
}
@@ -8,15 +8,21 @@ import {
ManyToMany, ManyToMany,
ManyToOne, ManyToOne,
OneToMany, OneToMany,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
Unique, Unique,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalFields = DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "product_category" }) @Entity({ tableName: "product_category" })
class ProductCategory { class ProductCategory {
[OptionalProps]?: OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -61,13 +67,18 @@ class ProductCategory {
}) })
category_children = new Collection<ProductCategory>(this) category_children = new Collection<ProductCategory>(this)
@Property({ onCreate: () => new Date(), columnType: "timestamptz" }) @Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at?: Date created_at?: Date
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
onUpdate: () => new Date(), onUpdate: () => new Date(),
columnType: "timestamptz", columnType: "timestamptz",
defaultRaw: "now()",
}) })
updated_at?: Date updated_at?: Date
@@ -13,13 +13,15 @@ import {
import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "product_collection" }) @Entity({ tableName: "product_collection" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductCollection { class ProductCollection {
[OptionalProps]?: OptionalRelations [OptionalProps]?: OptionalRelations | OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -40,6 +42,21 @@ class ProductCollection {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_collection_deleted_at" }) @Index({ name: "IDX_product_collection_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
+18 -1
View File
@@ -12,13 +12,15 @@ import {
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "image" }) @Entity({ tableName: "image" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductImage { class ProductImage {
[OptionalProps]?: OptionalRelations [OptionalProps]?: OptionalRelations | OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -30,6 +32,21 @@ class ProductImage {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_image_deleted_at" }) @Index({ name: "IDX_product_image_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@@ -10,14 +10,14 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { ProductOption, ProductVariant } from "./index" import { ProductOption, ProductVariant } from "./index"
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import { DAL } from "@medusajs/types"
type OptionalFields = type OptionalFields =
| "created_at"
| "updated_at"
| "allow_backorder" | "allow_backorder"
| "manage_inventory" | "manage_inventory"
| "option_id" | "option_id"
| "variant_id" | "variant_id"
| DAL.SoftDeletableEntityDateColumns
type OptionalRelations = "product" | "option" | "variant" type OptionalRelations = "product" | "option" | "variant"
@Entity({ tableName: "product_option_value" }) @Entity({ tableName: "product_option_value" })
@@ -53,6 +53,21 @@ class ProductOptionValue {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_option_value_deleted_at" }) @Index({ name: "IDX_product_option_value_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
+20 -1
View File
@@ -14,8 +14,12 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Product } from "./index" import { Product } from "./index"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { DAL } from "@medusajs/types"
type OptionalRelations = "values" | "product" type OptionalRelations =
| "values"
| "product"
| DAL.SoftDeletableEntityDateColumns
type OptionalFields = "product_id" type OptionalFields = "product_id"
@Entity({ tableName: "product_option" }) @Entity({ tableName: "product_option" })
@@ -46,6 +50,21 @@ class ProductOption {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_option_deleted_at" }) @Index({ name: "IDX_product_option_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
+18 -1
View File
@@ -12,13 +12,15 @@ import {
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import Product from "./product" import Product from "./product"
import { DAL } from "@medusajs/types"
type OptionalRelations = "products" type OptionalRelations = "products"
type OptionalFields = DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "product_tag" }) @Entity({ tableName: "product_tag" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductTag { class ProductTag {
[OptionalProps]?: OptionalRelations [OptionalProps]?: OptionalRelations | OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -29,6 +31,21 @@ class ProductTag {
@Property({ columnType: "jsonb", nullable: true }) @Property({ columnType: "jsonb", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_tag_deleted_at" }) @Index({ name: "IDX_product_tag_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@@ -3,15 +3,21 @@ import {
Entity, Entity,
Filter, Filter,
Index, Index,
OptionalProps,
PrimaryKey, PrimaryKey,
Property, Property,
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { DALUtils, generateEntityId } from "@medusajs/utils" import { DALUtils, generateEntityId } from "@medusajs/utils"
import { DAL } from "@medusajs/types"
type OptionalFields = DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "product_type" }) @Entity({ tableName: "product_type" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
class ProductType { class ProductType {
[OptionalProps]?: OptionalFields
@PrimaryKey({ columnType: "text" }) @PrimaryKey({ columnType: "text" })
id!: string id!: string
@@ -21,6 +27,21 @@ class ProductType {
@Property({ columnType: "json", nullable: true }) @Property({ columnType: "json", nullable: true })
metadata?: Record<string, unknown> | null metadata?: Record<string, unknown> | null
@Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date
@Property({
onCreate: () => new Date(),
onUpdate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
updated_at: Date
@Index({ name: "IDX_product_type_deleted_at" }) @Index({ name: "IDX_product_type_deleted_at" })
@Property({ columnType: "timestamptz", nullable: true }) @Property({ columnType: "timestamptz", nullable: true })
deleted_at?: Date deleted_at?: Date
@@ -15,14 +15,14 @@ import {
} from "@mikro-orm/core" } from "@mikro-orm/core"
import { Product } from "@models" import { Product } from "@models"
import ProductOptionValue from "./product-option-value" import ProductOptionValue from "./product-option-value"
import { DAL } from "@medusajs/types"
type OptionalFields = type OptionalFields =
| "created_at"
| "updated_at"
| "allow_backorder" | "allow_backorder"
| "manage_inventory" | "manage_inventory"
| "product" | "product"
| "product_id" | "product_id"
| DAL.SoftDeletableEntityDateColumns
@Entity({ tableName: "product_variant" }) @Entity({ tableName: "product_variant" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
@@ -108,13 +108,18 @@ class ProductVariant {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
product_id!: string product_id!: string
@Property({ onCreate: () => new Date(), columnType: "timestamptz" }) @Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date created_at: Date
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
onUpdate: () => new Date(), onUpdate: () => new Date(),
columnType: "timestamptz", columnType: "timestamptz",
defaultRaw: "now()",
}) })
updated_at: Date updated_at: Date
+8 -3
View File
@@ -27,6 +27,7 @@ import ProductTag from "./product-tag"
import ProductType from "./product-type" import ProductType from "./product-type"
import ProductVariant from "./product-variant" import ProductVariant from "./product-variant"
import ProductImage from "./product-image" import ProductImage from "./product-image"
import { DAL } from "@medusajs/types"
type OptionalRelations = "collection" | "type" type OptionalRelations = "collection" | "type"
type OptionalFields = type OptionalFields =
@@ -34,8 +35,7 @@ type OptionalFields =
| "type_id" | "type_id"
| "is_giftcard" | "is_giftcard"
| "discountable" | "discountable"
| "created_at" | DAL.SoftDeletableEntityDateColumns
| "updated_at"
@Entity({ tableName: "product" }) @Entity({ tableName: "product" })
@Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions)
@@ -154,13 +154,18 @@ class Product {
@Property({ columnType: "text", nullable: true }) @Property({ columnType: "text", nullable: true })
external_id?: string | null external_id?: string | null
@Property({ onCreate: () => new Date(), columnType: "timestamptz" }) @Property({
onCreate: () => new Date(),
columnType: "timestamptz",
defaultRaw: "now()",
})
created_at: Date created_at: Date
@Property({ @Property({
onCreate: () => new Date(), onCreate: () => new Date(),
onUpdate: () => new Date(), onUpdate: () => new Date(),
columnType: "timestamptz", columnType: "timestamptz",
defaultRaw: "now()",
}) })
updated_at: Date updated_at: Date
@@ -91,8 +91,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito
}, },
} }
delete whereOptions.parent_category_id if ("parent_category_id" in whereOptions) {
delete whereOptions.id delete whereOptions.parent_category_id
}
if ("id" in whereOptions) {
delete whereOptions.id
}
const descendantsForCategory = await manager.find( const descendantsForCategory = await manager.find(
ProductCategory, ProductCategory,
+54 -3
View File
@@ -35,7 +35,7 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
} }
async find( async find(
findOptions: DAL.FindOptions<Product> = { where: {} }, findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} },
context: Context = {} context: Context = {}
): Promise<Product[]> { ): Promise<Product[]> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -49,6 +49,11 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
await this.mutateNotInCategoriesConstraints(findOptions_) await this.mutateNotInCategoriesConstraints(findOptions_)
this.applyFreeTextSearchFilters<Product>(
findOptions_,
this.getFreeTextSearchConstraints
)
return await manager.find( return await manager.find(
Product, Product,
findOptions_.where as MikroFilterQuery<Product>, findOptions_.where as MikroFilterQuery<Product>,
@@ -57,7 +62,7 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
} }
async findAndCount( async findAndCount(
findOptions: DAL.FindOptions<Product> = { where: {} }, findOptions: DAL.FindOptions<Product & { q?: string }> = { where: {} },
context: Context = {} context: Context = {}
): Promise<[Product[], number]> { ): Promise<[Product[], number]> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
@@ -71,6 +76,11 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
await this.mutateNotInCategoriesConstraints(findOptions_) await this.mutateNotInCategoriesConstraints(findOptions_)
this.applyFreeTextSearchFilters<Product>(
findOptions_,
this.getFreeTextSearchConstraints
)
return await manager.findAndCount( return await manager.findAndCount(
Product, Product,
findOptions_.where as MikroFilterQuery<Product>, findOptions_.where as MikroFilterQuery<Product>,
@@ -88,7 +98,10 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
): Promise<void> { ): Promise<void> {
const manager = this.getActiveManager<SqlEntityManager>(context) const manager = this.getActiveManager<SqlEntityManager>(context)
if (findOptions.where.categories?.id?.["$nin"]) { if (
"categories" in findOptions.where &&
findOptions.where.categories?.id?.["$nin"]
) {
const productsInCategories = await manager.find( const productsInCategories = await manager.find(
Product, Product,
{ {
@@ -307,4 +320,42 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository<P
return products return products
} }
protected getFreeTextSearchConstraints(q: string) {
return [
{
description: {
$ilike: `%${q}%`,
},
},
{
title: {
$ilike: `%${q}%`,
},
},
{
collection: {
title: {
$ilike: `%${q}%`,
},
},
},
{
variants: {
$or: [
{
title: {
$ilike: `%${q}%`,
},
},
{
sku: {
$ilike: `%${q}%`,
},
},
],
},
},
]
}
} }
@@ -20,7 +20,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -45,7 +45,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -75,7 +75,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -113,7 +113,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
populate: [], populate: [],
withDeleted: undefined, withDeleted: undefined,
}, },
@@ -151,7 +151,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
withDeleted: undefined, withDeleted: undefined,
populate: ["tags"], populate: ["tags"],
}, },
@@ -189,7 +189,7 @@ describe("Product service", function () {
options: { options: {
fields: undefined, fields: undefined,
limit: 15, limit: 15,
offset: undefined, offset: 0,
withDeleted: undefined, withDeleted: undefined,
populate: ["tags"], populate: ["tags"],
}, },
+2
View File
@@ -0,0 +1,2 @@
export type EntityDateColumns = "created_at" | "updated_at"
export type SoftDeletableEntityDateColumns = "deleted_at" | EntityDateColumns
+4 -3
View File
@@ -2,8 +2,8 @@ import { Dictionary, FilterQuery, Order } from "./utils"
export { FilterQuery } from "./utils" export { FilterQuery } from "./utils"
export interface BaseFilterable<T> { export interface BaseFilterable<T> {
$and?: T $and?: (T | BaseFilterable<T>)[]
$or?: T $or?: (T | BaseFilterable<T>)[]
} }
export interface OptionsQuery<T, P extends string = never> { export interface OptionsQuery<T, P extends string = never> {
@@ -17,8 +17,9 @@ export interface OptionsQuery<T, P extends string = never> {
} }
export type FindOptions<T = any> = { export type FindOptions<T = any> = {
where: FilterQuery<T> where: FilterQuery<T> & BaseFilterable<FilterQuery<T>>
options?: OptionsQuery<T, any> options?: OptionsQuery<T, any>
} }
export * from "./repository-service" export * from "./repository-service"
export * from "./entity"
+28 -7
View File
@@ -3,19 +3,37 @@ export type JoinerRelationship = {
foreignKey: string foreignKey: string
primaryKey: string primaryKey: string
serviceName: string serviceName: string
inverse?: boolean // In an inverted relationship the foreign key is on the other service and the primary key is on the current service /**
isList?: boolean // Force the relationship to return a list * In an inverted relationship the foreign key is on the other service and the primary key is on the current service
args?: Record<string, any> // Extra arguments to pass to the remoteFetchData callback */
inverse?: boolean
/**
* Force the relationship to return a list
*/
isList?: boolean
/**
* Extra arguments to pass to the remoteFetchData callback
*/
args?: Record<string, any>
} }
export interface JoinerServiceConfigAlias { export interface JoinerServiceConfigAlias {
name: string name: string
args?: Record<string, any> // Extra arguments to pass to the remoteFetchData callback /**
* Extra arguments to pass to the remoteFetchData callback
*/
args?: Record<string, any>
} }
export interface JoinerServiceConfig { export interface JoinerServiceConfig {
serviceName: string serviceName: string
alias?: JoinerServiceConfigAlias | JoinerServiceConfigAlias[] // Property name to use as entrypoint to the service /**
* Property name to use as entrypoint to the service
*/
alias?: JoinerServiceConfigAlias | JoinerServiceConfigAlias[]
/**
* alias for deeper nested relationships (e.g. { 'price': 'prices.calculated_price_set.amount' })
*/
fieldAlias?: Record< fieldAlias?: Record<
string, string,
| string | string
@@ -23,14 +41,17 @@ export interface JoinerServiceConfig {
path: string path: string
forwardArgumentsOnPath: string[] forwardArgumentsOnPath: string[]
} }
> // alias for deeper nested relationships (e.g. { 'price': 'prices.calculated_price_set.amount' }) >
primaryKeys: string[] primaryKeys: string[]
relationships?: JoinerRelationship[] relationships?: JoinerRelationship[]
extends?: { extends?: {
serviceName: string serviceName: string
relationship: JoinerRelationship relationship: JoinerRelationship
}[] }[]
args?: Record<string, any> // Extra arguments to pass to the remoteFetchData callback /**
* Extra arguments to pass to the remoteFetchData callback
*/
args?: Record<string, any>
} }
export interface JoinerArgument { export interface JoinerArgument {
+52 -13
View File
@@ -36,8 +36,14 @@ export type InternalModuleDeclaration = {
*/ */
resolve?: string resolve?: string
options?: Record<string, unknown> options?: Record<string, unknown>
alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them /**
main?: boolean // If the module is the main module for the key when multiple ones are registered * If multiple modules are registered with the same key, the alias can be used to differentiate them
*/
alias?: string
/**
* If the module is the main module for the key when multiple ones are registered
*/
main?: boolean
} }
export type ExternalModuleDeclaration = { export type ExternalModuleDeclaration = {
@@ -48,8 +54,14 @@ export type ExternalModuleDeclaration = {
keepAlive: boolean keepAlive: boolean
} }
options?: Record<string, unknown> options?: Record<string, unknown>
alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them /**
main?: boolean // If the module is the main module for the key when multiple ones are registered * If multiple modules are registered with the same key, the alias can be used to differentiate them
*/
alias?: string
/**
* If the module is the main module for the key when multiple ones are registered
*/
main?: boolean
} }
export type ModuleResolution = { export type ModuleResolution = {
@@ -74,7 +86,10 @@ export type ModuleDefinition = {
* @deprecated property will be removed in future versions * @deprecated property will be removed in future versions
*/ */
isRequired?: boolean isRequired?: boolean
isQueryable?: boolean // If the module is queryable via Remote Joiner /**
* If the module is queryable via Remote Joiner
*/
isQueryable?: boolean
dependencies?: string[] dependencies?: string[]
defaultModuleDeclaration: defaultModuleDeclaration:
| InternalModuleDeclaration | InternalModuleDeclaration
@@ -136,12 +151,27 @@ export type ModuleJoinerConfig = Omit<
}[] }[]
serviceName?: string serviceName?: string
primaryKeys?: string[] primaryKeys?: string[]
isLink?: boolean // If the module is a link module /**
linkableKeys?: string[] // Keys that can be used to link to other modules * If the module is a link module
isReadOnlyLink?: boolean // If true it expands a RemoteQuery property but doesn't create a pivot table */
isLink?: boolean
/**
* Keys that can be used to link to other modules
*/
linkableKeys?: string[]
/**
* If true it expands a RemoteQuery property but doesn't create a pivot table
*/
isReadOnlyLink?: boolean
databaseConfig?: { databaseConfig?: {
tableName?: string // Name of the pivot table. If not provided it is auto generated /**
idPrefix?: string // Prefix for the id column. If not provided it is "link" * Name of the pivot table. If not provided it is auto generated
*/
tableName?: string
/**
* Prefix for the id column. If not provided it is "link"
*/
idPrefix?: string
extraFields?: Record< extraFields?: Record<
string, string,
{ {
@@ -169,15 +199,24 @@ export type ModuleJoinerConfig = Omit<
| "text" | "text"
defaultValue?: string defaultValue?: string
nullable?: boolean nullable?: boolean
options?: Record<string, unknown> // Mikro-orm options for the column /**
* Mikro-orm options for the column
*/
options?: Record<string, unknown>
} }
> >
} }
} }
export declare type ModuleJoinerRelationship = JoinerRelationship & { export declare type ModuleJoinerRelationship = JoinerRelationship & {
isInternalService?: boolean // If true, the relationship is an internal service from the medusa core TODO: Remove when there are no more "internal" services /**
deleteCascade?: boolean // If true, the link joiner will cascade deleting the relationship * If true, the relationship is an internal service from the medusa core TODO: Remove when there are no more "internal" services
*/
isInternalService?: boolean
/**
* If true, the link joiner will cascade deleting the relationship
*/
deleteCascade?: boolean
} }
export type ModuleExports = { export type ModuleExports = {
+3
View File
@@ -158,11 +158,14 @@ export interface ProductOptionValueDTO {
*/ */
export interface FilterableProductProps export interface FilterableProductProps
extends BaseFilterable<FilterableProductProps> { extends BaseFilterable<FilterableProductProps> {
q?: string
handle?: string | string[] handle?: string | string[]
id?: string | string[] id?: string | string[]
tags?: { value?: string[] } tags?: { value?: string[] }
categories?: { categories?: {
id?: string | string[] | OperatorMap<string> id?: string | string[] | OperatorMap<string>
is_internal?: boolean
is_active?: boolean
} }
category_id?: string | string[] | OperatorMap<string> category_id?: string | string[] | OperatorMap<string>
collection_id?: string | string[] | OperatorMap<string> collection_id?: string | string[] | OperatorMap<string>
@@ -6,7 +6,7 @@ import {
} from "@medusajs/types" } from "@medusajs/types"
import { isString } from "../../common" import { isString } from "../../common"
import { MedusaContext } from "../../decorators" import { MedusaContext } from "../../decorators"
import { InjectTransactionManager, buildQuery } from "../../modules-sdk" import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
import { import {
getSoftDeletedCascadedEntitiesIdsMappedBy, getSoftDeletedCascadedEntitiesIdsMappedBy,
transactionWrapper, transactionWrapper,
@@ -131,6 +131,22 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
return [entities, softDeletedEntitiesMap] return [entities, softDeletedEntitiesMap]
} }
applyFreeTextSearchFilters<T>(
findOptions: DAL.FindOptions<T & { q?: string }>,
retrieveConstraintsToApply: (q: string) => any[]
): void {
if (!("q" in findOptions.where) || !findOptions.where.q) {
return
}
const q = findOptions.where.q as string
delete findOptions.where.q
findOptions.where = {
$and: [findOptions.where, { $or: retrieveConstraintsToApply(q) }],
} as unknown as DAL.FilterQuery<T & { q?: string }>
}
} }
export abstract class MikroOrmAbstractTreeRepositoryBase<T = any> export abstract class MikroOrmAbstractTreeRepositoryBase<T = any>
+14 -1
View File
@@ -14,7 +14,11 @@ export function buildQuery<T = any, TDto = any>(
populate: deduplicate(config.relations ?? []), populate: deduplicate(config.relations ?? []),
fields: config.select as string[], fields: config.select as string[],
limit: config.take ?? 15, limit: config.take ?? 15,
offset: config.skip, offset: config.skip ?? 0,
}
if (config.order) {
findOptions.orderBy = config.order as DAL.OptionsQuery<T>["orderBy"]
} }
if (config.withDeleted) { if (config.withDeleted) {
@@ -29,6 +33,15 @@ export function buildQuery<T = any, TDto = any>(
function buildWhere(filters: Record<string, any> = {}, where = {}) { function buildWhere(filters: Record<string, any> = {}, where = {}) {
for (let [prop, value] of Object.entries(filters)) { for (let [prop, value] of Object.entries(filters)) {
if (["$or", "$and"].includes(prop)) {
where[prop] = value.map((val) => {
const deepWhere = {}
buildWhere(val, deepWhere)
return deepWhere
})
continue
}
if (Array.isArray(value)) { if (Array.isArray(value)) {
value = deduplicate(value) value = deduplicate(value)
where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value } where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }