From 30863fee529ed035f161c749fda3cd64fa48efb1 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Tue, 12 Sep 2023 17:55:05 +0200 Subject: [PATCH] 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 --- .changeset/nice-otters-run.md | 11 + .../definitions/product-shipping-profile.ts | 3 + .../routes/store/products/list-products.ts | 213 +++++++++- ...timestemps-to-product-shipping-profiles.ts | 22 + packages/medusa/src/models/line-item.ts | 4 +- .../medusa/src/repositories/sales-channel.ts | 23 +- .../src/repositories/shipping-profile.ts | 23 +- packages/medusa/src/services/sales-channel.ts | 16 + .../medusa/src/services/shipping-profile.ts | 30 ++ packages/modules-sdk/src/remote-query.ts | 1 + .../orchestration/src/joiner/remote-joiner.ts | 2 +- .../src/services/__tests__/currency.spec.ts | 12 +- packages/product/jest.config.js | 2 +- .../migrations/.snapshot-medusa-products.json | 383 +++++++++++++++--- .../src/migrations/Migration20230908084537.ts | 69 ++++ .../product/src/models/product-category.ts | 13 +- .../product/src/models/product-collection.ts | 19 +- packages/product/src/models/product-image.ts | 19 +- .../src/models/product-option-value.ts | 19 +- packages/product/src/models/product-option.ts | 21 +- packages/product/src/models/product-tag.ts | 19 +- packages/product/src/models/product-type.ts | 21 + .../product/src/models/product-variant.ts | 11 +- packages/product/src/models/product.ts | 11 +- .../src/repositories/product-category.ts | 9 +- packages/product/src/repositories/product.ts | 57 ++- .../src/services/__tests__/product.spec.ts | 12 +- packages/types/src/dal/entity.ts | 2 + packages/types/src/dal/index.ts | 7 +- packages/types/src/joiner/index.ts | 35 +- packages/types/src/modules-sdk/index.ts | 65 ++- packages/types/src/product/common.ts | 3 + .../src/dal/mikro-orm/mikro-orm-repository.ts | 18 +- packages/utils/src/modules-sdk/build-query.ts | 15 +- 34 files changed, 1066 insertions(+), 124 deletions(-) create mode 100644 .changeset/nice-otters-run.md create mode 100644 packages/medusa/src/migrations/1692870898425-add-timestemps-to-product-shipping-profiles.ts create mode 100644 packages/product/src/migrations/Migration20230908084537.ts create mode 100644 packages/types/src/dal/entity.ts 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 }