diff --git a/.changeset/nice-otters-run.md b/.changeset/nice-otters-run.md new file mode 100644 index 0000000000..8629722808 --- /dev/null +++ b/.changeset/nice-otters-run.md @@ -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 diff --git a/packages/link-modules/src/definitions/product-shipping-profile.ts b/packages/link-modules/src/definitions/product-shipping-profile.ts index 01d6546869..706b6b4970 100644 --- a/packages/link-modules/src/definitions/product-shipping-profile.ts +++ b/packages/link-modules/src/definitions/product-shipping-profile.ts @@ -33,6 +33,9 @@ export const ProductShippingProfile: ModuleJoinerConfig = { extends: [ { serviceName: Modules.PRODUCT, + fieldAlias: { + profile: "shipping_profile.profile", + }, relationship: { serviceName: LINKS.ProductShippingProfile, isInternalService: true, diff --git a/packages/medusa/src/api/routes/store/products/list-products.ts b/packages/medusa/src/api/routes/store/products/list-products.ts index b05c3bb9bb..fe5dbbd245 100644 --- a/packages/medusa/src/api/routes/store/products/list-products.ts +++ b/packages/medusa/src/api/routes/store/products/list-products.ts @@ -2,6 +2,7 @@ import { CartService, ProductService, ProductVariantInventoryService, + SalesChannelService, } from "../../../../services" import { IsArray, @@ -22,6 +23,8 @@ import SalesChannelFeatureFlag from "../../../../loaders/feature-flags/sales-cha import { cleanResponseData } from "../../../../utils/clean-response-data" import { defaultStoreCategoryScope } from "../product-categories" import { optionalBooleanMapper } from "../../../../utils/validators/is-boolean" +import IsolateProductDomain from "../../../../loaders/feature-flags/isolate-product-domain" +import { defaultStoreProductsFields } from "./index" /** * @oas [get] /store/products @@ -216,6 +219,8 @@ export default async (req, res) => { const pricingService: PricingService = req.scope.resolve("pricingService") const cartService: CartService = req.scope.resolve("cartService") + const featureFlagRouter = req.scope.resolve("featureFlagRouter") + const validated = req.validatedQuery as StoreGetProductsParams let { @@ -224,7 +229,6 @@ export default async (req, res) => { currency_code: currencyCode, ...filterableFields } = req.filterableFields - const listConfig = req.listConfig // 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[] = [] - 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) { 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 { @IsNumber() @IsOptional() diff --git a/packages/medusa/src/migrations/1692870898425-add-timestemps-to-product-shipping-profiles.ts b/packages/medusa/src/migrations/1692870898425-add-timestemps-to-product-shipping-profiles.ts new file mode 100644 index 0000000000..2d0eb81294 --- /dev/null +++ b/packages/medusa/src/migrations/1692870898425-add-timestemps-to-product-shipping-profiles.ts @@ -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 { + 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 { + 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"; + `) + } +} diff --git a/packages/medusa/src/models/line-item.ts b/packages/medusa/src/models/line-item.ts index 7f0187ba44..405fbc9865 100644 --- a/packages/medusa/src/models/line-item.ts +++ b/packages/medusa/src/models/line-item.ts @@ -178,7 +178,7 @@ export class LineItem extends BaseEntity { } } - @FeatureFlagDecorators(IsolateProductDomain.key, [BeforeUpdate]) + @FeatureFlagDecorators(IsolateProductDomain.key, [BeforeUpdate()]) beforeUpdate(): void { if ( this.variant && @@ -189,7 +189,7 @@ export class LineItem extends BaseEntity { } } - @FeatureFlagDecorators(IsolateProductDomain.key, [AfterLoad, AfterUpdate]) + @FeatureFlagDecorators(IsolateProductDomain.key, [AfterLoad(), AfterUpdate()]) afterUpdateOrLoad(): void { if (this.variant) { return diff --git a/packages/medusa/src/repositories/sales-channel.ts b/packages/medusa/src/repositories/sales-channel.ts index 648afec64d..5c8350d0bd 100644 --- a/packages/medusa/src/repositories/sales-channel.ts +++ b/packages/medusa/src/repositories/sales-channel.ts @@ -1,4 +1,4 @@ -import { Brackets, DeleteResult, FindOptionsWhere, In, ILike } from "typeorm" +import { DeleteResult, FindOptionsWhere, ILike, In } from "typeorm" import { SalesChannel } from "../models" import { ExtendedFindConfig } from "../types/common" import { dataSource } from "../loaders/database" @@ -76,6 +76,27 @@ export const SalesChannelRepository = dataSource .orIgnore() .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 diff --git a/packages/medusa/src/repositories/shipping-profile.ts b/packages/medusa/src/repositories/shipping-profile.ts index 3b777a703d..2d02c121c8 100644 --- a/packages/medusa/src/repositories/shipping-profile.ts +++ b/packages/medusa/src/repositories/shipping-profile.ts @@ -1,6 +1,25 @@ import { ShippingProfile } from "../models" import { dataSource } from "../loaders/database" -export const ShippingProfileRepository = - dataSource.getRepository(ShippingProfile) +export const ShippingProfileRepository = dataSource + .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 diff --git a/packages/medusa/src/services/sales-channel.ts b/packages/medusa/src/services/sales-channel.ts index 83b2a01932..b9d902ef28 100644 --- a/packages/medusa/src/services/sales-channel.ts +++ b/packages/medusa/src/services/sales-channel.ts @@ -302,6 +302,22 @@ class SalesChannelService extends TransactionBaseService { 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 * @param salesChannelId - The id of the sales channel on which to remove the products diff --git a/packages/medusa/src/services/shipping-profile.ts b/packages/medusa/src/services/shipping-profile.ts index 41f5128f54..788b4bdf8c 100644 --- a/packages/medusa/src/services/shipping-profile.ts +++ b/packages/medusa/src/services/shipping-profile.ts @@ -156,6 +156,36 @@ class ShippingProfileService extends TransactionBaseService { 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 { const profileRepository = this.activeManager_.withRepository( this.shippingProfileRepository_ diff --git a/packages/modules-sdk/src/remote-query.ts b/packages/modules-sdk/src/remote-query.ts index 3a80ed26b6..f0eb0680b1 100644 --- a/packages/modules-sdk/src/remote-query.ts +++ b/packages/modules-sdk/src/remote-query.ts @@ -157,6 +157,7 @@ export class RemoteQuery { "skip", "take", "limit", + "order", "offset", "cursor", "sort", diff --git a/packages/orchestration/src/joiner/remote-joiner.ts b/packages/orchestration/src/joiner/remote-joiner.ts index 2838a29b87..fc6b08a6d9 100644 --- a/packages/orchestration/src/joiner/remote-joiner.ts +++ b/packages/orchestration/src/joiner/remote-joiner.ts @@ -573,7 +573,7 @@ export class RemoteJoiner { const alias = fieldAlias[prop] as any const path = isString(alias) ? alias : alias.path - const fullPath = currentPath.concat(path.split(".")) + const fullPath = [...new Set(currentPath.concat(path.split(".")))] forwardArgumentsOnPath = forwardArgumentsOnPath.concat( (alias?.forwardArgumentsOnPath || []).map( diff --git a/packages/pricing/src/services/__tests__/currency.spec.ts b/packages/pricing/src/services/__tests__/currency.spec.ts index af8ca20a94..2c130ec5ba 100644 --- a/packages/pricing/src/services/__tests__/currency.spec.ts +++ b/packages/pricing/src/services/__tests__/currency.spec.ts @@ -24,7 +24,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, populate: [], }, }, @@ -48,7 +48,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, populate: [], withDeleted: undefined, }, @@ -78,7 +78,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, populate: [], withDeleted: undefined, }, @@ -116,7 +116,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, populate: [], withDeleted: undefined, }, @@ -154,7 +154,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, withDeleted: undefined, populate: ["tags"], }, @@ -192,7 +192,7 @@ describe("Currency service", function () { options: { fields: undefined, limit: 15, - offset: undefined, + offset: 0, withDeleted: undefined, populate: ["tags"], }, diff --git a/packages/product/jest.config.js b/packages/product/jest.config.js index 860ba90a49..fc8bdef49c 100644 --- a/packages/product/jest.config.js +++ b/packages/product/jest.config.js @@ -8,7 +8,7 @@ module.exports = { "^.+\\.[jt]s?$": [ "ts-jest", { - tsConfig: "tsconfig.spec.json", + tsconfig: "tsconfig.spec.json", isolatedModules: true, }, ], diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json index 523b4eed1c..41c941c362 100644 --- a/packages/product/src/migrations/.snapshot-medusa-products.json +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -1,5 +1,7 @@ { - "namespaces": ["public"], + "namespaces": [ + "public" + ], "name": "public", "tables": [ { @@ -97,6 +99,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" }, "updated_at": { @@ -107,6 +110,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" } }, @@ -115,21 +119,27 @@ "indexes": [ { "keyName": "IDX_product_category_path", - "columnNames": ["mpath"], + "columnNames": [ + "mpath" + ], "composite": false, "primary": false, "unique": false }, { "keyName": "IDX_product_category_handle", - "columnNames": ["handle"], + "columnNames": [ + "handle" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "product_category_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -139,9 +149,13 @@ "foreignKeys": { "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", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_category", "deleteRule": "set null", "updateRule": "cascade" @@ -186,6 +200,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -201,7 +237,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_collection_deleted_at", "primary": false, @@ -209,14 +247,18 @@ }, { "keyName": "IDX_product_collection_handle_unique", - "columnNames": ["handle"], + "columnNames": [ + "handle" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "product_collection_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -254,6 +296,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -269,14 +333,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["url"], + "columnNames": [ + "url" + ], "composite": false, "keyName": "IDX_product_image_url", "primary": false, "unique": false }, { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_image_deleted_at", "primary": false, @@ -284,7 +352,9 @@ }, { "keyName": "image_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -322,6 +392,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -337,7 +429,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_tag_deleted_at", "primary": false, @@ -345,7 +439,9 @@ }, { "keyName": "product_tag_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -383,6 +479,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -398,7 +516,9 @@ "schema": "public", "indexes": [ { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_type_deleted_at", "primary": false, @@ -406,7 +526,9 @@ }, { "keyName": "product_type_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -479,7 +601,12 @@ "autoincrement": false, "primary": false, "nullable": false, - "enumItems": ["draft", "proposed", "published", "rejected"], + "enumItems": [ + "draft", + "proposed", + "published", + "rejected" + ], "mappedType": "enum" }, "thumbnail": { @@ -608,6 +735,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" }, "updated_at": { @@ -618,6 +746,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" }, "deleted_at": { @@ -644,14 +773,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["type_id"], + "columnNames": [ + "type_id" + ], "composite": false, "keyName": "IDX_product_type_id", "primary": false, "unique": false }, { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_deleted_at", "primary": false, @@ -659,14 +792,18 @@ }, { "keyName": "IDX_product_handle_unique", - "columnNames": ["handle"], + "columnNames": [ + "handle" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "product_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -676,18 +813,26 @@ "foreignKeys": { "product_collection_id_foreign": { "constraintName": "product_collection_id_foreign", - "columnNames": ["collection_id"], + "columnNames": [ + "collection_id" + ], "localTableName": "public.product", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_collection", "deleteRule": "set null", "updateRule": "cascade" }, "product_type_id_foreign": { "constraintName": "product_type_id_foreign", - "columnNames": ["type_id"], + "columnNames": [ + "type_id" + ], "localTableName": "public.product", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_type", "deleteRule": "set null", "updateRule": "cascade" @@ -732,6 +877,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -747,14 +914,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "composite": false, "keyName": "IDX_product_option_product_id", "primary": false, "unique": false }, { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_option_deleted_at", "primary": false, @@ -762,7 +933,9 @@ }, { "keyName": "product_option_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -772,9 +945,13 @@ "foreignKeys": { "product_option_product_id_foreign": { "constraintName": "product_option_product_id_foreign", - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "localTableName": "public.product_option", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product", "updateRule": "cascade" } @@ -806,7 +983,10 @@ "indexes": [ { "keyName": "product_tags_pkey", - "columnNames": ["product_id", "product_tag_id"], + "columnNames": [ + "product_id", + "product_tag_id" + ], "composite": true, "primary": true, "unique": true @@ -816,18 +996,26 @@ "foreignKeys": { "product_tags_product_id_foreign": { "constraintName": "product_tags_product_id_foreign", - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "localTableName": "public.product_tags", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" }, "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", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_tag", "deleteRule": "cascade", "updateRule": "cascade" @@ -873,9 +1061,13 @@ "foreignKeys": { "product_images_product_id_foreign": { "constraintName": "product_images_product_id_foreign", - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "localTableName": "public.product_images", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" @@ -886,7 +1078,9 @@ "image_id" ], "localTableName": "public.product_images", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.image", "deleteRule": "cascade", "updateRule": "cascade" @@ -919,7 +1113,10 @@ "indexes": [ { "keyName": "product_category_product_pkey", - "columnNames": ["product_id", "product_category_id"], + "columnNames": [ + "product_id", + "product_category_id" + ], "composite": true, "primary": true, "unique": true @@ -929,18 +1126,26 @@ "foreignKeys": { "product_category_product_product_id_foreign": { "constraintName": "product_category_product_product_id_foreign", - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "localTableName": "public.product_category_product", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" }, "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", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_category", "deleteRule": "cascade", "updateRule": "cascade" @@ -1141,6 +1346,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" }, "updated_at": { @@ -1151,6 +1357,7 @@ "primary": false, "nullable": false, "length": 6, + "default": "now()", "mappedType": "datetime" }, "deleted_at": { @@ -1168,14 +1375,18 @@ "schema": "public", "indexes": [ { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_variant_deleted_at", "primary": false, "unique": false }, { - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "composite": false, "keyName": "IDX_product_variant_product_id", "primary": false, @@ -1183,35 +1394,45 @@ }, { "keyName": "IDX_product_variant_sku_unique", - "columnNames": ["sku"], + "columnNames": [ + "sku" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_barcode_unique", - "columnNames": ["barcode"], + "columnNames": [ + "barcode" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_ean_unique", - "columnNames": ["ean"], + "columnNames": [ + "ean" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "IDX_product_variant_upc_unique", - "columnNames": ["upc"], + "columnNames": [ + "upc" + ], "composite": false, "primary": false, "unique": true }, { "keyName": "product_variant_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -1221,9 +1442,13 @@ "foreignKeys": { "product_variant_product_id_foreign": { "constraintName": "product_variant_product_id_foreign", - "columnNames": ["product_id"], + "columnNames": [ + "product_id" + ], "localTableName": "public.product_variant", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product", "deleteRule": "cascade", "updateRule": "cascade" @@ -1277,6 +1502,28 @@ "nullable": true, "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": { "name": "deleted_at", "type": "timestamptz", @@ -1292,21 +1539,27 @@ "schema": "public", "indexes": [ { - "columnNames": ["option_id"], + "columnNames": [ + "option_id" + ], "composite": false, "keyName": "IDX_product_option_value_option_id", "primary": false, "unique": false }, { - "columnNames": ["variant_id"], + "columnNames": [ + "variant_id" + ], "composite": false, "keyName": "IDX_product_option_value_variant_id", "primary": false, "unique": false }, { - "columnNames": ["deleted_at"], + "columnNames": [ + "deleted_at" + ], "composite": false, "keyName": "IDX_product_option_value_deleted_at", "primary": false, @@ -1314,7 +1567,9 @@ }, { "keyName": "product_option_value_pkey", - "columnNames": ["id"], + "columnNames": [ + "id" + ], "composite": false, "primary": true, "unique": true @@ -1324,17 +1579,25 @@ "foreignKeys": { "product_option_value_option_id_foreign": { "constraintName": "product_option_value_option_id_foreign", - "columnNames": ["option_id"], + "columnNames": [ + "option_id" + ], "localTableName": "public.product_option_value", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_option", "updateRule": "cascade" }, "product_option_value_variant_id_foreign": { "constraintName": "product_option_value_variant_id_foreign", - "columnNames": ["variant_id"], + "columnNames": [ + "variant_id" + ], "localTableName": "public.product_option_value", - "referencedColumnNames": ["id"], + "referencedColumnNames": [ + "id" + ], "referencedTableName": "public.product_variant", "deleteRule": "cascade", "updateRule": "cascade" diff --git a/packages/product/src/migrations/Migration20230908084537.ts b/packages/product/src/migrations/Migration20230908084537.ts new file mode 100644 index 0000000000..ed3c55220b --- /dev/null +++ b/packages/product/src/migrations/Migration20230908084537.ts @@ -0,0 +1,69 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20230908084537 extends Migration { + + async up(): Promise { + 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 { + 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";'); + } + +} diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts index 229b9b7c51..c81a0a7563 100644 --- a/packages/product/src/models/product-category.ts +++ b/packages/product/src/models/product-category.ts @@ -8,15 +8,21 @@ import { ManyToMany, ManyToOne, OneToMany, + OptionalProps, PrimaryKey, Property, Unique, } from "@mikro-orm/core" import Product from "./product" +import { DAL } from "@medusajs/types" + +type OptionalFields = DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product_category" }) class ProductCategory { + [OptionalProps]?: OptionalFields + @PrimaryKey({ columnType: "text" }) id!: string @@ -61,13 +67,18 @@ class ProductCategory { }) category_children = new Collection(this) - @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + @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 diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts index 68edb7fb7d..63745eda0c 100644 --- a/packages/product/src/models/product-collection.ts +++ b/packages/product/src/models/product-collection.ts @@ -13,13 +13,15 @@ import { import { DALUtils, generateEntityId, kebabCase } from "@medusajs/utils" import Product from "./product" +import { DAL } from "@medusajs/types" type OptionalRelations = "products" +type OptionalFields = DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product_collection" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductCollection { - [OptionalProps]?: OptionalRelations + [OptionalProps]?: OptionalRelations | OptionalFields @PrimaryKey({ columnType: "text" }) id!: string @@ -40,6 +42,21 @@ class ProductCollection { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-image.ts b/packages/product/src/models/product-image.ts index adeb233502..ce25a8ea64 100644 --- a/packages/product/src/models/product-image.ts +++ b/packages/product/src/models/product-image.ts @@ -12,13 +12,15 @@ import { import { DALUtils, generateEntityId } from "@medusajs/utils" import Product from "./product" +import { DAL } from "@medusajs/types" type OptionalRelations = "products" +type OptionalFields = DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "image" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductImage { - [OptionalProps]?: OptionalRelations + [OptionalProps]?: OptionalRelations | OptionalFields @PrimaryKey({ columnType: "text" }) id!: string @@ -30,6 +32,21 @@ class ProductImage { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts index 655102fee4..a1037e969b 100644 --- a/packages/product/src/models/product-option-value.ts +++ b/packages/product/src/models/product-option-value.ts @@ -10,14 +10,14 @@ import { } from "@mikro-orm/core" import { ProductOption, ProductVariant } from "./index" import { DALUtils, generateEntityId } from "@medusajs/utils" +import { DAL } from "@medusajs/types" type OptionalFields = - | "created_at" - | "updated_at" | "allow_backorder" | "manage_inventory" | "option_id" | "variant_id" + | DAL.SoftDeletableEntityDateColumns type OptionalRelations = "product" | "option" | "variant" @Entity({ tableName: "product_option_value" }) @@ -53,6 +53,21 @@ class ProductOptionValue { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts index 09600ee27d..dab5fbc180 100644 --- a/packages/product/src/models/product-option.ts +++ b/packages/product/src/models/product-option.ts @@ -14,8 +14,12 @@ import { } from "@mikro-orm/core" import { Product } from "./index" 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" @Entity({ tableName: "product_option" }) @@ -46,6 +50,21 @@ class ProductOption { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-tag.ts b/packages/product/src/models/product-tag.ts index db3bba8b52..9f9d7ce648 100644 --- a/packages/product/src/models/product-tag.ts +++ b/packages/product/src/models/product-tag.ts @@ -12,13 +12,15 @@ import { import { DALUtils, generateEntityId } from "@medusajs/utils" import Product from "./product" +import { DAL } from "@medusajs/types" type OptionalRelations = "products" +type OptionalFields = DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product_tag" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductTag { - [OptionalProps]?: OptionalRelations + [OptionalProps]?: OptionalRelations | OptionalFields @PrimaryKey({ columnType: "text" }) id!: string @@ -29,6 +31,21 @@ class ProductTag { @Property({ columnType: "jsonb", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-type.ts b/packages/product/src/models/product-type.ts index fd6e460275..30a2cac909 100644 --- a/packages/product/src/models/product-type.ts +++ b/packages/product/src/models/product-type.ts @@ -3,15 +3,21 @@ import { Entity, Filter, Index, + OptionalProps, PrimaryKey, Property, } from "@mikro-orm/core" import { DALUtils, generateEntityId } from "@medusajs/utils" +import { DAL } from "@medusajs/types" + +type OptionalFields = DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product_type" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) class ProductType { + [OptionalProps]?: OptionalFields + @PrimaryKey({ columnType: "text" }) id!: string @@ -21,6 +27,21 @@ class ProductType { @Property({ columnType: "json", nullable: true }) metadata?: Record | 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" }) @Property({ columnType: "timestamptz", nullable: true }) deleted_at?: Date diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts index 6ba319f753..a66bc5bc0e 100644 --- a/packages/product/src/models/product-variant.ts +++ b/packages/product/src/models/product-variant.ts @@ -15,14 +15,14 @@ import { } from "@mikro-orm/core" import { Product } from "@models" import ProductOptionValue from "./product-option-value" +import { DAL } from "@medusajs/types" type OptionalFields = - | "created_at" - | "updated_at" | "allow_backorder" | "manage_inventory" | "product" | "product_id" + | DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product_variant" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @@ -108,13 +108,18 @@ class ProductVariant { @Property({ columnType: "text", nullable: true }) product_id!: string - @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + @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 diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts index 58e60bfba7..2415a148cb 100644 --- a/packages/product/src/models/product.ts +++ b/packages/product/src/models/product.ts @@ -27,6 +27,7 @@ import ProductTag from "./product-tag" import ProductType from "./product-type" import ProductVariant from "./product-variant" import ProductImage from "./product-image" +import { DAL } from "@medusajs/types" type OptionalRelations = "collection" | "type" type OptionalFields = @@ -34,8 +35,7 @@ type OptionalFields = | "type_id" | "is_giftcard" | "discountable" - | "created_at" - | "updated_at" + | DAL.SoftDeletableEntityDateColumns @Entity({ tableName: "product" }) @Filter(DALUtils.mikroOrmSoftDeletableFilterOptions) @@ -154,13 +154,18 @@ class Product { @Property({ columnType: "text", nullable: true }) external_id?: string | null - @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + @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 diff --git a/packages/product/src/repositories/product-category.ts b/packages/product/src/repositories/product-category.ts index ae602c7d3c..9ec9326b90 100644 --- a/packages/product/src/repositories/product-category.ts +++ b/packages/product/src/repositories/product-category.ts @@ -91,8 +91,13 @@ export class ProductCategoryRepository extends DALUtils.MikroOrmBaseTreeReposito }, } - delete whereOptions.parent_category_id - delete whereOptions.id + if ("parent_category_id" in whereOptions) { + delete whereOptions.parent_category_id + } + + if ("id" in whereOptions) { + delete whereOptions.id + } const descendantsForCategory = await manager.find( ProductCategory, diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts index 4dbc947c4a..3326e01064 100644 --- a/packages/product/src/repositories/product.ts +++ b/packages/product/src/repositories/product.ts @@ -35,7 +35,7 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

= { where: {} }, + findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise { const manager = this.getActiveManager(context) @@ -49,6 +49,11 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

( + findOptions_, + this.getFreeTextSearchConstraints + ) + return await manager.find( Product, findOptions_.where as MikroFilterQuery, @@ -57,7 +62,7 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

= { where: {} }, + findOptions: DAL.FindOptions = { where: {} }, context: Context = {} ): Promise<[Product[], number]> { const manager = this.getActiveManager(context) @@ -71,6 +76,11 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

( + findOptions_, + this.getFreeTextSearchConstraints + ) + return await manager.findAndCount( Product, findOptions_.where as MikroFilterQuery, @@ -88,7 +98,10 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

{ const manager = this.getActiveManager(context) - if (findOptions.where.categories?.id?.["$nin"]) { + if ( + "categories" in findOptions.where && + findOptions.where.categories?.id?.["$nin"] + ) { const productsInCategories = await manager.find( Product, { @@ -307,4 +320,42 @@ export class ProductRepository extends DALUtils.MikroOrmAbstractBaseRepository

{ - $and?: T - $or?: T + $and?: (T | BaseFilterable)[] + $or?: (T | BaseFilterable)[] } export interface OptionsQuery { @@ -17,8 +17,9 @@ export interface OptionsQuery { } export type FindOptions = { - where: FilterQuery + where: FilterQuery & BaseFilterable> options?: OptionsQuery } export * from "./repository-service" +export * from "./entity" diff --git a/packages/types/src/joiner/index.ts b/packages/types/src/joiner/index.ts index 77d91c59b2..479ec690ab 100644 --- a/packages/types/src/joiner/index.ts +++ b/packages/types/src/joiner/index.ts @@ -3,19 +3,37 @@ export type JoinerRelationship = { foreignKey: string primaryKey: 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 - args?: Record // Extra arguments to pass to the remoteFetchData callback + /** + * In an inverted relationship the foreign key is on the other service and the primary key is on the current service + */ + inverse?: boolean + /** + * Force the relationship to return a list + */ + isList?: boolean + /** + * Extra arguments to pass to the remoteFetchData callback + */ + args?: Record } export interface JoinerServiceConfigAlias { name: string - args?: Record // Extra arguments to pass to the remoteFetchData callback + /** + * Extra arguments to pass to the remoteFetchData callback + */ + args?: Record } export interface JoinerServiceConfig { 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< string, | string @@ -23,14 +41,17 @@ export interface JoinerServiceConfig { path: string forwardArgumentsOnPath: string[] } - > // alias for deeper nested relationships (e.g. { 'price': 'prices.calculated_price_set.amount' }) + > primaryKeys: string[] relationships?: JoinerRelationship[] extends?: { serviceName: string relationship: JoinerRelationship }[] - args?: Record // Extra arguments to pass to the remoteFetchData callback + /** + * Extra arguments to pass to the remoteFetchData callback + */ + args?: Record } export interface JoinerArgument { diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 7734f3ae0e..80db4db995 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -36,8 +36,14 @@ export type InternalModuleDeclaration = { */ resolve?: string options?: Record - 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 = { @@ -48,8 +54,14 @@ export type ExternalModuleDeclaration = { keepAlive: boolean } options?: Record - 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 = { @@ -74,7 +86,10 @@ export type ModuleDefinition = { * @deprecated property will be removed in future versions */ isRequired?: boolean - isQueryable?: boolean // If the module is queryable via Remote Joiner + /** + * If the module is queryable via Remote Joiner + */ + isQueryable?: boolean dependencies?: string[] defaultModuleDeclaration: | InternalModuleDeclaration @@ -136,12 +151,27 @@ export type ModuleJoinerConfig = Omit< }[] serviceName?: string primaryKeys?: string[] - isLink?: boolean // If the module is a link module - linkableKeys?: string[] // Keys that can be used to link to other modules - isReadOnlyLink?: boolean // If true it expands a RemoteQuery property but doesn't create a pivot table + /** + * If the module is a link module + */ + 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?: { - 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< string, { @@ -169,15 +199,24 @@ export type ModuleJoinerConfig = Omit< | "text" defaultValue?: string nullable?: boolean - options?: Record // Mikro-orm options for the column + /** + * Mikro-orm options for the column + */ + options?: Record } > } } 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 = { diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts index 95ac687a44..98a572d056 100644 --- a/packages/types/src/product/common.ts +++ b/packages/types/src/product/common.ts @@ -158,11 +158,14 @@ export interface ProductOptionValueDTO { */ export interface FilterableProductProps extends BaseFilterable { + q?: string handle?: string | string[] id?: string | string[] tags?: { value?: string[] } categories?: { id?: string | string[] | OperatorMap + is_internal?: boolean + is_active?: boolean } category_id?: string | string[] | OperatorMap collection_id?: string | string[] | OperatorMap diff --git a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts index 02fb9306c1..979b717d84 100644 --- a/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts +++ b/packages/utils/src/dal/mikro-orm/mikro-orm-repository.ts @@ -6,7 +6,7 @@ import { } from "@medusajs/types" import { isString } from "../../common" import { MedusaContext } from "../../decorators" -import { InjectTransactionManager, buildQuery } from "../../modules-sdk" +import { buildQuery, InjectTransactionManager } from "../../modules-sdk" import { getSoftDeletedCascadedEntitiesIdsMappedBy, transactionWrapper, @@ -131,6 +131,22 @@ export abstract class MikroOrmAbstractBaseRepository return [entities, softDeletedEntitiesMap] } + + applyFreeTextSearchFilters( + findOptions: DAL.FindOptions, + 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 + } } export abstract class MikroOrmAbstractTreeRepositoryBase diff --git a/packages/utils/src/modules-sdk/build-query.ts b/packages/utils/src/modules-sdk/build-query.ts index fcb220a05f..06893b964e 100644 --- a/packages/utils/src/modules-sdk/build-query.ts +++ b/packages/utils/src/modules-sdk/build-query.ts @@ -14,7 +14,11 @@ export function buildQuery( populate: deduplicate(config.relations ?? []), fields: config.select as string[], limit: config.take ?? 15, - offset: config.skip, + offset: config.skip ?? 0, + } + + if (config.order) { + findOptions.orderBy = config.order as DAL.OptionsQuery["orderBy"] } if (config.withDeleted) { @@ -29,6 +33,15 @@ export function buildQuery( function buildWhere(filters: Record = {}, where = {}) { 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)) { value = deduplicate(value) where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value }