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 <frane@medusajs.com>
Co-authored-by: Riqwan Thamir <rmthamir@gmail.com>
Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com>
This commit is contained in:
Adrien de Peretti
2023-06-09 20:47:24 +02:00
committed by GitHub
parent ffeeb84d97
commit 14c0f62f84
104 changed files with 6472 additions and 176 deletions

View File

@@ -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"
}
}

View File

@@ -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

View File

@@ -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)
})
})

View File

@@ -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")
})

View File

@@ -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", () => {

View File

@@ -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),
})

View File

@@ -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<string, ModuleResolution> => {
const moduleResolutions = {} as Record<string, ModuleResolution>
@@ -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 ?? {} : {},
}
}

View File

@@ -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(

View File

@@ -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<string, any> = new Map()
public static clearInstances(): void {
MedusaModule.instances_.clear()
}
public static async bootstrap(
moduleKey: string,
defaultPath: string,
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
moduleExports?: ModuleExports,
injectedDependencies?: Record<string, any>
): 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
}

View File

@@ -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"