From 14c0f62f84704a4c87beff3daaff60a52f5c88b8 Mon Sep 17 00:00:00 2001 From: Adrien de Peretti Date: Fri, 9 Jun 2023 20:47:24 +0200 Subject: [PATCH] feat: Product Module (#4161) * chore: boilerplate setup * wip: add Product, ProductTag, ProductType, ProductCollection models * wip: `IProductService` definition * wip: test function in index, build passing * fix: where condition * chore: get boilerplate working with modules sdk, create a boilerplate test, create product variant model, register services properly * chore: added variant to model * chore: changed definition details + add migrator * cleanup and update product entity * Update product unique index to include soft deleted * Migrations tests * generated migration * update dev orm config * add path aliases * WIP * chore: added boilerplate integration test + database helper + product variant migraiton + model * chore: remove old test utils * update ts and jest config to include path aliases * tweak config * WIP migrations variant * Migrations round * integration tests migrations polishing * chore: fixed issues with test db * fix path aliases when published * use ts-alias * fix connection loader * fixes * wip: product list * (WIP): Data access layer * (WIP): DAL cleanup services * wip: `ProductTag` DAL * wip: `ProductTag` expose list in product service * (WIP): Continue DAL and test list product filtering/populate * WIP: unit tests * chore: added tests for service - productvariant * chore: WIP finding issues with orm manager fork * WIP fix fields selection * chore: make text fixes work * (WIP) product integration * (WIP) product integration * chore: create a product in variant test * list product with relations * wip: `ProductTag` service + integrations * chore: added with and without serialization example * chore: remove only in spec * wip: `ProductCollection` service + integrations * uncomment product.variants * Update type IProductService * (WIP) type work * (WIP) Product variants relation * WIP: replacable data layer * (WIP): Use bundle types * WIP: update type * WIP upadte tests * WIP * wip: options/option values entites * (WIP): cleanup * wip: add option value to variant, fix collection * Integration tests for custom data access layer * update tests * chore: merge with latest branch * chore: scope tests to relations and add category to models/index * chore: ignore dist folders for jest * chore: modularize spec data file * improve DX * module fixture naming * chore: added category tests + fix model * chore: use kebab case * chore: allow scoping products by category id * chore: replace `kebabCase` import * feat: add `deleted_at` to options * improve typings * chore: wip * fix query util * revert webpack * fix: update option models, create option DTOs, tests wip, fix `deduplicateIfNecessary` returning `undefined` * fix: merge conflict * WIP connection * rm unsues deps * fix migrations * fix query util * chore: adds mpath on creation * WIP update types * improve typings * WIP typeings improvement * WIP * deps * chore: package medusa/product ot medusa-commerce/product * chore: added product categories service + descendants filter * add missing index * Add support for strict categories not in * Add support for strict categories not in * lint * rename module * rename module * Create small-ducks-doubt.md * yarn lock * update initialise * chore: fix/finalise DTOs * fix: wrong types in `IProductService` * fix type * Load database config from env if present (#4175) * Load database config from env if present * Load database config from env if present * options optionnal * update util * add defaults * improve filterable interfaces * fix import * fix types * remove medusa-telemetry from modules-sdk * WIP fixing webpack issues when bootstraping module * cleanup * improve loading driver options * cleanup * yarn lock * fix import * improve sdk types and naming * align orther modules initialise method * fix module tests with singleton module * fix module tests with singleton module * add up/down migration scripts * update types * scripts * cleanup migrations and scripts * hash module singleton * cleanup migration * cleanup * fix stringifyCircular usage * improvements * fix deps * fix deps * improve load config utils * improve load config utils * fix deps * add declaration to the build * update yarn * Do not resolve a module path if the exports are explicitly given * fix module registration resolution path when exports are provided. Explicitly check for false and assign an empty string in this scenario for segregation purpose * add comment * fix migration options to prevent set replica errors * chore: update types to a proper depedency * chore: update type package * add seed scripts * Add descriptive error during database config loading * use MedusaError * chore: added lodash to package * add more test to the database config loader util * create bin scripts * add bin * update argv retrieval * update package.json * chore: add product category to injected deps * chore: replace with product category service * move dotenv usage to the functions * do not load db if there is custom manager * chore: fix some tests on products repo * chore: fixed product spec * chore: skip products module on modules register * stringifyCircular update * chore: fix incorrect module resolution * fix: circular stringify and non required module loading * yarn lock * target es5 * chore: mikro-orm back to 5.7.4 * revert module registry * skip external modules * es2020 * update indexes, migration and integration tests * rm only * unit test script should only run unit tests * Exclude product integration from the unit tests and make use of the global integration script to run all packages integration tests * fix integration tests * improve setup * cleanup * log error on setup fail * Create enum like for package names * chore: remove EOL * chore: review part 2 * renamve gateway to productModuleService * chore: added filters and collections to productmoduleservice * chore: add collection to the singleton instance * chore: remove skipped test + add todo * fix indexes on fields and relations + update migration * update yarn lock * update idx * add foreign key * rename interface and add listCategories * rename product module definition --------- Co-authored-by: fPolic Co-authored-by: Riqwan Thamir Co-authored-by: Carlos R. L. Rodrigues --- .changeset/small-ducks-doubt.md | 7 + package.json | 2 +- packages/inventory/src/index.ts | 23 +- packages/inventory/src/initialize/index.ts | 2 + packages/inventory/src/module-definition.ts | 20 + packages/medusa/src/repositories/product.ts | 6 +- packages/modules-sdk/package.json | 5 +- packages/modules-sdk/src/definitions.ts | 19 +- .../src/loaders/__tests__/medusa-module.ts | 55 +- .../src/loaders/__tests__/module-loader.ts | 3 +- .../src/loaders/__tests__/register-modules.ts | 41 +- .../modules-sdk/src/loaders/module-loader.ts | 14 +- .../src/loaders/register-modules.ts | 30 +- .../src/loaders/utils/load-internal.ts | 21 +- packages/modules-sdk/src/medusa-module.ts | 33 +- packages/modules-sdk/tsconfig.json | 7 +- packages/product/.gitignore | 6 + .../integration-tests/__fixtures__/module.ts | 24 + .../product-category/data/index.ts | 28 + .../__fixtures__/product-category/index.ts | 31 + .../__fixtures__/product/data/categories.ts | 17 + .../__fixtures__/product/data/index.ts | 2 + .../__fixtures__/product/data/products.ts | 43 + .../__fixtures__/product/index.ts | 72 + .../integration-tests/__tests__/module.ts | 109 ++ .../services/product-category/index.ts | 233 +++ .../services/product-collection/index.ts | 106 ++ .../__tests__/services/product-tag/index.ts | 121 ++ .../services/product-variant/index.ts | 185 +++ .../__tests__/services/product/index.ts | 257 ++++ .../product/integration-tests/setup-env.js | 6 + packages/product/integration-tests/setup.js | 22 + .../integration-tests/utils/database.ts | 95 ++ .../product/integration-tests/utils/index.ts | 1 + packages/product/jest.config.js | 21 + packages/product/mikro-orm.config.dev.ts | 8 + packages/product/package.json | 62 + packages/product/src/index.ts | 10 + packages/product/src/initialize/index.ts | 34 + packages/product/src/loaders/connection.ts | 66 + packages/product/src/loaders/container.ts | 92 ++ packages/product/src/loaders/index.ts | 2 + .../migrations/.snapshot-medusa-products.json | 1257 +++++++++++++++++ .../src/migrations/Migration20230609132805.ts | 57 + packages/product/src/models/index.ts | 6 + .../product/src/models/product-category.ts | 101 ++ .../product/src/models/product-collection.ts | 42 + .../src/models/product-option-value.ts | 41 + packages/product/src/models/product-option.ts | 48 + packages/product/src/models/product-tag.ts | 36 + packages/product/src/models/product-type.ts | 25 + .../product/src/models/product-variant.ts | 141 ++ packages/product/src/models/product.ts | 150 ++ packages/product/src/module-definition.ts | 18 + packages/product/src/repositories/index.ts | 5 + .../src/repositories/product-category.ts | 140 ++ .../src/repositories/product-collection.ts | 74 + .../product/src/repositories/product-tag.ts | 74 + .../src/repositories/product-variant.ts | 74 + packages/product/src/repositories/product.ts | 112 ++ .../src/scripts/bin/run-migration-down.ts | 7 + .../src/scripts/bin/run-migration-up.ts | 7 + packages/product/src/scripts/bin/run-seed.ts | 17 + packages/product/src/scripts/index.ts | 2 + .../product/src/scripts/migration-down.ts | 44 + packages/product/src/scripts/migration-up.ts | 44 + packages/product/src/scripts/seed.ts | 108 ++ .../src/services/__tests__/product.spec.ts | 113 ++ packages/product/src/services/index.ts | 6 + .../product/src/services/product-category.ts | 34 + .../src/services/product-collection.ts | 30 + .../src/services/product-module-service.ts | 143 ++ packages/product/src/services/product-tag.ts | 29 + .../product/src/services/product-variant.ts | 24 + packages/product/src/services/product.ts | 62 + packages/product/src/types/index.ts | 18 + .../__tests__/load-database-config.spec.ts | 127 ++ .../product/src/utils/create-connection.ts | 37 + packages/product/src/utils/index.ts | 3 + .../product/src/utils/load-database-config.ts | 82 ++ packages/product/src/utils/query/index.ts | 44 + packages/product/tsconfig.json | 36 + packages/product/tsconfig.spec.json | 5 + packages/stock-location/src/index.ts | 20 +- .../stock-location/src/initialize/index.ts | 2 + .../stock-location/src/module-definition.ts | 19 + packages/types/src/bundles.ts | 2 + packages/types/src/common/common.ts | 2 + packages/types/src/dal/index.ts | 24 + packages/types/src/dal/repository-service.ts | 12 + packages/types/src/dal/utils.ts | 127 ++ packages/types/src/index.ts | 3 + packages/types/src/modules-sdk/index.ts | 4 +- packages/types/src/product-category/index.ts | 1 + .../types/src/product-category/repository.ts | 5 + packages/types/src/product/common.ts | 166 +++ packages/types/src/product/index.ts | 2 + packages/types/src/product/service.ts | 58 + packages/utils/src/common/index.ts | 7 +- packages/utils/src/common/lower-case-first.ts | 3 + packages/utils/src/common/simple-hash.ts | 8 + .../utils/src/common/stringify-circular.ts | 62 + packages/utils/src/common/to-kebab-case.ts | 5 + yarn.lock | 752 +++++++++- 104 files changed, 6472 insertions(+), 176 deletions(-) create mode 100644 .changeset/small-ducks-doubt.md create mode 100644 packages/inventory/src/module-definition.ts create mode 100644 packages/product/.gitignore create mode 100644 packages/product/integration-tests/__fixtures__/module.ts create mode 100644 packages/product/integration-tests/__fixtures__/product-category/data/index.ts create mode 100644 packages/product/integration-tests/__fixtures__/product-category/index.ts create mode 100644 packages/product/integration-tests/__fixtures__/product/data/categories.ts create mode 100644 packages/product/integration-tests/__fixtures__/product/data/index.ts create mode 100644 packages/product/integration-tests/__fixtures__/product/data/products.ts create mode 100644 packages/product/integration-tests/__fixtures__/product/index.ts create mode 100644 packages/product/integration-tests/__tests__/module.ts create mode 100644 packages/product/integration-tests/__tests__/services/product-category/index.ts create mode 100644 packages/product/integration-tests/__tests__/services/product-collection/index.ts create mode 100644 packages/product/integration-tests/__tests__/services/product-tag/index.ts create mode 100644 packages/product/integration-tests/__tests__/services/product-variant/index.ts create mode 100644 packages/product/integration-tests/__tests__/services/product/index.ts create mode 100644 packages/product/integration-tests/setup-env.js create mode 100644 packages/product/integration-tests/setup.js create mode 100644 packages/product/integration-tests/utils/database.ts create mode 100644 packages/product/integration-tests/utils/index.ts create mode 100644 packages/product/jest.config.js create mode 100644 packages/product/mikro-orm.config.dev.ts create mode 100644 packages/product/package.json create mode 100644 packages/product/src/index.ts create mode 100644 packages/product/src/initialize/index.ts create mode 100644 packages/product/src/loaders/connection.ts create mode 100644 packages/product/src/loaders/container.ts create mode 100644 packages/product/src/loaders/index.ts create mode 100644 packages/product/src/migrations/.snapshot-medusa-products.json create mode 100644 packages/product/src/migrations/Migration20230609132805.ts create mode 100644 packages/product/src/models/index.ts create mode 100644 packages/product/src/models/product-category.ts create mode 100644 packages/product/src/models/product-collection.ts create mode 100644 packages/product/src/models/product-option-value.ts create mode 100644 packages/product/src/models/product-option.ts create mode 100644 packages/product/src/models/product-tag.ts create mode 100644 packages/product/src/models/product-type.ts create mode 100644 packages/product/src/models/product-variant.ts create mode 100644 packages/product/src/models/product.ts create mode 100644 packages/product/src/module-definition.ts create mode 100644 packages/product/src/repositories/index.ts create mode 100644 packages/product/src/repositories/product-category.ts create mode 100644 packages/product/src/repositories/product-collection.ts create mode 100644 packages/product/src/repositories/product-tag.ts create mode 100644 packages/product/src/repositories/product-variant.ts create mode 100644 packages/product/src/repositories/product.ts create mode 100644 packages/product/src/scripts/bin/run-migration-down.ts create mode 100644 packages/product/src/scripts/bin/run-migration-up.ts create mode 100644 packages/product/src/scripts/bin/run-seed.ts create mode 100644 packages/product/src/scripts/index.ts create mode 100644 packages/product/src/scripts/migration-down.ts create mode 100644 packages/product/src/scripts/migration-up.ts create mode 100644 packages/product/src/scripts/seed.ts create mode 100644 packages/product/src/services/__tests__/product.spec.ts create mode 100644 packages/product/src/services/index.ts create mode 100644 packages/product/src/services/product-category.ts create mode 100644 packages/product/src/services/product-collection.ts create mode 100644 packages/product/src/services/product-module-service.ts create mode 100644 packages/product/src/services/product-tag.ts create mode 100644 packages/product/src/services/product-variant.ts create mode 100644 packages/product/src/services/product.ts create mode 100644 packages/product/src/types/index.ts create mode 100644 packages/product/src/utils/__tests__/load-database-config.spec.ts create mode 100644 packages/product/src/utils/create-connection.ts create mode 100644 packages/product/src/utils/index.ts create mode 100644 packages/product/src/utils/load-database-config.ts create mode 100644 packages/product/src/utils/query/index.ts create mode 100644 packages/product/tsconfig.json create mode 100644 packages/product/tsconfig.spec.json create mode 100644 packages/stock-location/src/module-definition.ts create mode 100644 packages/types/src/dal/index.ts create mode 100644 packages/types/src/dal/repository-service.ts create mode 100644 packages/types/src/dal/utils.ts create mode 100644 packages/types/src/product-category/index.ts create mode 100644 packages/types/src/product-category/repository.ts create mode 100644 packages/types/src/product/common.ts create mode 100644 packages/types/src/product/index.ts create mode 100644 packages/types/src/product/service.ts create mode 100644 packages/utils/src/common/lower-case-first.ts create mode 100644 packages/utils/src/common/simple-hash.ts create mode 100644 packages/utils/src/common/stringify-circular.ts create mode 100644 packages/utils/src/common/to-kebab-case.ts diff --git a/.changeset/small-ducks-doubt.md b/.changeset/small-ducks-doubt.md new file mode 100644 index 0000000000..c521580b4c --- /dev/null +++ b/.changeset/small-ducks-doubt.md @@ -0,0 +1,7 @@ +--- +"@medusajs/types": patch +"@medusajs/modules-sdk": patch +"@medusajs/utils": patch +--- + +feat(product): Experimental product module diff --git a/package.json b/package.json index eca283d8ed..ba805c2fb2 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "prettier": "prettier", "jest": "jest", "test": "turbo run test --no-daemon", - "test:integration": "turbo run test:integration --no-daemon", + "test:integration": "turbo run test:integration --no-daemon --filter='./packages/*'", "test:integration:api": "turbo run test:integration --no-daemon --filter=integration-tests-api", "test:integration:plugins": "turbo run test:integration --no-daemon --filter=integration-tests-plugins", "test:integration:repositories": "turbo run test:integration --no-daemon --filter=integration-tests-repositories", diff --git a/packages/inventory/src/index.ts b/packages/inventory/src/index.ts index 9d646b97c4..d3159f3c9a 100644 --- a/packages/inventory/src/index.ts +++ b/packages/inventory/src/index.ts @@ -1,27 +1,8 @@ -import loadConnection from "./loaders/connection" -import loadContainer from "./loaders/container" - -import migrations from "./migrations" import { revertMigration, runMigrations } from "./migrations/run-migration" -import * as InventoryModels from "./models" -import InventoryService from "./services/inventory" - -import { ModuleExports } from "@medusajs/modules-sdk" - -const service = InventoryService -const loaders = [loadContainer, loadConnection] -const models = Object.values(InventoryModels) - -const moduleDefinition: ModuleExports = { - service, - migrations, - loaders, - models, - runMigrations, - revertMigration, -} +import { moduleDefinition } from "./module-definition" export default moduleDefinition + export * from "./initialize" export { revertMigration, runMigrations } from "./migrations/run-migration" export * from "./types" diff --git a/packages/inventory/src/initialize/index.ts b/packages/inventory/src/initialize/index.ts index 5c2e7a91d8..99461e9e6f 100644 --- a/packages/inventory/src/initialize/index.ts +++ b/packages/inventory/src/initialize/index.ts @@ -5,6 +5,7 @@ import { Modules, } from "@medusajs/modules-sdk" import { IEventBusService, IInventoryService } from "@medusajs/types" +import { moduleDefinition } from "../module-definition" import { InventoryServiceInitializeOptions } from "../types" export const initialize = async ( @@ -18,6 +19,7 @@ export const initialize = async ( serviceKey, "@medusajs/inventory", options as InternalModuleDeclaration | ExternalModuleDeclaration, + moduleDefinition, injectedDependencies ) diff --git a/packages/inventory/src/module-definition.ts b/packages/inventory/src/module-definition.ts new file mode 100644 index 0000000000..40912d71d5 --- /dev/null +++ b/packages/inventory/src/module-definition.ts @@ -0,0 +1,20 @@ +import InventoryService from "./services/inventory" +import loadContainer from "./loaders/container" +import loadConnection from "./loaders/connection" +import * as InventoryModels from "./models" +import { ModuleExports } from "@medusajs/types" +import migrations from "./migrations" +import { revertMigration, runMigrations } from "./migrations/run-migration" + +const service = InventoryService +const loaders = [loadContainer, loadConnection] +const models = Object.values(InventoryModels) + +export const moduleDefinition: ModuleExports = { + service, + migrations, + loaders, + models, + runMigrations, + revertMigration, +} diff --git a/packages/medusa/src/repositories/product.ts b/packages/medusa/src/repositories/product.ts index 47c791a37a..d11a49f992 100644 --- a/packages/medusa/src/repositories/product.ts +++ b/packages/medusa/src/repositories/product.ts @@ -13,15 +13,15 @@ import { SalesChannel, } from "../models" import { dataSource } from "../loaders/database" -import { cloneDeep, groupBy, map, merge } from "lodash" -import { ExtendedFindConfig } from "../types/common" +import { objectToStringPath } from "@medusajs/utils" +import { ExtendedFindConfig } from "@medusajs/types" import { applyOrdering, getGroupedRelations, queryEntityWithIds, queryEntityWithoutRelations, } from "../utils/repository" -import { objectToStringPath } from "@medusajs/utils" +import { cloneDeep, groupBy, map, merge } from "lodash" export type DefaultWithoutRelations = Omit< ExtendedFindConfig, diff --git a/packages/modules-sdk/package.json b/packages/modules-sdk/package.json index 18678f2b27..c2666d58c0 100644 --- a/packages/modules-sdk/package.json +++ b/packages/modules-sdk/package.json @@ -26,13 +26,12 @@ "@medusajs/types": "1.8.7", "@medusajs/utils": "1.9.0", "awilix": "^8.0.0", - "glob": "7.1.6", - "medusa-telemetry": "^0.0.16", "resolve-cwd": "^3.0.0" }, "scripts": { "prepare": "cross-env NODE_ENV=production yarn run build", "build": "tsc --build", - "test": "jest" + "test": "jest", + "watch": "tsc --build --watch" } } diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 8c8303bd05..8a74888e2c 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -1,7 +1,7 @@ import { - ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + ModuleDefinition, } from "@medusajs/types" export enum Modules { @@ -9,6 +9,7 @@ export enum Modules { STOCK_LOCATION = "stockLocationService", INVENTORY = "inventoryService", CACHE = "cacheService", + PRODUCT = "productModuleService", } export const ModulesDefinition: { [key: string]: ModuleDefinition } = { @@ -63,9 +64,25 @@ export const ModulesDefinition: { [key: string]: ModuleDefinition } = { resources: MODULE_RESOURCE_TYPE.SHARED, }, }, + [Modules.PRODUCT]: { + key: Modules.PRODUCT, + registrationName: Modules.PRODUCT, + defaultPackage: false, + label: "ProductModuleService", + isRequired: false, + canOverride: true, + dependencies: [], + defaultModuleDeclaration: { + scope: MODULE_SCOPE.EXTERNAL, + }, + }, } export const MODULE_DEFINITIONS: ModuleDefinition[] = Object.values(ModulesDefinition) +export const MODULE_PACKAGE_NAMES = { + [Modules.PRODUCT]: "@medusajs/product", +} + export default MODULE_DEFINITIONS diff --git a/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts b/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts index c7eea10511..317e048184 100644 --- a/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts +++ b/packages/modules-sdk/src/loaders/__tests__/medusa-module.ts @@ -24,38 +24,37 @@ describe("Medusa Module", () => { }) it("MedusaModule bootstrap - Singleton instances", async () => { - await MedusaModule.bootstrap( - "moduleKey", - "@path", - { - scope: MODULE_SCOPE.INTERNAL, - resources: MODULE_RESOURCE_TYPE.ISOLATED, - resolve: "@path", - options: { - abc: 123, - }, - } as InternalModuleDeclaration, - {} - ) + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) expect(mockRegisterMedusaModule).toBeCalledTimes(1) expect(mockModuleLoader).toBeCalledTimes(1) - await MedusaModule.bootstrap( - "moduleKey", - "@path", - { - scope: MODULE_SCOPE.INTERNAL, - resources: MODULE_RESOURCE_TYPE.ISOLATED, - resolve: "@path", - options: { - abc: 123, - }, - } as InternalModuleDeclaration, - {} - ) + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + abc: 123, + }, + } as InternalModuleDeclaration) - expect(mockRegisterMedusaModule).toBeCalledTimes(1) - expect(mockModuleLoader).toBeCalledTimes(1) + await MedusaModule.bootstrap("moduleKey", "@path", { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.ISOLATED, + resolve: "@path", + options: { + different_options: "abc", + }, + } as InternalModuleDeclaration) + + expect(mockRegisterMedusaModule).toBeCalledTimes(2) + expect(mockModuleLoader).toBeCalledTimes(2) }) }) diff --git a/packages/modules-sdk/src/loaders/__tests__/module-loader.ts b/packages/modules-sdk/src/loaders/__tests__/module-loader.ts index 5a419785e0..3c210a209d 100644 --- a/packages/modules-sdk/src/loaders/__tests__/module-loader.ts +++ b/packages/modules-sdk/src/loaders/__tests__/module-loader.ts @@ -5,7 +5,6 @@ import { } from "@medusajs/types" import { createMedusaContainer } from "medusa-core-utils" import { EOL } from "os" -import { trackInstallation } from "../__mocks__/medusa-telemetry" import { moduleLoader } from "../module-loader" const logger = { @@ -81,6 +80,7 @@ describe("modules loader", () => { {} ) + /* expect(trackInstallation).toHaveBeenCalledWith( { module: moduleResolutions.testService.definition.key, @@ -88,6 +88,7 @@ describe("modules loader", () => { }, "module" ) + */ expect(testService).toBeTruthy() expect(typeof testService).toEqual("object") }) diff --git a/packages/modules-sdk/src/loaders/__tests__/register-modules.ts b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts index 4f25ff0ee7..d1e20fb683 100644 --- a/packages/modules-sdk/src/loaders/__tests__/register-modules.ts +++ b/packages/modules-sdk/src/loaders/__tests__/register-modules.ts @@ -1,8 +1,8 @@ import { InternalModuleDeclaration, - ModuleDefinition, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + ModuleDefinition, } from "@medusajs/types" import MODULE_DEFINITIONS from "../../definitions" import { registerModules } from "../register-modules" @@ -77,29 +77,30 @@ describe("module definitions loader", () => { ) } }) + }) - it("Resolves module with no resolution path when not given custom resolution path as false as default package", () => { - const definition = { - ...defaultDefinition, - defaultPackage: false as false, - } + it("Module with no resolution path when not given custom resolution path, false as default package and required", () => { + const definition = { + ...defaultDefinition, + defaultPackage: false as false, + isRequired: true, + } - MODULE_DEFINITIONS.push(definition) + MODULE_DEFINITIONS.push(definition) - const res = registerModules({}) + const res = registerModules({}) - expect(res[defaultDefinition.key]).toEqual( - expect.objectContaining({ - resolutionPath: false, - definition: definition, - options: {}, - moduleDeclaration: { - scope: "internal", - resources: "shared", - }, - }) - ) - }) + expect(res[defaultDefinition.key]).toEqual( + expect.objectContaining({ + resolutionPath: false, + definition: definition, + options: {}, + moduleDeclaration: { + scope: "internal", + resources: "shared", + }, + }) + ) }) describe("string config", () => { diff --git a/packages/modules-sdk/src/loaders/module-loader.ts b/packages/modules-sdk/src/loaders/module-loader.ts index 09fe95ccf6..afc282915b 100644 --- a/packages/modules-sdk/src/loaders/module-loader.ts +++ b/packages/modules-sdk/src/loaders/module-loader.ts @@ -1,8 +1,8 @@ import { Logger, MedusaContainer, - ModuleResolution, MODULE_SCOPE, + ModuleResolution, } from "@medusajs/types" import { asValue } from "awilix" import { EOL } from "os" @@ -16,11 +16,17 @@ async function loadModule( resolution: ModuleResolution, logger: Logger ): Promise<{ error?: Error } | void> { - const registrationName = resolution.definition.registrationName + const modDefinition = resolution.definition + const registrationName = modDefinition.registrationName const { scope, resources } = resolution.moduleDeclaration ?? ({} as any) - if (scope === MODULE_SCOPE.EXTERNAL) { + const canSkip = + !resolution.resolutionPath && + !modDefinition.isRequired && + !modDefinition.defaultPackage + + if (scope === MODULE_SCOPE.EXTERNAL && !canSkip) { // TODO: implement external Resolvers // return loadExternalModule(...) throw new Error("External Modules are not supported yet.") @@ -41,7 +47,7 @@ async function loadModule( } } - if (!resolution.resolutionPath) { + if (resolution.resolutionPath === false) { container.register({ [registrationName]: asValue(undefined), }) diff --git a/packages/modules-sdk/src/loaders/register-modules.ts b/packages/modules-sdk/src/loaders/register-modules.ts index a33bc8eed0..7b101192cd 100644 --- a/packages/modules-sdk/src/loaders/register-modules.ts +++ b/packages/modules-sdk/src/loaders/register-modules.ts @@ -3,8 +3,10 @@ import { InternalModuleDeclaration, MODULE_SCOPE, ModuleDefinition, + ModuleExports, ModuleResolution, } from "@medusajs/types" +import { isObject } from "@medusajs/utils" import resolveCwd from "resolve-cwd" import MODULE_DEFINITIONS from "../definitions" @@ -21,11 +23,16 @@ export const registerModules = ( for (const definition of MODULE_DEFINITIONS) { const customConfig = projectModules[definition.key] - const isObj = typeof customConfig === "object" + const canSkip = + !customConfig && !definition.isRequired && !definition.defaultPackage + + const isObj = isObject(customConfig) if (isObj && customConfig.scope === MODULE_SCOPE.EXTERNAL) { // TODO: getExternalModuleResolution(...) - throw new Error("External Modules are not supported yet.") + if (!canSkip) { + throw new Error("External Modules are not supported yet.") + } } moduleResolutions[definition.key] = getInternalModuleResolution( @@ -39,7 +46,8 @@ export const registerModules = ( export const registerMedusaModule = ( moduleKey: string, - moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration + moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration, + moduleExports?: ModuleExports ): Record => { const moduleResolutions = {} as Record @@ -55,7 +63,8 @@ export const registerMedusaModule = ( moduleResolutions[definition.key] = getInternalModuleResolution( definition, - moduleDeclaration as InternalModuleDeclaration + moduleDeclaration as InternalModuleDeclaration, + moduleExports ) } @@ -64,12 +73,14 @@ export const registerMedusaModule = ( function getInternalModuleResolution( definition: ModuleDefinition, - moduleConfig: InternalModuleDeclaration | false | string + moduleConfig: InternalModuleDeclaration | false | string, + moduleExports?: ModuleExports ): ModuleResolution { if (typeof moduleConfig === "boolean") { if (!moduleConfig && definition.isRequired) { throw new Error(`Module: ${definition.label} is required`) } + if (!moduleConfig) { return { resolutionPath: false, @@ -86,9 +97,11 @@ function getInternalModuleResolution( // If user added a module and it's overridable, we resolve that instead const isString = typeof moduleConfig === "string" if (definition.canOverride && (isString || (isObj && moduleConfig.resolve))) { - resolutionPath = resolveCwd( - isString ? moduleConfig : (moduleConfig.resolve as string) - ) + resolutionPath = !moduleExports + ? resolveCwd(isString ? moduleConfig : (moduleConfig.resolve as string)) + : // Explicitly assign an empty string, later, we will check if the value is exactly false. + // This allows to continue the module loading while using the module exports instead of re importing the module itself during the process. + "" } const moduleDeclaration = isObj ? moduleConfig : {} @@ -106,6 +119,7 @@ function getInternalModuleResolution( ...definition.defaultModuleDeclaration, ...moduleDeclaration, }, + moduleExports, options: isObj ? moduleConfig.options ?? {} : {}, } } diff --git a/packages/modules-sdk/src/loaders/utils/load-internal.ts b/packages/modules-sdk/src/loaders/utils/load-internal.ts index b755cfc158..e2f78c122d 100644 --- a/packages/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/modules-sdk/src/loaders/utils/load-internal.ts @@ -3,14 +3,13 @@ import { InternalModuleDeclaration, Logger, MedusaContainer, - ModuleExports, - ModuleResolution, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + ModuleExports, + ModuleResolution, } from "@medusajs/types" import { createMedusaContainer } from "@medusajs/utils" import { asFunction, asValue } from "awilix" -import { trackInstallation } from "medusa-telemetry" export async function loadInternalModule( container: MedusaContainer, @@ -24,7 +23,13 @@ export async function loadInternalModule( let loadedModule: ModuleExports try { - loadedModule = (await import(resolution.resolutionPath as string)).default + // When loading manually, we pass the exports to be loaded, meaning that we do not need to import the package to find + // the exports. This is useful when a package export an initialize function which will bootstrap itself and therefore + // does not need to import the package that is currently being loaded as it would create a + // circular reference. + loadedModule = + resolution.moduleExports ?? + (await import(resolution.resolutionPath as string)).default } catch (error) { if ( resolution.definition.isRequired && @@ -118,14 +123,6 @@ export async function loadInternalModule( ) }).singleton(), }) - - trackInstallation( - { - module: resolution.definition.key, - resolution: resolution.resolutionPath, - }, - "module" - ) } export async function loadModuleMigrations( diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 5f86a62a0c..8831ae09aa 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -3,8 +3,13 @@ import { InternalModuleDeclaration, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + ModuleExports, } from "@medusajs/types" -import { createMedusaContainer } from "@medusajs/utils" +import { + createMedusaContainer, + simpleHash, + stringifyCircular, +} from "@medusajs/utils" import { asValue } from "awilix" import { moduleLoader, registerMedusaModule } from "./loaders" import { loadModuleMigrations } from "./loaders/utils" @@ -18,16 +23,24 @@ const logger: any = { export class MedusaModule { private static instances_: Map = new Map() + public static clearInstances(): void { + MedusaModule.instances_.clear() + } public static async bootstrap( moduleKey: string, defaultPath: string, declaration?: InternalModuleDeclaration | ExternalModuleDeclaration, + moduleExports?: ModuleExports, injectedDependencies?: Record ): Promise<{ [key: string]: any }> { - if (MedusaModule.instances_.has(moduleKey)) { - return MedusaModule.instances_.get(moduleKey) + const hashKey = simpleHash( + stringifyCircular({ moduleKey, defaultPath, declaration }) + ) + + if (MedusaModule.instances_.has(hashKey)) { + return MedusaModule.instances_.get(hashKey) } let modDeclaration = declaration @@ -48,9 +61,17 @@ export class MedusaModule { } } - const moduleResolutions = registerMedusaModule(moduleKey, modDeclaration!) + const moduleResolutions = registerMedusaModule( + moduleKey, + modDeclaration!, + moduleExports + ) - await moduleLoader({ container, moduleResolutions, logger }) + await moduleLoader({ + container, + moduleResolutions, + logger, + }) const services = {} @@ -61,7 +82,7 @@ export class MedusaModule { services[keyName] = container.resolve(registrationName) } - MedusaModule.instances_.set(moduleKey, services) + MedusaModule.instances_.set(hashKey, services) return services } diff --git a/packages/modules-sdk/tsconfig.json b/packages/modules-sdk/tsconfig.json index 2c99c7503d..fa7a7380a0 100644 --- a/packages/modules-sdk/tsconfig.json +++ b/packages/modules-sdk/tsconfig.json @@ -15,11 +15,12 @@ "strictFunctionTypes": true, "noImplicitThis": true, "allowJs": true, - "skipLibCheck": true + "skipLibCheck": true, + "downlevelIteration": true // to use ES5 specific tooling }, - "include": ["src"], + "include": ["./src/**/*"], "exclude": [ - "dist", + "./dist/**/*", "./src/**/__tests__", "./src/**/__mocks__", "node_modules" diff --git a/packages/product/.gitignore b/packages/product/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/product/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/product/integration-tests/__fixtures__/module.ts b/packages/product/integration-tests/__fixtures__/module.ts new file mode 100644 index 0000000000..c9c1b18522 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/module.ts @@ -0,0 +1,24 @@ +import { FindOptions, RepositoryService } from "@medusajs/types" + +class CustomRepository implements RepositoryService { + constructor() {} + + find(options?: FindOptions): Promise { + throw new Error("Method not implemented.") + } + + findAndCount(options?: FindOptions): Promise<[any[], number]> { + throw new Error("Method not implemented.") + } +} + +CustomRepository.prototype.find = jest.fn().mockImplementation(async () => []) +CustomRepository.prototype.findAndCount = jest + .fn() + .mockImplementation(async () => []) + +export class ProductRepository extends CustomRepository {} +export class ProductTagRepository extends CustomRepository {} +export class ProductCollectionRepository extends CustomRepository {} +export class ProductVariantRepository extends CustomRepository {} +export class ProductCategoryRepository extends CustomRepository {} diff --git a/packages/product/integration-tests/__fixtures__/product-category/data/index.ts b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts new file mode 100644 index 0000000000..9b75d1e42b --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product-category/data/index.ts @@ -0,0 +1,28 @@ +export const productCategoriesData = [ + { + id: "category-0", + name: "category 0", + parent_category_id: null + }, + { + id: "category-1", + name: "category 1", + parent_category_id: "category-0" + }, + { + id: "category-1-a", + name: "category 1 a", + parent_category_id: "category-1", + }, + { + id: "category-1-b", + name: "category 1 b", + parent_category_id: "category-1", + is_internal: true, + }, + { + id: "category-1-b-1", + name: "category 1 b 1", + parent_category_id: "category-1-b" + }, +] diff --git a/packages/product/integration-tests/__fixtures__/product-category/index.ts b/packages/product/integration-tests/__fixtures__/product-category/index.ts new file mode 100644 index 0000000000..7a9b15e699 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product-category/index.ts @@ -0,0 +1,31 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { ProductCategory } from "@models" + +export async function createProductCategories( + manager: SqlEntityManager, + categoriesData: any[] +): Promise { + const categories: ProductCategory[] = [] + + for (let categoryData of categoriesData) { + let categoryDataClone = { ...categoryData } + let parentCategory: ProductCategory | null = null + const parentCategoryId = categoryDataClone.parent_category_id as string + delete categoryDataClone.parent_category_id + + if (parentCategoryId) { + parentCategory = await manager.findOne(ProductCategory, parentCategoryId) + } + + const category = await manager.create(ProductCategory, { + ...categoryDataClone, + parent_category: parentCategory, + }) + + categories.push(category) + } + + await manager.persistAndFlush(categories) + + return categories +} diff --git a/packages/product/integration-tests/__fixtures__/product/data/categories.ts b/packages/product/integration-tests/__fixtures__/product/data/categories.ts new file mode 100644 index 0000000000..50e78f3bf4 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product/data/categories.ts @@ -0,0 +1,17 @@ +export const categoriesData = [ + { + id: "category-0", + name: "category 0", + parent_category_id: null + }, + { + id: "category-1", + name: "category 1", + parent_category_id: "category-0" + }, + { + id: "category-1-a", + name: "category 1 a", + parent_category_id: "category-1" + }, +] diff --git a/packages/product/integration-tests/__fixtures__/product/data/index.ts b/packages/product/integration-tests/__fixtures__/product/data/index.ts new file mode 100644 index 0000000000..522f377ab3 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product/data/index.ts @@ -0,0 +1,2 @@ +export * from "./categories" +export * from "./products" diff --git a/packages/product/integration-tests/__fixtures__/product/data/products.ts b/packages/product/integration-tests/__fixtures__/product/data/products.ts new file mode 100644 index 0000000000..b43eb5c0c5 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product/data/products.ts @@ -0,0 +1,43 @@ +import { ProductTypes } from "@medusajs/types" + +export const productsData = [ + { + id: "test-1", + title: "product 1", + status: ProductTypes.ProductStatus.PUBLISHED, + tags: [ + { + id: "tag-1", + value: "France", + }, + ], + }, + { + id: "test-2", + title: "product", + status: ProductTypes.ProductStatus.PUBLISHED, + tags: [ + { + id: "tag-2", + value: "Germany", + }, + ], + }, +] + +export const variantsData = [ + { + id: "test-1", + title: "variant title", + sku: "sku 1", + product: { id: productsData[0].id }, + inventory_quantity: 10, + }, + { + id: "test-2", + title: "variant title", + sku: "sku 2", + product: { id: productsData[1].id }, + inventory_quantity: 10, + }, +] diff --git a/packages/product/integration-tests/__fixtures__/product/index.ts b/packages/product/integration-tests/__fixtures__/product/index.ts new file mode 100644 index 0000000000..1fa7158c28 --- /dev/null +++ b/packages/product/integration-tests/__fixtures__/product/index.ts @@ -0,0 +1,72 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { + Product, + ProductCategory, + ProductCollection, + ProductVariant, +} from "@models" +import ProductOption from "../../../src/models/product-option" + +export async function createProductAndTags( + manager: SqlEntityManager, + data: any[] +) { + const products: any[] = data.map((productData) => { + return manager.create(Product, productData) + }) + + await manager.persistAndFlush(products) + + return products +} + +export async function createProductVariants( + manager: SqlEntityManager, + data: any[] +) { + const variants: any[] = data.map((variantsData) => { + return manager.create(ProductVariant, variantsData) + }) + + await manager.persistAndFlush(variants) + + return variants +} + +export async function createCollections( + manager: SqlEntityManager, + collectionData: any[] +) { + const collections: any[] = collectionData.map((collectionData) => { + return manager.create(ProductCollection, collectionData) + }) + + await manager.persistAndFlush(collections) + + return collections +} + +export async function createOptions( + manager: SqlEntityManager, + optionsData: any[] +) { + const options: any[] = optionsData.map((o) => { + return manager.create(ProductOption, o) + }) + + await manager.persistAndFlush(options) + + return options +} + +export async function assignCategoriesToProduct( + manager: SqlEntityManager, + product: Product, + categories: ProductCategory[] +) { + product.categories.add(categories) + + await manager.persistAndFlush(product) + + return product +} diff --git a/packages/product/integration-tests/__tests__/module.ts b/packages/product/integration-tests/__tests__/module.ts new file mode 100644 index 0000000000..f7a38aca6d --- /dev/null +++ b/packages/product/integration-tests/__tests__/module.ts @@ -0,0 +1,109 @@ +import { MedusaModule } from "@medusajs/modules-sdk" +import { Product } from "@models" +import { initialize } from "../../src" +import * as CustomRepositories from "../__fixtures__/module" +import { ProductRepository } from "../__fixtures__/module" +import { createProductAndTags } from "../__fixtures__/product" +import { productsData } from "../__fixtures__/product/data" +import { DB_URL, TestDatabase } from "../utils" + +const beforeEach_ = async () => { + await TestDatabase.setupDatabase() + return await TestDatabase.forkManager() +} + +const afterEach_ = async () => { + await TestDatabase.clearDatabase() +} + +describe("Product module", function () { + describe("Using built-in data access layer", function () { + let module + let products: Product[] + + beforeEach(async () => { + const testManager = await beforeEach_() + products = await createProductAndTags(testManager, productsData) + + module = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, + }, + }) + }) + + afterEach(afterEach_) + + it("should initialize", async () => { + expect(module).toBeDefined() + }) + + it("should return a list of product", async () => { + const products = await module.list() + expect(products).toHaveLength(2) + }) + }) + + describe("Using custom data access layer", function () { + let module + let products: Product[] + + beforeEach(async () => { + const testManager = await beforeEach_() + + products = await createProductAndTags(testManager, productsData) + + module = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, + }, + repositories: CustomRepositories, + }) + }) + + afterEach(afterEach_) + + it("should initialize", async () => { + expect(module).toBeDefined() + }) + + it("should return a list of product", async () => { + const products = await module.list() + + expect(ProductRepository.prototype.find).toHaveBeenCalled() + expect(products).toHaveLength(0) + }) + }) + + describe("Using custom data access layer and connection", function () { + let module + let products: Product[] + + beforeEach(async () => { + const testManager = await beforeEach_() + products = await createProductAndTags(testManager, productsData) + + MedusaModule.clearInstances() + + module = await initialize({ + manager: testManager, + repositories: CustomRepositories, + }) + }) + + afterEach(afterEach_) + + it("should initialize and return a list of product", async () => { + expect(module).toBeDefined() + }) + + it("should return a list of product", async () => { + const products = await module.list() + + expect(ProductRepository.prototype.find).toHaveBeenCalled() + expect(products).toHaveLength(0) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product-category/index.ts b/packages/product/integration-tests/__tests__/services/product-category/index.ts new file mode 100644 index 0000000000..89bd83881b --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-category/index.ts @@ -0,0 +1,233 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { ProductCategoryService } from "@services" +import { ProductCategoryRepository } from "@repositories" +import { ProductCategory } from "@models" + +import { TestDatabase } from "../../../utils" +import { createProductCategories } from "../../../__fixtures__/product-category" +import { productCategoriesData } from "../../../__fixtures__/product-category/data" + +jest.setTimeout(30000) + +describe("ProductCategory Service", () => { + let service: ProductCategoryService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let productCategories!: ProductCategory[] + + beforeEach(async () => { + await TestDatabase.setupDatabase() + repositoryManager = await TestDatabase.forkManager() + + const productCategoryRepository = new ProductCategoryRepository({ + manager: repositoryManager, + }) + + service = new ProductCategoryService({ + productCategoryRepository, + }) + }) + + afterEach(async () => { + await TestDatabase.clearDatabase() + }) + + describe("list", () => { + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + productCategories = await createProductCategories( + testManager, + productCategoriesData + ) + }) + + it("lists all product categories", async () => { + const productCategoryResults = await service.list( + {}, + { + select: ["id", "parent_category_id"] as any, + } + ) + + expect(productCategoryResults).toEqual([ + expect.objectContaining({ + id: "category-0", + parent_category_id: null, + }), + expect.objectContaining({ + id: "category-1", + parent_category: expect.objectContaining({ + id: "category-0", + }), + }), + expect.objectContaining({ + id: "category-1-a", + parent_category: expect.objectContaining({ + id: "category-1", + }), + }), + expect.objectContaining({ + id: "category-1-b", + parent_category: expect.objectContaining({ + id: "category-1", + }), + }), + expect.objectContaining({ + id: "category-1-b-1", + parent_category: expect.objectContaining({ + id: "category-1-b", + }), + }), + ]) + }) + + it("scopes product categories by parent_category_id", async () => { + let productCategoryResults = await service.list( + { parent_category_id: null }, + { + select: ["id"], + } + ) + + expect(productCategoryResults).toEqual([ + expect.objectContaining({ + id: "category-0", + }), + ]) + + productCategoryResults = await service.list({ + parent_category_id: "category-0", + }) + + expect(productCategoryResults).toEqual([ + expect.objectContaining({ + id: "category-1", + }), + ]) + + productCategoryResults = await service.list({ + parent_category_id: ["category-1-b", "category-0"], + }) + + expect(productCategoryResults).toEqual([ + expect.objectContaining({ + id: "category-1", + }), + expect.objectContaining({ + id: "category-1-b-1", + }), + ]) + }) + + it("includes the entire list of descendants when include_descendants_tree is true", async () => { + const productCategoryResults = await service.list( + { + parent_category_id: null, + include_descendants_tree: true, + }, + { + select: ["id", "handle"] as any, + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + expect.objectContaining({ + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + category_children: [ + expect.objectContaining({ + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: "category-0", + category_children: [ + expect.objectContaining({ + id: "category-1-a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + parent_category_id: "category-1", + parent_category: "category-1", + category_children: [], + }), + expect.objectContaining({ + id: "category-1-b", + handle: "category-1-b", + mpath: "category-0.category-1.category-1-b.", + parent_category_id: "category-1", + parent_category: "category-1", + category_children: [ + expect.objectContaining({ + id: "category-1-b-1", + handle: "category-1-b-1", + mpath: + "category-0.category-1.category-1-b.category-1-b-1.", + parent_category_id: "category-1-b", + parent_category: "category-1-b", + category_children: [], + }), + ], + }), + ], + }), + ], + }), + ]) + }) + + it("scopes children when include_descendants_tree is true", async () => { + const productCategoryResults = await service.list( + { + parent_category_id: null, + include_descendants_tree: true, + is_internal: false, + }, + { + select: ["id", "handle"] as any, + } + ) + + const serializedObject = JSON.parse( + JSON.stringify(productCategoryResults) + ) + + expect(serializedObject).toEqual([ + expect.objectContaining({ + id: "category-0", + handle: "category-0", + mpath: "category-0.", + parent_category_id: null, + parent_category: null, + category_children: [ + expect.objectContaining({ + id: "category-1", + handle: "category-1", + mpath: "category-0.category-1.", + parent_category_id: "category-0", + parent_category: "category-0", + category_children: [ + expect.objectContaining({ + id: "category-1-a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + parent_category_id: "category-1", + parent_category: "category-1", + category_children: [], + }), + ], + }), + ], + }), + ]) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product-collection/index.ts b/packages/product/integration-tests/__tests__/services/product-collection/index.ts new file mode 100644 index 0000000000..a8b64aaaea --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-collection/index.ts @@ -0,0 +1,106 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { ProductCollection } from "@models" +import { ProductCollectionService } from "@services" +import { ProductCollectionRepository } from "@repositories" + +import { TestDatabase } from "../../../utils" +import { createCollections } from "../../../__fixtures__/product" + +jest.setTimeout(30000) + +describe("Product Service", () => { + let service: ProductCollectionService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let collectionsData: ProductCollection[] = [] + + beforeEach(async () => { + await TestDatabase.setupDatabase() + repositoryManager = await TestDatabase.forkManager() + + const productCollectionRepository = new ProductCollectionRepository({ + manager: repositoryManager, + }) + + service = new ProductCollectionService({ + productCollectionRepository, + }) + }) + + afterEach(async () => { + await TestDatabase.clearDatabase() + }) + + describe("list", () => { + const data = [ + { + id: "test-1", + title: "col 1", + }, + { + id: "test-2", + title: "col 2", + }, + { + id: "test-3", + title: "col 3 extra", + }, + { + id: "test-4", + title: "col 4 extra", + }, + ] + + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + collectionsData = await createCollections(testManager, data) + }) + + it("list product collections", async () => { + const tagsResults = await service.list() + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "test-1", + title: "col 1", + }), + expect.objectContaining({ + id: "test-2", + title: "col 2", + }), + expect.objectContaining({ + id: "test-3", + title: "col 3 extra", + }), + expect.objectContaining({ + id: "test-4", + title: "col 4 extra", + }), + ]) + }) + + it("list product collections by id", async () => { + const tagsResults = await service.list({ id: data![0].id }) + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "test-1", + title: "col 1", + }), + ]) + }) + + it("list product collections by title matching string", async () => { + const tagsResults = await service.list({ title: "col 3 extra" }) + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "test-3", + title: "col 3 extra", + }), + ]) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product-tag/index.ts b/packages/product/integration-tests/__tests__/services/product-tag/index.ts new file mode 100644 index 0000000000..8b5a7d9c9a --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-tag/index.ts @@ -0,0 +1,121 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" + +import { ProductTagService } from "@services" +import { ProductTagRepository } from "@repositories" +import { Product } from "@models" + +import { TestDatabase } from "../../../utils" +import { createProductAndTags } from "../../../__fixtures__/product" +import { ProductTypes } from "@medusajs/types" + +jest.setTimeout(30000) + +describe("Product Service", () => { + let service: ProductTagService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let data!: Product[] + + beforeEach(async () => { + await TestDatabase.setupDatabase() + repositoryManager = await TestDatabase.forkManager() + + const productTagRepository = new ProductTagRepository({ + manager: repositoryManager, + }) + + service = new ProductTagService({ + productTagRepository, + }) + }) + + afterEach(async () => { + await TestDatabase.clearDatabase() + }) + + describe("list", () => { + const productsData = [ + { + id: "test-1", + title: "product 1", + status: ProductTypes.ProductStatus.PUBLISHED, + tags: [ + { + id: "tag-1", + value: "France", + }, + ], + }, + { + id: "test-2", + title: "product", + status: ProductTypes.ProductStatus.PUBLISHED, + tags: [ + { + id: "tag-2", + value: "Germany", + }, + { + id: "tag-3", + value: "United States", + }, + { + id: "tag-4", + value: "United Kingdom", + }, + ], + }, + ] + + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + data = await createProductAndTags(testManager, productsData) + }) + + it("list product tags", async () => { + const tagsResults = await service.list() + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "tag-1", + value: "France", + }), + expect.objectContaining({ + id: "tag-2", + value: "Germany", + }), + expect.objectContaining({ + id: "tag-3", + value: "United States", + }), + expect.objectContaining({ + id: "tag-4", + value: "United Kingdom", + }), + ]) + }) + + it("list product tags by id", async () => { + const tagsResults = await service.list({ id: data[0].tags![0].id }) + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "tag-1", + value: "France", + }), + ]) + }) + + it("list product tags by value matching string", async () => { + const tagsResults = await service.list({ value: "united kingdom" }) + + expect(tagsResults).toEqual([ + expect.objectContaining({ + id: "tag-4", + value: "United Kingdom", + }), + ]) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product-variant/index.ts b/packages/product/integration-tests/__tests__/services/product-variant/index.ts new file mode 100644 index 0000000000..9d60165a94 --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product-variant/index.ts @@ -0,0 +1,185 @@ +import { TestDatabase } from "../../../utils" +import { ProductVariantService } from "@services" +import { ProductVariantRepository } from "@repositories" +import { Product, ProductTag, ProductVariant } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { Collection } from "@mikro-orm/core" +import { ProductTypes } from "@medusajs/types" +import { ProductOption } from "@medusajs/client-types" +import { + createOptions, + createProductAndTags, + createProductVariants, +} from "../../../__fixtures__/product" +import { productsData, variantsData } from "../../../__fixtures__/product/data" + +describe("ProductVariant Service", () => { + let service: ProductVariantService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let variantOne: ProductVariant + let variantTwo: ProductVariant + let productOne: Product + + beforeEach(async () => { + await TestDatabase.setupDatabase() + repositoryManager = await TestDatabase.forkManager() + + const productVariantRepository = new ProductVariantRepository({ + manager: repositoryManager, + }) + + service = new ProductVariantService({ productVariantRepository }) + }) + + afterEach(async () => { + await TestDatabase.clearDatabase() + }) + + describe("list", () => { + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + productOne = testManager.create(Product, { + id: "product-1", + title: "product 1", + status: ProductTypes.ProductStatus.PUBLISHED, + }) + + variantOne = testManager.create(ProductVariant, { + id: "test-1", + title: "variant 1", + inventory_quantity: 10, + product: productOne, + }) + + variantTwo = testManager.create(ProductVariant, { + id: "test-2", + title: "variant", + inventory_quantity: 10, + product: productOne, + }) + + await testManager.persistAndFlush([variantOne, variantTwo]) + }) + + it("selecting by properties, scopes out the results", async () => { + const results = await service.list({ + id: variantOne.id, + }) + + expect(results).toEqual([ + expect.objectContaining({ + id: variantOne.id, + title: "variant 1", + inventory_quantity: "10", + }), + ]) + }) + + it("passing a limit, scopes the result to the limit", async () => { + const results = await service.list( + {}, + { + take: 1, + } + ) + + expect(results).toEqual([ + expect.objectContaining({ + id: variantOne.id, + }), + ]) + }) + + it("passing populate, scopes the results of the response", async () => { + const results = await service.list( + { + id: "test-1", + }, + { + select: ["id", "title", "product.title"] as any, + relations: ["product"], + } + ) + + expect(results).toEqual([ + expect.objectContaining({ + id: "test-1", + title: "variant 1", + product: expect.objectContaining({ + id: "product-1", + title: "product 1", + tags: expect.any(Collection), + variants: expect.any(Collection), + }), + }), + ]) + + expect(JSON.parse(JSON.stringify(results))).toEqual([ + { + id: "test-1", + title: "variant 1", + product_id: "product-1", + product: { + id: "product-1", + title: "product 1", + }, + }, + ]) + }) + }) + + describe("relation: options", () => { + let products: Product[] + let variants: ProductVariant[] + let options: ProductOption[] + + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + products = (await createProductAndTags( + testManager, + productsData + )) as Product[] + variants = (await createProductVariants( + testManager, + variantsData + )) as ProductVariant[] + + options = await createOptions(testManager, [ + { + id: "test-option-1", + title: "size", + product: products[0], + values: [ + { id: "value-1", value: "XS", variant: products[0].variants[0] }, + { id: "value-1", value: "XL", variant: products[0].variants[0] }, + ], + }, + { + id: "test-option-2", + title: "color", + product: products[0], + value: "blue", + variant: products[0].variants[0], + }, + ]) + }) + + it("filter by options relation", async () => { + const variants = await service.list( + { options: { id: ["value-1"] } }, + { relations: ["options"] } + ) + + expect(JSON.parse(JSON.stringify(variants))).toEqual([ + expect.objectContaining({ + id: "test-1", + title: "variant title", + sku: "sku 1", + }), + ]) + }) + }) +}) diff --git a/packages/product/integration-tests/__tests__/services/product/index.ts b/packages/product/integration-tests/__tests__/services/product/index.ts new file mode 100644 index 0000000000..08fc942b40 --- /dev/null +++ b/packages/product/integration-tests/__tests__/services/product/index.ts @@ -0,0 +1,257 @@ +import { TestDatabase } from "../../../utils" +import { + ProductService, + ProductTagService, + ProductVariantService, +} from "@services" +import { ProductRepository } from "@repositories" +import { Product, ProductCategory, ProductVariant } from "@models" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { ProductDTO } from "@medusajs/types" + +import { createProductCategories } from "../../../__fixtures__/product-category" +import { + assignCategoriesToProduct, + createProductAndTags, + createProductVariants, +} from "../../../__fixtures__/product" +import { + categoriesData, + productsData, + variantsData, +} from "../../../__fixtures__/product/data" + +const productVariantService = { + list: jest.fn(), +} as unknown as ProductVariantService +const productTagService = { + list: jest.fn(), +} as unknown as ProductTagService + +jest.setTimeout(30000) + +describe("Product Service", () => { + let service: ProductService + let testManager: SqlEntityManager + let repositoryManager: SqlEntityManager + let products!: Product[] + let variants!: ProductVariant[] + let categories!: ProductCategory[] + + beforeEach(async () => { + await TestDatabase.setupDatabase() + repositoryManager = await TestDatabase.forkManager() + + const productRepository = new ProductRepository({ + manager: repositoryManager, + }) + + service = new ProductService({ + productRepository, + productVariantService, + productTagService, + }) + }) + + afterEach(async () => { + await TestDatabase.clearDatabase() + }) + + describe("list", () => { + describe("relation: tags", () => { + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + products = await createProductAndTags(testManager, productsData) + }) + + it("filter by id and including relations", async () => { + const productsResult = await service.list( + { + id: products[0].id, + }, + { + relations: ["tags"], + } + ) + + productsResult.forEach((product, index) => { + const tags = product.tags.toArray() + + expect(product).toEqual( + expect.objectContaining({ + id: productsData[index].id, + title: productsData[index].title, + }) + ) + + tags.forEach((tag, tagIndex) => { + expect(tag).toEqual( + expect.objectContaining({ + ...productsData[index].tags[tagIndex], + }) + ) + }) + }) + }) + + it("filter by id and without relations", async () => { + const productsResult = await service.list({ + id: products[0].id, + }) + + productsResult.forEach((product, index) => { + const tags = product.tags.getItems(false) + + expect(product).toEqual( + expect.objectContaining({ + id: productsData[index].id, + title: productsData[index].title, + }) + ) + + expect(tags.length).toBe(0) + }) + }) + }) + + describe("relation: categories", () => { + let workingProduct: Product + let workingCategory: ProductCategory + + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + products = await createProductAndTags(testManager, productsData) + workingProduct = products.find((p) => p.id === "test-1") as Product + categories = await createProductCategories(testManager, categoriesData) + workingCategory = (await testManager.findOne( + ProductCategory, + "category-1" + )) as ProductCategory + + workingProduct = await assignCategoriesToProduct( + testManager, + workingProduct, + categories + ) + }) + + it("filter by categories relation and scope fields", async () => { + const products = await service.list( + { + id: workingProduct.id, + categories: { id: [workingCategory.id] }, + }, + { + select: [ + "title", + "categories.name", + "categories.handle", + "categories.mpath", + ] as (keyof ProductDTO)[], + relations: ["categories"], + } + ) + + const product = products.find( + (p) => p.id === workingProduct.id + ) as unknown as Product + + expect(product).toEqual( + expect.objectContaining({ + id: workingProduct.id, + title: workingProduct.title, + }) + ) + + expect(product.categories.toArray()).toEqual([ + { + id: "category-0", + name: "category 0", + handle: "category-0", + mpath: "category-0.", + }, + { + id: "category-1", + name: "category 1", + handle: "category-1", + mpath: "category-0.category-1.", + }, + { + id: "category-1-a", + name: "category 1 a", + handle: "category-1-a", + mpath: "category-0.category-1.category-1-a.", + }, + ]) + }) + + it("returns empty array when querying for a category that doesnt exist", async () => { + const products = await service.list( + { + id: workingProduct.id, + categories: { id: ["category-doesnt-exist-id"] }, + }, + { + select: [ + "title", + "categories.name", + "categories.handle", + ] as (keyof ProductDTO)[], + relations: ["categories"], + } + ) + + expect(products).toEqual([]) + }) + }) + + describe("relation: variants", () => { + beforeEach(async () => { + testManager = await TestDatabase.forkManager() + + products = await createProductAndTags(testManager, productsData) + variants = await createProductVariants(testManager, variantsData) + }) + + it("filter by id and including relations", async () => { + const productsResult = await service.list( + { + id: products[0].id, + }, + { + relations: ["variants"], + } + ) + + productsResult.forEach((product, index) => { + const variants = product.variants.toArray() + + expect(product).toEqual( + expect.objectContaining({ + id: productsData[index].id, + title: productsData[index].title, + }) + ) + + variants.forEach((variant, variantIndex) => { + const expectedVariant = variantsData.filter( + (d) => d.product.id === product.id + )[variantIndex] + + const variantProduct = variant.product + + expect(variant).toEqual( + expect.objectContaining({ + id: expectedVariant.id, + sku: expectedVariant.sku, + title: expectedVariant.title, + }) + ) + }) + }) + }) + }) + }) +}) diff --git a/packages/product/integration-tests/setup-env.js b/packages/product/integration-tests/setup-env.js new file mode 100644 index 0000000000..f2c14d8dbb --- /dev/null +++ b/packages/product/integration-tests/setup-env.js @@ -0,0 +1,6 @@ +if (typeof process.env.DB_TEMP_NAME === "undefined") { + const tempName = parseInt(process.env.JEST_WORKER_ID || "1") + process.env.DB_TEMP_NAME = `medusa-integration-${tempName}` +} + +process.env.MEDUSA_PRODUCT_DB_SCHEMA = "medusa-product" diff --git a/packages/product/integration-tests/setup.js b/packages/product/integration-tests/setup.js new file mode 100644 index 0000000000..1d31c09d98 --- /dev/null +++ b/packages/product/integration-tests/setup.js @@ -0,0 +1,22 @@ +const { dropDatabase } = require("pg-god") + +const DB_HOST = process.env.DB_HOST +const DB_USERNAME = process.env.DB_USERNAME +const DB_PASSWORD = process.env.DB_PASSWORD +const DB_NAME = process.env.DB_TEMP_NAME + +const pgGodCredentials = { + user: DB_USERNAME, + password: DB_PASSWORD, + host: DB_HOST, +} + +afterAll(async () => { + try { + await dropDatabase({ databaseName: DB_NAME }, pgGodCredentials) + } catch (e) { + console.error( + `This might fail if it is run during the unit tests since there is no database to drop. Otherwise, please check what is the issue. ${e}` + ) + } +}) diff --git a/packages/product/integration-tests/utils/database.ts b/packages/product/integration-tests/utils/database.ts new file mode 100644 index 0000000000..be8e26ecfe --- /dev/null +++ b/packages/product/integration-tests/utils/database.ts @@ -0,0 +1,95 @@ +import { TSMigrationGenerator } from "@mikro-orm/migrations" +import { MikroORM, Options, SqlEntityManager } from "@mikro-orm/postgresql" +import * as ProductModels from "@models" +import * as process from "process" + +const DB_HOST = process.env.DB_HOST ?? "localhost" +const DB_USERNAME = process.env.DB_USERNAME ?? "" +const DB_PASSWORD = process.env.DB_PASSWORD +const DB_NAME = process.env.DB_TEMP_NAME +export const DB_URL = `postgres://${DB_USERNAME}${ + DB_PASSWORD ? `:${DB_PASSWORD}` : "" +}@${DB_HOST}/${DB_NAME}` + +const ORMConfig: Options = { + type: "postgresql", + clientUrl: DB_URL, + entities: Object.values(ProductModels), + schema: process.env.MEDUSA_PRODUCT_DB_SCHEMA, + debug: false, + migrations: { + path: "../../src/migrations", + pathTs: "../../src/migrations", + glob: "!(*.d).{js,ts}", + silent: true, + dropTables: true, + transactional: true, + allOrNothing: true, + safe: false, + generator: TSMigrationGenerator, + }, +} + +interface TestDatabase { + orm: MikroORM | null + manager: SqlEntityManager | null + + setupDatabase(): Promise + clearDatabase(): Promise + getManager(): SqlEntityManager + forkManager(): Promise + getORM(): MikroORM +} + +export const TestDatabase: TestDatabase = { + orm: null, + manager: null, + + getManager() { + if (this.manager === null) { + throw new Error("manager entity not available") + } + + return this.manager + }, + + async forkManager() { + if (this.manager === null) { + throw new Error("manager entity not available") + } + + return await this.manager.fork() + }, + + getORM() { + if (this.orm === null) { + throw new Error("orm entity not available") + } + + return this.orm + }, + + async setupDatabase() { + // Initializing the ORM + this.orm = await MikroORM.init(ORMConfig) + + if (this.orm === null) { + throw new Error("ORM not configured") + } + + this.manager = await this.orm.em + + await this.orm.schema.refreshDatabase() // ensure db exists and is fresh + }, + + async clearDatabase() { + if (this.orm === null) { + throw new Error("ORM not configured") + } + + await this.orm.close() + + this.orm = null + this.manager = null + }, +} diff --git a/packages/product/integration-tests/utils/index.ts b/packages/product/integration-tests/utils/index.ts new file mode 100644 index 0000000000..6b917ed30e --- /dev/null +++ b/packages/product/integration-tests/utils/index.ts @@ -0,0 +1 @@ +export * from "./database" diff --git a/packages/product/jest.config.js b/packages/product/jest.config.js new file mode 100644 index 0000000000..dab6097238 --- /dev/null +++ b/packages/product/jest.config.js @@ -0,0 +1,21 @@ +module.exports = { + moduleNameMapper: { + "^@models": "/src/models", + "^@services": "/src/services", + "^@repositories": "/src/repositories", + }, + globals: { + "ts-jest": { + tsConfig: "tsconfig.spec.json", + isolatedModules: false, + }, + }, + transform: { + "^.+\\.[jt]s?$": "ts-jest", + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["/integration-tests/setup-env.js"], + setupFilesAfterEnv: ["/integration-tests/setup.js"], +} diff --git a/packages/product/mikro-orm.config.dev.ts b/packages/product/mikro-orm.config.dev.ts new file mode 100644 index 0000000000..6eb4f1cc96 --- /dev/null +++ b/packages/product/mikro-orm.config.dev.ts @@ -0,0 +1,8 @@ +import * as entities from "./src/models" + +module.exports = { + entities: Object.values(entities), + schema: "public", + clientUrl: "postgres://postgres@localhost/medusa-products", + type: "postgresql", +} diff --git a/packages/product/package.json b/packages/product/package.json new file mode 100644 index 0000000000..4a75fcc802 --- /dev/null +++ b/packages/product/package.json @@ -0,0 +1,62 @@ +{ + "name": "@medusajs/product", + "version": "0.0.1", + "description": "Medusa Product module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "bin": { + "medusa-product-migrations-down": "dist/scripts/bin/run-migration-down.js", + "medusa-product-migrations-up": "dist/scripts/bin/run-migration-up.js", + "medusa-product-seed": "dist/scripts/bin/run-seed.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/product" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "scripts": { + "watch": "tsc --build --watch", + "watch:test": "tsc --build tsconfig.spec.json --watch", + "prepare": "cross-env NODE_ENV=production yarn run build", + "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", + "test": "jest --runInBand --forceExit -- src/**/__tests__/**/*.ts", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", + "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", + "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", + "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", + "migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up", + "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" + }, + "devDependencies": { + "@mikro-orm/cli": "5.7.4", + "@mikro-orm/migrations": "5.7.4", + "cross-env": "^5.2.1", + "jest": "^25.5.4", + "medusa-test-utils": "^1.1.40", + "pg-god": "^1.0.12", + "rimraf": "^5.0.0", + "ts-jest": "^25.5.1", + "ts-node": "^10.9.1", + "tsc-alias": "^1.8.6", + "typescript": "^4.4.4" + }, + "dependencies": { + "@medusajs/modules-sdk": "^1.8.3", + "@medusajs/types": "*", + "@medusajs/utils": "^1.8.2", + "@mikro-orm/core": "5.7.4", + "@mikro-orm/migrations": "5.7.4", + "@mikro-orm/postgresql": "5.7.4", + "awilix": "^8.0.0", + "dotenv": "^16.1.4", + "lodash": "^4.17.21" + } +} diff --git a/packages/product/src/index.ts b/packages/product/src/index.ts new file mode 100644 index 0000000000..45da1e739d --- /dev/null +++ b/packages/product/src/index.ts @@ -0,0 +1,10 @@ +import { moduleDefinition } from "./module-definition" + +export default moduleDefinition + +export * from "./scripts" +export * from "./initialize" +export * from "./types" +export * from "./loaders" +export * from "./models" +export * from "./services" diff --git a/packages/product/src/initialize/index.ts b/packages/product/src/initialize/index.ts new file mode 100644 index 0000000000..1ccfff1622 --- /dev/null +++ b/packages/product/src/initialize/index.ts @@ -0,0 +1,34 @@ +import { + ExternalModuleDeclaration, + InternalModuleDeclaration, + MedusaModule, + MODULE_PACKAGE_NAMES, + Modules, +} from "@medusajs/modules-sdk" +import { IProductModuleService } from "@medusajs/types" +import { moduleDefinition } from "../module-definition" +import { + InitializeModuleInjectableDependencies, + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" + +export const initialize = async ( + options?: + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions + | ExternalModuleDeclaration, + injectedDependencies?: InitializeModuleInjectableDependencies +): Promise => { + const serviceKey = Modules.PRODUCT + + const loaded = await MedusaModule.bootstrap( + serviceKey, + MODULE_PACKAGE_NAMES[Modules.PRODUCT], + options as InternalModuleDeclaration | ExternalModuleDeclaration, + moduleDefinition, + injectedDependencies + ) + + return loaded[serviceKey] as IProductModuleService +} diff --git a/packages/product/src/loaders/connection.ts b/packages/product/src/loaders/connection.ts new file mode 100644 index 0000000000..53d448b7d0 --- /dev/null +++ b/packages/product/src/loaders/connection.ts @@ -0,0 +1,66 @@ +import { asValue } from "awilix" + +import { + InternalModuleDeclaration, + LoaderOptions, + MODULE_RESOURCE_TYPE, + MODULE_SCOPE, +} from "@medusajs/modules-sdk" +import { MedusaError } from "@medusajs/utils" + +import { EntitySchema } from "@mikro-orm/core" + +import * as ProductModels from "@models" +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { createConnection, loadDatabaseConfig } from "../utils" + +export default async ( + { + options, + container, + }: LoaderOptions< + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions + >, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + if ( + moduleDeclaration?.scope === MODULE_SCOPE.INTERNAL && + moduleDeclaration.resources === MODULE_RESOURCE_TYPE.SHARED + ) { + return + } + + const customManager = ( + options as ProductServiceInitializeCustomDataLayerOptions + )?.manager + + if (!customManager) { + const dbData = loadDatabaseConfig(options) + await loadDefault({ database: dbData, container }) + } else { + container.register({ + manager: asValue(customManager), + }) + } +} + +async function loadDefault({ database, container }) { + if (!database) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + `Database config is not present at module config "options.database"` + ) + } + + const entities = Object.values(ProductModels) as unknown as EntitySchema[] + + const orm = await createConnection(database, entities) + + container.register({ + manager: asValue(orm.em.fork()), + }) +} diff --git a/packages/product/src/loaders/container.ts b/packages/product/src/loaders/container.ts new file mode 100644 index 0000000000..120b6ce2b0 --- /dev/null +++ b/packages/product/src/loaders/container.ts @@ -0,0 +1,92 @@ +import { LoaderOptions } from "@medusajs/modules-sdk" + +import { asClass } from "awilix" +import { + ProductCategoryService, + ProductModuleService, + ProductService, + ProductTagService, + ProductVariantService, + ProductCollectionService, +} from "@services" +import * as DefaultRepositories from "@repositories" +import { + ProductCategoryRepository, + ProductCollectionRepository, + ProductRepository, + ProductTagRepository, + ProductVariantRepository, +} from "@repositories" +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { Constructor, DAL } from "@medusajs/types" +import { lowerCaseFirst } from "@medusajs/utils" + +export default async ({ + container, + options, +}: LoaderOptions< + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions +>): Promise => { + const customRepositories = ( + options as ProductServiceInitializeCustomDataLayerOptions + )?.repositories + + container.register({ + productModuleService: asClass(ProductModuleService).singleton(), + productService: asClass(ProductService).singleton(), + productCategoryService: asClass(ProductCategoryService).singleton(), + productVariantService: asClass(ProductVariantService).singleton(), + productTagService: asClass(ProductTagService).singleton(), + productCollectionService: asClass(ProductCollectionService).singleton(), + }) + + if (customRepositories) { + await loadCustomRepositories({ customRepositories, container }) + } else { + await loadDefaultRepositories({ container }) + } +} + +function loadDefaultRepositories({ container }) { + container.register({ + productRepository: asClass(ProductRepository).singleton(), + productVariantRepository: asClass(ProductVariantRepository).singleton(), + productTagRepository: asClass(ProductTagRepository).singleton(), + productCategoryRepository: asClass(ProductCategoryRepository).singleton(), + productCollectionRepository: asClass( + ProductCollectionRepository + ).singleton(), + }) +} + +/** + * Load the repositories from the custom repositories object. If a repository is not + * present in the custom repositories object, the default repository will be used. + * + * @param customRepositories + * @param container + */ +function loadCustomRepositories({ customRepositories, container }) { + const customRepositoriesMap = new Map(Object.entries(customRepositories)) + + Object.entries(DefaultRepositories).forEach(([key, DefaultRepository]) => { + let finalRepository = customRepositoriesMap.get(key) + + if ( + !finalRepository || + !(finalRepository as Constructor).prototype.find + ) { + finalRepository = DefaultRepository + } + + container.register({ + [lowerCaseFirst(key)]: asClass( + finalRepository as Constructor + ).singleton(), + }) + }) +} diff --git a/packages/product/src/loaders/index.ts b/packages/product/src/loaders/index.ts new file mode 100644 index 0000000000..3614963d8c --- /dev/null +++ b/packages/product/src/loaders/index.ts @@ -0,0 +1,2 @@ +export * from "./connection" +export * from "./container" diff --git a/packages/product/src/migrations/.snapshot-medusa-products.json b/packages/product/src/migrations/.snapshot-medusa-products.json new file mode 100644 index 0000000000..0150443c95 --- /dev/null +++ b/packages/product/src/migrations/.snapshot-medusa-products.json @@ -0,0 +1,1257 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "name": { + "name": "name", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "''", + "mappedType": "text" + }, + "handle": { + "name": "handle", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "mpath": { + "name": "mpath", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "is_internal": { + "name": "is_internal", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "rank": { + "name": "rank", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "0", + "mappedType": "decimal" + }, + "parent_category_id": { + "name": "parent_category_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_category", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_product_category_path", + "columnNames": [ + "mpath" + ], + "composite": false, + "primary": false, + "unique": false + }, + { + "keyName": "IDX_product_category_handle", + "columnNames": [ + "handle" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "product_category_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_category_parent_category_id_foreign": { + "constraintName": "product_category_parent_category_id_foreign", + "columnNames": [ + "parent_category_id" + ], + "localTableName": "public.product_category", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_category", + "deleteRule": "set null", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "handle": { + "name": "handle", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_collection", + "schema": "public", + "indexes": [ + { + "keyName": "IDX_product_collection_handle_unique", + "columnNames": [ + "handle" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "product_collection_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_tag", + "schema": "public", + "indexes": [ + { + "keyName": "product_tag_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "json", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_type", + "schema": "public", + "indexes": [ + { + "keyName": "product_type_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "handle": { + "name": "handle", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "subtitle": { + "name": "subtitle", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "description": { + "name": "description", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "is_giftcard": { + "name": "is_giftcard", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "status": { + "name": "status", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "enumItems": [ + "draft", + "proposed", + "published", + "rejected" + ], + "mappedType": "enum" + }, + "thumbnail": { + "name": "thumbnail", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "weight": { + "name": "weight", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "length": { + "name": "length", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "height": { + "name": "height", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "width": { + "name": "width", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "origin_country": { + "name": "origin_country", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "hs_code": { + "name": "hs_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "mid_code": { + "name": "mid_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "material": { + "name": "material", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "collection_id": { + "name": "collection_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "type_id": { + "name": "type_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "discountable": { + "name": "discountable", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, + "external_id": { + "name": "external_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + } + }, + "name": "product", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "type_id" + ], + "composite": false, + "keyName": "IDX_product_type_id", + "primary": false, + "unique": false + }, + { + "keyName": "IDX_product_handle_unique", + "columnNames": [ + "handle" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "product_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_collection_id_foreign": { + "constraintName": "product_collection_id_foreign", + "columnNames": [ + "collection_id" + ], + "localTableName": "public.product", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_collection", + "deleteRule": "set null", + "updateRule": "cascade" + }, + "product_type_id_foreign": { + "constraintName": "product_type_id_foreign", + "columnNames": [ + "type_id" + ], + "localTableName": "public.product", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_type", + "deleteRule": "set null", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_option", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "product_id" + ], + "composite": false, + "keyName": "IDX_product_option_product_id", + "primary": false, + "unique": false + }, + { + "keyName": "product_option_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_option_product_id_foreign": { + "constraintName": "product_option_product_id_foreign", + "columnNames": [ + "product_id" + ], + "localTableName": "public.product_option", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "product_tag_id": { + "name": "product_tag_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "product_tags", + "schema": "public", + "indexes": [ + { + "keyName": "product_tags_pkey", + "columnNames": [ + "product_id", + "product_tag_id" + ], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_tags_product_id_foreign": { + "constraintName": "product_tags_product_id_foreign", + "columnNames": [ + "product_id" + ], + "localTableName": "public.product_tags", + "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" + ], + "localTableName": "public.product_tags", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_tag", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "product_category_id": { + "name": "product_category_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "product_category_product", + "schema": "public", + "indexes": [ + { + "keyName": "product_category_product_pkey", + "columnNames": [ + "product_id", + "product_category_id" + ], + "composite": true, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_category_product_product_id_foreign": { + "constraintName": "product_category_product_product_id_foreign", + "columnNames": [ + "product_id" + ], + "localTableName": "public.product_category_product", + "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" + ], + "localTableName": "public.product_category_product", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_category", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "title": { + "name": "title", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "sku": { + "name": "sku", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "barcode": { + "name": "barcode", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "ean": { + "name": "ean", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "upc": { + "name": "upc", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "inventory_quantity": { + "name": "inventory_quantity", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "decimal" + }, + "allow_backorder": { + "name": "allow_backorder", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "false", + "mappedType": "boolean" + }, + "manage_inventory": { + "name": "manage_inventory", + "type": "boolean", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "default": "true", + "mappedType": "boolean" + }, + "hs_code": { + "name": "hs_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "origin_country": { + "name": "origin_country", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "mid_code": { + "name": "mid_code", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "material": { + "name": "material", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "text" + }, + "weight": { + "name": "weight", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "length": { + "name": "length", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "height": { + "name": "height", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "width": { + "name": "width", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "variant_rank": { + "name": "variant_rank", + "type": "numeric", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "decimal" + }, + "created_at": { + "name": "created_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "length": 6, + "mappedType": "datetime" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + }, + "product_id": { + "name": "product_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "product_variant", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "product_id" + ], + "composite": false, + "keyName": "IDX_product_variant_product_id_index", + "primary": false, + "unique": false + }, + { + "keyName": "IDX_product_variant_sku_unique", + "columnNames": [ + "sku" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "IDX_product_variant_barcode_unique", + "columnNames": [ + "barcode" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "IDX_product_variant_ean_unique", + "columnNames": [ + "ean" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "IDX_product_variant_upc_unique", + "columnNames": [ + "upc" + ], + "composite": false, + "primary": false, + "unique": true + }, + { + "keyName": "product_variant_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_variant_product_id_foreign": { + "constraintName": "product_variant_product_id_foreign", + "columnNames": [ + "product_id" + ], + "localTableName": "public.product_variant", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + }, + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "value": { + "name": "value", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "option_id": { + "name": "option_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "variant_id": { + "name": "variant_id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "mappedType": "json" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamptz", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": true, + "length": 6, + "mappedType": "datetime" + } + }, + "name": "product_option_value", + "schema": "public", + "indexes": [ + { + "columnNames": [ + "option_id" + ], + "composite": false, + "keyName": "IDX_product_option_value_product_option", + "primary": false, + "unique": false + }, + { + "keyName": "product_option_value_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": { + "product_option_value_option_id_foreign": { + "constraintName": "product_option_value_option_id_foreign", + "columnNames": [ + "option_id" + ], + "localTableName": "public.product_option_value", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_option", + "updateRule": "cascade" + }, + "product_option_value_variant_id_foreign": { + "constraintName": "product_option_value_variant_id_foreign", + "columnNames": [ + "variant_id" + ], + "localTableName": "public.product_option_value", + "referencedColumnNames": [ + "id" + ], + "referencedTableName": "public.product_variant", + "deleteRule": "cascade", + "updateRule": "cascade" + } + } + } + ] +} diff --git a/packages/product/src/migrations/Migration20230609132805.ts b/packages/product/src/migrations/Migration20230609132805.ts new file mode 100644 index 0000000000..43baf27f65 --- /dev/null +++ b/packages/product/src/migrations/Migration20230609132805.ts @@ -0,0 +1,57 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20230609132805 extends Migration { + + async up(): Promise { + this.addSql('create table "product_category" ("id" text not null, "name" text not null, "description" text not null default \'\', "handle" text not null, "mpath" text not null, "is_active" boolean not null default false, "is_internal" boolean not null default false, "rank" numeric not null default 0, "parent_category_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, constraint "product_category_pkey" primary key ("id"));'); + this.addSql('create index "IDX_product_category_path" on "product_category" ("mpath");'); + this.addSql('alter table "product_category" add constraint "IDX_product_category_handle" unique ("handle");'); + + this.addSql('create table "product_collection" ("id" text not null, "title" text not null, "handle" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_collection_pkey" primary key ("id"));'); + this.addSql('alter table "product_collection" add constraint "IDX_product_collection_handle_unique" unique ("handle");'); + + this.addSql('create table "product_tag" ("id" text not null, "value" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_tag_pkey" primary key ("id"));'); + + this.addSql('create table "product_type" ("id" text not null, "value" text not null, "metadata" json null, "deleted_at" timestamptz null, constraint "product_type_pkey" primary key ("id"));'); + + this.addSql('create table "product" ("id" text not null, "title" text not null, "handle" text not null, "subtitle" text null, "description" text null, "is_giftcard" boolean not null default false, "status" text check ("status" in (\'draft\', \'proposed\', \'published\', \'rejected\')) not null, "thumbnail" text null, "weight" text null, "length" text null, "height" text null, "width" text null, "origin_country" text null, "hs_code" text null, "mid_code" text null, "material" text null, "collection_id" text null, "type_id" text null, "discountable" boolean not null default true, "external_id" text null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "metadata" jsonb null, constraint "product_pkey" primary key ("id"));'); + this.addSql('create index "IDX_product_type_id" on "product" ("type_id");'); + this.addSql('alter table "product" add constraint "IDX_product_handle_unique" unique ("handle");'); + + this.addSql('create table "product_option" ("id" text not null, "title" text not null, "product_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_pkey" primary key ("id"));'); + this.addSql('create index "IDX_product_option_product_id" on "product_option" ("product_id");'); + + this.addSql('create table "product_tags" ("product_id" text not null, "product_tag_id" text not null, constraint "product_tags_pkey" primary key ("product_id", "product_tag_id"));'); + + this.addSql('create table "product_category_product" ("product_id" text not null, "product_category_id" text not null, constraint "product_category_product_pkey" primary key ("product_id", "product_category_id"));'); + + this.addSql('create table "product_variant" ("id" text not null, "title" text not null, "sku" text null, "barcode" text null, "ean" text null, "upc" text null, "inventory_quantity" numeric not null, "allow_backorder" boolean not null default false, "manage_inventory" boolean not null default true, "hs_code" text null, "origin_country" text null, "mid_code" text null, "material" text null, "weight" numeric null, "length" numeric null, "height" numeric null, "width" numeric null, "metadata" jsonb null, "variant_rank" numeric null, "created_at" timestamptz not null, "updated_at" timestamptz not null, "deleted_at" timestamptz null, "product_id" text not null, constraint "product_variant_pkey" primary key ("id"));'); + this.addSql('create index "IDX_product_variant_product_id_index" on "product_variant" ("product_id");'); + this.addSql('alter table "product_variant" add constraint "IDX_product_variant_sku_unique" unique ("sku");'); + this.addSql('alter table "product_variant" add constraint "IDX_product_variant_barcode_unique" unique ("barcode");'); + this.addSql('alter table "product_variant" add constraint "IDX_product_variant_ean_unique" unique ("ean");'); + this.addSql('alter table "product_variant" add constraint "IDX_product_variant_upc_unique" unique ("upc");'); + + this.addSql('create table "product_option_value" ("id" text not null, "value" text not null, "option_id" text not null, "variant_id" text not null, "metadata" jsonb null, "deleted_at" timestamptz null, constraint "product_option_value_pkey" primary key ("id"));'); + this.addSql('create index "IDX_product_option_value_product_option" on "product_option_value" ("option_id");'); + + this.addSql('alter table "product_category" add constraint "product_category_parent_category_id_foreign" foreign key ("parent_category_id") references "product_category" ("id") on update cascade on delete set null;'); + + this.addSql('alter table "product" add constraint "product_collection_id_foreign" foreign key ("collection_id") references "product_collection" ("id") on update cascade on delete set null;'); + this.addSql('alter table "product" add constraint "product_type_id_foreign" foreign key ("type_id") references "product_type" ("id") on update cascade on delete set null;'); + + this.addSql('alter table "product_option" add constraint "product_option_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade;'); + + this.addSql('alter table "product_tags" add constraint "product_tags_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table "product_tags" add constraint "product_tags_product_tag_id_foreign" foreign key ("product_tag_id") references "product_tag" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table "product_category_product" add constraint "product_category_product_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + this.addSql('alter table "product_category_product" add constraint "product_category_product_product_category_id_foreign" foreign key ("product_category_id") references "product_category" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table "product_variant" add constraint "product_variant_product_id_foreign" foreign key ("product_id") references "product" ("id") on update cascade on delete cascade;'); + + this.addSql('alter table "product_option_value" add constraint "product_option_value_option_id_foreign" foreign key ("option_id") references "product_option" ("id") on update cascade;'); + this.addSql('alter table "product_option_value" add constraint "product_option_value_variant_id_foreign" foreign key ("variant_id") references "product_variant" ("id") on update cascade on delete cascade;'); + } + +} diff --git a/packages/product/src/models/index.ts b/packages/product/src/models/index.ts new file mode 100644 index 0000000000..6575294d57 --- /dev/null +++ b/packages/product/src/models/index.ts @@ -0,0 +1,6 @@ +export { default as Product } from "./product" +export { default as ProductCategory } from "./product-category" +export { default as ProductCollection } from "./product-collection" +export { default as ProductTag } from "./product-tag" +export { default as ProductType } from "./product-type" +export { default as ProductVariant } from "./product-variant" diff --git a/packages/product/src/models/product-category.ts b/packages/product/src/models/product-category.ts new file mode 100644 index 0000000000..91ccc0358a --- /dev/null +++ b/packages/product/src/models/product-category.ts @@ -0,0 +1,101 @@ +import { generateEntityId, kebabCase } from "@medusajs/utils" +import { + BeforeCreate, + Collection, + Entity, + EventArgs, + Index, + ManyToMany, + ManyToOne, + OneToMany, + PrimaryKey, + Property, + Unique, +} from "@mikro-orm/core" + +import Product from "./product" + +@Entity({ tableName: "product_category" }) +class ProductCategory { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text", nullable: false }) + name: string + + @Property({ columnType: "text", default: "", nullable: false }) + description?: string + + @Unique({ + name: "IDX_product_category_handle", + properties: ["handle"], + }) + @Property({ columnType: "text", nullable: false }) + handle?: string + + @Index({ + name: "IDX_product_category_path", + properties: ["mpath"], + }) + @Property({ columnType: "text", nullable: false }) + mpath?: string + + @Property({ columnType: "boolean", default: false }) + is_active?: boolean + + @Property({ columnType: "boolean", default: false }) + is_internal?: boolean + + @Property({ columnType: "numeric", nullable: false, default: 0 }) + rank?: number + + @Property({ columnType: "text", nullable: true }) + parent_category_id?: string + + @ManyToOne(() => ProductCategory, { nullable: true }) + parent_category: ProductCategory + + @OneToMany({ + entity: () => ProductCategory, + mappedBy: (productCategory) => productCategory.parent_category, + }) + category_children = new Collection(this) + + @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + }) + updated_at: Date + + @ManyToMany(() => Product, (product) => product.categories) + products = new Collection(this) + + @BeforeCreate() + async onCreate(args: EventArgs) { + this.id = generateEntityId(this.id, "pcat") + + if (!this.handle) { + this.handle = kebabCase(this.name) + } + + const { em } = args + const parentCategoryId = args.changeSet?.entity?.parent_category?.id + let parentCategory: ProductCategory | null = null + + if (parentCategoryId) { + parentCategory = await em.findOne(ProductCategory, parentCategoryId) + } + + if (parentCategory) { + this.mpath = `${parentCategory?.mpath}${this.id}.` + } else { + this.mpath = `${this.id}.` + } + } +} + +export default ProductCategory diff --git a/packages/product/src/models/product-collection.ts b/packages/product/src/models/product-collection.ts new file mode 100644 index 0000000000..90a7830e86 --- /dev/null +++ b/packages/product/src/models/product-collection.ts @@ -0,0 +1,42 @@ +import { + BeforeCreate, + Entity, + PrimaryKey, + Property, + Unique, +} from "@mikro-orm/core" + +import { generateEntityId, kebabCase } from "@medusajs/utils" + +@Entity({ tableName: "product_collection" }) +class ProductCollection { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + title: string + + @Property({ columnType: "text" }) + @Unique({ + name: "IDX_product_collection_handle_unique", + properties: ["handle"], + }) + handle: string + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "pcol") + + if (!this.handle) { + this.handle = kebabCase(this.title) + } + } +} + +export default ProductCollection diff --git a/packages/product/src/models/product-option-value.ts b/packages/product/src/models/product-option-value.ts new file mode 100644 index 0000000000..814955a91e --- /dev/null +++ b/packages/product/src/models/product-option-value.ts @@ -0,0 +1,41 @@ +import { + BeforeCreate, + Entity, + ManyToOne, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import { generateEntityId } from "@medusajs/utils" + +import ProductOption from "./product-option" +import { ProductVariant } from "./index" + +@Entity({ tableName: "product_option_value" }) +class ProductOptionValue { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + value: string + + @ManyToOne(() => ProductOption, { + index: "IDX_product_option_value_product_option", + }) + option: ProductOption + + @ManyToOne(() => ProductVariant, { onDelete: "cascade" }) + variant: ProductVariant + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @BeforeCreate() + beforeCreate() { + this.id = generateEntityId(this.id, "optval") + } +} + +export default ProductOptionValue diff --git a/packages/product/src/models/product-option.ts b/packages/product/src/models/product-option.ts new file mode 100644 index 0000000000..28f1419c32 --- /dev/null +++ b/packages/product/src/models/product-option.ts @@ -0,0 +1,48 @@ +import { + BeforeCreate, + Cascade, + Collection, + Entity, + ManyToOne, + OneToMany, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import { generateEntityId } from "@medusajs/utils" +import { Product } from "./index" +import ProductOptionValue from "./product-option-value" + +@Entity({ tableName: "product_option" }) +class ProductOption { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + title: string + + @Property({ persist: false }) + product_id!: number + + @ManyToOne(() => Product, { + index: "IDX_product_option_product_id", + }) + product: Product + + @OneToMany(() => ProductOptionValue, (value) => value.option, { + cascade: [Cascade.REMOVE], + }) + values = new Collection(this) + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @BeforeCreate() + beforeCreate() { + this.id = generateEntityId(this.id, "opt") + } +} + +export default ProductOption diff --git a/packages/product/src/models/product-tag.ts b/packages/product/src/models/product-tag.ts new file mode 100644 index 0000000000..5fc16a00d7 --- /dev/null +++ b/packages/product/src/models/product-tag.ts @@ -0,0 +1,36 @@ +import { + BeforeCreate, + Collection, + Entity, + ManyToMany, + PrimaryKey, + Property, +} from "@mikro-orm/core" + +import { generateEntityId } from "@medusajs/utils" +import Product from "./product" + +@Entity({ tableName: "product_tag" }) +class ProductTag { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + value: string + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @ManyToMany(() => Product, (product) => product.tags) + products = new Collection(this) + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "ptag") + } +} + +export default ProductTag diff --git a/packages/product/src/models/product-type.ts b/packages/product/src/models/product-type.ts new file mode 100644 index 0000000000..df4b3618df --- /dev/null +++ b/packages/product/src/models/product-type.ts @@ -0,0 +1,25 @@ +import { BeforeCreate, Entity, PrimaryKey, Property } from "@mikro-orm/core" + +import { generateEntityId } from "@medusajs/utils" + +@Entity({ tableName: "product_type" }) +class ProductType { + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + value: string + + @Property({ columnType: "json", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "ptyp") + } +} + +export default ProductType diff --git a/packages/product/src/models/product-variant.ts b/packages/product/src/models/product-variant.ts new file mode 100644 index 0000000000..85fdea578e --- /dev/null +++ b/packages/product/src/models/product-variant.ts @@ -0,0 +1,141 @@ +import { + BeforeCreate, + Cascade, + Collection, + Entity, + ManyToOne, + OneToMany, + OptionalProps, + PrimaryKey, + Property, + Unique, +} from "@mikro-orm/core" +import { generateEntityId } from "@medusajs/utils" +import { Product } from "@models" +import ProductOptionValue from "./product-option-value" + +type OptionalFields = + | "created_at" + | "updated_at" + | "updated_at" + | "deleted_at" + | "allow_backorder" + | "manage_inventory" + | "product" + | "product_id" + +@Entity({ tableName: "product_variant" }) +class ProductVariant { + [OptionalProps]?: OptionalFields + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + title: string + + @Property({ columnType: "text", nullable: true }) + @Unique({ + name: "IDX_product_variant_sku_unique", + properties: ["sku"], + }) + sku?: string | null + + @Property({ columnType: "text", nullable: true }) + @Unique({ + name: "IDX_product_variant_barcode_unique", + properties: ["barcode"], + }) + barcode?: string | null + + @Property({ columnType: "text", nullable: true }) + @Unique({ + name: "IDX_product_variant_ean_unique", + properties: ["ean"], + }) + ean?: string | null + + @Property({ columnType: "text", nullable: true }) + @Unique({ + name: "IDX_product_variant_upc_unique", + properties: ["upc"], + }) + upc?: string | null + + // Note: Upon serialization, this turns to a string. This is on purpose, because you would loose + // precision if you cast numeric to JS number, as JS number is a float. + // Ref: https://github.com/mikro-orm/mikro-orm/issues/2295 + @Property({ columnType: "numeric" }) + inventory_quantity: number + + @Property({ columnType: "boolean", default: false }) + allow_backorder: boolean + + @Property({ columnType: "boolean", default: true }) + manage_inventory: boolean + + @Property({ columnType: "text", nullable: true }) + hs_code?: string | null + + @Property({ columnType: "text", nullable: true }) + origin_country?: string | null + + @Property({ columnType: "text", nullable: true }) + mid_code?: string | null + + @Property({ columnType: "text", nullable: true }) + material?: string | null + + @Property({ columnType: "numeric", nullable: true }) + weight?: number | null + + @Property({ columnType: "numeric", nullable: true }) + length?: number | null + + @Property({ columnType: "numeric", nullable: true }) + height?: number | null + + @Property({ columnType: "numeric", nullable: true }) + width?: number | null + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @Property({ columnType: "numeric", nullable: true }) + variant_rank?: number | null + + @Property({ persist: false }) + product_id!: string + + @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @ManyToOne(() => Product, { + onDelete: "cascade", + index: "IDX_product_variant_product_id_index", + fieldName: "product_id", + }) + product!: Product + + @OneToMany(() => ProductOptionValue, (optionValue) => optionValue.variant, { + cascade: [Cascade.PERSIST, Cascade.REMOVE], + }) + options = new Collection(this) + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "variant") + } +} + +export default ProductVariant diff --git a/packages/product/src/models/product.ts b/packages/product/src/models/product.ts new file mode 100644 index 0000000000..c3659a1382 --- /dev/null +++ b/packages/product/src/models/product.ts @@ -0,0 +1,150 @@ +import { + BeforeCreate, + Collection, + Entity, + Enum, + ManyToMany, + ManyToOne, + OneToMany, + OptionalProps, + PrimaryKey, + Property, + Unique, +} from "@mikro-orm/core" + +import { ProductTypes } from "@medusajs/types" +import { generateEntityId, kebabCase } from "@medusajs/utils" +import ProductCategory from "./product-category" +import ProductCollection from "./product-collection" +import ProductOption from "./product-option" +import ProductTag from "./product-tag" +import ProductType from "./product-type" +import ProductVariant from "./product-variant" + +type OptionalRelations = "collection" | "type" +type OptionalFields = + | "is_giftcard" + | "discountable" + | "created_at" + | "updated_at" + | "deleted_at" + +@Entity({ tableName: "product" }) +class Product { + [OptionalProps]?: OptionalRelations | OptionalFields + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + title: string + + @Property({ columnType: "text" }) + @Unique({ + name: "IDX_product_handle_unique", + properties: ["handle"], + }) + handle?: string | null + + @Property({ columnType: "text", nullable: true }) + subtitle?: string | null + + @Property({ columnType: "text", nullable: true }) + description?: string | null + + @Property({ columnType: "boolean", default: false }) + is_giftcard!: boolean + + @Enum(() => ProductTypes.ProductStatus) + status!: ProductTypes.ProductStatus + + // TODO: add images model + // images: Image[] + + @Property({ columnType: "text", nullable: true }) + thumbnail?: string | null + + @OneToMany(() => ProductOption, (o) => o.product) + options = new Collection(this) + + @OneToMany(() => ProductVariant, (variant) => variant.product) + variants = new Collection(this) + + @Property({ columnType: "text", nullable: true }) + weight?: number | null + + @Property({ columnType: "text", nullable: true }) + length?: number | null + + @Property({ columnType: "text", nullable: true }) + height?: number | null + + @Property({ columnType: "text", nullable: true }) + width?: number | null + + @Property({ columnType: "text", nullable: true }) + origin_country?: string | null + + @Property({ columnType: "text", nullable: true }) + hs_code?: string | null + + @Property({ columnType: "text", nullable: true }) + mid_code?: string | null + + @Property({ columnType: "text", nullable: true }) + material?: string | null + + @ManyToOne(() => ProductCollection, { nullable: true }) + collection!: ProductCollection + + @ManyToOne(() => ProductType, { + nullable: true, + index: "IDX_product_type_id", + }) + type!: ProductType + + @ManyToMany(() => ProductTag, "products", { + owner: true, + pivotTable: "product_tags", + index: "IDX_product_tag_id", + }) + tags = new Collection(this) + + @ManyToMany(() => ProductCategory, "products", { + owner: true, + pivotTable: "product_category_product", + }) + categories = new Collection(this) + + @Property({ columnType: "boolean", default: true }) + discountable: boolean + + @Property({ columnType: "text", nullable: true }) + external_id?: string | null + + @Property({ onCreate: () => new Date(), columnType: "timestamptz" }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date + + @Property({ columnType: "jsonb", nullable: true }) + metadata?: Record | null + + @BeforeCreate() + beforeCreate() { + this.id = generateEntityId(this.id, "prod") + if (!this.handle) { + this.handle = kebabCase(this.title) + } + } +} + +export default Product diff --git a/packages/product/src/module-definition.ts b/packages/product/src/module-definition.ts new file mode 100644 index 0000000000..956865776f --- /dev/null +++ b/packages/product/src/module-definition.ts @@ -0,0 +1,18 @@ +import { ModuleExports } from "@medusajs/types" +import { ProductModuleService } from "@services" +import loadContainer from "./loaders/container" +import loadConnection from "./loaders/connection" +import * as ProductModels from "@models" +import { revertMigration, runMigrations } from "./scripts" + +const service = ProductModuleService +const loaders = [loadContainer, loadConnection] as any +const models = Object.values(ProductModels) + +export const moduleDefinition: ModuleExports = { + service, + loaders, + models, + runMigrations, + revertMigration, +} diff --git a/packages/product/src/repositories/index.ts b/packages/product/src/repositories/index.ts new file mode 100644 index 0000000000..cd99d6ec97 --- /dev/null +++ b/packages/product/src/repositories/index.ts @@ -0,0 +1,5 @@ +export { ProductRepository } from "./product" +export { ProductTagRepository } from "./product-tag" +export { ProductVariantRepository } from "./product-variant" +export { ProductCollectionRepository } from "./product-collection" +export { ProductCategoryRepository } from "./product-category" diff --git a/packages/product/src/repositories/product-category.ts b/packages/product/src/repositories/product-category.ts new file mode 100644 index 0000000000..1551f0d48c --- /dev/null +++ b/packages/product/src/repositories/product-category.ts @@ -0,0 +1,140 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, + LoadStrategy, +} from "@mikro-orm/core" +import { deduplicateIfNecessary } from "../utils" +import { ProductCategory } from "@models" +import { DAL, ProductCategoryTransformOptions } from "@medusajs/types" +import groupBy from "lodash/groupBy" + +export class ProductCategoryRepository + implements DAL.RepositoryService +{ + protected readonly manager_: SqlEntityManager + + constructor({ manager }) { + this.manager_ = manager.fork() + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + transformOptions: ProductCategoryTransformOptions = {}, + context: { transaction?: any } = {} + ): Promise { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + const { includeDescendantsTree } = transformOptions + + findOptions_.options ??= {} + const fields = (findOptions_.options.fields ??= []) + findOptions_.options.limit ??= 15 + + // Ref: Building descendants + // mpath and parent_category_id needs to be added to the query for the tree building to be done accurately + if (includeDescendantsTree) { + fields.indexOf("mpath") === -1 && fields.push("mpath") + fields.indexOf("parent_category_id") === -1 && + fields.push("parent_category_id") + } + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + const productCategories = await this.manager_.find( + ProductCategory, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + + if (!includeDescendantsTree) { + return productCategories + } + + return this.buildProductCategoriesWithDescendants( + productCategories, + findOptions_ + ) + } + + async buildProductCategoriesWithDescendants( + productCategories: ProductCategory[], + findOptions: DAL.FindOptions = { where: {} } + ): Promise { + for (let productCategory of productCategories) { + const whereOptions = { + ...findOptions.where, + mpath: { + $like: `${productCategory.mpath}%`, + }, + } + delete whereOptions.parent_category_id + + const descendantsForCategory = await this.manager_.find( + ProductCategory, + whereOptions as MikroFilterQuery, + findOptions.options as MikroOptions + ) + + const descendantsByParentId = groupBy( + descendantsForCategory, + (pc) => pc.parent_category_id + ) + + const addChildrenToCategory = (category, children) => { + category.category_children = (children || []).map((categoryChild) => { + const moreChildren = descendantsByParentId[categoryChild.id] || [] + + return addChildrenToCategory(categoryChild, moreChildren) + }) + + return category + } + + let children = descendantsByParentId[productCategory.id] || [] + productCategory = addChildrenToCategory(productCategory, children) + } + + return productCategories + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + transformOptions: ProductCategoryTransformOptions = {}, + context: { transaction?: any } = {} + ): Promise<[ProductCategory[], number]> { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return await this.manager_.findAndCount( + ProductCategory, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } +} diff --git a/packages/product/src/repositories/product-collection.ts b/packages/product/src/repositories/product-collection.ts new file mode 100644 index 0000000000..c32b0c71d0 --- /dev/null +++ b/packages/product/src/repositories/product-collection.ts @@ -0,0 +1,74 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { ProductCollection } from "@models" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, + LoadStrategy, +} from "@mikro-orm/core" +import { deduplicateIfNecessary } from "../utils" +import { DAL } from "@medusajs/types" + +export class ProductCollectionRepository implements DAL.RepositoryService { + protected readonly manager_: SqlEntityManager + constructor({ manager }) { + this.manager_ = manager.fork() + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.find( + ProductCollection, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as T[] + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise<[T[], number]> { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.findAndCount( + ProductCollection, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as [T[], number] + } +} diff --git a/packages/product/src/repositories/product-tag.ts b/packages/product/src/repositories/product-tag.ts new file mode 100644 index 0000000000..5fc13f1fde --- /dev/null +++ b/packages/product/src/repositories/product-tag.ts @@ -0,0 +1,74 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, + LoadStrategy, +} from "@mikro-orm/core" +import { deduplicateIfNecessary } from "../utils" +import { ProductTag } from "@models" +import { DAL } from "@medusajs/types" + +export class ProductTagRepository implements DAL.RepositoryService { + protected readonly manager_: SqlEntityManager + constructor({ manager }) { + this.manager_ = manager.fork() + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.find( + ProductTag, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as T[] + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise<[T[], number]> { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.findAndCount( + ProductTag, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as [T[], number] + } +} diff --git a/packages/product/src/repositories/product-variant.ts b/packages/product/src/repositories/product-variant.ts new file mode 100644 index 0000000000..939bc500bc --- /dev/null +++ b/packages/product/src/repositories/product-variant.ts @@ -0,0 +1,74 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, + LoadStrategy, +} from "@mikro-orm/core" +import { deduplicateIfNecessary } from "../utils" +import { ProductVariant } from "@models" +import { DAL } from "@medusajs/types" + +export class ProductVariantRepository implements DAL.RepositoryService { + protected readonly manager_: SqlEntityManager + constructor({ manager }) { + this.manager_ = manager.fork() + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.find( + ProductVariant, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as T[] + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise<[T[], number]> { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + return (await this.manager_.findAndCount( + ProductVariant, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + )) as unknown as [T[], number] + } +} diff --git a/packages/product/src/repositories/product.ts b/packages/product/src/repositories/product.ts new file mode 100644 index 0000000000..bd6d4f5e5f --- /dev/null +++ b/packages/product/src/repositories/product.ts @@ -0,0 +1,112 @@ +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { Product } from "@models" +import { + FilterQuery as MikroFilterQuery, + FindOptions as MikroOptions, + LoadStrategy, +} from "@mikro-orm/core" +import { deduplicateIfNecessary } from "../utils" +import { DAL } from "@medusajs/types" + +export class ProductRepository implements DAL.RepositoryService { + protected readonly manager_: SqlEntityManager + constructor({ manager }) { + this.manager_ = manager.fork() + } + + async find( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise { + // Spread is used to cssopy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + await this.mutateNotInCategoriesConstraints(findOptions_) + + return await this.manager_.find( + Product, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + async findAndCount( + findOptions: DAL.FindOptions = { where: {} }, + context: { transaction?: any } = {} + ): Promise<[Product[], number]> { + // Spread is used to copy the options in case of manipulation to prevent side effects + const findOptions_ = { ...findOptions } + + findOptions_.options ??= {} + findOptions_.options.limit ??= 15 + + if (findOptions_.options.populate) { + deduplicateIfNecessary(findOptions_.options.populate) + } + + if (context.transaction) { + Object.assign(findOptions_.options, { ctx: context.transaction }) + } + + Object.assign(findOptions_.options, { + strategy: LoadStrategy.SELECT_IN, + }) + + await this.mutateNotInCategoriesConstraints(findOptions_) + + return await this.manager_.findAndCount( + Product, + findOptions_.where as MikroFilterQuery, + findOptions_.options as MikroOptions + ) + } + + /** + * In order to be able to have a strict not in categories, and prevent a product + * to be return in the case it also belongs to other categories, we need to + * first find all products that are in the categories, and then exclude them + */ + private async mutateNotInCategoriesConstraints( + findOptions: DAL.FindOptions = { where: {} } + ): Promise { + if (findOptions.where.categories?.id?.["$nin"]) { + const productsInCategories = await this.manager_.find( + Product, + { + categories: { + id: { $in: findOptions.where.categories.id["$nin"] }, + }, + }, + { + fields: ["id"], + } + ) + + const productIds = productsInCategories.map((product) => product.id) + + if (productIds.length) { + findOptions.where.id = { $nin: productIds } + delete findOptions.where.categories?.id + + if (Object.keys(findOptions.where.categories).length === 0) { + delete findOptions.where.categories + } + } + } + } +} diff --git a/packages/product/src/scripts/bin/run-migration-down.ts b/packages/product/src/scripts/bin/run-migration-down.ts new file mode 100644 index 0000000000..4bc0980ca5 --- /dev/null +++ b/packages/product/src/scripts/bin/run-migration-down.ts @@ -0,0 +1,7 @@ +import { revertMigration } from "../migration-down" + +export default (async () => { + const { config } = await import("dotenv") + config() + await revertMigration() +})() diff --git a/packages/product/src/scripts/bin/run-migration-up.ts b/packages/product/src/scripts/bin/run-migration-up.ts new file mode 100644 index 0000000000..f2a64b8863 --- /dev/null +++ b/packages/product/src/scripts/bin/run-migration-up.ts @@ -0,0 +1,7 @@ +import { runMigrations } from "../migration-up" + +export default (async () => { + const { config } = await import("dotenv") + config() + await runMigrations() +})() diff --git a/packages/product/src/scripts/bin/run-seed.ts b/packages/product/src/scripts/bin/run-seed.ts new file mode 100644 index 0000000000..4fd877533f --- /dev/null +++ b/packages/product/src/scripts/bin/run-seed.ts @@ -0,0 +1,17 @@ +import { run } from "../seed" +import { EOL } from "os" + +const args = process.argv +const path = args.pop() as string + +export default (async () => { + const { config } = await import("dotenv") + config() + if (!path) { + throw new Error( + `filePath is required.${EOL}Example: node_modules/@medusajs/product/dist/scripts/bin/run-seed.js ` + ) + } + + await run({ path }) +})() diff --git a/packages/product/src/scripts/index.ts b/packages/product/src/scripts/index.ts new file mode 100644 index 0000000000..cfa5c5ddf5 --- /dev/null +++ b/packages/product/src/scripts/index.ts @@ -0,0 +1,2 @@ +export * from "./migration-up" +export * from "./migration-down" diff --git a/packages/product/src/scripts/migration-down.ts b/packages/product/src/scripts/migration-down.ts new file mode 100644 index 0000000000..c81d4858ec --- /dev/null +++ b/packages/product/src/scripts/migration-down.ts @@ -0,0 +1,44 @@ +import { LoaderOptions, Logger } from "@medusajs/types" +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { createConnection, loadDatabaseConfig } from "../utils" +import * as ProductModels from "@models" +import { EntitySchema } from "@mikro-orm/core" + +/** + * This script is only valid for mikro orm managers. If a user provide a custom manager + * he is in charge of reverting the migrations. + * @param options + * @param logger + * @param moduleDeclaration + */ +export async function revertMigration({ + options, + logger, +}: Pick< + LoaderOptions< + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions + >, + "options" | "logger" +> = {}) { + logger ??= console as unknown as Logger + + const dbData = loadDatabaseConfig(options) + const entities = Object.values(ProductModels) as unknown as EntitySchema[] + + const orm = await createConnection(dbData, entities) + + try { + const migrator = orm.getMigrator() + await migrator.down() + + logger?.info("Product module migration executed") + } catch (error) { + logger?.error(`Product module migration failed to run - Error: ${error}`) + } + + await orm.close() +} diff --git a/packages/product/src/scripts/migration-up.ts b/packages/product/src/scripts/migration-up.ts new file mode 100644 index 0000000000..88cb1b2eba --- /dev/null +++ b/packages/product/src/scripts/migration-up.ts @@ -0,0 +1,44 @@ +import { LoaderOptions, Logger } from "@medusajs/types" +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { createConnection, loadDatabaseConfig } from "../utils" +import * as ProductModels from "@models" +import { EntitySchema } from "@mikro-orm/core" + +/** + * This script is only valid for mikro orm managers. If a user provide a custom manager + * he is in charge of running the migrations. + * @param options + * @param logger + * @param moduleDeclaration + */ +export async function runMigrations({ + options, + logger, +}: Pick< + LoaderOptions< + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions + >, + "options" | "logger" +> = {}) { + logger ??= console as unknown as Logger + + const dbData = loadDatabaseConfig(options) + const entities = Object.values(ProductModels) as unknown as EntitySchema[] + + const orm = await createConnection(dbData, entities) + + try { + const migrator = orm.getMigrator() + await migrator.up() + + logger?.info("Product module migration executed") + } catch (error) { + logger?.error(`Product module migration failed to run - Error: ${error}`) + } + + await orm.close() +} diff --git a/packages/product/src/scripts/seed.ts b/packages/product/src/scripts/seed.ts new file mode 100644 index 0000000000..f5b0a4a04e --- /dev/null +++ b/packages/product/src/scripts/seed.ts @@ -0,0 +1,108 @@ +import { createConnection, loadDatabaseConfig } from "../utils" +import * as ProductModels from "@models" +import { Product, ProductCategory, ProductVariant } from "@models" +import { EntitySchema } from "@mikro-orm/core" +import { LoaderOptions, Logger } from "@medusajs/types" +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { EOL } from "os" +import { SqlEntityManager } from "@mikro-orm/postgresql" +import { resolve } from "path" + +export async function run({ + options, + logger, + path, +}: Partial< + Pick< + LoaderOptions< + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions + >, + "options" | "logger" + > +> & { + path: string +}) { + logger?.info(`Loading seed data from ${path}...`) + const { productCategoriesData, productsData, variantsData } = await import( + resolve(process.cwd(), path) + ).catch((e) => { + logger?.error( + `Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following productCategoriesData, productsData, variantsData.${EOL}${e}` + ) + throw e + }) + + logger ??= console as unknown as Logger + + const dbData = loadDatabaseConfig(options) + const entities = Object.values(ProductModels) as unknown as EntitySchema[] + + const orm = await createConnection(dbData, entities) + const manager = orm.em.fork() + + try { + logger?.info("Inserting product categories, products and variants...") + await createProductCategories(manager, productCategoriesData) + await createProducts(manager, productsData) + await createProductVariants(manager, variantsData) + } catch (e) { + logger?.error( + `Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}` + ) + } + + await orm.close(true) +} + +async function createProductCategories( + manager: SqlEntityManager, + categoriesData: any[] +): Promise { + const categories: ProductCategory[] = [] + + for (let categoryData of categoriesData) { + let categoryDataClone = { ...categoryData } + let parentCategory: ProductCategory | null = null + const parentCategoryId = categoryDataClone.parent_category_id as string + delete categoryDataClone.parent_category_id + + if (parentCategoryId) { + parentCategory = await manager.findOne(ProductCategory, parentCategoryId) + } + + const category = await manager.create(ProductCategory, { + ...categoryDataClone, + parent_category: parentCategory, + }) + + categories.push(category) + } + + await manager.persistAndFlush(categories) + + return categories +} + +async function createProducts(manager: SqlEntityManager, data: any[]) { + const products: any[] = data.map((productData) => { + return manager.create(Product, productData) + }) + + await manager.persistAndFlush(products) + + return products +} + +async function createProductVariants(manager: SqlEntityManager, data: any[]) { + const variants: any[] = data.map((variantsData) => { + return manager.create(ProductVariant, variantsData) + }) + + await manager.persistAndFlush(variants) + + return variants +} diff --git a/packages/product/src/services/__tests__/product.spec.ts b/packages/product/src/services/__tests__/product.spec.ts new file mode 100644 index 0000000000..cdeb04d228 --- /dev/null +++ b/packages/product/src/services/__tests__/product.spec.ts @@ -0,0 +1,113 @@ +import { asClass, asValue, createContainer } from "awilix" +import { ProductService } from "@services" + +const container = createContainer() +container.register({ + productRepository: asValue({ + find: jest.fn().mockResolvedValue([]), + findAndCount: jest.fn().mockResolvedValue([[], 0]), + }), + productVariantService: asValue({ + list: jest.fn().mockResolvedValue([]), + }), + productTagService: asValue({ + list: jest.fn().mockResolvedValue([]), + }), + productService: asClass(ProductService), +}) + +describe("Product service", function () { + beforeEach(function () { + jest.clearAllMocks() + }) + + it("should list products", async function () { + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") + + const filters = {} + const config = { + relations: [], + } + + await productService.list(filters, config) + + expect(productRepository.find).toHaveBeenCalledWith({ + where: {}, + options: { + fields: undefined, + limit: undefined, + offset: undefined, + populate: [], + }, + }) + }) + + it("should list products with filters", async function () { + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") + + const filters = { + tags: { + value: { + $in: ["test"], + } + } + } + const config = { + relations: [], + } + + await productService.list(filters, config) + + expect(productRepository.find).toHaveBeenCalledWith({ + where: { + tags: { + value: { + $in: ["test"] + } + }, + }, + options: { + fields: undefined, + limit: undefined, + offset: undefined, + populate: [], + }, + }) + }) + + it("should list products with filters and relations", async function () { + const productService = container.resolve("productService") + const productRepository = container.resolve("productRepository") + + const filters = { + tags: { + value: { + $in: ["test"], + } + } + } + const config = { + relations: ["tags"], + } + + await productService.list(filters, config) + + expect(productRepository.find).toHaveBeenCalledWith({ + where: { + tags: { + value: { + $in: ["test"] + } + }, + }, + options: { + fields: undefined, + limit: undefined, + offset: undefined, + populate: ["tags"], + }, + }) + }) +}) diff --git a/packages/product/src/services/index.ts b/packages/product/src/services/index.ts new file mode 100644 index 0000000000..3bc71008b9 --- /dev/null +++ b/packages/product/src/services/index.ts @@ -0,0 +1,6 @@ +export { default as ProductModuleService } from "./product-module-service" +export { default as ProductService } from "./product" +export { default as ProductTagService } from "./product-tag" +export { default as ProductVariantService } from "./product-variant" +export { default as ProductCollectionService } from "./product-collection" +export { default as ProductCategoryService } from "./product-category" diff --git a/packages/product/src/services/product-category.ts b/packages/product/src/services/product-category.ts new file mode 100644 index 0000000000..8ab228f9c3 --- /dev/null +++ b/packages/product/src/services/product-category.ts @@ -0,0 +1,34 @@ +import { ProductCategory } from "@models" +import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + productCategoryRepository: DAL.RepositoryService +} + +export default class ProductCategoryService { + protected readonly productCategoryRepository_: DAL.RepositoryService + + constructor({ productCategoryRepository }: InjectedDependencies) { + this.productCategoryRepository_ = productCategoryRepository + } + + async list( + filters: ProductTypes.FilterableProductCategoryProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const transformOptions = { + includeDescendantsTree: filters?.include_descendants_tree || false + } + delete filters.include_descendants_tree + + const queryOptions = buildQuery(filters, config) + queryOptions.where ??= {} + + return await this.productCategoryRepository_.find( + queryOptions, + transformOptions, + ) + } +} diff --git a/packages/product/src/services/product-collection.ts b/packages/product/src/services/product-collection.ts new file mode 100644 index 0000000000..49d5efe354 --- /dev/null +++ b/packages/product/src/services/product-collection.ts @@ -0,0 +1,30 @@ +import { ProductCollection } from "@models" +import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + productCollectionRepository: DAL.RepositoryService +} + +export default class ProductCollectionService { + protected readonly productCollectionRepository_: DAL.RepositoryService + + constructor({ productCollectionRepository }: InjectedDependencies) { + this.productCollectionRepository_ = productCollectionRepository + } + + async list( + filters: ProductTypes.FilterableProductCollectionProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const queryOptions = buildQuery(filters, config) + queryOptions.where ??= {} + + if (filters.title) { + queryOptions.where["title"] = { $like: filters.title } + } + + return await this.productCollectionRepository_.find(queryOptions) + } +} diff --git a/packages/product/src/services/product-module-service.ts b/packages/product/src/services/product-module-service.ts new file mode 100644 index 0000000000..785b026c0f --- /dev/null +++ b/packages/product/src/services/product-module-service.ts @@ -0,0 +1,143 @@ +import { + ProductCategoryService, + ProductCollectionService, + ProductService, + ProductTagService, + ProductVariantService, +} from "@services" +import { + Product, + ProductCategory, + ProductCollection, + ProductTag, + ProductVariant, +} from "@models" +import { FindConfig, ProductTypes, SharedContext } from "@medusajs/types" + +type InjectedDependencies = { + productService: ProductService + productVariantService: ProductVariantService + productTagService: ProductTagService + productCategoryService: ProductCategoryService + productCollectionService: ProductCollectionService +} + +export default class ProductModuleService< + TProduct = Product, + TProductVariant = ProductVariant, + TProductTag = ProductTag, + TProductCollection = ProductCollection, + TProductCategory = ProductCategory +> implements + ProductTypes.IProductModuleService< + TProduct, + TProductVariant, + TProductTag, + TProductCollection, + TProductCategory + > +{ + protected readonly productService_: ProductService + protected readonly productVariantService: ProductVariantService + protected readonly productCategoryService: ProductCategoryService + protected readonly productTagService: ProductTagService + protected readonly productCollectionService: ProductCollectionService + + constructor({ + productService, + productVariantService, + productTagService, + productCategoryService, + productCollectionService, + }: InjectedDependencies) { + this.productService_ = productService + this.productVariantService = productVariantService + this.productTagService = productTagService + this.productCategoryService = productCategoryService + this.productCollectionService = productCollectionService + } + + async list( + filters: ProductTypes.FilterableProductProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const products = await this.productService_.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(products)) + } + + async listAndCount( + filters: ProductTypes.FilterableProductProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise<[ProductTypes.ProductDTO[], number]> { + const [products, count] = await this.productService_.listAndCount( + filters, + config, + sharedContext + ) + + return [JSON.parse(JSON.stringify(products)), count] + } + + async listVariants( + filters: ProductTypes.FilterableProductVariantProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const variants = await this.productVariantService.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(variants)) + } + + async listTags( + filters: ProductTypes.FilterableProductTagProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const tags = await this.productTagService.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(tags)) + } + + async listCollections( + filters: ProductTypes.FilterableProductCollectionProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const collections = await this.productCollectionService.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(collections)) + } + + async listCategories( + filters: ProductTypes.FilterableProductCategoryProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const categories = await this.productCategoryService.list( + filters, + config, + sharedContext + ) + + return JSON.parse(JSON.stringify(categories)) + } +} diff --git a/packages/product/src/services/product-tag.ts b/packages/product/src/services/product-tag.ts new file mode 100644 index 0000000000..56b42d0ec9 --- /dev/null +++ b/packages/product/src/services/product-tag.ts @@ -0,0 +1,29 @@ +import { ProductTag } from "@models" +import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + productTagRepository: DAL.RepositoryService +} + +export default class ProductTagService { + protected readonly productTagRepository_: DAL.RepositoryService + + constructor({ productTagRepository }: InjectedDependencies) { + this.productTagRepository_ = productTagRepository + } + + async list( + filters: ProductTypes.FilterableProductTagProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const queryOptions = buildQuery(filters, config) + + if (filters.value) { + queryOptions.where["value"] = { $ilike: filters.value } + } + + return await this.productTagRepository_.find(queryOptions) + } +} diff --git a/packages/product/src/services/product-variant.ts b/packages/product/src/services/product-variant.ts new file mode 100644 index 0000000000..729aea47b5 --- /dev/null +++ b/packages/product/src/services/product-variant.ts @@ -0,0 +1,24 @@ +import { ProductVariant } from "@models" +import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + productVariantRepository: DAL.RepositoryService +} + +export default class ProductVariantService { + protected readonly productVariantRepository_: DAL.RepositoryService + + constructor({ productVariantRepository }: InjectedDependencies) { + this.productVariantRepository_ = productVariantRepository + } + + async list( + filters: ProductTypes.FilterableProductVariantProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + const queryOptions = buildQuery(filters, config) + return await this.productVariantRepository_.find(queryOptions) + } +} diff --git a/packages/product/src/services/product.ts b/packages/product/src/services/product.ts new file mode 100644 index 0000000000..2a5666c6cf --- /dev/null +++ b/packages/product/src/services/product.ts @@ -0,0 +1,62 @@ +import { ProductTagService, ProductVariantService } from "@services" +import { Product } from "@models" +import { DAL, FindConfig, ProductTypes, SharedContext } from "@medusajs/types" +import { buildQuery } from "../utils" + +type InjectedDependencies = { + productRepository: DAL.RepositoryService + productVariantService: ProductVariantService + productTagService: ProductTagService +} + +export default class ProductService { + protected readonly productRepository_: DAL.RepositoryService + + constructor({ productRepository }: InjectedDependencies) { + this.productRepository_ = productRepository + } + + async list( + filters: ProductTypes.FilterableProductProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise { + if (filters.category_ids) { + if (Array.isArray(filters.category_ids)) { + filters.categories = { + id: { $in: filters.category_ids }, + } + } else { + filters.categories = { + id: filters.category_ids, + } + } + delete filters.category_ids + } + + const queryOptions = buildQuery(filters, config) + return await this.productRepository_.find(queryOptions) + } + + async listAndCount( + filters: ProductTypes.FilterableProductProps = {}, + config: FindConfig = {}, + sharedContext?: SharedContext + ): Promise<[TEntity[], number]> { + if (filters.category_ids) { + if (Array.isArray(filters.category_ids)) { + filters.categories = { + id: { $in: filters.category_ids }, + } + } else { + filters.categories = { + id: filters.category_ids, + } + } + delete filters.category_ids + } + + const queryOptions = buildQuery(filters, config) + return await this.productRepository_.findAndCount(queryOptions) + } +} diff --git a/packages/product/src/types/index.ts b/packages/product/src/types/index.ts new file mode 100644 index 0000000000..8bd3ca130b --- /dev/null +++ b/packages/product/src/types/index.ts @@ -0,0 +1,18 @@ +import { Constructor, DAL, IEventBusService } from "@medusajs/types" + +export type ProductServiceInitializeOptions = { + database: { + clientUrl: string + schema?: string + driverOptions?: Record + } +} + +export type ProductServiceInitializeCustomDataLayerOptions = { + manager?: any + repositories?: { [key: string]: Constructor } +} + +export type InitializeModuleInjectableDependencies = { + eventBusService?: IEventBusService +} diff --git a/packages/product/src/utils/__tests__/load-database-config.spec.ts b/packages/product/src/utils/__tests__/load-database-config.spec.ts new file mode 100644 index 0000000000..fffe7f8fec --- /dev/null +++ b/packages/product/src/utils/__tests__/load-database-config.spec.ts @@ -0,0 +1,127 @@ +import { loadDatabaseConfig } from "../load-database-config" + +describe("loadDatabaseConfig", function () { + afterEach(() => { + delete process.env.POSTGRES_URL + delete process.env.PRODUCT_POSTGRES_URL + }) + + it("should return the local configuration using the environment variable", function () { + process.env.POSTGRES_URL = "postgres://localhost:5432/medusa" + let config = loadDatabaseConfig() + + expect(config).toEqual({ + clientUrl: process.env.POSTGRES_URL, + driverOptions: { + connection: { + ssl: false, + }, + }, + schema: "", + }) + + delete process.env.POSTGRES_URL + process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa" + config = loadDatabaseConfig() + + expect(config).toEqual({ + clientUrl: process.env.PRODUCT_POSTGRES_URL, + driverOptions: { + connection: { + ssl: false, + }, + }, + schema: "", + }) + }) + + it("should return the remote configuration using the environment variable", function () { + process.env.POSTGRES_URL = "postgres://https://test.com:5432/medusa" + let config = loadDatabaseConfig() + + expect(config).toEqual({ + clientUrl: process.env.POSTGRES_URL, + driverOptions: { + connection: { + ssl: { + rejectUnauthorized: false, + }, + }, + }, + schema: "", + }) + + delete process.env.POSTGRES_URL + process.env.PRODUCT_POSTGRES_URL = "postgres://https://test.com:5432/medusa" + config = loadDatabaseConfig() + + expect(config).toEqual({ + clientUrl: process.env.PRODUCT_POSTGRES_URL, + driverOptions: { + connection: { + ssl: { + rejectUnauthorized: false, + }, + }, + }, + schema: "", + }) + }) + + it("should return the local configuration using the options", function () { + process.env.POSTGRES_URL = "postgres://localhost:5432/medusa" + const options = { + database: { + clientUrl: "postgres://localhost:5432/medusa-test", + }, + } + + const config = loadDatabaseConfig(options) + + expect(config).toEqual({ + clientUrl: options.database.clientUrl, + driverOptions: { + connection: { + ssl: false, + }, + }, + schema: "", + }) + }) + + it("should return the remote configuration using the options", function () { + process.env.POSTGRES_URL = "postgres://localhost:5432/medusa" + const options = { + database: { + clientUrl: "postgres://https://test.com:5432/medusa-test", + }, + } + + const config = loadDatabaseConfig(options) + + expect(config).toEqual({ + clientUrl: options.database.clientUrl, + driverOptions: { + connection: { + ssl: { + rejectUnauthorized: false, + }, + }, + }, + schema: "", + }) + }) + + it("should throw if no clientUrl is provided", function () { + let error + try { + loadDatabaseConfig() + } catch (e) { + error = e + } + + expect(error.message).toEqual( + "No database clientUrl provided. Please provide the clientUrl through the PRODUCT_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function." + ) + }) +}) diff --git a/packages/product/src/utils/create-connection.ts b/packages/product/src/utils/create-connection.ts new file mode 100644 index 0000000000..3ebe2f31b9 --- /dev/null +++ b/packages/product/src/utils/create-connection.ts @@ -0,0 +1,37 @@ +import { resolve } from "path" +import { MikroORM, PostgreSqlDriver } from "@mikro-orm/postgresql" +import { ProductServiceInitializeOptions } from "../types" +import { TSMigrationGenerator } from "@mikro-orm/migrations" + +export async function createConnection( + database: ProductServiceInitializeOptions["database"], + entities: any[] +) { + const orm = await MikroORM.init({ + discovery: { disableDynamicFileAccess: true }, + entities, + debug: process.env.NODE_ENV === "development", + baseDir: process.cwd(), + clientUrl: database.clientUrl, + schema: database.schema ?? "public", + driverOptions: database.driverOptions ?? { + connection: { ssl: true }, + }, + tsNode: process.env.APP_ENV === "development", + type: "postgresql", + migrations: { + path: resolve(__dirname, "../../dist/migrations"), + pathTs: resolve(__dirname, "../../src/migrations"), + glob: "!(*.d).{js,ts}", + disableForeignKeys: false, + silent: false, + dropTables: false, + transactional: true, + allOrNothing: true, + safe: true, + generator: TSMigrationGenerator, + }, + }) + + return orm +} diff --git a/packages/product/src/utils/index.ts b/packages/product/src/utils/index.ts new file mode 100644 index 0000000000..0d51de5829 --- /dev/null +++ b/packages/product/src/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./query" +export * from "./create-connection" +export * from "./load-database-config" diff --git a/packages/product/src/utils/load-database-config.ts b/packages/product/src/utils/load-database-config.ts new file mode 100644 index 0000000000..081c868a8f --- /dev/null +++ b/packages/product/src/utils/load-database-config.ts @@ -0,0 +1,82 @@ +import { + ProductServiceInitializeCustomDataLayerOptions, + ProductServiceInitializeOptions, +} from "../types" +import { MedusaError } from "@medusajs/utils" + +function getEnv(key: string): string { + const value = process.env[`PRODUCT_${key}`] ?? process.env[`${key}`] + return value ?? "" +} + +function isProductServiceInitializeOptions( + obj: unknown +): obj is ProductServiceInitializeOptions { + return !!(obj as ProductServiceInitializeOptions)?.database +} + +function getDefaultDriverOptions( + clientUrl: string +): ProductServiceInitializeOptions["database"]["driverOptions"] { + const localOptions = { + connection: { + ssl: false, + }, + } + + const remoteOptions = { + connection: { + ssl: { + rejectUnauthorized: false, + }, + }, + } + + if (clientUrl) { + return clientUrl.match(/localhost/i) ? localOptions : remoteOptions + } + + return process.env.NODE_ENV?.match(/prod/i) + ? remoteOptions + : process.env.NODE_ENV?.match(/dev/i) + ? localOptions + : {} +} + +/** + * Load the config for the database connection. The options can be retrieved + * through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object. + * @param options + */ +export function loadDatabaseConfig( + options?: + | ProductServiceInitializeOptions + | ProductServiceInitializeCustomDataLayerOptions +): ProductServiceInitializeOptions["database"] { + const clientUrl = getEnv("POSTGRES_URL") + + const database: ProductServiceInitializeOptions["database"] = { + clientUrl: getEnv("POSTGRES_URL"), + schema: getEnv("POSTGRES_SCHEMA") ?? "public", + driverOptions: JSON.parse( + getEnv("POSTGRES_DRIVER_OPTIONS") || + JSON.stringify(getDefaultDriverOptions(clientUrl)) + ), + } + + if (isProductServiceInitializeOptions(options)) { + database.clientUrl = options.database.clientUrl ?? database.clientUrl + database.schema = options.database.schema ?? database.schema + database.driverOptions = + options.database.driverOptions ?? + getDefaultDriverOptions(database.clientUrl) + } + + if (!database.clientUrl) { + throw new MedusaError( + MedusaError.Types.INVALID_ARGUMENT, + "No database clientUrl provided. Please provide the clientUrl through the PRODUCT_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function." + ) + } + return database +} diff --git a/packages/product/src/utils/query/index.ts b/packages/product/src/utils/query/index.ts new file mode 100644 index 0000000000..2efce91f4b --- /dev/null +++ b/packages/product/src/utils/query/index.ts @@ -0,0 +1,44 @@ +/** + * Move to a new build query utils + */ +import { DAL, FindConfig } from "@medusajs/types" +import { isObject } from "@medusajs/utils" + +export function deduplicateIfNecessary(collection: T | T[]) { + return Array.isArray(collection) ? [...new Set(collection)] : collection +} + +export function buildQuery( + filters: Record = {}, + config: FindConfig = {} +): DAL.FindOptions { + const where: DAL.FilterQuery = {} + buildWhere(filters, where) + + const findOptions: DAL.OptionsQuery = { + populate: config.relations ?? [], + fields: config.select, + limit: config.take, + offset: config.skip, + } as any + + return { where, options: findOptions } +} + +function buildWhere(filters: Record = {}, where = {}) { + for (let [prop, value] of Object.entries(filters)) { + if (Array.isArray(value)) { + value = deduplicateIfNecessary(value) + where[prop] = ["$in", "$nin"].includes(prop) ? value : { $in: value } + continue + } + + if (isObject(value)) { + where[prop] = {} + buildWhere(value, where[prop]) + continue + } + + where[prop] = value + } +} diff --git a/packages/product/tsconfig.json b/packages/product/tsconfig.json new file mode 100644 index 0000000000..213e38fc55 --- /dev/null +++ b/packages/product/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": false, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true, // to use ES5 specific tooling + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"] + } + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/product/tsconfig.spec.json b/packages/product/tsconfig.spec.json new file mode 100644 index 0000000000..b887bbfa39 --- /dev/null +++ b/packages/product/tsconfig.spec.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "integration-tests"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/stock-location/src/index.ts b/packages/stock-location/src/index.ts index 88d1dd53d3..d3159f3c9a 100644 --- a/packages/stock-location/src/index.ts +++ b/packages/stock-location/src/index.ts @@ -1,24 +1,8 @@ -import { ModuleExports } from "@medusajs/modules-sdk" -import loadConnection from "./loaders/connection" -import migrations from "./migrations" import { revertMigration, runMigrations } from "./migrations/run-migration" -import * as StockLocationModels from "./models" -import StockLocationService from "./services/stock-location" - -const service = StockLocationService -const loaders = [loadConnection] -const models = Object.values(StockLocationModels) - -const moduleDefinition: ModuleExports = { - service, - migrations, - loaders, - models, - runMigrations, - revertMigration, -} +import { moduleDefinition } from "./module-definition" export default moduleDefinition + export * from "./initialize" export { revertMigration, runMigrations } from "./migrations/run-migration" export * from "./types" diff --git a/packages/stock-location/src/initialize/index.ts b/packages/stock-location/src/initialize/index.ts index def606cfd6..152f6187d2 100644 --- a/packages/stock-location/src/initialize/index.ts +++ b/packages/stock-location/src/initialize/index.ts @@ -6,6 +6,7 @@ import { } from "@medusajs/modules-sdk" import { IEventBusService, IStockLocationService } from "@medusajs/types" import { StockLocationServiceInitializeOptions } from "../types" +import { moduleDefinition } from "../module-definition" export const initialize = async ( options: StockLocationServiceInitializeOptions | ExternalModuleDeclaration, @@ -18,6 +19,7 @@ export const initialize = async ( serviceKey, "@medusajs/stock-location", options as InternalModuleDeclaration | ExternalModuleDeclaration, + moduleDefinition, injectedDependencies ) diff --git a/packages/stock-location/src/module-definition.ts b/packages/stock-location/src/module-definition.ts new file mode 100644 index 0000000000..6fdf83e2ed --- /dev/null +++ b/packages/stock-location/src/module-definition.ts @@ -0,0 +1,19 @@ +import StockLocationService from "./services/stock-location" +import loadConnection from "./loaders/connection" +import * as StockLocationModels from "./models" +import { ModuleExports } from "@medusajs/types" +import migrations from "./migrations" +import { revertMigration, runMigrations } from "./migrations/run-migration" + +const service = StockLocationService +const loaders = [loadConnection] +const models = Object.values(StockLocationModels) + +export const moduleDefinition: ModuleExports = { + service, + migrations, + loaders, + models, + runMigrations, + revertMigration, +} diff --git a/packages/types/src/bundles.ts b/packages/types/src/bundles.ts index 47c58e68fa..95114a9785 100644 --- a/packages/types/src/bundles.ts +++ b/packages/types/src/bundles.ts @@ -2,7 +2,9 @@ export * as CacheTypes from "./cache" export * as CommonTypes from "./common" export * as EventBusTypes from "./event-bus" export * as InventoryTypes from "./inventory" +export * as ProductTypes from "./product" export * as ModulesSdkTypes from "./modules-sdk" export * as SearchTypes from "./search" export * as StockLocationTypes from "./stock-location" export * as TransactionBaseTypes from "./transaction-base" +export * as DAL from "./dal" diff --git a/packages/types/src/common/common.ts b/packages/types/src/common/common.ts index 265a07f640..dd47065706 100644 --- a/packages/types/src/common/common.ts +++ b/packages/types/src/common/common.ts @@ -126,6 +126,8 @@ export type DeleteResponse = { } export interface EmptyQueryParams {} +// TODO: Build a tree repository options from this +export interface RepositoryTransformOptions {} export interface DateComparisonOperator { lt?: Date diff --git a/packages/types/src/dal/index.ts b/packages/types/src/dal/index.ts new file mode 100644 index 0000000000..fd52c3e0ae --- /dev/null +++ b/packages/types/src/dal/index.ts @@ -0,0 +1,24 @@ +import { Dictionary, FilterQuery, Order } from "./utils" + +export { FilterQuery } from "./utils" +export interface BaseFilterable { + $and?: T + $or?: T +} + +export interface OptionsQuery { + populate?: string[] + orderBy?: Order | Order[] + limit?: number + offset?: number + fields?: string[] + groupBy?: string | string[] + filters?: Dictionary | string[] | boolean +} + +export type FindOptions = { + where: FilterQuery + options?: OptionsQuery +} + +export * from "./repository-service" diff --git a/packages/types/src/dal/repository-service.ts b/packages/types/src/dal/repository-service.ts new file mode 100644 index 0000000000..33817b9988 --- /dev/null +++ b/packages/types/src/dal/repository-service.ts @@ -0,0 +1,12 @@ +import { FindOptions } from "./index" +import { RepositoryTransformOptions } from "../common" + +/** + * Data access layer (DAL) interface to implements for any repository service. + * This layer helps to separate the business logic (service layer) from accessing the + * ORM directly and allows to switch to another ORM without changing the business logic. + */ +export interface RepositoryService { + find(options?: FindOptions, transformOptions?: RepositoryTransformOptions): Promise + findAndCount(options?: FindOptions, transformOptions?: RepositoryTransformOptions): Promise<[T[], number]> +} diff --git a/packages/types/src/dal/utils.ts b/packages/types/src/dal/utils.ts new file mode 100644 index 0000000000..5794d3ae04 --- /dev/null +++ b/packages/types/src/dal/utils.ts @@ -0,0 +1,127 @@ +type ExpandProperty = T extends (infer U)[] ? NonNullable : NonNullable + +export type Dictionary = { + [k: string]: T +} + +type Query = T extends object + ? T extends Scalar + ? never + : FilterQuery + : FilterValue + +type ExpandScalar = + | null + | (T extends string ? string | RegExp : T extends Date ? Date | string : T) +type Scalar = + | boolean + | number + | string + | bigint + | symbol + | Date + | RegExp + | Buffer + | { + toHexString(): string + } + +type ExcludeFunctions = T[K] extends Function + ? never + : K extends symbol + ? never + : K + +type ReadonlyPrimary = T extends any[] ? Readonly : T + +declare const PrimaryKeyType: unique symbol +type Primary = T extends { + [PrimaryKeyType]?: infer PK +} + ? ReadonlyPrimary + : T extends { + _id?: infer PK + } + ? ReadonlyPrimary | string + : T extends { + uuid?: infer PK + } + ? ReadonlyPrimary + : T extends { + id?: infer PK + } + ? ReadonlyPrimary + : never + +export type OperatorMap = { + $and?: Query[] + $or?: Query[] + $eq?: ExpandScalar | ExpandScalar[] + $ne?: ExpandScalar + $in?: ExpandScalar[] + $nin?: ExpandScalar[] + $not?: Query + $gt?: ExpandScalar + $gte?: ExpandScalar + $lt?: ExpandScalar + $lte?: ExpandScalar + $like?: string + $re?: string + $ilike?: string + $fulltext?: string + $overlap?: string[] + $contains?: string[] + $contained?: string[] + $exists?: boolean +} + +type FilterValue2 = T | ExpandScalar | Primary +type FilterValue = + | OperatorMap> + | FilterValue2 + | FilterValue2[] + | null + +type PrevLimit = [never, 1, 2, 3] + +export type FilterQuery = Prev extends never + ? never + : { + [Key in keyof T]?: T[Key] extends + | boolean + | number + | string + | bigint + | symbol + | Date + ? T[Key] | OperatorMap + : T[Key] extends infer U + ? U extends { [x: number]: infer V } + ? V extends object + ? FilterQuery, PrevLimit[Prev]> + : never + : never + : never + } + +declare enum QueryOrder { + ASC = "ASC", + DESC = "DESC", + asc = "asc", + desc = "desc", +} + +type QueryOrderKeysFlat = QueryOrder | 1 | -1 | keyof typeof QueryOrder +type QueryOrderKeys = QueryOrderKeysFlat | QueryOrderMap +type QueryOrderMap = { + [K in keyof T as ExcludeFunctions]?: QueryOrderKeys< + ExpandProperty + > +} + +export type Order = { + [key in keyof T]?: + | "ASC" + | "DESC" + | Order ? T[key][0] : T[key]> +} diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 8ae9e7d579..63df9857f3 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -4,7 +4,10 @@ export * from "./common" export * from "./event-bus" export * from "./inventory" export * from "./modules-sdk" +export * from "./product" +export * from "./product-category" export * from "./search" export * from "./shared-context" export * from "./stock-location" export * from "./transaction-base" +export * from "./dal" diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 81573630c6..13f20a065e 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -1,5 +1,6 @@ import { Logger as _Logger } from "winston" import { MedusaContainer } from "../common/medusa-container" + export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" @@ -39,7 +40,7 @@ export type InternalModuleDeclaration = { export type ExternalModuleDeclaration = { scope: MODULE_SCOPE.EXTERNAL - server: { + server?: { type: "http" url: string keepAlive: boolean @@ -52,6 +53,7 @@ export type ModuleResolution = { options?: Record dependencies?: string[] moduleDeclaration?: InternalModuleDeclaration | ExternalModuleDeclaration + moduleExports?: ModuleExports } export type ModuleDefinition = { diff --git a/packages/types/src/product-category/index.ts b/packages/types/src/product-category/index.ts new file mode 100644 index 0000000000..b1d08c5baf --- /dev/null +++ b/packages/types/src/product-category/index.ts @@ -0,0 +1 @@ +export * from "./repository" diff --git a/packages/types/src/product-category/repository.ts b/packages/types/src/product-category/repository.ts new file mode 100644 index 0000000000..4fda420340 --- /dev/null +++ b/packages/types/src/product-category/repository.ts @@ -0,0 +1,5 @@ +import { RepositoryTransformOptions } from '../common' + +export interface ProductCategoryTransformOptions extends RepositoryTransformOptions { + includeDescendantsTree?: boolean +} diff --git a/packages/types/src/product/common.ts b/packages/types/src/product/common.ts new file mode 100644 index 0000000000..b44c8f9df7 --- /dev/null +++ b/packages/types/src/product/common.ts @@ -0,0 +1,166 @@ +import { BaseFilterable } from "../dal" +import { OperatorMap } from "../dal/utils" + +export enum ProductStatus { + DRAFT = "draft", + PROPOSED = "proposed", + PUBLISHED = "published", + REJECTED = "rejected", +} + +/** + * DTO in and out of the module (module API) + */ + +export interface ProductDTO { + id: string + title: string + handle?: string | null + subtitle?: string | null + description?: string | null + is_giftcard: boolean + status: ProductStatus + thumbnail?: string | null + weight?: number | null + length?: number | null + height?: number | null + origin_country?: string | null + hs_code?: string | null + mid_code?: string | null + material?: string | null + collection: ProductCollectionDTO + categories?: ProductCategoryDTO[] | null + type: ProductTypeDTO[] + tags: ProductTagDTO[] + variants: ProductVariantDTO[] + options: ProductOptionDTO[] + discountable?: boolean + external_id?: string | null + created_at?: string | Date + updated_at?: string | Date + deleted_at?: string | Date +} + +export interface ProductVariantDTO { + id: string + title: string + sku?: string | null + barcode?: string | null + ean?: string | null + upc?: string | null + inventory_quantity: number + allow_backorder?: boolean + manage_inventory?: boolean + hs_code?: string | null + origin_country?: string | null + mid_code?: string | null + material?: string | null + weight?: number | null + length?: number | null + height?: number | null + width?: number | null + options: ProductOptionValueDTO + metadata?: Record | null + product: ProductDTO + variant_rank?: number | null + created_at: string | Date + updated_at: string | Date + deleted_at: string | Date +} + +export interface ProductCategoryDTO { + id: string + name: string + description?: string + handle?: string + is_active?: boolean + is_internal?: boolean + rank?: number + parent_category?: ProductCategoryDTO + category_children: ProductCategoryDTO[] + created_at: string | Date + updated_at: string | Date +} + +export interface ProductTagDTO { + id: string + value: string + metadata?: Record | null + products: ProductDTO[] +} + +export interface ProductCollectionDTO { + id: string + title: string + handle: string + metadata?: Record | null + deleted_at?: string | Date +} + +export interface ProductTypeDTO { + id: string + value: string + metadata?: Record | null + deleted_at?: string | Date +} + +export interface ProductOptionDTO { + id: string + title: string + product: ProductDTO + values: ProductOptionValueDTO[] + metadata?: Record | null + deleted_at?: string | Date +} + +export interface ProductOptionValueDTO { + id: string + value: string + option: ProductOptionDTO + variant: ProductVariantDTO + metadata?: Record | null + deleted_at?: string | Date +} + +/** + * Filters/Config (module API input filters and config) + */ +export interface FilterableProductProps + extends BaseFilterable { + handle?: string | string[] + id?: string | string[] + tags?: { value?: string[] } + categories?: { + id?: string | string[] | OperatorMap + } + category_ids?: string | string[] | OperatorMap +} + +export interface FilterableProductTagProps + extends BaseFilterable { + id?: string | string[] + value?: string +} + +export interface FilterableProductCollectionProps + extends BaseFilterable { + id?: string | string[] + title?: string +} + +export interface FilterableProductVariantProps + extends BaseFilterable { + id?: string | string[] + sku?: string | string[] + options?: { id?: string[] } +} + +export interface FilterableProductCategoryProps + extends BaseFilterable { + id?: string | string[] + parent_category_id?: string | string[] | null + handle?: string | string[] + is_active?: boolean + is_internal?: boolean + include_descendants_tree?: boolean +} diff --git a/packages/types/src/product/index.ts b/packages/types/src/product/index.ts new file mode 100644 index 0000000000..1aa665fd54 --- /dev/null +++ b/packages/types/src/product/index.ts @@ -0,0 +1,2 @@ +export * from "./service" +export * from "./common" diff --git a/packages/types/src/product/service.ts b/packages/types/src/product/service.ts new file mode 100644 index 0000000000..0f74f34876 --- /dev/null +++ b/packages/types/src/product/service.ts @@ -0,0 +1,58 @@ +import { + FilterableProductCategoryProps, + FilterableProductCollectionProps, + FilterableProductProps, + FilterableProductTagProps, + FilterableProductVariantProps, + ProductCategoryDTO, + ProductCollectionDTO, + ProductDTO, + ProductTagDTO, + ProductVariantDTO, +} from "./common" +import { FindConfig } from "../common" +import { SharedContext } from "../shared-context" + +export interface IProductModuleService< + TProduct = any, + TProductVariant = any, + TProductTag = any, + TProductCollection = any, + TProductCategory = any +> { + list( + filter: FilterableProductProps, + config?: FindConfig, + context?: SharedContext + ): Promise + + listAndCount( + filter: FilterableProductProps, + config?: FindConfig, + context?: SharedContext + ): Promise<[ProductDTO[], number]> + + listTags( + filter: FilterableProductTagProps, + config?: FindConfig, + context?: SharedContext + ): Promise + + listVariants( + filter: FilterableProductVariantProps, + config?: FindConfig, + context?: SharedContext + ): Promise + + listCollections( + filter: FilterableProductCollectionProps, + config?: FindConfig, + context?: SharedContext + ): Promise + + listCategories( + filters: FilterableProductCategoryProps, + config?: FindConfig, + sharedContext?: SharedContext + ): Promise +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index af7af561a5..beb84ecb81 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -1,3 +1,4 @@ +export * from "./build-query" export * from "./errors" export * from "./generate-entity-id" export * from "./get-config-file" @@ -6,9 +7,13 @@ export * from "./is-defined" export * from "./is-email" export * from "./is-object" export * from "./is-string" -export * from "./object-to-string-path" +export * from "./lower-case-first" export * from "./medusa-container" +export * from "./object-to-string-path" export * from "./set-metadata" +export * from "./simple-hash" export * from "./wrap-handler" +export * from "./to-kebab-case" +export * from "./stringify-circular" export * from "./build-query" export * from "./handle-postgres-database-error" diff --git a/packages/utils/src/common/lower-case-first.ts b/packages/utils/src/common/lower-case-first.ts new file mode 100644 index 0000000000..6c033d8884 --- /dev/null +++ b/packages/utils/src/common/lower-case-first.ts @@ -0,0 +1,3 @@ +export function lowerCaseFirst(str: string): string { + return str.charAt(0).toLowerCase() + str.slice(1) +} diff --git a/packages/utils/src/common/simple-hash.ts b/packages/utils/src/common/simple-hash.ts new file mode 100644 index 0000000000..448037d483 --- /dev/null +++ b/packages/utils/src/common/simple-hash.ts @@ -0,0 +1,8 @@ +// DJB2 hash function +export function simpleHash(text: string): string { + let hash = 5381 + for (let i = 0; i < text.length; i++) { + hash = (hash << 5) + hash + text.charCodeAt(i) + } + return hash.toString(16) +} diff --git a/packages/utils/src/common/stringify-circular.ts b/packages/utils/src/common/stringify-circular.ts new file mode 100644 index 0000000000..d65dbbf1da --- /dev/null +++ b/packages/utils/src/common/stringify-circular.ts @@ -0,0 +1,62 @@ +const isObject = (value: any): value is object => + typeof value === "object" && + value != null && + !(value instanceof Boolean) && + !(value instanceof Date) && + !(value instanceof Number) && + !(value instanceof RegExp) && + !(value instanceof String) + +const isPrimitive = (val) => { + return val !== Object(val) +} + +function decycle(object: any, replacer?: Function | null) { + const objects = new WeakMap() + + function deepCopy(value, path) { + let oldPath + let newObj + + if (replacer != null) { + value = replacer(value) + } + + if (isObject(value)) { + oldPath = objects.get(value) + if (oldPath !== undefined) { + return { $ref: oldPath } + } + + objects.set(value, path) + + if (Array.isArray(value)) { + newObj = [] + value.forEach((el, idx) => { + newObj[idx] = deepCopy(el, path + "[" + idx + "]") + }) + } else { + newObj = {} + Object.keys(value).forEach((name) => { + newObj[name] = deepCopy( + value[name], + path + "[" + JSON.stringify(name) + "]" + ) + }) + } + return newObj + } + + return !isPrimitive(value) ? value + "" : value + } + + return deepCopy(object, "$") +} + +export function stringifyCircular( + object: any, + replacer?: Function | null, + space?: number +): string { + return JSON.stringify(decycle(object, replacer), null, space) +} diff --git a/packages/utils/src/common/to-kebab-case.ts b/packages/utils/src/common/to-kebab-case.ts new file mode 100644 index 0000000000..6b42ee3985 --- /dev/null +++ b/packages/utils/src/common/to-kebab-case.ts @@ -0,0 +1,5 @@ +export const kebabCase = (string) => + string + .replace(/([a-z])([A-Z])/g, "$1-$2") + .replace(/[\s_]+/g, "-") + .toLowerCase() diff --git a/yarn.lock b/yarn.lock index 874de958d9..4996135ea4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4112,6 +4112,20 @@ __metadata: languageName: node linkType: hard +"@isaacs/cliui@npm:^8.0.2": + version: 8.0.2 + resolution: "@isaacs/cliui@npm:8.0.2" + dependencies: + string-width: ^5.1.2 + string-width-cjs: "npm:string-width@^4.2.0" + strip-ansi: ^7.0.1 + strip-ansi-cjs: "npm:strip-ansi@^6.0.1" + wrap-ansi: ^8.1.0 + wrap-ansi-cjs: "npm:wrap-ansi@^7.0.0" + checksum: b1bf42535d49f11dc137f18d5e4e63a28c5569de438a221c369483731e9dac9fb797af554e8bf02b6192d1e5eba6e6402cf93900c3d0ac86391d00d04876789e + languageName: node + linkType: hard + "@istanbuljs/load-nyc-config@npm:^1.0.0": version: 1.1.0 resolution: "@istanbuljs/load-nyc-config@npm:1.1.0" @@ -4132,6 +4146,17 @@ __metadata: languageName: node linkType: hard +"@jercle/yargonaut@npm:1.1.5": + version: 1.1.5 + resolution: "@jercle/yargonaut@npm:1.1.5" + dependencies: + chalk: ^4.1.2 + figlet: ^1.5.2 + parent-require: ^1.0.0 + checksum: 7a3a891ebafc97c78aa38354a2bb2c2b2a467f03fd653b09d2ae70420582330917ffb35869d9cbe48396c5a18cef34cdda5704a2df0b0a6383a75297a3661f66 + languageName: node + linkType: hard + "@jest/console@npm:^25.5.0": version: 25.5.0 resolution: "@jest/console@npm:25.5.0" @@ -6225,7 +6250,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/modules-sdk@1.8.7, @medusajs/modules-sdk@workspace:packages/modules-sdk": +"@medusajs/modules-sdk@1.8.7, @medusajs/modules-sdk@^1.8.3, @medusajs/modules-sdk@workspace:packages/modules-sdk": version: 0.0.0-use.local resolution: "@medusajs/modules-sdk@workspace:packages/modules-sdk" dependencies: @@ -6233,9 +6258,7 @@ __metadata: "@medusajs/utils": 1.9.0 awilix: ^8.0.0 cross-env: ^5.2.1 - glob: 7.1.6 jest: ^25.5.4 - medusa-telemetry: ^0.0.16 resolve-cwd: ^3.0.0 ts-jest: ^25.5.1 typescript: ^4.4.4 @@ -6288,6 +6311,36 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/product@workspace:packages/product": + version: 0.0.0-use.local + resolution: "@medusajs/product@workspace:packages/product" + dependencies: + "@medusajs/modules-sdk": ^1.8.3 + "@medusajs/types": "*" + "@medusajs/utils": ^1.8.2 + "@mikro-orm/cli": 5.7.4 + "@mikro-orm/core": 5.7.4 + "@mikro-orm/migrations": 5.7.4 + "@mikro-orm/postgresql": 5.7.4 + awilix: ^8.0.0 + cross-env: ^5.2.1 + dotenv: ^16.1.4 + jest: ^25.5.4 + lodash: ^4.17.21 + medusa-test-utils: ^1.1.40 + pg-god: ^1.0.12 + rimraf: ^5.0.0 + ts-jest: ^25.5.1 + ts-node: ^10.9.1 + tsc-alias: ^1.8.6 + typescript: ^4.4.4 + bin: + medusa-product-migrations-down: dist/scripts/bin/run-migration-down.js + medusa-product-migrations-up: dist/scripts/bin/run-migration-up.js + medusa-product-seed: dist/scripts/bin/run-seed.js + languageName: unknown + linkType: soft + "@medusajs/stock-location@workspace:packages/stock-location": version: 0.0.0-use.local resolution: "@medusajs/stock-location@workspace:packages/stock-location" @@ -6304,7 +6357,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/types@1.8.7, @medusajs/types@workspace:packages/types": +"@medusajs/types@*, @medusajs/types@1.8.7, @medusajs/types@workspace:packages/types": version: 0.0.0-use.local resolution: "@medusajs/types@workspace:packages/types" dependencies: @@ -6316,7 +6369,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/utils@1.9.0, @medusajs/utils@workspace:packages/utils": +"@medusajs/utils@1.9.0, @medusajs/utils@^1.8.2, @medusajs/utils@workspace:packages/utils": version: 0.0.0-use.local resolution: "@medusajs/utils@workspace:packages/utils" dependencies: @@ -6338,6 +6391,223 @@ __metadata: languageName: node linkType: hard +"@mikro-orm/cli@npm:5.7.4": + version: 5.7.4 + resolution: "@mikro-orm/cli@npm:5.7.4" + dependencies: + "@jercle/yargonaut": 1.1.5 + "@mikro-orm/core": ~5.7.4 + "@mikro-orm/knex": ~5.7.4 + fs-extra: 11.1.1 + tsconfig-paths: 4.2.0 + yargs: 17.7.2 + peerDependencies: + "@mikro-orm/better-sqlite": ^5.0.0 + "@mikro-orm/entity-generator": ^5.0.0 + "@mikro-orm/mariadb": ^5.0.0 + "@mikro-orm/migrations": ^5.0.0 + "@mikro-orm/migrations-mongodb": ^5.0.0 + "@mikro-orm/mongodb": ^5.0.0 + "@mikro-orm/mysql": ^5.0.0 + "@mikro-orm/postgresql": ^5.0.0 + "@mikro-orm/seeder": ^5.0.0 + "@mikro-orm/sqlite": ^5.0.0 + peerDependenciesMeta: + "@mikro-orm/better-sqlite": + optional: true + "@mikro-orm/entity-generator": + optional: true + "@mikro-orm/mariadb": + optional: true + "@mikro-orm/migrations": + optional: true + "@mikro-orm/migrations-mongodb": + optional: true + "@mikro-orm/mongodb": + optional: true + "@mikro-orm/mysql": + optional: true + "@mikro-orm/postgresql": + optional: true + "@mikro-orm/seeder": + optional: true + "@mikro-orm/sqlite": + optional: true + bin: + mikro-orm: cli.js + mikro-orm-esm: esm.js + checksum: f5d48acf7255dde32a0ef9511788cb597e44247d017721fdad5d728cbbcd7c6ae783fa6c494d356a6bb22eb79d6c769d4e6663e3547d3777c7adf2fa62031d0b + languageName: node + linkType: hard + +"@mikro-orm/core@npm:5.7.4": + version: 5.7.4 + resolution: "@mikro-orm/core@npm:5.7.4" + dependencies: + acorn-loose: 8.3.0 + acorn-walk: 8.2.0 + dotenv: 16.0.3 + fs-extra: 11.1.1 + globby: 11.1.0 + mikro-orm: ~5.7.4 + reflect-metadata: 0.1.13 + peerDependencies: + "@mikro-orm/better-sqlite": ^5.0.0 + "@mikro-orm/entity-generator": ^5.0.0 + "@mikro-orm/mariadb": ^5.0.0 + "@mikro-orm/migrations": ^5.0.0 + "@mikro-orm/migrations-mongodb": ^5.0.0 + "@mikro-orm/mongodb": ^5.0.0 + "@mikro-orm/mysql": ^5.0.0 + "@mikro-orm/postgresql": ^5.0.0 + "@mikro-orm/seeder": ^5.0.0 + "@mikro-orm/sqlite": ^5.0.0 + peerDependenciesMeta: + "@mikro-orm/better-sqlite": + optional: true + "@mikro-orm/entity-generator": + optional: true + "@mikro-orm/mariadb": + optional: true + "@mikro-orm/migrations": + optional: true + "@mikro-orm/migrations-mongodb": + optional: true + "@mikro-orm/mongodb": + optional: true + "@mikro-orm/mysql": + optional: true + "@mikro-orm/postgresql": + optional: true + "@mikro-orm/seeder": + optional: true + "@mikro-orm/sqlite": + optional: true + checksum: e1d5b3bf71a83c4ab8cf82caa944899a39738f59624ea6bb4a136fef3c3c7056f424bbc165a22adb6278dd6f8eb23d12c30553803fbe3e501b852b26d41cd2f2 + languageName: node + linkType: hard + +"@mikro-orm/core@npm:~5.7.4": + version: 5.7.11 + resolution: "@mikro-orm/core@npm:5.7.11" + dependencies: + acorn-loose: 8.3.0 + acorn-walk: 8.2.0 + dotenv: 16.1.3 + fs-extra: 11.1.1 + globby: 11.1.0 + mikro-orm: ~5.7.11 + reflect-metadata: 0.1.13 + peerDependencies: + "@mikro-orm/better-sqlite": ^5.0.0 + "@mikro-orm/entity-generator": ^5.0.0 + "@mikro-orm/mariadb": ^5.0.0 + "@mikro-orm/migrations": ^5.0.0 + "@mikro-orm/migrations-mongodb": ^5.0.0 + "@mikro-orm/mongodb": ^5.0.0 + "@mikro-orm/mysql": ^5.0.0 + "@mikro-orm/postgresql": ^5.0.0 + "@mikro-orm/seeder": ^5.0.0 + "@mikro-orm/sqlite": ^5.0.0 + peerDependenciesMeta: + "@mikro-orm/better-sqlite": + optional: true + "@mikro-orm/entity-generator": + optional: true + "@mikro-orm/mariadb": + optional: true + "@mikro-orm/migrations": + optional: true + "@mikro-orm/migrations-mongodb": + optional: true + "@mikro-orm/mongodb": + optional: true + "@mikro-orm/mysql": + optional: true + "@mikro-orm/postgresql": + optional: true + "@mikro-orm/seeder": + optional: true + "@mikro-orm/sqlite": + optional: true + checksum: d51836a4bd24f64a28a52d0b5fb94f23d9d8f50352b818cfb91b92c256794b9d6f421a4652ff341ee072082bf0680b9daeb929ebbc274dafd94e5159c476d6f2 + languageName: node + linkType: hard + +"@mikro-orm/knex@npm:~5.7.4": + version: 5.7.11 + resolution: "@mikro-orm/knex@npm:5.7.11" + dependencies: + fs-extra: 11.1.1 + knex: 2.4.2 + sqlstring: 2.3.3 + peerDependencies: + "@mikro-orm/core": ^5.0.0 + "@mikro-orm/entity-generator": ^5.0.0 + "@mikro-orm/migrations": ^5.0.0 + better-sqlite3: "*" + mssql: "*" + mysql: "*" + mysql2: "*" + pg: "*" + sqlite3: "*" + peerDependenciesMeta: + "@mikro-orm/entity-generator": + optional: true + "@mikro-orm/migrations": + optional: true + better-sqlite3: + optional: true + mssql: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + sqlite3: + optional: true + checksum: 946296a3f2fc5ae9f51875789574e23546696f9c34f0ef0584027e0cc3254b82568754a7c86387c1a3f0beb34ada89ca0e60f72e98bbbdc2c48dddc7aa909428 + languageName: node + linkType: hard + +"@mikro-orm/migrations@npm:5.7.4": + version: 5.7.4 + resolution: "@mikro-orm/migrations@npm:5.7.4" + dependencies: + "@mikro-orm/knex": ~5.7.4 + fs-extra: 11.1.1 + knex: 2.4.2 + umzug: 3.2.1 + peerDependencies: + "@mikro-orm/core": ^5.0.0 + checksum: d87c031760209a4f2bda67a0d884d18714e6ada88eba906dd2f11936530745bcc82912765cad458fa58499934c2c14845aa6eeaf6afdeebd43910a2876162325 + languageName: node + linkType: hard + +"@mikro-orm/postgresql@npm:5.7.4": + version: 5.7.4 + resolution: "@mikro-orm/postgresql@npm:5.7.4" + dependencies: + "@mikro-orm/knex": ~5.7.4 + pg: 8.10.0 + peerDependencies: + "@mikro-orm/core": ^5.0.0 + "@mikro-orm/entity-generator": ^5.0.0 + "@mikro-orm/migrations": ^5.0.0 + "@mikro-orm/seeder": ^5.0.0 + peerDependenciesMeta: + "@mikro-orm/entity-generator": + optional: true + "@mikro-orm/migrations": + optional: true + "@mikro-orm/seeder": + optional: true + checksum: 07bc1984b0d8e4799b7e4c3c4e6c04cbeac47e235dc9e56e8f5c3bd6e5a4a71a059b6961fc3a0c285c703a3184d8440bdd4aacdbbc1f7b22f3a82dca145a7e56 + languageName: node + linkType: hard + "@mischnic/json-sourcemap@npm:^0.1.0": version: 0.1.0 resolution: "@mischnic/json-sourcemap@npm:0.1.0" @@ -7220,6 +7490,13 @@ __metadata: languageName: node linkType: hard +"@pkgjs/parseargs@npm:^0.11.0": + version: 0.11.0 + resolution: "@pkgjs/parseargs@npm:0.11.0" + checksum: 5bd7576bb1b38a47a7fc7b51ac9f38748e772beebc56200450c4a817d712232b8f1d3ef70532c80840243c657d491cf6a6be1e3a214cff907645819fdc34aadd + languageName: node + linkType: hard + "@pmmmwh/react-refresh-webpack-plugin@npm:^0.4.3": version: 0.4.3 resolution: "@pmmmwh/react-refresh-webpack-plugin@npm:0.4.3" @@ -8207,6 +8484,18 @@ __metadata: languageName: node linkType: hard +"@rushstack/ts-command-line@npm:^4.12.2": + version: 4.14.0 + resolution: "@rushstack/ts-command-line@npm:4.14.0" + dependencies: + "@types/argparse": 1.0.38 + argparse: ~1.0.9 + colors: ~1.2.1 + string-argv: ~0.3.1 + checksum: f0672a5ed71c11873bf43b221209069b013f94b989497c2383285664083289a182f4d57ce27055fa6034c31d3c956ed7a5a2a7e921126f31d8f580fec3cf3fa4 + languageName: node + linkType: hard + "@segment/analytics-core@npm:1.2.2": version: 1.2.2 resolution: "@segment/analytics-core@npm:1.2.2" @@ -11050,6 +11339,13 @@ __metadata: languageName: node linkType: hard +"@types/argparse@npm:1.0.38": + version: 1.0.38 + resolution: "@types/argparse@npm:1.0.38" + checksum: 4fc892da5df16923f48180da2d1f4562fa8b0507cf636b24780444fa0a1d7321d4dc0c0ecbee6152968823f5a2ae0d321b4f8c705a489bf1ae1245bdeb0868fd + languageName: node + linkType: hard + "@types/aria-query@npm:^5.0.1": version: 5.0.1 resolution: "@types/aria-query@npm:5.0.1" @@ -13195,6 +13491,15 @@ __metadata: languageName: node linkType: hard +"acorn-loose@npm:8.3.0": + version: 8.3.0 + resolution: "acorn-loose@npm:8.3.0" + dependencies: + acorn: ^8.5.0 + checksum: 970f790a584a2f1703a04711cdc588f424fd7bc2fb37ad8e0b9d6ceaf9c8c6a77f9ce102ce5250259fc96aedbdf346546ed1b496299bc13ed4d1b6fdb2d92f61 + languageName: node + linkType: hard + "acorn-node@npm:^1.8.2": version: 1.8.2 resolution: "acorn-node@npm:1.8.2" @@ -13206,6 +13511,13 @@ __metadata: languageName: node linkType: hard +"acorn-walk@npm:8.2.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": + version: 8.2.0 + resolution: "acorn-walk@npm:8.2.0" + checksum: dbe92f5b2452c93e960c5594e666dd1fae141b965ff2cb4a1e1d0381e3e4db4274c5ce4ffa3d681a86ca2a8d4e29d5efc0670a08e23fd2800051ea387df56ca2 + languageName: node + linkType: hard + "acorn-walk@npm:^6.0.1": version: 6.2.0 resolution: "acorn-walk@npm:6.2.0" @@ -13220,13 +13532,6 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0": - version: 8.2.0 - resolution: "acorn-walk@npm:8.2.0" - checksum: dbe92f5b2452c93e960c5594e666dd1fae141b965ff2cb4a1e1d0381e3e4db4274c5ce4ffa3d681a86ca2a8d4e29d5efc0670a08e23fd2800051ea387df56ca2 - languageName: node - linkType: hard - "acorn@npm:^6.0.1, acorn@npm:^6.4.1": version: 6.4.2 resolution: "acorn@npm:6.4.2" @@ -13706,7 +14011,7 @@ __metadata: languageName: node linkType: hard -"argparse@npm:^1.0.7": +"argparse@npm:^1.0.7, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" dependencies: @@ -16741,6 +17046,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:2.0.19, colorette@npm:^2.0.16": + version: 2.0.19 + resolution: "colorette@npm:2.0.19" + checksum: 2bcc9134095750fece6e88167011499b964b78bf0ea953469130ddb1dba3c8fe6c03debb0ae181e710e2be10900d117460f980483a7df4ba4a1bac3b182ecb64 + languageName: node + linkType: hard + "colorette@npm:^1.2.0, colorette@npm:^1.2.2, colorette@npm:^1.4.0": version: 1.4.0 resolution: "colorette@npm:1.4.0" @@ -16748,10 +17060,10 @@ __metadata: languageName: node linkType: hard -"colorette@npm:^2.0.16": - version: 2.0.19 - resolution: "colorette@npm:2.0.19" - checksum: 2bcc9134095750fece6e88167011499b964b78bf0ea953469130ddb1dba3c8fe6c03debb0ae181e710e2be10900d117460f980483a7df4ba4a1bac3b182ecb64 +"colors@npm:~1.2.1": + version: 1.2.5 + resolution: "colors@npm:1.2.5" + checksum: f4acebf2d2da9b4f8afb770361d14c01034bcb43add4cae493e7d186dcd7e0c5e2b440520fbfdf636e872606a0eb86b1f69fcf2f087df2876a4e222612539ee0 languageName: node linkType: hard @@ -16851,7 +17163,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:^9.4.1": +"commander@npm:^9.0.0, commander@npm:^9.1.0, commander@npm:^9.4.1": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: 5f7784fbda2aaec39e89eb46f06a999e00224b3763dc65976e05929ec486e174fe9aac2655f03ba6a5e83875bd173be5283dc19309b7c65954701c02025b3c1d @@ -18038,7 +18350,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1": +"debug@npm:4, debug@npm:4.3.4, debug@npm:^4.0.0, debug@npm:^4.0.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:~4.3.1": version: 4.3.4 resolution: "debug@npm:4.3.4" dependencies: @@ -18815,13 +19127,27 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.3": +"dotenv@npm:16.0.3, dotenv@npm:^16.0.3": version: 16.0.3 resolution: "dotenv@npm:16.0.3" checksum: 109457ac5f9e930ca8066ea33887b6f839ab24d647a7a8b49ddcd1f32662e2c35591c5e5b9819063e430148a664d0927f0cbe60cf9575d89bc524f47ff7e78f0 languageName: node linkType: hard +"dotenv@npm:16.1.3": + version: 16.1.3 + resolution: "dotenv@npm:16.1.3" + checksum: d80483222c5f129c6e5bc4ce46be563497eb39a7aa034245408e570f09fa770d73da3e019454394c197593535ecb908b0f5699ce721f5313ffd06ee44bcab85f + languageName: node + linkType: hard + +"dotenv@npm:^16.1.4": + version: 16.1.4 + resolution: "dotenv@npm:16.1.4" + checksum: 47cf5ce136bf2a5e8402fc9855d95848973cfee423fe0adf1a7cd565c842c51e5b8c95889b075140c26b092b74dd2a319970ff496cd7159ab3f1fc58edfc0ede + languageName: node + linkType: hard + "dotenv@npm:^7.0.0": version: 7.0.0 resolution: "dotenv@npm:7.0.0" @@ -18948,6 +19274,13 @@ __metadata: languageName: node linkType: hard +"emittery@npm:^0.12.1": + version: 0.12.1 + resolution: "emittery@npm:0.12.1" + checksum: 7a8395bdcebd6bd42054469c93f453308f93d67a81f8fe08f7047c824b4623794f03aefd0a23e73d967bb3b9f722ba7eff216c808bb80caaa7d13c42227e06c8 + languageName: node + linkType: hard + "emittery@npm:^0.13.1": version: 0.13.1 resolution: "emittery@npm:0.13.1" @@ -20363,6 +20696,13 @@ __metadata: languageName: node linkType: hard +"esm@npm:^3.2.25": + version: 3.2.25 + resolution: "esm@npm:3.2.25" + checksum: 8e60e8075506a7ce28681c30c8f54623fe18a251c364cd481d86719fc77f58aa055b293d80632d9686d5408aaf865ffa434897dc9fd9153c8b3f469fad23f094 + languageName: node + linkType: hard + "espree@npm:^6.1.2": version: 6.2.1 resolution: "espree@npm:6.2.1" @@ -21283,6 +21623,15 @@ __metadata: languageName: node linkType: hard +"figlet@npm:^1.5.2": + version: 1.6.0 + resolution: "figlet@npm:1.6.0" + bin: + figlet: bin/index.js + checksum: 98e19ee0c112b3a0c9a23751842c97c0c11d748b485674b9bf4037338fe98afbc6f943e3514949ae4bf75dc759322452bdf2487c4b000d17514e98f176849d1b + languageName: node + linkType: hard + "figures@npm:^1.0.1": version: 1.7.0 resolution: "figures@npm:1.7.0" @@ -21669,6 +22018,16 @@ __metadata: languageName: node linkType: hard +"foreground-child@npm:^3.1.0": + version: 3.1.1 + resolution: "foreground-child@npm:3.1.1" + dependencies: + cross-spawn: ^7.0.0 + signal-exit: ^4.0.1 + checksum: 9700a0285628abaeb37007c9a4d92bd49f67210f09067638774338e146c8e9c825c5c877f072b2f75f41dc6a2d0be8664f79ffc03f6576649f54a84fb9b47de0 + languageName: node + linkType: hard + "forever-agent@npm:~0.6.1": version: 0.6.1 resolution: "forever-agent@npm:0.6.1" @@ -21866,6 +22225,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:11.1.1": + version: 11.1.1 + resolution: "fs-extra@npm:11.1.1" + dependencies: + graceful-fs: ^4.2.0 + jsonfile: ^6.0.1 + universalify: ^2.0.0 + checksum: a2480243d7dcfa7d723c5f5b24cf4eba02a6ccece208f1524a2fbde1c629492cfb9a59e4b6d04faff6fbdf71db9fdc8ef7f396417a02884195a625f5d8dc9427 + languageName: node + linkType: hard + "fs-extra@npm:8.1.0, fs-extra@npm:^8.1, fs-extra@npm:^8.1.0": version: 8.1.0 resolution: "fs-extra@npm:8.1.0" @@ -21922,6 +22292,16 @@ __metadata: languageName: node linkType: hard +"fs-jetpack@npm:^4.3.1": + version: 4.3.1 + resolution: "fs-jetpack@npm:4.3.1" + dependencies: + minimatch: ^3.0.2 + rimraf: ^2.6.3 + checksum: 5d27e829233de005505417bae2f55412ae65ff63a57b68ac6d3cd8dde29ed9f0797c2a83356d20237bf74f516db8e40636c5fc238b49b4414b3d9339e60f7914 + languageName: node + linkType: hard + "fs-minipass@npm:^1.2.7": version: 1.2.7 resolution: "fs-minipass@npm:1.2.7" @@ -22743,6 +23123,13 @@ __metadata: languageName: node linkType: hard +"getopts@npm:2.3.0": + version: 2.3.0 + resolution: "getopts@npm:2.3.0" + checksum: edbcbd7020e9d87dc41e4ad9add5eb3873ae61339a62431bd92a461be2c0eaa9ec33b6fd0d67fa1b44feedffcf1cf28d6f9dbdb7d604cb1617eaba146a33cbca + languageName: node + linkType: hard + "getpass@npm:^0.1.1": version: 0.1.7 resolution: "getpass@npm:0.1.7" @@ -22853,6 +23240,21 @@ __metadata: languageName: node linkType: hard +"glob@npm:^10.2.5": + version: 10.2.7 + resolution: "glob@npm:10.2.7" + dependencies: + foreground-child: ^3.1.0 + jackspeak: ^2.0.3 + minimatch: ^9.0.1 + minipass: ^5.0.0 || ^6.0.2 + path-scurry: ^1.7.0 + bin: + glob: dist/cjs/src/bin.js + checksum: 15b742f933c4302cca278527a720c1300ba67b92975005e54e0fb85fee85aff1c45e71fbac386a2e190e64b7b17897b5ae1bc6cbd2cdd96a62c0dc55c8fb076f + languageName: node + linkType: hard + "glob@npm:^6.0.1": version: 6.0.4 resolution: "glob@npm:6.0.4" @@ -23009,7 +23411,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.0.4, globby@npm:^11.1.0": +"globby@npm:11.1.0, globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -25484,6 +25886,19 @@ __metadata: languageName: node linkType: hard +"jackspeak@npm:^2.0.3": + version: 2.2.1 + resolution: "jackspeak@npm:2.2.1" + dependencies: + "@isaacs/cliui": ^8.0.2 + "@pkgjs/parseargs": ^0.11.0 + dependenciesMeta: + "@pkgjs/parseargs": + optional: true + checksum: 510860a5d1eaf12cba509a09a8f7d1696090bfa7c8ae75c6d9c836890d2897409f3b3dd91039cf0020627d6eba8c024f571ae4d78bd956162b07794ddfb9dd62 + languageName: node + linkType: hard + "jest-changed-files@npm:^25.5.0": version: 25.5.0 resolution: "jest-changed-files@npm:25.5.0" @@ -28382,6 +28797,45 @@ __metadata: languageName: node linkType: hard +"knex@npm:2.4.2": + version: 2.4.2 + resolution: "knex@npm:2.4.2" + dependencies: + colorette: 2.0.19 + commander: ^9.1.0 + debug: 4.3.4 + escalade: ^3.1.1 + esm: ^3.2.25 + get-package-type: ^0.1.0 + getopts: 2.3.0 + interpret: ^2.2.0 + lodash: ^4.17.21 + pg-connection-string: 2.5.0 + rechoir: ^0.8.0 + resolve-from: ^5.0.0 + tarn: ^3.0.2 + tildify: 2.0.0 + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + bin: + knex: bin/cli.js + checksum: b6e2582671ac1503edf073e011b1db1b4d5c719a7acad5d8453d70b45b45296c3fba43f91ea3d26956986fb0ec70a7d26b8af4dfcf5ac1507c6674fc2186400b + languageName: node + linkType: hard + "kuler@npm:^2.0.0": version: 2.0.0 resolution: "kuler@npm:2.0.0" @@ -29297,6 +29751,13 @@ __metadata: languageName: node linkType: hard +"lru-cache@npm:^9.1.1": + version: 9.1.2 + resolution: "lru-cache@npm:9.1.2" + checksum: 886811ab451332c899c230274e7e51507c15e5b3b18f0b39fb55f558978d58799a0b1a50e04d60a448d8c970ff4e6ee718bb119083ca88abb78930284f1e0900 + languageName: node + linkType: hard + "lru-queue@npm:^0.1.0": version: 0.1.0 resolution: "lru-queue@npm:0.1.0" @@ -30698,6 +31159,13 @@ __metadata: languageName: node linkType: hard +"mikro-orm@npm:~5.7.11, mikro-orm@npm:~5.7.4": + version: 5.7.11 + resolution: "mikro-orm@npm:5.7.11" + checksum: 32bbd2366d0c56ec4592a8131c1e28ae578b33dfc2501bc2646f51fab8181c6a795fe81fae798d22a2791fd5600b414e527bcb35208b786e7a0d9ae3558b4ccd + languageName: node + linkType: hard + "miller-rabin@npm:^4.0.0": version: 4.0.1 resolution: "miller-rabin@npm:4.0.1" @@ -30876,6 +31344,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^9.0.1": + version: 9.0.1 + resolution: "minimatch@npm:9.0.1" + dependencies: + brace-expansion: ^2.0.1 + checksum: aa043eb8822210b39888a5d0d28df0017b365af5add9bd522f180d2a6962de1cbbf1bdeacdb1b17f410dc3336bc8d76fb1d3e814cdc65d00c2f68e01f0010096 + languageName: node + linkType: hard + "minimist-options@npm:^4.0.2": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -30971,6 +31448,13 @@ __metadata: languageName: node linkType: hard +"minipass@npm:^5.0.0 || ^6.0.2": + version: 6.0.2 + resolution: "minipass@npm:6.0.2" + checksum: 3878076578f44ef4078ceed10af2cfebbec1b6217bf9f7a3d8b940da8153769db29bf88498b2de0d1e0c12dfb7b634c5729b7ca03457f46435e801578add210a + languageName: node + linkType: hard + "minizlib@npm:^1.3.3": version: 1.3.3 resolution: "minizlib@npm:1.3.3" @@ -31393,6 +31877,13 @@ __metadata: languageName: node linkType: hard +"mylas@npm:^2.1.9": + version: 2.1.13 + resolution: "mylas@npm:2.1.13" + checksum: 093dfaf88f444d9da956c68a61ddcfe05ab9411c0024b0c7f4d721639ba7d311ea8f9c052ce617344e67d40982f67614cd634b525b923048bf9a191234968c9c + languageName: node + linkType: hard + "mz@npm:^2.4.0, mz@npm:^2.7.0": version: 2.7.0 resolution: "mz@npm:2.7.0" @@ -32875,6 +33366,13 @@ __metadata: languageName: node linkType: hard +"parent-require@npm:^1.0.0": + version: 1.0.0 + resolution: "parent-require@npm:1.0.0" + checksum: 58eb17553192027a596bb3b13f567e4933894964ad47a9f9054a5dc4776e60e13903e937b2eec6e7afff8a34a4fc91b5397fdc6206aa2d50ba95b87059b4f2e0 + languageName: node + linkType: hard + "parse-asn1@npm:^5.0.0, parse-asn1@npm:^5.1.5": version: 5.1.6 resolution: "parse-asn1@npm:5.1.6" @@ -33243,6 +33741,16 @@ __metadata: languageName: node linkType: hard +"path-scurry@npm:^1.7.0": + version: 1.9.2 + resolution: "path-scurry@npm:1.9.2" + dependencies: + lru-cache: ^9.1.1 + minipass: ^5.0.0 || ^6.0.2 + checksum: 99a3461a1ebc5269165170a744367a900802dc1ecc8a17a8c9700cca9b00b0938c8a06d57ec9bc9a485e430fd37c647f4029ccaf31b5f9dacedaf685cef3e69a + languageName: node + linkType: hard + "path-to-regexp@npm:0.1.7": version: 0.1.7 resolution: "path-to-regexp@npm:0.1.7" @@ -33348,7 +33856,7 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.5.0": +"pg-connection-string@npm:2.5.0, pg-connection-string@npm:^2.5.0": version: 2.5.0 resolution: "pg-connection-string@npm:2.5.0" checksum: 4b1650132d8000d68864db774c4a99d98acf1cb90525045402b93d3b5a5f36500e5934c653598256cd86b3a310ba4639fbf1e8e1e04cefa46840838541b7626c @@ -33452,6 +33960,26 @@ __metadata: languageName: node linkType: hard +"pg@npm:8.10.0": + version: 8.10.0 + resolution: "pg@npm:8.10.0" + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: ^2.5.0 + pg-pool: ^3.6.0 + pg-protocol: ^1.6.0 + pg-types: ^2.1.0 + pgpass: 1.x + peerDependencies: + pg-native: ">=3.0.1" + peerDependenciesMeta: + pg-native: + optional: true + checksum: 9c8e5b4af598d44b31caa55a2e5f1e841a2018e852d1e237a42c85a17ae6b68b8f3fc2ea270d5e02532be88c550124047ed28da5e0d55701b87dc4383b2dc846 + languageName: node + linkType: hard + "pg@npm:^8.10.0": version: 8.11.0 resolution: "pg@npm:8.11.0" @@ -33672,6 +34200,15 @@ __metadata: languageName: node linkType: hard +"plimit-lit@npm:^1.2.6": + version: 1.5.0 + resolution: "plimit-lit@npm:1.5.0" + dependencies: + queue-lit: ^1.5.0 + checksum: fe2d31ecab11185e24c874e6b2b741e19515dcb943e571a703a9a1621813b94299a486718ea8efa68fd70f239294fa909669eeb8685ff74e29bf1a6d2b6bff12 + languageName: node + linkType: hard + "pluralize@npm:^8.0.0": version: 8.0.0 resolution: "pluralize@npm:8.0.0" @@ -33711,6 +34248,13 @@ __metadata: languageName: node linkType: hard +"pony-cause@npm:^2.1.2": + version: 2.1.10 + resolution: "pony-cause@npm:2.1.10" + checksum: 55ad0ca52039895f273c69e55fc9fe882deff38689dc5962558bfa16cce0ea7cb5bb7b67d0c43ec9c3e7edeb81f81ee8c1113014930d77b2cbac5adc4ac7fb64 + languageName: node + linkType: hard + "pop-iterate@npm:^1.0.1": version: 1.0.1 resolution: "pop-iterate@npm:1.0.1" @@ -35020,6 +35564,13 @@ __metadata: languageName: node linkType: hard +"queue-lit@npm:^1.5.0": + version: 1.5.0 + resolution: "queue-lit@npm:1.5.0" + checksum: e6b8ba9cdfca2c775e3f93bbd13870e77c9214f8590fa1ca203f3a2b8357d8d1eb44cdf707eca7fbfd60cef0faa74352778bf57b710b27869713b18acdc0d281 + languageName: node + linkType: hard + "queue-microtask@npm:^1.2.2": version: 1.2.3 resolution: "queue-microtask@npm:1.2.3" @@ -35979,6 +36530,15 @@ __metadata: languageName: node linkType: hard +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" + dependencies: + resolve: ^1.20.0 + checksum: 1a30074124a22abbd5d44d802dac26407fa72a0a95f162aa5504ba8246bc5452f8b1a027b154d9bdbabcd8764920ff9333d934c46a8f17479c8912e92332f3ff + languageName: node + linkType: hard + "recursive-readdir@npm:^2.2.2": version: 2.2.2 resolution: "recursive-readdir@npm:2.2.2" @@ -36115,7 +36675,7 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:^0.1.13": +"reflect-metadata@npm:0.1.13, reflect-metadata@npm:^0.1.13": version: 0.1.13 resolution: "reflect-metadata@npm:0.1.13" checksum: 728bff0b376b05639fd11ed80c648b61f7fe653c5b506d7ca118e58b6752b9b00810fe0c86227ecf02bd88da6251ab3eb19fd403aaf2e9ff5ef36a2fda643026 @@ -36822,6 +37382,17 @@ __metadata: languageName: node linkType: hard +"rimraf@npm:^5.0.0": + version: 5.0.1 + resolution: "rimraf@npm:5.0.1" + dependencies: + glob: ^10.2.5 + bin: + rimraf: dist/cjs/src/bin.js + checksum: 9e6062c0aea96f384dd937e6bb06b624c881de2eee79a83d3068193183d44eb9b1f3f68a27a54b9ca8cce56bf34c2951ff4239b093b970e0501a091907031f52 + languageName: node + linkType: hard + "rimraf@npm:~2.4.0": version: 2.4.5 resolution: "rimraf@npm:2.4.5" @@ -37757,6 +38328,13 @@ __metadata: languageName: node linkType: hard +"signal-exit@npm:^4.0.1": + version: 4.0.2 + resolution: "signal-exit@npm:4.0.2" + checksum: 3c36ae214f4774b4a7cbbd2d090b2864f8da4dc3f9140ba5b76f38bea7605c7aa8042adf86e48ee8a0955108421873f9b0f20281c61b8a65da4d9c1c1de4929f + languageName: node + linkType: hard + "signedsource@npm:^1.0.0": version: 1.0.0 resolution: "signedsource@npm:1.0.0" @@ -38256,6 +38834,13 @@ __metadata: languageName: node linkType: hard +"sqlstring@npm:2.3.3": + version: 2.3.3 + resolution: "sqlstring@npm:2.3.3" + checksum: 3b5dd7badb3d6312f494cfa6c9a381ee630fbe3dbd571c4c9eb8ecdb99a7bf5a1f7a5043191d768797f6b3c04eed5958ac6a5f948b998f0a138294c6d3125fbd + languageName: node + linkType: hard + "sshpk@npm:^1.7.0": version: 1.17.0 resolution: "sshpk@npm:1.17.0" @@ -38546,6 +39131,13 @@ __metadata: languageName: node linkType: hard +"string-argv@npm:~0.3.1": + version: 0.3.2 + resolution: "string-argv@npm:0.3.2" + checksum: 75c02a83759ad1722e040b86823909d9a2fc75d15dd71ec4b537c3560746e33b5f5a07f7332d1e3f88319909f82190843aa2f0a0d8c8d591ec08e93d5b8dec82 + languageName: node + linkType: hard + "string-env-interpolation@npm:1.0.1": version: 1.0.1 resolution: "string-env-interpolation@npm:1.0.1" @@ -38600,7 +39192,7 @@ __metadata: languageName: node linkType: hard -"string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": +"string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^1.0.2 || 2 || 3 || 4, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" dependencies: @@ -38767,6 +39359,15 @@ __metadata: languageName: node linkType: hard +"strip-ansi-cjs@npm:strip-ansi@^6.0.1, strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": + version: 6.0.1 + resolution: "strip-ansi@npm:6.0.1" + dependencies: + ansi-regex: ^5.0.1 + checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 + languageName: node + linkType: hard + "strip-ansi@npm:^3.0.0, strip-ansi@npm:^3.0.1": version: 3.0.1 resolution: "strip-ansi@npm:3.0.1" @@ -38785,15 +39386,6 @@ __metadata: languageName: node linkType: hard -"strip-ansi@npm:^6.0.0, strip-ansi@npm:^6.0.1": - version: 6.0.1 - resolution: "strip-ansi@npm:6.0.1" - dependencies: - ansi-regex: ^5.0.1 - checksum: 1ae5f212a126fe5b167707f716942490e3933085a5ff6c008ab97ab2f272c8025d3aa218b7bd6ab25729ca20cc81cddb252102f8751e13482a5199e873680952 - languageName: node - linkType: hard - "strip-ansi@npm:^7.0.1": version: 7.0.1 resolution: "strip-ansi@npm:7.0.1" @@ -39371,6 +39963,13 @@ __metadata: languageName: node linkType: hard +"tarn@npm:^3.0.2": + version: 3.0.2 + resolution: "tarn@npm:3.0.2" + checksum: ea2344e3d21936111176375bd6f34eba69a38ef1bc59434d523fd313166f8a28a47b0a847846c119f72dcf2c1e1231596d74ac3fcfc3cc73966b3d293a327269 + languageName: node + linkType: hard + "telejson@npm:^6.0.8": version: 6.0.8 resolution: "telejson@npm:6.0.8" @@ -39565,6 +40164,13 @@ __metadata: languageName: node linkType: hard +"tildify@npm:2.0.0": + version: 2.0.0 + resolution: "tildify@npm:2.0.0" + checksum: 57961810a6915f47bdba7da7fa66a5f12597a0495fa016785de197b02e7ba9994ffebb30569294061bbf6d9395c6b1319d830076221e5a3f49f1318bc749565c + languageName: node + linkType: hard + "timers-browserify@npm:^2.0.4": version: 2.0.12 resolution: "timers-browserify@npm:2.0.12" @@ -40154,6 +40760,22 @@ __metadata: languageName: node linkType: hard +"tsc-alias@npm:^1.8.6": + version: 1.8.6 + resolution: "tsc-alias@npm:1.8.6" + dependencies: + chokidar: ^3.5.3 + commander: ^9.0.0 + globby: ^11.0.4 + mylas: ^2.1.9 + normalize-path: ^3.0.0 + plimit-lit: ^1.2.6 + bin: + tsc-alias: dist/bin/index.js + checksum: 0af7162780e7c5d017697b31f59c3f71f3be2b2e313048b0a9fc5fec96e7ba98286b33c9a8275aaf285631559ff18eeffd36422ef863802de784d25e09f1a989 + languageName: node + linkType: hard + "tsc-watch@npm:^4.5.0": version: 4.6.2 resolution: "tsc-watch@npm:4.6.2" @@ -40171,6 +40793,17 @@ __metadata: languageName: node linkType: hard +"tsconfig-paths@npm:4.2.0": + version: 4.2.0 + resolution: "tsconfig-paths@npm:4.2.0" + dependencies: + json5: ^2.2.2 + minimist: ^1.2.6 + strip-bom: ^3.0.0 + checksum: 09a5877402d082bb1134930c10249edeebc0211f36150c35e1c542e5b91f1047b1ccf7da1e59babca1ef1f014c525510f4f870de7c9bda470c73bb4e2721b3ea + languageName: node + linkType: hard + "tsconfig-paths@npm:^3.14.1": version: 3.14.1 resolution: "tsconfig-paths@npm:3.14.1" @@ -40497,7 +41130,7 @@ __metadata: languageName: node linkType: hard -"type-fest@npm:^2.13.0": +"type-fest@npm:^2.13.0, type-fest@npm:^2.18.0": version: 2.19.0 resolution: "type-fest@npm:2.19.0" checksum: a5a7ecf2e654251613218c215c7493574594951c08e52ab9881c9df6a6da0aeca7528c213c622bc374b4e0cb5c443aa3ab758da4e3c959783ce884c3194e12cb @@ -40799,6 +41432,20 @@ __metadata: languageName: node linkType: hard +"umzug@npm:3.2.1": + version: 3.2.1 + resolution: "umzug@npm:3.2.1" + dependencies: + "@rushstack/ts-command-line": ^4.12.2 + emittery: ^0.12.1 + fs-jetpack: ^4.3.1 + glob: ^8.0.3 + pony-cause: ^2.1.2 + type-fest: ^2.18.0 + checksum: ff5d417c5f0211e8c3c2529c347313ecef5db3ff4b219c71098e09884674387ac14870749ecf42ac26aabcf7559207b855fb0016eb8c50f7cd979f816d4b1545 + languageName: node + linkType: hard + "unbox-primitive@npm:^1.0.2": version: 1.0.2 resolution: "unbox-primitive@npm:1.0.2" @@ -42415,6 +43062,17 @@ __metadata: languageName: node linkType: hard +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0, wrap-ansi@npm:^7.0.0": + version: 7.0.0 + resolution: "wrap-ansi@npm:7.0.0" + dependencies: + ansi-styles: ^4.0.0 + string-width: ^4.1.0 + strip-ansi: ^6.0.0 + checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da + languageName: node + linkType: hard + "wrap-ansi@npm:^6.0.1, wrap-ansi@npm:^6.2.0": version: 6.2.0 resolution: "wrap-ansi@npm:6.2.0" @@ -42426,17 +43084,6 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^7.0.0": - version: 7.0.0 - resolution: "wrap-ansi@npm:7.0.0" - dependencies: - ansi-styles: ^4.0.0 - string-width: ^4.1.0 - strip-ansi: ^6.0.0 - checksum: d15fc12c11e4cbc4044a552129ebc75ee3f57aa9c1958373a4db0292d72282f54373b536103987a4a7594db1ef6a4f10acf92978f79b98c49306a4b58c77d4da - languageName: node - linkType: hard - "wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" @@ -42847,6 +43494,21 @@ __metadata: languageName: node linkType: hard +"yargs@npm:17.7.2": + version: 17.7.2 + resolution: "yargs@npm:17.7.2" + dependencies: + cliui: ^8.0.1 + escalade: ^3.1.1 + get-caller-file: ^2.0.5 + require-directory: ^2.1.1 + string-width: ^4.2.3 + y18n: ^5.0.5 + yargs-parser: ^21.1.1 + checksum: ccd7e723e61ad5965fffbb791366db689572b80cca80e0f96aad968dfff4156cd7cd1ad18607afe1046d8241e6fb2d6c08bf7fa7bfb5eaec818735d8feac8f05 + languageName: node + linkType: hard + "yargs@npm:^15.1.0, yargs@npm:^15.3.1, yargs@npm:^15.4.1": version: 15.4.1 resolution: "yargs@npm:15.4.1"