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:
committed by
GitHub
parent
ffeeb84d97
commit
14c0f62f84
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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),
|
||||
})
|
||||
|
||||
@@ -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 ?? {} : {},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user