feat(link-modules,modules-sdk, utils, types, products) - Remote Link and Link modules (#4695)
What:
- Definition of all Modules links
- `link-modules` package to manage the creation of all pre-defined link or custom ones
```typescript
import { initialize as iniInventory } from "@medusajs/inventory";
import { initialize as iniProduct } from "@medusajs/product";
import {
initialize as iniLinks,
runMigrations as migrateLinks
} from "@medusajs/link-modules";
await Promise.all([iniInventory(), iniProduct()]);
await migrateLinks(); // create tables based on previous loaded modules
await iniLinks(); // load link based on previous loaded modules
await iniLinks(undefined, [
{
serviceName: "product_custom_translation_service_link",
isLink: true,
databaseConfig: {
tableName: "product_transalations",
},
alias: [
{
name: "translations",
},
],
primaryKeys: ["id", "product_id", "translation_id"],
relationships: [
{
serviceName: Modules.PRODUCT,
primaryKey: "id",
foreignKey: "product_id",
alias: "product",
},
{
serviceName: "custom_translation_service",
primaryKey: "id",
foreignKey: "translation_id",
alias: "transalation",
deleteCascade: true,
},
],
extends: [
{
serviceName: Modules.PRODUCT,
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "translations",
isList: true,
},
},
{
serviceName: "custom_translation_service",
relationship: {
serviceName: "product_custom_translation_service_link",
primaryKey: "product_id",
foreignKey: "id",
alias: "product_link",
},
},
],
},
]); // custom links
```
Remote Link
```typescript
import { RemoteLink, Modules } from "@medusajs/modules-sdk";
// [...] initialize modules and links
const remoteLink = new RemoteLink();
// upsert the relationship
await remoteLink.create({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
data: { // optional additional fields
required_quantity: 5
}
});
// dismiss (doesn't cascade)
await remoteLink.dismiss({ // one (object) or many (array)
[Modules.PRODUCT]: {
variant_id: "var_abc",
},
[Modules.INVENTORY]: {
inventory_item_id: "iitem_abc",
},
});
// delete
await remoteLink.delete({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
// restore
await remoteLink.restore({
// every key is a module
[Modules.PRODUCT]: {
// every key is a linkable field
variant_id: "var_abc", // single or multiple values
},
});
```
Co-authored-by: Riqwan Thamir <5105988+riqwan@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
bc4c9e0d32
commit
4d16acf5f0
7
.changeset/poor-ants-deliver.md
Normal file
7
.changeset/poor-ants-deliver.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@medusajs/link-modules": patch
|
||||
"@medusajs/modules-sdk": patch
|
||||
"@medusajs/types": patch
|
||||
---
|
||||
|
||||
Add extra fields to link modules
|
||||
9
.changeset/selfish-needles-beg.md
Normal file
9
.changeset/selfish-needles-beg.md
Normal file
@@ -0,0 +1,9 @@
|
||||
---
|
||||
"@medusajs/orchestration": minor
|
||||
"@medusajs/link-modules": minor
|
||||
"@medusajs/modules-sdk": minor
|
||||
"@medusajs/types": minor
|
||||
"@medusajs/utils": minor
|
||||
---
|
||||
|
||||
Medusa App Loader
|
||||
13
.changeset/warm-bikes-enjoy.md
Normal file
13
.changeset/warm-bikes-enjoy.md
Normal file
@@ -0,0 +1,13 @@
|
||||
---
|
||||
"@medusajs/stock-location": minor
|
||||
"@medusajs/orchestration": minor
|
||||
"@medusajs/link-modules": minor
|
||||
"@medusajs/modules-sdk": minor
|
||||
"@medusajs/inventory": minor
|
||||
"@medusajs/product": minor
|
||||
"@medusajs/medusa": minor
|
||||
"@medusajs/types": minor
|
||||
"@medusajs/utils": minor
|
||||
---
|
||||
|
||||
introduce @medusajs/link-modules
|
||||
188
integration-tests/plugins/__tests__/link-modules/index.ts
Normal file
188
integration-tests/plugins/__tests__/link-modules/index.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { initialize, runMigrations } from "@medusajs/link-modules"
|
||||
import { MedusaModule, ModuleJoinerConfig } from "@medusajs/modules-sdk"
|
||||
|
||||
jest.setTimeout(5000000)
|
||||
|
||||
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 DB_URL = `postgres://${DB_USERNAME}:${DB_PASSWORD}@${DB_HOST}/${DB_NAME}`
|
||||
|
||||
describe("Link Modules", () => {
|
||||
let links
|
||||
const linkDefinition: ModuleJoinerConfig[] = [
|
||||
{
|
||||
serviceName: "linkServiceName",
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "linkTableName",
|
||||
idPrefix: "prefix",
|
||||
extraFields: {
|
||||
extra_field: {
|
||||
type: "integer",
|
||||
defaultValue: "-1",
|
||||
},
|
||||
another_field: {
|
||||
type: "string",
|
||||
nullable: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "moduleA",
|
||||
primaryKey: "id",
|
||||
foreignKey: "product_id",
|
||||
alias: "product",
|
||||
},
|
||||
{
|
||||
serviceName: "moduleB",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
const dbConfig = {
|
||||
database: {
|
||||
clientUrl: DB_URL,
|
||||
},
|
||||
}
|
||||
|
||||
beforeAll(async () => {
|
||||
jest.spyOn(MedusaModule, "getLoadedModules").mockImplementation((() => {
|
||||
return [{ moduleA: [{}] }, { moduleB: [{}] }]
|
||||
}) as any)
|
||||
|
||||
await runMigrations({ options: dbConfig }, linkDefinition)
|
||||
links = await initialize(dbConfig, linkDefinition)
|
||||
})
|
||||
|
||||
afterAll(async () => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
it("Should insert values in a declared link", async function () {
|
||||
// simple
|
||||
await links.linkServiceName.create("modA_id", "modB_id")
|
||||
|
||||
// extra fields
|
||||
await links.linkServiceName.create("123", "abc", {
|
||||
extra_field: 333,
|
||||
another_field: "value**",
|
||||
})
|
||||
|
||||
// bulk
|
||||
await links.linkServiceName.create([
|
||||
["111", "aaa", { another_field: "test" }],
|
||||
["222", "bbb"],
|
||||
["333", "ccc", { extra_field: 2 }],
|
||||
["444", "bbb"],
|
||||
])
|
||||
|
||||
const values = await links.linkServiceName.list()
|
||||
|
||||
expect(values).toEqual([
|
||||
{
|
||||
product_id: "modA_id",
|
||||
inventory_item_id: "modB_id",
|
||||
id: expect.stringMatching("prefix_.+"),
|
||||
extra_field: -1,
|
||||
another_field: null,
|
||||
created_at: expect.any(Date),
|
||||
updated_at: expect.any(Date),
|
||||
deleted_at: null,
|
||||
},
|
||||
expect.objectContaining({
|
||||
product_id: "123",
|
||||
inventory_item_id: "abc",
|
||||
id: expect.stringMatching("prefix_.+"),
|
||||
extra_field: 333,
|
||||
another_field: "value**",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "111",
|
||||
inventory_item_id: "aaa",
|
||||
extra_field: -1,
|
||||
another_field: "test",
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "222",
|
||||
inventory_item_id: "bbb",
|
||||
extra_field: -1,
|
||||
another_field: null,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "333",
|
||||
inventory_item_id: "ccc",
|
||||
id: expect.stringMatching("prefix_.+"),
|
||||
extra_field: 2,
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "444",
|
||||
inventory_item_id: "bbb",
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("Should dismiss the link of a given pair of keys", async function () {
|
||||
// simple
|
||||
const dismissSingle = await links.linkServiceName.dismiss(
|
||||
"modA_id",
|
||||
"modB_id"
|
||||
)
|
||||
|
||||
// bulk
|
||||
const dismissMulti = await links.linkServiceName.dismiss([
|
||||
["111", "aaa"],
|
||||
["333", "ccc"],
|
||||
])
|
||||
|
||||
expect(dismissSingle).toEqual([
|
||||
expect.objectContaining({
|
||||
product_id: "modA_id",
|
||||
inventory_item_id: "modB_id",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
|
||||
expect(dismissMulti).toEqual([
|
||||
expect.objectContaining({
|
||||
product_id: "111",
|
||||
inventory_item_id: "aaa",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "333",
|
||||
inventory_item_id: "ccc",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
it("Should delete all the links related to a given key", async function () {
|
||||
await links.linkServiceName.softDelete({
|
||||
inventory_item_id: "bbb",
|
||||
})
|
||||
|
||||
const values = await links.linkServiceName.list(
|
||||
{ inventory_item_id: "bbb" },
|
||||
{ withDeleted: true }
|
||||
)
|
||||
|
||||
expect(values).toEqual([
|
||||
expect.objectContaining({
|
||||
product_id: "222",
|
||||
inventory_item_id: "bbb",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
expect.objectContaining({
|
||||
product_id: "444",
|
||||
inventory_item_id: "bbb",
|
||||
deleted_at: expect.any(Date),
|
||||
}),
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.8",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.8",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.10",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.2",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
|
||||
@@ -19,11 +19,11 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.10",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.2",
|
||||
"jest": "^29.6.3",
|
||||
"medusa-test-utils": "^1.1.40",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "tsc --build --watch",
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.11",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.8.8",
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { JoinerServiceConfig } from "@medusajs/types"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
|
||||
export const joinerConfig: JoinerServiceConfig = {
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.INVENTORY,
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: [
|
||||
"inventory_item_id",
|
||||
"inventory_level_id",
|
||||
"reservation_item_id",
|
||||
],
|
||||
alias: [
|
||||
{
|
||||
name: "inventory_items",
|
||||
|
||||
@@ -11,8 +11,8 @@ import {
|
||||
IInventoryService,
|
||||
InventoryItemDTO,
|
||||
InventoryLevelDTO,
|
||||
JoinerServiceConfig,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
ModuleJoinerConfig,
|
||||
ReservationItemDTO,
|
||||
SharedContext,
|
||||
UpdateInventoryLevelInput,
|
||||
@@ -59,7 +59,7 @@ export default class InventoryService implements IInventoryService {
|
||||
this.reservationItemService_ = reservationItemService
|
||||
}
|
||||
|
||||
__joinerConfig(): JoinerServiceConfig {
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
|
||||
6
packages/link-modules/.gitignore
vendored
Normal file
6
packages/link-modules/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
/dist
|
||||
node_modules
|
||||
.DS_store
|
||||
.env*
|
||||
.env
|
||||
*.sql
|
||||
13
packages/link-modules/jest.config.js
Normal file
13
packages/link-modules/jest.config.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
}
|
||||
46
packages/link-modules/package.json
Normal file
46
packages/link-modules/package.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "@medusajs/link-modules",
|
||||
"version": "0.0.1",
|
||||
"description": "Medusa Default Link Modules Package and Definitions",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/medusajs/medusa",
|
||||
"directory": "packages/link-modules"
|
||||
},
|
||||
"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 --passWithNoTests --runInBand --bail --forceExit",
|
||||
"test:integration": "jest --passWithNoTests"
|
||||
},
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^29.6.3",
|
||||
"pg-god": "^1.0.12",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.6",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.8.8",
|
||||
"@medusajs/types": "^1.8.11",
|
||||
"@medusajs/utils": "^1.9.2",
|
||||
"@mikro-orm/core": "5.7.12",
|
||||
"@mikro-orm/postgresql": "5.7.12",
|
||||
"awilix": "^8.0.0"
|
||||
}
|
||||
}
|
||||
2
packages/link-modules/src/definitions/index.ts
Normal file
2
packages/link-modules/src/definitions/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from "./inventory-level-stock-location"
|
||||
export * from "./product-variant-inventory-item"
|
||||
@@ -0,0 +1,19 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
|
||||
export const InventoryLevelStockLocation: ModuleJoinerConfig = {
|
||||
isLink: true,
|
||||
isReadOnlyLink: true,
|
||||
extends: [
|
||||
{
|
||||
serviceName: Modules.INVENTORY,
|
||||
relationship: {
|
||||
serviceName: Modules.STOCK_LOCATION,
|
||||
primaryKey: "id",
|
||||
foreignKey: "location_id",
|
||||
alias: "stock_locations",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { LINKS } from "../links"
|
||||
|
||||
export const ProductVariantInventoryItem: ModuleJoinerConfig = {
|
||||
serviceName: LINKS.ProductVariantInventoryItem,
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "product_variant_inventory_item",
|
||||
idPrefix: "pvitem",
|
||||
extraFields: {
|
||||
required_quantity: {
|
||||
type: "integer",
|
||||
defaultValue: "1",
|
||||
},
|
||||
},
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "product_variant_inventory_item",
|
||||
},
|
||||
{
|
||||
name: "product_variant_inventory_items",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["id", "variant_id", "inventory_item_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
args: {
|
||||
methodSuffix: "Variants",
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: Modules.INVENTORY,
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
deleteCascade: true,
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: Modules.PRODUCT,
|
||||
relationship: {
|
||||
serviceName: LINKS.ProductVariantInventoryItem,
|
||||
primaryKey: "variant_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_items",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: Modules.INVENTORY,
|
||||
relationship: {
|
||||
serviceName: LINKS.ProductVariantInventoryItem,
|
||||
primaryKey: "inventory_item_id",
|
||||
foreignKey: "id",
|
||||
alias: "variant_link",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
4
packages/link-modules/src/index.ts
Normal file
4
packages/link-modules/src/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export * from "./initialize"
|
||||
export * from "./types"
|
||||
export * from "./loaders"
|
||||
export * from "./services"
|
||||
190
packages/link-modules/src/initialize/index.ts
Normal file
190
packages/link-modules/src/initialize/index.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import { InternalModuleDeclaration, MedusaModule } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
ILinkModule,
|
||||
LinkModuleDefinition,
|
||||
LoaderOptions,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
lowerCaseFirst,
|
||||
simpleHash,
|
||||
} from "@medusajs/utils"
|
||||
import * as linkDefinitions from "../definitions"
|
||||
import { getMigration } from "../migration"
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
import { composeLinkName } from "../utils"
|
||||
import { getLinkModuleDefinition } from "./module-definition"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
| ExternalModuleDeclaration
|
||||
| InternalModuleDeclaration,
|
||||
modulesDefinition?: ModuleJoinerConfig[],
|
||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||
): Promise<{ [link: string]: ILinkModule }> => {
|
||||
const allLinks = {}
|
||||
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.keys(mod)[0]
|
||||
)
|
||||
|
||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||
modulesDefinition ?? []
|
||||
)
|
||||
|
||||
for (const linkDefinition of allLinksToLoad) {
|
||||
const definition = JSON.parse(JSON.stringify(linkDefinition))
|
||||
|
||||
const [primary, foreign] = definition.relationships ?? []
|
||||
|
||||
if (definition.relationships?.length !== 2 && !definition.isReadOnlyLink) {
|
||||
throw new Error(
|
||||
`Link module ${definition.serviceName} can only link 2 modules.`
|
||||
)
|
||||
} else if (
|
||||
foreign?.foreignKey?.split(",").length > 1 &&
|
||||
!definition.isReadOnlyLink
|
||||
) {
|
||||
throw new Error(`Foreign key cannot be a composed key.`)
|
||||
}
|
||||
|
||||
const serviceKey = !definition.isReadOnlyLink
|
||||
? lowerCaseFirst(
|
||||
definition.serviceName ??
|
||||
composeLinkName(
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey
|
||||
)
|
||||
)
|
||||
: simpleHash(JSON.stringify(definition.extends))
|
||||
|
||||
if (modulesLoadedKeys.includes(serviceKey)) {
|
||||
continue
|
||||
} else if (serviceKey in allLinks) {
|
||||
throw new Error(`Link module ${serviceKey} already defined.`)
|
||||
}
|
||||
|
||||
if (definition.isReadOnlyLink) {
|
||||
const extended: any[] = []
|
||||
for (const extension of definition.extends ?? []) {
|
||||
if (
|
||||
modulesLoadedKeys.includes(extension.serviceName) &&
|
||||
modulesLoadedKeys.includes(extension.relationship.serviceName)
|
||||
) {
|
||||
extended.push(extension)
|
||||
}
|
||||
}
|
||||
|
||||
definition.extends = extended
|
||||
if (extended.length === 0) {
|
||||
continue
|
||||
}
|
||||
} else if (
|
||||
!modulesLoadedKeys.includes(primary.serviceName) ||
|
||||
!modulesLoadedKeys.includes(foreign.serviceName)
|
||||
) {
|
||||
// TODO: This should be uncommented when all modules are done
|
||||
// continue
|
||||
}
|
||||
|
||||
const moduleDefinition = getLinkModuleDefinition(
|
||||
definition,
|
||||
primary,
|
||||
foreign
|
||||
) as ModuleExports
|
||||
|
||||
const linkModuleDefinition: LinkModuleDefinition = {
|
||||
key: serviceKey,
|
||||
registrationName: serviceKey,
|
||||
label: serviceKey,
|
||||
defaultModuleDeclaration: {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: injectedDependencies?.[
|
||||
ContainerRegistrationKeys.PG_CONNECTION
|
||||
]
|
||||
? MODULE_RESOURCE_TYPE.SHARED
|
||||
: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
},
|
||||
}
|
||||
|
||||
const loaded = await MedusaModule.bootstrapLink(
|
||||
linkModuleDefinition,
|
||||
options as InternalModuleDeclaration,
|
||||
moduleDefinition,
|
||||
injectedDependencies
|
||||
)
|
||||
|
||||
allLinks[serviceKey as string] = Object.values(loaded)[0]
|
||||
}
|
||||
|
||||
return allLinks
|
||||
}
|
||||
|
||||
export async function runMigrations(
|
||||
{
|
||||
options,
|
||||
logger,
|
||||
}: Omit<LoaderOptions<ModuleServiceInitializeOptions>, "container">,
|
||||
modulesDefinition?: ModuleJoinerConfig[]
|
||||
) {
|
||||
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.keys(mod)[0]
|
||||
)
|
||||
|
||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||
modulesDefinition ?? []
|
||||
)
|
||||
|
||||
const allLinks = new Set<string>()
|
||||
for (const definition of allLinksToLoad) {
|
||||
if (definition.isReadOnlyLink) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (definition.relationships?.length !== 2) {
|
||||
throw new Error(
|
||||
`Link module ${definition.serviceName} must have 2 relationships.`
|
||||
)
|
||||
}
|
||||
|
||||
const [primary, foreign] = definition.relationships ?? []
|
||||
const serviceKey = lowerCaseFirst(
|
||||
definition.serviceName ??
|
||||
composeLinkName(
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey
|
||||
)
|
||||
)
|
||||
|
||||
if (modulesLoadedKeys.includes(serviceKey)) {
|
||||
continue
|
||||
} else if (allLinks.has(serviceKey)) {
|
||||
throw new Error(`Link module ${serviceKey} already exists.`)
|
||||
}
|
||||
|
||||
allLinks.add(serviceKey)
|
||||
|
||||
if (
|
||||
!modulesLoadedKeys.includes(primary.serviceName) ||
|
||||
!modulesLoadedKeys.includes(foreign.serviceName)
|
||||
) {
|
||||
// TODO: This should be uncommented when all modules are done
|
||||
// continue
|
||||
}
|
||||
|
||||
const migrate = getMigration(definition, serviceKey, primary, foreign)
|
||||
await migrate({ options, logger })
|
||||
}
|
||||
}
|
||||
24
packages/link-modules/src/initialize/module-definition.ts
Normal file
24
packages/link-modules/src/initialize/module-definition.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import {
|
||||
JoinerRelationship,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
} from "@medusajs/types"
|
||||
import { getModuleService, getReadOnlyModuleService } from "@services"
|
||||
import { getLoaders } from "../loaders"
|
||||
|
||||
export function getLinkModuleDefinition(
|
||||
joinerConfig: ModuleJoinerConfig,
|
||||
primary: JoinerRelationship,
|
||||
foreign: JoinerRelationship
|
||||
): ModuleExports {
|
||||
return {
|
||||
service: joinerConfig.isReadOnlyLink
|
||||
? getReadOnlyModuleService(joinerConfig)
|
||||
: getModuleService(joinerConfig),
|
||||
loaders: getLoaders({
|
||||
joinerConfig,
|
||||
primary,
|
||||
foreign,
|
||||
}),
|
||||
}
|
||||
}
|
||||
11
packages/link-modules/src/links.ts
Normal file
11
packages/link-modules/src/links.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { composeLinkName } from "./utils"
|
||||
|
||||
export const LINKS = {
|
||||
ProductVariantInventoryItem: composeLinkName(
|
||||
Modules.PRODUCT,
|
||||
"variant_id",
|
||||
Modules.INVENTORY,
|
||||
"inventory_item_id"
|
||||
),
|
||||
}
|
||||
31
packages/link-modules/src/loaders/connection.ts
Normal file
31
packages/link-modules/src/loaders/connection.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
InternalModuleDeclaration,
|
||||
LoaderOptions,
|
||||
ModuleServiceInitializeCustomDataLayerOptions,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
|
||||
export function connectionLoader(entity: EntitySchema) {
|
||||
return async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
logger,
|
||||
}: LoaderOptions<
|
||||
| ModuleServiceInitializeOptions
|
||||
| ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
await ModulesSdkUtils.mikroOrmConnectionLoader({
|
||||
entities: [entity],
|
||||
container,
|
||||
options,
|
||||
moduleDeclaration,
|
||||
logger,
|
||||
})
|
||||
}
|
||||
}
|
||||
40
packages/link-modules/src/loaders/container.ts
Normal file
40
packages/link-modules/src/loaders/container.ts
Normal file
@@ -0,0 +1,40 @@
|
||||
import { BaseRepository, getLinkRepository } from "@repositories"
|
||||
import { LinkService, getModuleService } from "@services"
|
||||
|
||||
import { LoaderOptions } from "@medusajs/modules-sdk"
|
||||
import {
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
ModulesSdkTypes,
|
||||
} from "@medusajs/types"
|
||||
import { asClass, asValue } from "awilix"
|
||||
|
||||
export function containerLoader(entity, joinerConfig: ModuleJoinerConfig) {
|
||||
return async (
|
||||
{
|
||||
options,
|
||||
container,
|
||||
}: LoaderOptions<
|
||||
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||
>,
|
||||
moduleDeclaration?: InternalModuleDeclaration
|
||||
): Promise<void> => {
|
||||
const [primary, foreign] = joinerConfig.relationships!
|
||||
|
||||
container.register({
|
||||
joinerConfig: asValue(joinerConfig),
|
||||
primaryKey: asValue(primary.foreignKey.split(",")),
|
||||
foreignKey: asValue(foreign.foreignKey),
|
||||
extraFields: asValue(
|
||||
Object.keys(joinerConfig.databaseConfig?.extraFields || {})
|
||||
),
|
||||
|
||||
linkModuleService: asClass(getModuleService(joinerConfig)).singleton(),
|
||||
linkService: asClass(LinkService).singleton(),
|
||||
|
||||
baseRepository: asClass(BaseRepository).singleton(),
|
||||
linkRepository: asClass(getLinkRepository(entity)).singleton(),
|
||||
})
|
||||
}
|
||||
}
|
||||
26
packages/link-modules/src/loaders/index.ts
Normal file
26
packages/link-modules/src/loaders/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import {
|
||||
JoinerRelationship,
|
||||
ModuleJoinerConfig,
|
||||
ModuleLoaderFunction,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { generateEntity } from "../utils"
|
||||
import { connectionLoader } from "./connection"
|
||||
import { containerLoader } from "./container"
|
||||
|
||||
export function getLoaders({
|
||||
joinerConfig,
|
||||
primary,
|
||||
foreign,
|
||||
}: {
|
||||
joinerConfig: ModuleJoinerConfig
|
||||
primary: JoinerRelationship
|
||||
foreign: JoinerRelationship
|
||||
}): ModuleLoaderFunction[] {
|
||||
if (joinerConfig.isReadOnlyLink) {
|
||||
return []
|
||||
}
|
||||
|
||||
const entity = generateEntity(joinerConfig, primary, foreign)
|
||||
return [connectionLoader(entity), containerLoader(entity, joinerConfig)]
|
||||
}
|
||||
78
packages/link-modules/src/migration/index.ts
Normal file
78
packages/link-modules/src/migration/index.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
JoinerRelationship,
|
||||
LoaderOptions,
|
||||
Logger,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeOptions,
|
||||
} from "@medusajs/types"
|
||||
import { generateEntity } from "../utils"
|
||||
|
||||
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
|
||||
|
||||
export function getMigration(
|
||||
joinerConfig: ModuleJoinerConfig,
|
||||
serviceName: string,
|
||||
primary: JoinerRelationship,
|
||||
foreign: JoinerRelationship
|
||||
) {
|
||||
return async function runMigrations(
|
||||
{
|
||||
options,
|
||||
logger,
|
||||
}: Pick<
|
||||
LoaderOptions<ModuleServiceInitializeOptions>,
|
||||
"options" | "logger"
|
||||
> = {} as any
|
||||
) {
|
||||
logger ??= console as unknown as Logger
|
||||
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("link_modules", options)
|
||||
const entity = generateEntity(joinerConfig, primary, foreign)
|
||||
|
||||
const orm = await DALUtils.mikroOrmCreateConnection(dbData, [entity])
|
||||
|
||||
const tableName = entity.meta.collection
|
||||
|
||||
let hasTable = false
|
||||
try {
|
||||
await orm.em.getConnection().execute(`SELECT 1 FROM ${tableName} LIMIT 0`)
|
||||
hasTable = true
|
||||
} catch {}
|
||||
|
||||
const generator = orm.getSchemaGenerator()
|
||||
if (hasTable) {
|
||||
const updateSql = await generator.getUpdateSchemaSQL()
|
||||
const entityUpdates = updateSql
|
||||
.split(";")
|
||||
.map((sql) => sql.trim())
|
||||
.filter((sql) =>
|
||||
sql.toLowerCase().includes(`alter table "${tableName.toLowerCase()}"`)
|
||||
)
|
||||
|
||||
if (entityUpdates.length > 0) {
|
||||
try {
|
||||
await generator.execute(entityUpdates.join(";"))
|
||||
logger.info(`Link module "${serviceName}" migration executed`)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Link module "${serviceName}" migration failed to run - Error: ${error}`
|
||||
)
|
||||
}
|
||||
} else {
|
||||
logger.info(`Skipping "${tableName}" migration.`)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await generator.createSchema()
|
||||
|
||||
logger.info(`Link module "${serviceName}" migration executed`)
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Link module "${serviceName}" migration failed to run - Error: ${error}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
await orm.close()
|
||||
}
|
||||
}
|
||||
2
packages/link-modules/src/repositories/index.ts
Normal file
2
packages/link-modules/src/repositories/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||
export { getLinkRepository } from "./link"
|
||||
104
packages/link-modules/src/repositories/link.ts
Normal file
104
packages/link-modules/src/repositories/link.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { Context, FindOptions, ModuleJoinerConfig } from "@medusajs/types"
|
||||
import {
|
||||
EntitySchema,
|
||||
LoadStrategy,
|
||||
FilterQuery as MikroFilterQuery,
|
||||
FindOptions as MikroOptions,
|
||||
} from "@mikro-orm/core"
|
||||
|
||||
import {
|
||||
MikroOrmAbstractBaseRepository,
|
||||
generateEntityId,
|
||||
} from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
|
||||
export function getLinkRepository(model: EntitySchema) {
|
||||
return class LinkRepository extends MikroOrmAbstractBaseRepository<object> {
|
||||
readonly manager_: SqlEntityManager
|
||||
readonly model_: EntitySchema
|
||||
readonly joinerConfig_: ModuleJoinerConfig
|
||||
|
||||
constructor({
|
||||
manager,
|
||||
joinerConfig,
|
||||
}: {
|
||||
manager: SqlEntityManager
|
||||
joinerConfig: ModuleJoinerConfig
|
||||
}) {
|
||||
// @ts-ignore
|
||||
super(...arguments)
|
||||
this.manager_ = manager
|
||||
this.model_ = model
|
||||
this.joinerConfig_ = joinerConfig
|
||||
}
|
||||
|
||||
async find(
|
||||
findOptions: FindOptions<unknown> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<unknown[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.find(
|
||||
this.model_,
|
||||
findOptions_.where as MikroFilterQuery<unknown>,
|
||||
findOptions_.options as MikroOptions<any>
|
||||
)
|
||||
}
|
||||
|
||||
async findAndCount(
|
||||
findOptions: FindOptions<object> = { where: {} },
|
||||
context: Context = {}
|
||||
): Promise<[object[], number]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const findOptions_ = { ...findOptions }
|
||||
findOptions_.options ??= {}
|
||||
|
||||
Object.assign(findOptions_.options, {
|
||||
strategy: LoadStrategy.SELECT_IN,
|
||||
})
|
||||
|
||||
return await manager.findAndCount(
|
||||
this.model_,
|
||||
findOptions_.where as MikroFilterQuery<unknown>,
|
||||
findOptions_.options as MikroOptions<any>
|
||||
)
|
||||
}
|
||||
|
||||
async delete(data: any, context: Context = {}): Promise<void> {
|
||||
const filter = {}
|
||||
for (const key in data) {
|
||||
filter[key] = {
|
||||
$in: Array.isArray(data[key]) ? data[key] : [data[key]],
|
||||
}
|
||||
}
|
||||
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
await manager.nativeDelete(this.model_, data, {})
|
||||
}
|
||||
|
||||
async create(data: object[], context: Context = {}): Promise<object[]> {
|
||||
const manager = this.getActiveManager<SqlEntityManager>(context)
|
||||
|
||||
const links = data.map((link: any) => {
|
||||
link.id = generateEntityId(
|
||||
link.id,
|
||||
this.joinerConfig_.databaseConfig?.idPrefix ?? "link"
|
||||
)
|
||||
|
||||
return manager.create(this.model_, link)
|
||||
})
|
||||
|
||||
await manager.upsertMany(this.model_, links)
|
||||
|
||||
return links
|
||||
}
|
||||
}
|
||||
}
|
||||
22
packages/link-modules/src/services/dynamic-service-class.ts
Normal file
22
packages/link-modules/src/services/dynamic-service-class.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { Constructor, ILinkModule, ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { LinkModuleService } from "@services"
|
||||
|
||||
export function getModuleService(
|
||||
joinerConfig: ModuleJoinerConfig
|
||||
): Constructor<ILinkModule> {
|
||||
const joinerConfig_ = JSON.parse(JSON.stringify(joinerConfig))
|
||||
delete joinerConfig_.databaseConfig
|
||||
return class LinkService extends LinkModuleService<unknown> {
|
||||
override __joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig_ as ModuleJoinerConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function getReadOnlyModuleService(joinerConfig: ModuleJoinerConfig) {
|
||||
return class ReadOnlyLinkService {
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig as ModuleJoinerConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
3
packages/link-modules/src/services/index.ts
Normal file
3
packages/link-modules/src/services/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export * from "./dynamic-service-class"
|
||||
export { default as LinkService } from "./link"
|
||||
export { default as LinkModuleService } from "./link-module-service"
|
||||
301
packages/link-modules/src/services/link-module-service.ts
Normal file
301
packages/link-modules/src/services/link-module-service.ts
Normal file
@@ -0,0 +1,301 @@
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FindConfig,
|
||||
ILinkModule,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
RestoreReturn,
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MapToConfig,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
mapObjectTo,
|
||||
} from "@medusajs/utils"
|
||||
import { LinkService } from "@services"
|
||||
import { shouldForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
baseRepository: DAL.RepositoryService
|
||||
linkService: LinkService<any>
|
||||
primaryKey: string | string[]
|
||||
foreignKey: string
|
||||
extraFields: string[]
|
||||
}
|
||||
|
||||
export default class LinkModuleService<TLink> implements ILinkModule {
|
||||
protected baseRepository_: DAL.RepositoryService
|
||||
protected readonly linkService_: LinkService<TLink>
|
||||
protected primaryKey_: string[]
|
||||
protected foreignKey_: string
|
||||
protected extraFields_: string[]
|
||||
|
||||
constructor(
|
||||
{
|
||||
baseRepository,
|
||||
linkService,
|
||||
primaryKey,
|
||||
foreignKey,
|
||||
extraFields,
|
||||
}: InjectedDependencies,
|
||||
readonly moduleDeclaration: InternalModuleDeclaration
|
||||
) {
|
||||
this.baseRepository_ = baseRepository
|
||||
this.linkService_ = linkService
|
||||
this.primaryKey_ = !Array.isArray(primaryKey) ? [primaryKey] : primaryKey
|
||||
this.foreignKey_ = foreignKey
|
||||
this.extraFields_ = extraFields
|
||||
}
|
||||
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return {} as ModuleJoinerConfig
|
||||
}
|
||||
|
||||
private buildData(
|
||||
primaryKeyData: string | string[],
|
||||
foreignKeyData: string,
|
||||
extra: Record<string, unknown> = {}
|
||||
) {
|
||||
if (this.primaryKey_.length > 1) {
|
||||
if (
|
||||
!Array.isArray(primaryKeyData) ||
|
||||
primaryKeyData.length !== this.primaryKey_.length
|
||||
) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Primary key data must be an array ${this.primaryKey_.length} values`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
return {
|
||||
[pk]: primaryKeyData,
|
||||
[this.foreignKey_]: foreignKeyData,
|
||||
...extra,
|
||||
}
|
||||
}
|
||||
|
||||
private isValidKeyName(name: string) {
|
||||
return this.primaryKey_.concat(this.foreignKey_).includes(name)
|
||||
}
|
||||
|
||||
private validateFields(data: any) {
|
||||
const keys = Object.keys(data)
|
||||
if (!keys.every((k) => this.isValidKeyName(k))) {
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.INVALID_DATA,
|
||||
`Invalid field name provided. Valid field names are ${this.primaryKey_.concat(
|
||||
this.foreignKey_
|
||||
)}`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async retrieve(
|
||||
primaryKeyData: string | string[],
|
||||
foreignKeyData: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<unknown> {
|
||||
const filter = this.buildData(primaryKeyData, foreignKeyData)
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(filter)
|
||||
const entry = await this.linkService_.list(queryOptions, {}, sharedContext)
|
||||
|
||||
if (!entry?.length) {
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const errMessage = `${pk}[${primaryKeyData}] and ${this.foreignKey_}[${foreignKeyData}]`
|
||||
|
||||
throw new MedusaError(
|
||||
MedusaError.Types.NOT_FOUND,
|
||||
`Entry ${errMessage} was not found`
|
||||
)
|
||||
}
|
||||
|
||||
return entry[0]
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async list(
|
||||
filters: Record<string, unknown> = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<unknown[]> {
|
||||
const rows = await this.linkService_.list(filters, config, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(rows)
|
||||
}
|
||||
|
||||
@InjectManager("baseRepository_")
|
||||
async listAndCount(
|
||||
filters: Record<string, unknown> = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[unknown[], number]> {
|
||||
const [rows, count] = await this.linkService_.listAndCount(
|
||||
filters,
|
||||
config,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return [await this.baseRepository_.serialize<object[]>(rows), count]
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async create(
|
||||
primaryKeyOrBulkData:
|
||||
| string
|
||||
| string[]
|
||||
| [string | string[], string, Record<string, unknown>][],
|
||||
foreignKeyData?: string,
|
||||
extraFields?: Record<string, unknown>,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const data: unknown[] = []
|
||||
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
|
||||
for (const [primaryKey, foreignKey, extra] of primaryKeyOrBulkData) {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKey as string | string[],
|
||||
foreignKey as string,
|
||||
extra as Record<string, unknown>
|
||||
)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKeyOrBulkData as string | string[],
|
||||
foreignKeyData!,
|
||||
extraFields
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const links = await this.linkService_.create(data, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(links)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async dismiss(
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
) {
|
||||
const data: unknown[] = []
|
||||
if (foreignKeyData === undefined && Array.isArray(primaryKeyOrBulkData)) {
|
||||
for (const [primaryKey, foreignKey] of primaryKeyOrBulkData) {
|
||||
data.push(this.buildData(primaryKey, foreignKey as string))
|
||||
}
|
||||
} else {
|
||||
data.push(
|
||||
this.buildData(
|
||||
primaryKeyOrBulkData as string | string[],
|
||||
foreignKeyData!
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
const links = await this.linkService_.dismiss(data, sharedContext)
|
||||
|
||||
return await this.baseRepository_.serialize<object[]>(links)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async delete(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
this.validateFields(data)
|
||||
|
||||
await this.linkService_.delete(data, sharedContext)
|
||||
}
|
||||
|
||||
async softDelete(
|
||||
data: any,
|
||||
{ returnLinkableKeys }: SoftDeleteReturn = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, unknown[]> | void> {
|
||||
this.validateFields(data)
|
||||
|
||||
let [, cascadedEntitiesMap] = await this.softDelete_(data, sharedContext)
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
LinkModel: [
|
||||
{ mapTo: pk, valueFrom: pk },
|
||||
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
|
||||
],
|
||||
}
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
protected async softDelete_(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
return await this.linkService_.softDelete(data, sharedContext)
|
||||
}
|
||||
|
||||
async restore(
|
||||
data: any,
|
||||
{ returnLinkableKeys }: RestoreReturn = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<string, unknown[]> | void> {
|
||||
this.validateFields(data)
|
||||
|
||||
let [, cascadedEntitiesMap] = await this.restore_(data, sharedContext)
|
||||
|
||||
const pk = this.primaryKey_.join(",")
|
||||
const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
LinkModel: [
|
||||
{ mapTo: pk, valueFrom: pk },
|
||||
{ mapTo: this.foreignKey_, valueFrom: this.foreignKey_ },
|
||||
],
|
||||
}
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<Record<string, string[]>>(
|
||||
cascadedEntitiesMap,
|
||||
entityNameToLinkableKeysMap,
|
||||
{
|
||||
pick: returnLinkableKeys,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
return await this.linkService_.restore(data, sharedContext)
|
||||
}
|
||||
}
|
||||
117
packages/link-modules/src/services/link.ts
Normal file
117
packages/link-modules/src/services/link.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import { Context, FindConfig } from "@medusajs/types"
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
MedusaContext,
|
||||
ModulesSdkUtils,
|
||||
} from "@medusajs/utils"
|
||||
import { doNotForceTransaction } from "../utils"
|
||||
|
||||
type InjectedDependencies = {
|
||||
linkRepository: any
|
||||
}
|
||||
|
||||
export default class LinkService<TEntity> {
|
||||
protected readonly linkRepository_: any
|
||||
|
||||
constructor({ linkRepository }: InjectedDependencies) {
|
||||
this.linkRepository_ = linkRepository
|
||||
}
|
||||
|
||||
@InjectManager("linkRepository_")
|
||||
async list(
|
||||
filters: unknown = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(
|
||||
filters as any,
|
||||
config
|
||||
)
|
||||
return await this.linkRepository_.find(queryOptions, sharedContext)
|
||||
}
|
||||
|
||||
@InjectManager("linkRepository_")
|
||||
async listAndCount(
|
||||
filters = {},
|
||||
config: FindConfig<unknown> = {},
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[TEntity[], number]> {
|
||||
const queryOptions = ModulesSdkUtils.buildQuery<unknown>(filters, config)
|
||||
return await this.linkRepository_.findAndCount(queryOptions, sharedContext)
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async create(
|
||||
data: unknown[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
return await this.linkRepository_.create(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async dismiss(
|
||||
data: unknown[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
const filter: any = []
|
||||
for (const pair of data) {
|
||||
filter.push({
|
||||
$and: Object.entries(pair as object).map(([key, value]) => ({
|
||||
[key]: value,
|
||||
})),
|
||||
})
|
||||
}
|
||||
|
||||
const [rows] = await this.linkRepository_.softDelete(
|
||||
{ $or: filter },
|
||||
{
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
}
|
||||
)
|
||||
|
||||
return rows
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async delete(
|
||||
data: unknown,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<void> {
|
||||
await this.linkRepository_.delete(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async softDelete(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
const filter = {}
|
||||
for (const key in data) {
|
||||
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
|
||||
}
|
||||
|
||||
return await this.linkRepository_.softDelete(filter, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
|
||||
@InjectTransactionManager(doNotForceTransaction, "linkRepository_")
|
||||
async restore(
|
||||
data: any,
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<[string[], Record<string, string[]>]> {
|
||||
const filter = {}
|
||||
for (const key in data) {
|
||||
filter[key] = { $in: Array.isArray(data[key]) ? data[key] : [data[key]] }
|
||||
}
|
||||
|
||||
return await this.linkRepository_.restore(data, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
}
|
||||
}
|
||||
5
packages/link-modules/src/types/index.ts
Normal file
5
packages/link-modules/src/types/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { Logger } from "@medusajs/types"
|
||||
|
||||
export type InitializeModuleInjectableDependencies = {
|
||||
logger?: Logger
|
||||
}
|
||||
9
packages/link-modules/src/utils/compose-link-name.ts
Normal file
9
packages/link-modules/src/utils/compose-link-name.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { lowerCaseFirst, toPascalCase } from "@medusajs/utils"
|
||||
|
||||
export const composeLinkName = (...args) => {
|
||||
return lowerCaseFirst(toPascalCase(composeTableName(...args.concat("link"))))
|
||||
}
|
||||
|
||||
export const composeTableName = (...args) => {
|
||||
return args.map((name) => name.replace(/(_id|Service)$/gi, "")).join("_")
|
||||
}
|
||||
110
packages/link-modules/src/utils/generate-entity.ts
Normal file
110
packages/link-modules/src/utils/generate-entity.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { JoinerRelationship, ModuleJoinerConfig } from "@medusajs/types"
|
||||
import {
|
||||
SoftDeletableFilterKey,
|
||||
mikroOrmSoftDeletableFilterOptions,
|
||||
simpleHash,
|
||||
} from "@medusajs/utils"
|
||||
|
||||
import { EntitySchema } from "@mikro-orm/core"
|
||||
import { composeTableName } from "./compose-link-name"
|
||||
|
||||
function getClass(...properties) {
|
||||
return class LinkModel {
|
||||
constructor(...values) {
|
||||
properties.forEach((name, idx) => {
|
||||
this[name] = values[idx]
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function generateEntity(
|
||||
joinerConfig: ModuleJoinerConfig,
|
||||
primary: JoinerRelationship,
|
||||
foreign: JoinerRelationship
|
||||
) {
|
||||
const fieldNames = primary.foreignKey.split(",").concat(foreign.foreignKey)
|
||||
|
||||
const tableName =
|
||||
joinerConfig.databaseConfig?.tableName ??
|
||||
composeTableName(
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey
|
||||
)
|
||||
|
||||
const fields = fieldNames.reduce((acc, curr) => {
|
||||
acc[curr] = {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
primary: true,
|
||||
}
|
||||
return acc
|
||||
}, {})
|
||||
|
||||
const extraFields = joinerConfig.databaseConfig?.extraFields ?? {}
|
||||
|
||||
for (const column in extraFields) {
|
||||
fieldNames.push(column)
|
||||
|
||||
fields[column] = {
|
||||
type: extraFields[column].type,
|
||||
nullable: !!extraFields[column].nullable,
|
||||
defaultRaw: extraFields[column].defaultValue,
|
||||
...(extraFields[column].options ?? {}),
|
||||
}
|
||||
}
|
||||
|
||||
const hashTableName = simpleHash(tableName)
|
||||
|
||||
return new EntitySchema({
|
||||
class: getClass(
|
||||
...fieldNames.concat("created_at", "updated_at", "deleted_at")
|
||||
) as any,
|
||||
tableName,
|
||||
properties: {
|
||||
id: {
|
||||
type: "string",
|
||||
nullable: false,
|
||||
},
|
||||
...fields,
|
||||
created_at: {
|
||||
type: "Date",
|
||||
nullable: false,
|
||||
defaultRaw: "CURRENT_TIMESTAMP",
|
||||
},
|
||||
updated_at: {
|
||||
type: "Date",
|
||||
nullable: false,
|
||||
defaultRaw: "CURRENT_TIMESTAMP",
|
||||
},
|
||||
deleted_at: { type: "Date", nullable: true },
|
||||
},
|
||||
filters: {
|
||||
[SoftDeletableFilterKey]: mikroOrmSoftDeletableFilterOptions,
|
||||
},
|
||||
indexes: [
|
||||
{
|
||||
properties: ["id"],
|
||||
name: "IDX_id_" + hashTableName,
|
||||
},
|
||||
{
|
||||
properties: primary.foreignKey.split(","),
|
||||
name:
|
||||
"IDX_" +
|
||||
primary.foreignKey.split(",").join("_") +
|
||||
"_" +
|
||||
hashTableName,
|
||||
},
|
||||
{
|
||||
properties: foreign.foreignKey,
|
||||
name: "IDX_" + foreign.foreignKey + "_" + hashTableName,
|
||||
},
|
||||
{
|
||||
properties: ["deleted_at"],
|
||||
name: "IDX_deleted_at_" + hashTableName,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
12
packages/link-modules/src/utils/index.ts
Normal file
12
packages/link-modules/src/utils/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { MODULE_RESOURCE_TYPE } from "@medusajs/types"
|
||||
|
||||
export * from "./compose-link-name"
|
||||
export * from "./generate-entity"
|
||||
|
||||
export function shouldForceTransaction(target: any): boolean {
|
||||
return target.moduleDeclaration?.resources === MODULE_RESOURCE_TYPE.ISOLATED
|
||||
}
|
||||
|
||||
export function doNotForceTransaction(): boolean {
|
||||
return false
|
||||
}
|
||||
35
packages/link-modules/tsconfig.json
Normal file
35
packages/link-modules/tsconfig.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"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": {
|
||||
"@services": ["./src/services"],
|
||||
"@repositories": ["./src/repositories"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": [
|
||||
"dist",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__",
|
||||
"./src/**/__fixtures__",
|
||||
"node_modules"
|
||||
]
|
||||
}
|
||||
5
packages/link-modules/tsconfig.spec.json
Normal file
5
packages/link-modules/tsconfig.spec.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["src", "integration-tests"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { stringifyNullProperties } from "../../src/utils"
|
||||
const { stringifyNullProperties } = require("../../utils")
|
||||
|
||||
describe("stringifyNullProperties", () => {
|
||||
test("returns empty object on no props", () => {
|
||||
@@ -1,4 +1,10 @@
|
||||
{
|
||||
"extends": "./tsconfig.json",
|
||||
"include": ["test"],
|
||||
"include": [
|
||||
"./src/**/*",
|
||||
"index.d.ts",
|
||||
"./src/**/__tests__",
|
||||
"./src/**/__mocks__"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -18,10 +18,10 @@
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/orchestration": "^0.2.0",
|
||||
|
||||
27
packages/modules-sdk/src/__mocks__/inventory-module.ts
Normal file
27
packages/modules-sdk/src/__mocks__/inventory-module.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
export const InventoryModule = {
|
||||
__definition: {
|
||||
key: "inventoryService",
|
||||
registrationName: "inventoryService",
|
||||
defaultPackage: false,
|
||||
label: "InventoryService",
|
||||
isRequired: false,
|
||||
canOverride: true,
|
||||
isQueryable: true,
|
||||
dependencies: [],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "inventoryService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: [
|
||||
"inventory_item_id",
|
||||
"inventory_level_id",
|
||||
"reservation_item_id",
|
||||
],
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
export const InventoryStockLocationLink = {
|
||||
__definition: {
|
||||
key: "inventoryStockLocationLink",
|
||||
registrationName: "inventoryStockLocationLink",
|
||||
defaultPackage: "",
|
||||
label: "inventoryStockLocationLink",
|
||||
canOverride: true,
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
isLink: true,
|
||||
alias: [
|
||||
{
|
||||
name: "inventory_level_stock_location",
|
||||
},
|
||||
{
|
||||
name: "inventory_level_stock_locations",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["inventory_level_id", "stock_location_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_level_id",
|
||||
alias: "inventory_level",
|
||||
args: {},
|
||||
},
|
||||
{
|
||||
serviceName: "stockLocationService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "stock_location_id",
|
||||
alias: "stock_location",
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
relationship: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
primaryKey: "inventory_level_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_location_items",
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: "stockLocationService",
|
||||
relationship: {
|
||||
serviceName: "inventoryStockLocationLink",
|
||||
primaryKey: "stock_location_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_location_items",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: jest.fn(
|
||||
async (
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string
|
||||
) => {}
|
||||
),
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
77
packages/modules-sdk/src/__mocks__/product-inventory-link.ts
Normal file
77
packages/modules-sdk/src/__mocks__/product-inventory-link.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
export const ProductInventoryLinkModule = {
|
||||
__definition: {
|
||||
key: "productVariantInventoryInventoryItemLink",
|
||||
registrationName: "productVariantInventoryInventoryItemLink",
|
||||
defaultPackage: "",
|
||||
label: "productVariantInventoryInventoryItemLink",
|
||||
canOverride: true,
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
isLink: true,
|
||||
databaseConfig: {
|
||||
tableName: "product_variant_inventory_item",
|
||||
},
|
||||
alias: [
|
||||
{
|
||||
name: "product_variant_inventory_item",
|
||||
},
|
||||
{
|
||||
name: "product_variant_inventory_items",
|
||||
},
|
||||
],
|
||||
primaryKeys: ["variant_id", "inventory_item_id"],
|
||||
relationships: [
|
||||
{
|
||||
serviceName: "productService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
args: {},
|
||||
deleteCascade: true,
|
||||
},
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
deleteCascade: true,
|
||||
},
|
||||
],
|
||||
extends: [
|
||||
{
|
||||
serviceName: "productService",
|
||||
relationship: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "variant_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_items",
|
||||
isList: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
serviceName: "inventoryService",
|
||||
relationship: {
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "inventory_item_id",
|
||||
foreignKey: "id",
|
||||
alias: "variant_link",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
create: jest.fn(
|
||||
async (
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string
|
||||
) => {}
|
||||
),
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
24
packages/modules-sdk/src/__mocks__/product-module.ts
Normal file
24
packages/modules-sdk/src/__mocks__/product-module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const ProductModule = {
|
||||
__definition: {
|
||||
key: "productService",
|
||||
registrationName: "productModuleService",
|
||||
defaultPackage: false,
|
||||
label: "ProductModuleService",
|
||||
isRequired: false,
|
||||
canOverride: true,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusModuleService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "productService",
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: ["product_id", "variant_id"],
|
||||
alias: [],
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
24
packages/modules-sdk/src/__mocks__/stock-location-module.ts
Normal file
24
packages/modules-sdk/src/__mocks__/stock-location-module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
export const StockLocationModule = {
|
||||
__definition: {
|
||||
key: "stockLocationService",
|
||||
registrationName: "stockLocationService",
|
||||
defaultPackage: false,
|
||||
label: "StockLocationService",
|
||||
isRequired: false,
|
||||
canOverride: true,
|
||||
isQueryable: true,
|
||||
dependencies: ["eventBusService"],
|
||||
defaultModuleDeclaration: {
|
||||
scope: "internal",
|
||||
resources: "shared",
|
||||
},
|
||||
},
|
||||
__joinerConfig: {
|
||||
serviceName: "stockLocationService",
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: ["stock_location_id"],
|
||||
alias: [],
|
||||
},
|
||||
|
||||
softDelete: jest.fn(() => {}),
|
||||
}
|
||||
202
packages/modules-sdk/src/__tests__/remote-link.spec.ts
Normal file
202
packages/modules-sdk/src/__tests__/remote-link.spec.ts
Normal file
@@ -0,0 +1,202 @@
|
||||
import { InventoryModule } from "../__mocks__/inventory-module"
|
||||
import { InventoryStockLocationLink } from "../__mocks__/inventory-stock-location-link"
|
||||
import { ProductInventoryLinkModule } from "../__mocks__/product-inventory-link"
|
||||
import { ProductModule } from "../__mocks__/product-module"
|
||||
import { StockLocationModule } from "../__mocks__/stock-location-module"
|
||||
|
||||
import { RemoteLink } from "../remote-link"
|
||||
|
||||
const allModules = [
|
||||
// modules
|
||||
ProductModule,
|
||||
InventoryModule,
|
||||
StockLocationModule,
|
||||
// links
|
||||
ProductInventoryLinkModule,
|
||||
InventoryStockLocationLink,
|
||||
]
|
||||
describe("Remote Link", function () {
|
||||
it("Should get all loaded modules and compose their relationships", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
const relations = remoteLink.getRelationships()
|
||||
|
||||
const prodInventoryLink = relations.get(
|
||||
"productVariantInventoryInventoryItemLink"
|
||||
)
|
||||
const prodModule = relations.get("productService")
|
||||
const inventoryModule = relations.get("inventoryService")
|
||||
|
||||
expect(prodInventoryLink?.get("variant_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "variant_id",
|
||||
alias: "variant",
|
||||
deleteCascade: true,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(prodInventoryLink?.get("inventory_item_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "inventoryService",
|
||||
primaryKey: "id",
|
||||
foreignKey: "inventory_item_id",
|
||||
alias: "inventory",
|
||||
deleteCascade: true,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(prodModule?.get("variant_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "variant_id",
|
||||
foreignKey: "id",
|
||||
alias: "inventory_items",
|
||||
isList: true,
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
|
||||
expect(inventoryModule?.get("inventory_item_id")).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
serviceName: "productVariantInventoryInventoryItemLink",
|
||||
primaryKey: "inventory_item_id",
|
||||
foreignKey: "id",
|
||||
alias: "variant_link",
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
}),
|
||||
])
|
||||
)
|
||||
})
|
||||
|
||||
it("Should call the correct link module to create relation between 2 keys", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
await remoteLink.create([
|
||||
{
|
||||
productService: {
|
||||
variant_id: "var_123",
|
||||
},
|
||||
inventoryService: {
|
||||
inventory_item_id: "inv_123",
|
||||
},
|
||||
},
|
||||
{
|
||||
productService: {
|
||||
variant_id: "var_abc",
|
||||
},
|
||||
inventoryService: {
|
||||
inventory_item_id: "inv_abc",
|
||||
},
|
||||
},
|
||||
{
|
||||
inventoryService: {
|
||||
inventory_level_id: "ilev_123",
|
||||
},
|
||||
stockLocationService: {
|
||||
stock_location_id: "loc_123",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
expect(ProductInventoryLinkModule.create).toBeCalledWith([
|
||||
["var_123", "inv_123"],
|
||||
["var_abc", "inv_abc"],
|
||||
])
|
||||
expect(InventoryStockLocationLink.create).toBeCalledWith([
|
||||
["ilev_123", "loc_123"],
|
||||
])
|
||||
})
|
||||
|
||||
it("Should call delete in cascade all the modules involved in the link", async function () {
|
||||
const remoteLink = new RemoteLink(allModules as any)
|
||||
|
||||
ProductInventoryLinkModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
variant_id: ["var_123"],
|
||||
inventory_item_id: ["inv_123"],
|
||||
}
|
||||
})
|
||||
|
||||
ProductModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
product_id: ["prod_123", "prod_abc"],
|
||||
variant_id: ["var_123", "var_abc"],
|
||||
}
|
||||
})
|
||||
|
||||
InventoryModule.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
inventory_item_id: ["inv_123"],
|
||||
inventory_level_id: ["ilev_123"],
|
||||
}
|
||||
})
|
||||
|
||||
InventoryStockLocationLink.softDelete.mockImplementation(() => {
|
||||
return {
|
||||
inventory_level_id: ["ilev_123"],
|
||||
stock_location_id: ["loc_123"],
|
||||
}
|
||||
})
|
||||
|
||||
await remoteLink.delete({
|
||||
productService: {
|
||||
variant_id: "var_123",
|
||||
},
|
||||
})
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toBeCalledTimes(2)
|
||||
expect(ProductModule.softDelete).toBeCalledTimes(1)
|
||||
expect(InventoryModule.softDelete).toBeCalledTimes(1)
|
||||
expect(InventoryStockLocationLink.softDelete).toBeCalledTimes(1)
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
{ variant_id: ["var_123"] },
|
||||
{ returnLinkableKeys: ["variant_id", "inventory_item_id"] }
|
||||
)
|
||||
|
||||
expect(ProductInventoryLinkModule.softDelete).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
{ variant_id: ["var_abc"] },
|
||||
{ returnLinkableKeys: ["variant_id", "inventory_item_id"] }
|
||||
)
|
||||
|
||||
expect(ProductModule.softDelete).toBeCalledWith(
|
||||
{ id: ["var_123"] },
|
||||
{ returnLinkableKeys: ["product_id", "variant_id"] }
|
||||
)
|
||||
|
||||
expect(InventoryModule.softDelete).toBeCalledWith(
|
||||
{ id: ["inv_123"] },
|
||||
{
|
||||
returnLinkableKeys: [
|
||||
"inventory_item_id",
|
||||
"inventory_level_id",
|
||||
"reservation_item_id",
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
expect(InventoryStockLocationLink.softDelete).toBeCalledWith(
|
||||
{
|
||||
inventory_level_id: ["ilev_123"],
|
||||
},
|
||||
{ returnLinkableKeys: ["inventory_level_id", "stock_location_id"] }
|
||||
)
|
||||
})
|
||||
})
|
||||
@@ -1,6 +1,8 @@
|
||||
export * from "@medusajs/types/dist/modules-sdk"
|
||||
export * from "./definitions"
|
||||
export * from "./loaders"
|
||||
export * from "./medusa-app"
|
||||
export * from "./medusa-module"
|
||||
export * from "./module-helper"
|
||||
export * from "./remote-link"
|
||||
export * from "./remote-query"
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import {
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
MODULE_SCOPE,
|
||||
MedusaContainer,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { asValue } from "awilix"
|
||||
import { EOL } from "os"
|
||||
import { ModulesHelper } from "../module-helper"
|
||||
@@ -11,53 +12,6 @@ import { loadInternalModule } from "./utils"
|
||||
|
||||
export const moduleHelper = new ModulesHelper()
|
||||
|
||||
async function loadModule(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
logger: Logger
|
||||
): Promise<{ error?: Error } | void> {
|
||||
const modDefinition = resolution.definition
|
||||
const registrationName = modDefinition.registrationName
|
||||
|
||||
const { scope, resources } = resolution.moduleDeclaration ?? ({} as any)
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
if (!scope || (scope === MODULE_SCOPE.INTERNAL && !resources)) {
|
||||
let message = `The module ${resolution.definition.label} has to define its scope (internal | external)`
|
||||
if (scope === MODULE_SCOPE.INTERNAL && !resources) {
|
||||
message = `The module ${resolution.definition.label} is missing its resources config`
|
||||
}
|
||||
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
|
||||
return {
|
||||
error: new Error(message),
|
||||
}
|
||||
}
|
||||
|
||||
if (resolution.resolutionPath === false) {
|
||||
container.register({
|
||||
[registrationName]: asValue(undefined),
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return await loadInternalModule(container, resolution, logger)
|
||||
}
|
||||
|
||||
export const moduleLoader = async ({
|
||||
container,
|
||||
moduleResolutions,
|
||||
@@ -94,7 +48,48 @@ export const moduleLoader = async ({
|
||||
}, {})
|
||||
)
|
||||
|
||||
container.register({
|
||||
modulesHelper: asValue(moduleHelper),
|
||||
})
|
||||
container.register("modulesHelper", asValue(moduleHelper))
|
||||
}
|
||||
|
||||
async function loadModule(
|
||||
container: MedusaContainer,
|
||||
resolution: ModuleResolution,
|
||||
logger: Logger
|
||||
): Promise<{ error?: Error } | void> {
|
||||
const modDefinition = resolution.definition
|
||||
const registrationName = modDefinition.registrationName
|
||||
|
||||
const { scope, resources } = resolution.moduleDeclaration ?? ({} as any)
|
||||
|
||||
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.")
|
||||
}
|
||||
|
||||
if (!scope || (scope === MODULE_SCOPE.INTERNAL && !resources)) {
|
||||
let message = `The module ${resolution.definition.label} has to define its scope (internal | external)`
|
||||
if (scope === MODULE_SCOPE.INTERNAL && !resources) {
|
||||
message = `The module ${resolution.definition.label} is missing its resources config`
|
||||
}
|
||||
|
||||
container.register(registrationName, asValue(undefined))
|
||||
|
||||
return {
|
||||
error: new Error(message),
|
||||
}
|
||||
}
|
||||
|
||||
if (resolution.resolutionPath === false) {
|
||||
container.register(registrationName, asValue(undefined))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
return await loadInternalModule(container, resolution, logger)
|
||||
}
|
||||
|
||||
@@ -6,9 +6,10 @@ import {
|
||||
ModuleExports,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { isObject } from "@medusajs/utils"
|
||||
import resolveCwd from "resolve-cwd"
|
||||
import MODULE_DEFINITIONS from "../definitions"
|
||||
import { MODULE_DEFINITIONS, ModulesDefinition } from "../definitions"
|
||||
|
||||
export const registerModules = (
|
||||
modules?: Record<
|
||||
@@ -46,34 +47,57 @@ export const registerModules = (
|
||||
|
||||
export const registerMedusaModule = (
|
||||
moduleKey: string,
|
||||
moduleDeclaration: InternalModuleDeclaration | ExternalModuleDeclaration,
|
||||
moduleDeclaration:
|
||||
| Partial<InternalModuleDeclaration | ExternalModuleDeclaration>
|
||||
| string
|
||||
| false,
|
||||
moduleExports?: ModuleExports,
|
||||
definition?: ModuleDefinition
|
||||
): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
const modDefinition = definition ?? ModulesDefinition[moduleKey]
|
||||
|
||||
if (modDefinition === undefined) {
|
||||
throw new Error(`Module: ${moduleKey} is not defined.`)
|
||||
}
|
||||
|
||||
if (
|
||||
isObject(moduleDeclaration) &&
|
||||
moduleDeclaration?.scope === MODULE_SCOPE.EXTERNAL
|
||||
) {
|
||||
// TODO: getExternalModuleResolution(...)
|
||||
throw new Error("External Modules are not supported yet.")
|
||||
}
|
||||
|
||||
moduleResolutions[moduleKey] = getInternalModuleResolution(
|
||||
modDefinition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
}
|
||||
|
||||
export const registerMedusaLinkModule = (
|
||||
definition: ModuleDefinition,
|
||||
moduleDeclaration: Partial<InternalModuleDeclaration>,
|
||||
moduleExports?: ModuleExports
|
||||
): Record<string, ModuleResolution> => {
|
||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||
|
||||
for (const definition of MODULE_DEFINITIONS) {
|
||||
if (definition.key !== moduleKey) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (moduleDeclaration.scope === MODULE_SCOPE.EXTERNAL) {
|
||||
// TODO: getExternalModuleResolution(...)
|
||||
throw new Error("External Modules are not supported yet.")
|
||||
}
|
||||
|
||||
moduleResolutions[definition.key] = getInternalModuleResolution(
|
||||
definition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
}
|
||||
moduleResolutions[definition.key] = getInternalModuleResolution(
|
||||
definition,
|
||||
moduleDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
return moduleResolutions
|
||||
}
|
||||
|
||||
function getInternalModuleResolution(
|
||||
definition: ModuleDefinition,
|
||||
moduleConfig: InternalModuleDeclaration | false | string,
|
||||
moduleConfig: InternalModuleDeclaration | string | false,
|
||||
moduleExports?: ModuleExports
|
||||
): ModuleResolution {
|
||||
if (typeof moduleConfig === "boolean") {
|
||||
@@ -116,7 +140,7 @@ function getInternalModuleResolution(
|
||||
),
|
||||
],
|
||||
moduleDeclaration: {
|
||||
...definition.defaultModuleDeclaration,
|
||||
...(definition.defaultModuleDeclaration ?? {}),
|
||||
...moduleDeclaration,
|
||||
},
|
||||
moduleExports,
|
||||
|
||||
@@ -2,9 +2,9 @@ import {
|
||||
Constructor,
|
||||
InternalModuleDeclaration,
|
||||
Logger,
|
||||
MedusaContainer,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
MedusaContainer,
|
||||
ModuleExports,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
@@ -30,9 +30,14 @@ export async function loadInternalModule(
|
||||
// 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
|
||||
const path = resolution.resolutionPath as string
|
||||
|
||||
if (resolution.moduleExports) {
|
||||
loadedModule = resolution.moduleExports
|
||||
} else {
|
||||
loadedModule = await import(path)
|
||||
loadedModule = (loadedModule as any).default
|
||||
}
|
||||
} catch (error) {
|
||||
if (
|
||||
resolution.definition.isRequired &&
|
||||
|
||||
183
packages/modules-sdk/src/medusa-app.ts
Normal file
183
packages/modules-sdk/src/medusa-app.ts
Normal file
@@ -0,0 +1,183 @@
|
||||
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
||||
import {
|
||||
LoadedModule,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleConfig,
|
||||
ModuleJoinerConfig,
|
||||
ModuleServiceInitializeOptions,
|
||||
RemoteJoinerQuery,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
ContainerRegistrationKeys,
|
||||
ModulesSdkUtils,
|
||||
isObject,
|
||||
} from "@medusajs/utils"
|
||||
import { MODULE_PACKAGE_NAMES, Modules } from "./definitions"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { RemoteLink } from "./remote-link"
|
||||
import { RemoteQuery } from "./remote-query"
|
||||
|
||||
export type MedusaModuleConfig = (Partial<ModuleConfig> | Modules)[]
|
||||
export type SharedResources = {
|
||||
database?: ModuleServiceInitializeOptions["database"] & {
|
||||
pool?: {
|
||||
name?: string
|
||||
afterCreate?: Function
|
||||
min?: number
|
||||
max?: number
|
||||
refreshIdle?: boolean
|
||||
idleTimeoutMillis?: number
|
||||
reapIntervalMillis?: number
|
||||
returnToHead?: boolean
|
||||
priorityRange?: number
|
||||
log?: (message: string, logLevel: string) => void
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function MedusaApp({
|
||||
sharedResourcesConfig,
|
||||
modulesConfigPath,
|
||||
modulesConfig,
|
||||
linkModules,
|
||||
remoteFetchData,
|
||||
}: {
|
||||
sharedResourcesConfig?: SharedResources
|
||||
loadedModules?: LoadedModule[]
|
||||
modulesConfigPath?: string
|
||||
modulesConfig?: MedusaModuleConfig
|
||||
linkModules?: ModuleJoinerConfig | ModuleJoinerConfig[]
|
||||
remoteFetchData?: RemoteFetchDataCallback
|
||||
} = {}): Promise<{
|
||||
modules: Record<string, LoadedModule | LoadedModule[]>
|
||||
link: RemoteLink | undefined
|
||||
query: (
|
||||
query: string | RemoteJoinerQuery,
|
||||
variables?: Record<string, unknown>
|
||||
) => Promise<any>
|
||||
}> {
|
||||
const modules: MedusaModuleConfig =
|
||||
modulesConfig ??
|
||||
(await import(process.cwd() + (modulesConfigPath ?? "/modules-config")))
|
||||
.default
|
||||
|
||||
const injectedDependencies: any = {}
|
||||
|
||||
const dbData = ModulesSdkUtils.loadDatabaseConfig(
|
||||
"medusa",
|
||||
sharedResourcesConfig as ModuleServiceInitializeOptions,
|
||||
true
|
||||
)!
|
||||
const { pool } = sharedResourcesConfig?.database ?? {}
|
||||
|
||||
if (dbData?.clientUrl) {
|
||||
const { knex } = await import("knex")
|
||||
const dbConnection = knex({
|
||||
client: "pg",
|
||||
searchPath: dbData.schema || "public",
|
||||
connection: {
|
||||
connectionString: dbData.clientUrl,
|
||||
ssl: (dbData.driverOptions?.connection as any).ssl! ?? false,
|
||||
},
|
||||
pool: {
|
||||
// https://knexjs.org/guide/#pool
|
||||
...(pool ?? {}),
|
||||
min: pool?.min ?? 0,
|
||||
},
|
||||
})
|
||||
|
||||
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION] = dbConnection
|
||||
}
|
||||
|
||||
const allModules: Record<string, LoadedModule | LoadedModule[]> = {}
|
||||
|
||||
await Promise.all(
|
||||
modules.map(async (mod: Partial<ModuleConfig> | Modules) => {
|
||||
let key: Modules | string = mod as Modules
|
||||
let path: string
|
||||
let declaration: any = {}
|
||||
|
||||
if (isObject(mod)) {
|
||||
if (!mod.module) {
|
||||
throw new Error(
|
||||
`Module ${JSON.stringify(mod)} is missing module name.`
|
||||
)
|
||||
}
|
||||
|
||||
key = mod.module
|
||||
path = mod.path ?? MODULE_PACKAGE_NAMES[key]
|
||||
|
||||
declaration = { ...mod }
|
||||
delete declaration.definition
|
||||
} else {
|
||||
path = MODULE_PACKAGE_NAMES[mod as Modules]
|
||||
}
|
||||
|
||||
if (!path) {
|
||||
throw new Error(`Module ${key} is missing path.`)
|
||||
}
|
||||
|
||||
declaration.scope ??= MODULE_SCOPE.INTERNAL
|
||||
|
||||
if (
|
||||
declaration.scope === MODULE_SCOPE.INTERNAL &&
|
||||
!declaration.resources
|
||||
) {
|
||||
declaration.resources = MODULE_RESOURCE_TYPE.SHARED
|
||||
}
|
||||
|
||||
const loaded = (await MedusaModule.bootstrap(
|
||||
key,
|
||||
path,
|
||||
declaration,
|
||||
undefined,
|
||||
injectedDependencies,
|
||||
isObject(mod) ? mod.definition : undefined
|
||||
)) as LoadedModule
|
||||
|
||||
if (allModules[key] && !Array.isArray(allModules[key])) {
|
||||
allModules[key] = []
|
||||
}
|
||||
|
||||
if (allModules[key]) {
|
||||
;(allModules[key] as LoadedModule[]).push(loaded[key])
|
||||
} else {
|
||||
allModules[key] = loaded[key]
|
||||
}
|
||||
|
||||
return loaded
|
||||
})
|
||||
)
|
||||
|
||||
let link: RemoteLink | undefined = undefined
|
||||
let query: (
|
||||
query: string | RemoteJoinerQuery,
|
||||
variables?: Record<string, unknown>
|
||||
) => Promise<any>
|
||||
|
||||
try {
|
||||
const { initialize: initializeLinks } = await import(
|
||||
"@medusajs/link-modules" as string
|
||||
)
|
||||
await initializeLinks({}, linkModules, injectedDependencies)
|
||||
|
||||
link = new RemoteLink()
|
||||
} catch (err) {
|
||||
console.warn("Error initializing link modules.", err)
|
||||
}
|
||||
|
||||
const remoteQuery = new RemoteQuery(undefined, remoteFetchData)
|
||||
query = async (
|
||||
query: string | RemoteJoinerQuery,
|
||||
variables?: Record<string, unknown>
|
||||
) => {
|
||||
return await remoteQuery.query(query, variables)
|
||||
}
|
||||
|
||||
return {
|
||||
modules: allModules,
|
||||
link,
|
||||
query,
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,13 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
LinkModuleDefinition,
|
||||
LoadedModule,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
MODULE_SCOPE,
|
||||
ModuleDefinition,
|
||||
ModuleExports,
|
||||
ModuleJoinerConfig,
|
||||
ModuleResolution,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
@@ -12,7 +15,11 @@ import {
|
||||
simpleHash,
|
||||
stringifyCircular,
|
||||
} from "@medusajs/utils"
|
||||
import { moduleLoader, registerMedusaModule } from "./loaders"
|
||||
import {
|
||||
moduleLoader,
|
||||
registerMedusaLinkModule,
|
||||
registerMedusaModule,
|
||||
} from "./loaders"
|
||||
|
||||
import { asValue } from "awilix"
|
||||
import { loadModuleMigrations } from "./loaders/utils"
|
||||
@@ -36,6 +43,7 @@ declare global {
|
||||
type ModuleAlias = {
|
||||
key: string
|
||||
hash: string
|
||||
isLink: boolean
|
||||
alias?: string
|
||||
main?: boolean
|
||||
}
|
||||
@@ -125,7 +133,8 @@ export class MedusaModule {
|
||||
defaultPath: string,
|
||||
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
|
||||
moduleExports?: ModuleExports,
|
||||
injectedDependencies?: Record<string, any>
|
||||
injectedDependencies?: Record<string, any>,
|
||||
moduleDefinition?: ModuleDefinition
|
||||
): Promise<{
|
||||
[key: string]: T
|
||||
}> {
|
||||
@@ -157,10 +166,10 @@ export class MedusaModule {
|
||||
|
||||
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
|
||||
modDeclaration = {
|
||||
scope: MODULE_SCOPE.INTERNAL,
|
||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
scope: declaration?.scope || MODULE_SCOPE.INTERNAL,
|
||||
resources: declaration?.resources || MODULE_RESOURCE_TYPE.ISOLATED,
|
||||
resolve: defaultPath,
|
||||
options: declaration,
|
||||
options: declaration?.options ?? declaration,
|
||||
alias: declaration?.alias,
|
||||
main: declaration?.main,
|
||||
}
|
||||
@@ -177,6 +186,118 @@ export class MedusaModule {
|
||||
const moduleResolutions = registerMedusaModule(
|
||||
moduleKey,
|
||||
modDeclaration!,
|
||||
moduleExports,
|
||||
moduleDefinition
|
||||
)
|
||||
|
||||
try {
|
||||
await moduleLoader({
|
||||
container,
|
||||
moduleResolutions,
|
||||
logger,
|
||||
})
|
||||
} catch (err) {
|
||||
errorLoading(err)
|
||||
throw err
|
||||
}
|
||||
|
||||
const services = {}
|
||||
|
||||
for (const resolution of Object.values(
|
||||
moduleResolutions
|
||||
) as ModuleResolution[]) {
|
||||
const keyName = resolution.definition.key
|
||||
const registrationName = resolution.definition.registrationName
|
||||
|
||||
services[keyName] = container.resolve(registrationName)
|
||||
services[keyName].__definition = resolution.definition
|
||||
|
||||
if (resolution.definition.isQueryable) {
|
||||
const joinerConfig: ModuleJoinerConfig = await services[
|
||||
keyName
|
||||
].__joinerConfig()
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
}
|
||||
|
||||
MedusaModule.registerModule(keyName, {
|
||||
key: keyName,
|
||||
hash: hashKey,
|
||||
alias: modDeclaration.alias ?? hashKey,
|
||||
main: !!modDeclaration.main,
|
||||
isLink: false,
|
||||
})
|
||||
}
|
||||
|
||||
MedusaModule.instances_.set(hashKey, services)
|
||||
finishLoading(services)
|
||||
MedusaModule.loading_.delete(hashKey)
|
||||
|
||||
return services
|
||||
}
|
||||
|
||||
public static async bootstrapLink(
|
||||
definition: LinkModuleDefinition,
|
||||
declaration?: InternalModuleDeclaration,
|
||||
moduleExports?: ModuleExports,
|
||||
injectedDependencies?: Record<string, any>
|
||||
): Promise<{
|
||||
[key: string]: unknown
|
||||
}> {
|
||||
const moduleKey = definition.key
|
||||
const hashKey = simpleHash(stringifyCircular({ moduleKey, declaration }))
|
||||
|
||||
if (MedusaModule.instances_.has(hashKey)) {
|
||||
return MedusaModule.instances_.get(hashKey)
|
||||
}
|
||||
|
||||
if (MedusaModule.loading_.has(hashKey)) {
|
||||
return MedusaModule.loading_.get(hashKey)
|
||||
}
|
||||
|
||||
let finishLoading: any
|
||||
let errorLoading: any
|
||||
MedusaModule.loading_.set(
|
||||
hashKey,
|
||||
new Promise((resolve, reject) => {
|
||||
finishLoading = resolve
|
||||
errorLoading = reject
|
||||
})
|
||||
)
|
||||
|
||||
let modDeclaration =
|
||||
declaration ?? ({} as Partial<InternalModuleDeclaration>)
|
||||
|
||||
const moduleDefinition: ModuleDefinition = {
|
||||
key: definition.key,
|
||||
registrationName: definition.key,
|
||||
dependencies: definition.dependencies,
|
||||
defaultPackage: "",
|
||||
label: definition.label,
|
||||
canOverride: true,
|
||||
isRequired: false,
|
||||
isQueryable: true,
|
||||
defaultModuleDeclaration: definition.defaultModuleDeclaration,
|
||||
}
|
||||
|
||||
modDeclaration = {
|
||||
resolve: "",
|
||||
options: declaration,
|
||||
alias: declaration?.alias,
|
||||
main: declaration?.main,
|
||||
}
|
||||
|
||||
const container = createMedusaContainer()
|
||||
|
||||
if (injectedDependencies) {
|
||||
for (const service in injectedDependencies) {
|
||||
container.register(service, asValue(injectedDependencies[service]))
|
||||
}
|
||||
}
|
||||
|
||||
const moduleResolutions = registerMedusaLinkModule(
|
||||
moduleDefinition,
|
||||
modDeclaration as InternalModuleDeclaration,
|
||||
moduleExports
|
||||
)
|
||||
|
||||
@@ -203,9 +324,17 @@ export class MedusaModule {
|
||||
services[keyName].__definition = resolution.definition
|
||||
|
||||
if (resolution.definition.isQueryable) {
|
||||
services[keyName].__joinerConfig = await services[
|
||||
const joinerConfig: ModuleJoinerConfig = await services[
|
||||
keyName
|
||||
].__joinerConfig()
|
||||
|
||||
services[keyName].__joinerConfig = joinerConfig
|
||||
|
||||
if (!joinerConfig.isLink) {
|
||||
throw new Error(
|
||||
"MedusaModule.bootstrapLink must be used only for Link Modules"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
MedusaModule.registerModule(keyName, {
|
||||
@@ -213,6 +342,7 @@ export class MedusaModule {
|
||||
hash: hashKey,
|
||||
alias: modDeclaration.alias ?? hashKey,
|
||||
main: !!modDeclaration.main,
|
||||
isLink: true,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
439
packages/modules-sdk/src/remote-link.ts
Normal file
439
packages/modules-sdk/src/remote-link.ts
Normal file
@@ -0,0 +1,439 @@
|
||||
import {
|
||||
ILinkModule,
|
||||
LoadedModule,
|
||||
ModuleJoinerRelationship,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { isObject, toPascalCase } from "@medusajs/utils"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
|
||||
export type DeleteEntityInput = {
|
||||
[moduleName: string]: { [linkableKey: string]: string | string[] }
|
||||
}
|
||||
export type RestoreEntityInput = DeleteEntityInput
|
||||
|
||||
type LinkDefinition = {
|
||||
[moduleName: string]: {
|
||||
[fieldName: string]: string
|
||||
}
|
||||
} & {
|
||||
data?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type RemoteRelationship = ModuleJoinerRelationship & {
|
||||
isPrimary: boolean
|
||||
isForeign: boolean
|
||||
}
|
||||
|
||||
type LoadedLinkModule = LoadedModule & ILinkModule
|
||||
type DeleteEntities = { [key: string]: string[] }
|
||||
type RemovedIds = {
|
||||
[serviceName: string]: DeleteEntities
|
||||
}
|
||||
type RestoredIds = RemovedIds
|
||||
|
||||
type CascadeError = {
|
||||
serviceName: string
|
||||
method: String
|
||||
args: any
|
||||
error: Error
|
||||
}
|
||||
|
||||
export class RemoteLink {
|
||||
private modulesMap: Map<string, LoadedLinkModule> = new Map()
|
||||
private relationsPairs: Map<string, LoadedLinkModule> = new Map()
|
||||
private relations: Map<string, Map<string, RemoteRelationship[]>> = new Map()
|
||||
|
||||
constructor(modulesLoaded?: LoadedModule[]) {
|
||||
if (!modulesLoaded?.length) {
|
||||
modulesLoaded = MedusaModule.getLoadedModules().map(
|
||||
(mod) => Object.values(mod)[0]
|
||||
)
|
||||
}
|
||||
|
||||
for (const mod of modulesLoaded) {
|
||||
this.addModule(mod)
|
||||
}
|
||||
}
|
||||
|
||||
public addModule(mod: LoadedModule): void {
|
||||
if (!mod.__definition.isQueryable || mod.__joinerConfig.isReadOnlyLink) {
|
||||
return
|
||||
}
|
||||
|
||||
const joinerConfig = mod.__joinerConfig
|
||||
|
||||
const serviceName = joinerConfig.isLink
|
||||
? joinerConfig.serviceName!
|
||||
: mod.__definition.key
|
||||
|
||||
if (this.modulesMap.has(serviceName)) {
|
||||
throw new Error(
|
||||
`Duplicated instance of module ${serviceName} is not allowed.`
|
||||
)
|
||||
}
|
||||
|
||||
if (joinerConfig.relationships?.length) {
|
||||
if (joinerConfig.isLink) {
|
||||
const [primary, foreign] = joinerConfig.relationships
|
||||
const key = [
|
||||
primary.serviceName,
|
||||
primary.foreignKey,
|
||||
foreign.serviceName,
|
||||
foreign.foreignKey,
|
||||
].join("-")
|
||||
this.relationsPairs.set(key, mod as unknown as LoadedLinkModule)
|
||||
}
|
||||
for (const relationship of joinerConfig.relationships) {
|
||||
if (joinerConfig.isLink && !relationship.deleteCascade) {
|
||||
continue
|
||||
}
|
||||
|
||||
this.addRelationship(serviceName, {
|
||||
...relationship,
|
||||
isPrimary: false,
|
||||
isForeign: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (joinerConfig.extends?.length) {
|
||||
for (const service of joinerConfig.extends) {
|
||||
const relationship = service.relationship
|
||||
this.addRelationship(service.serviceName, {
|
||||
...relationship,
|
||||
serviceName: serviceName,
|
||||
isPrimary: true,
|
||||
isForeign: false,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
this.modulesMap.set(serviceName, mod as unknown as LoadedLinkModule)
|
||||
}
|
||||
|
||||
private addRelationship(
|
||||
serviceName: string,
|
||||
relationship: RemoteRelationship
|
||||
): void {
|
||||
const { primaryKey, foreignKey } = relationship
|
||||
|
||||
if (!this.relations.has(serviceName)) {
|
||||
this.relations.set(serviceName, new Map())
|
||||
}
|
||||
|
||||
const key = relationship.isPrimary ? primaryKey : foreignKey
|
||||
const serviceMap = this.relations.get(serviceName)!
|
||||
if (!serviceMap.has(key)) {
|
||||
serviceMap.set(key, [])
|
||||
}
|
||||
|
||||
serviceMap.get(key)!.push(relationship)
|
||||
}
|
||||
|
||||
getLinkModule(
|
||||
moduleA: string,
|
||||
moduleAKey: string,
|
||||
moduleB: string,
|
||||
moduleBKey: string
|
||||
) {
|
||||
const key = [moduleA, moduleAKey, moduleB, moduleBKey].join("-")
|
||||
return this.relationsPairs.get(key)
|
||||
}
|
||||
|
||||
getRelationships(): Map<string, Map<string, RemoteRelationship[]>> {
|
||||
return this.relations
|
||||
}
|
||||
|
||||
private getLinkableKeys(mod: LoadedLinkModule) {
|
||||
return (
|
||||
mod.__joinerConfig.linkableKeys ?? mod.__joinerConfig.primaryKeys ?? []
|
||||
)
|
||||
}
|
||||
|
||||
private async executeCascade(
|
||||
removedServices: DeleteEntityInput,
|
||||
method: "softDelete" | "restore"
|
||||
): Promise<[CascadeError[] | null, RemovedIds]> {
|
||||
const removedIds: RemovedIds = {}
|
||||
const returnIdsList: RemovedIds = {}
|
||||
const processedIds: Record<string, Set<string>> = {}
|
||||
|
||||
const services = Object.keys(removedServices).map((serviceName) => {
|
||||
const deleteKeys = {}
|
||||
|
||||
for (const field in removedServices[serviceName]) {
|
||||
deleteKeys[field] = Array.isArray(removedServices[serviceName][field])
|
||||
? removedServices[serviceName][field]
|
||||
: [removedServices[serviceName][field]]
|
||||
}
|
||||
|
||||
return { serviceName, deleteKeys }
|
||||
})
|
||||
|
||||
const errors: CascadeError[] = []
|
||||
const cascade = async (
|
||||
services: { serviceName: string; deleteKeys: DeleteEntities }[],
|
||||
isCascading: boolean = false
|
||||
): Promise<RemovedIds> => {
|
||||
if (errors.length) {
|
||||
return returnIdsList
|
||||
}
|
||||
|
||||
const servicePromises = services.map(async (serviceInfo) => {
|
||||
const serviceRelations = this.relations.get(serviceInfo.serviceName)!
|
||||
|
||||
if (!serviceRelations) {
|
||||
return
|
||||
}
|
||||
|
||||
const values = serviceInfo.deleteKeys
|
||||
|
||||
const deletePromises: Promise<void>[] = []
|
||||
|
||||
for (const field in values) {
|
||||
const relatedServices = serviceRelations.get(field)
|
||||
|
||||
if (!relatedServices || !values[field]?.length) {
|
||||
continue
|
||||
}
|
||||
|
||||
const relatedServicesPromises = relatedServices.map(
|
||||
async (relatedService) => {
|
||||
const { serviceName, primaryKey, args } = relatedService
|
||||
const processedHash = `${serviceName}-${primaryKey}`
|
||||
|
||||
if (!processedIds[processedHash]) {
|
||||
processedIds[processedHash] = new Set()
|
||||
}
|
||||
|
||||
const unprocessedIds = values[field].filter(
|
||||
(id) => !processedIds[processedHash].has(id)
|
||||
)
|
||||
|
||||
if (!unprocessedIds.length) {
|
||||
return
|
||||
}
|
||||
|
||||
unprocessedIds.forEach((id) => {
|
||||
processedIds[processedHash].add(id)
|
||||
})
|
||||
|
||||
let cascadeDelKeys: DeleteEntities = {}
|
||||
cascadeDelKeys[primaryKey] = unprocessedIds
|
||||
const service: ILinkModule = this.modulesMap.get(serviceName)!
|
||||
|
||||
const returnFields = this.getLinkableKeys(
|
||||
service as LoadedLinkModule
|
||||
)
|
||||
|
||||
let deletedEntities: Record<string, string[]> = {}
|
||||
|
||||
try {
|
||||
if (args?.methodSuffix) {
|
||||
method += toPascalCase(args.methodSuffix)
|
||||
}
|
||||
|
||||
const removed = await service[method](cascadeDelKeys, {
|
||||
returnLinkableKeys: returnFields,
|
||||
})
|
||||
|
||||
deletedEntities = removed as Record<string, string[]>
|
||||
} catch (error) {
|
||||
errors.push({
|
||||
serviceName,
|
||||
method,
|
||||
args: cascadeDelKeys,
|
||||
error: JSON.parse(
|
||||
JSON.stringify(error, Object.getOwnPropertyNames(error))
|
||||
),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if (Object.keys(deletedEntities).length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
removedIds[serviceName] = {
|
||||
...deletedEntities,
|
||||
}
|
||||
|
||||
if (!isCascading) {
|
||||
returnIdsList[serviceName] = {
|
||||
...deletedEntities,
|
||||
}
|
||||
} else {
|
||||
const [mainKey] = returnFields
|
||||
|
||||
if (!returnIdsList[serviceName]) {
|
||||
returnIdsList[serviceName] = {}
|
||||
}
|
||||
if (!returnIdsList[serviceName][mainKey]) {
|
||||
returnIdsList[serviceName][mainKey] = []
|
||||
}
|
||||
|
||||
returnIdsList[serviceName][mainKey] = [
|
||||
...new Set(
|
||||
returnIdsList[serviceName][mainKey].concat(
|
||||
deletedEntities[mainKey]
|
||||
)
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
Object.keys(deletedEntities).forEach((key) => {
|
||||
deletedEntities[key].forEach((id) => {
|
||||
const hash = `${serviceName}-${key}`
|
||||
if (!processedIds[hash]) {
|
||||
processedIds[hash] = new Set()
|
||||
}
|
||||
|
||||
processedIds[hash].add(id)
|
||||
})
|
||||
})
|
||||
|
||||
await cascade(
|
||||
[
|
||||
{
|
||||
serviceName: serviceName,
|
||||
deleteKeys: deletedEntities as DeleteEntities,
|
||||
},
|
||||
],
|
||||
true
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
deletePromises.push(...relatedServicesPromises)
|
||||
}
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
})
|
||||
|
||||
await Promise.all(servicePromises)
|
||||
return returnIdsList
|
||||
}
|
||||
|
||||
const result = await cascade(services)
|
||||
|
||||
return [errors.length ? errors : null, result]
|
||||
}
|
||||
|
||||
async create(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<
|
||||
string,
|
||||
[string | string[], string, Record<string, unknown>?][]
|
||||
>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const extraFields = rel.data
|
||||
delete rel.data
|
||||
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
`Module to link ${moduleA}[${moduleAKey}] and ${moduleB}[${moduleBKey}] was not found.`
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
|
||||
const fields: unknown[] = [pkValue, rel[moduleB][moduleBKey]]
|
||||
if (isObject(extraFields)) {
|
||||
fields.push(extraFields)
|
||||
}
|
||||
|
||||
serviceLinks.get(service.__definition.key)?.push(fields as any)
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
promises.push(service.create(links))
|
||||
}
|
||||
|
||||
const created = await Promise.all(promises)
|
||||
return created.flat()
|
||||
}
|
||||
|
||||
async dismiss(link: LinkDefinition | LinkDefinition[]): Promise<unknown[]> {
|
||||
const allLinks = Array.isArray(link) ? link : [link]
|
||||
const serviceLinks = new Map<string, [string | string[], string][]>()
|
||||
|
||||
for (const rel of allLinks) {
|
||||
const mods = Object.keys(rel)
|
||||
if (mods.length > 2) {
|
||||
throw new Error(`Only two modules can be linked.`)
|
||||
}
|
||||
|
||||
const [moduleA, moduleB] = mods
|
||||
const pk = Object.keys(rel[moduleA])
|
||||
const moduleAKey = pk.join(",")
|
||||
const moduleBKey = Object.keys(rel[moduleB]).join(",")
|
||||
|
||||
const service = this.getLinkModule(
|
||||
moduleA,
|
||||
moduleAKey,
|
||||
moduleB,
|
||||
moduleBKey
|
||||
)
|
||||
|
||||
if (!service) {
|
||||
throw new Error(
|
||||
`Module to dismiss link ${moduleA}[${moduleAKey}] and ${moduleB}[${moduleBKey}] was not found.`
|
||||
)
|
||||
} else if (!serviceLinks.has(service.__definition.key)) {
|
||||
serviceLinks.set(service.__definition.key, [])
|
||||
}
|
||||
|
||||
const pkValue =
|
||||
pk.length === 1 ? rel[moduleA][pk[0]] : pk.map((k) => rel[moduleA][k])
|
||||
|
||||
serviceLinks
|
||||
.get(service.__definition.key)
|
||||
?.push([pkValue, rel[moduleB][moduleBKey]])
|
||||
}
|
||||
|
||||
const promises: Promise<unknown[]>[] = []
|
||||
for (const [serviceName, links] of serviceLinks) {
|
||||
const service = this.modulesMap.get(serviceName)!
|
||||
promises.push(service.dismiss(links))
|
||||
}
|
||||
|
||||
const created = await Promise.all(promises)
|
||||
return created.flat()
|
||||
}
|
||||
|
||||
async delete(
|
||||
removedServices: DeleteEntityInput
|
||||
): Promise<[CascadeError[] | null, RemovedIds]> {
|
||||
return await this.executeCascade(removedServices, "softDelete")
|
||||
}
|
||||
|
||||
async restore(
|
||||
removedServices: DeleteEntityInput
|
||||
): Promise<[CascadeError[] | null, RestoredIds]> {
|
||||
return await this.executeCascade(removedServices, "restore")
|
||||
}
|
||||
}
|
||||
@@ -2,28 +2,23 @@ import {
|
||||
JoinerRelationship,
|
||||
JoinerServiceConfig,
|
||||
LoadedModule,
|
||||
ModuleJoinerConfig,
|
||||
RemoteExpandProperty,
|
||||
RemoteJoinerQuery,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { RemoteFetchDataCallback, RemoteJoiner } from "@medusajs/orchestration"
|
||||
import { isString, toPascalCase } from "@medusajs/utils"
|
||||
import { MedusaModule } from "./medusa-module"
|
||||
import { RemoteJoiner } from "@medusajs/orchestration"
|
||||
import { toPascalCase } from "@medusajs/utils"
|
||||
|
||||
export class RemoteQuery {
|
||||
private remoteJoiner: RemoteJoiner
|
||||
private modulesMap: Map<string, LoadedModule> = new Map()
|
||||
private customRemoteFetchData?: RemoteFetchDataCallback
|
||||
|
||||
constructor(
|
||||
modulesLoaded?: LoadedModule[],
|
||||
remoteFetchData?: (
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: JoinerRelationship
|
||||
) => Promise<{
|
||||
data: unknown[] | { [path: string]: unknown[] }
|
||||
path?: string
|
||||
}>
|
||||
customRemoteFetchData?: RemoteFetchDataCallback
|
||||
) {
|
||||
if (!modulesLoaded?.length) {
|
||||
modulesLoaded = MedusaModule.getLoadedModules().map(
|
||||
@@ -31,25 +26,28 @@ export class RemoteQuery {
|
||||
)
|
||||
}
|
||||
|
||||
const servicesConfig: JoinerServiceConfig[] = []
|
||||
const servicesConfig: ModuleJoinerConfig[] = []
|
||||
for (const mod of modulesLoaded) {
|
||||
if (!mod.__definition.isQueryable) {
|
||||
continue
|
||||
}
|
||||
|
||||
if (this.modulesMap.has(mod.__definition.key)) {
|
||||
const serviceName = mod.__definition.key
|
||||
|
||||
if (this.modulesMap.has(serviceName)) {
|
||||
throw new Error(
|
||||
`Duplicated instance of module ${mod.__definition.key} is not allowed.`
|
||||
`Duplicated instance of module ${serviceName} is not allowed.`
|
||||
)
|
||||
}
|
||||
|
||||
this.modulesMap.set(mod.__definition.key, mod)
|
||||
this.modulesMap.set(serviceName, mod)
|
||||
servicesConfig.push(mod.__joinerConfig)
|
||||
}
|
||||
|
||||
this.customRemoteFetchData = customRemoteFetchData
|
||||
this.remoteJoiner = new RemoteJoiner(
|
||||
servicesConfig,
|
||||
remoteFetchData ?? this.remoteFetchData.bind(this)
|
||||
servicesConfig as JoinerServiceConfig[],
|
||||
this.remoteFetchData.bind(this)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -69,14 +67,20 @@ export class RemoteQuery {
|
||||
|
||||
private static getAllFieldsAndRelations(
|
||||
data: any,
|
||||
prefix = ""
|
||||
): { select: string[]; relations: string[] } {
|
||||
prefix = "",
|
||||
args: Record<string, unknown[]> = {}
|
||||
): {
|
||||
select: string[]
|
||||
relations: string[]
|
||||
args: Record<string, unknown[]>
|
||||
} {
|
||||
let fields: Set<string> = new Set()
|
||||
let relations: string[] = []
|
||||
|
||||
data.fields?.forEach((field: string) => {
|
||||
fields.add(prefix ? `${prefix}.${field}` : field)
|
||||
})
|
||||
args[prefix] = data.args
|
||||
|
||||
if (data.expands) {
|
||||
for (const property in data.expands) {
|
||||
@@ -87,7 +91,8 @@ export class RemoteQuery {
|
||||
|
||||
const result = RemoteQuery.getAllFieldsAndRelations(
|
||||
data.expands[property],
|
||||
newPrefix
|
||||
newPrefix,
|
||||
args
|
||||
)
|
||||
|
||||
result.select.forEach(fields.add, fields)
|
||||
@@ -95,7 +100,7 @@ export class RemoteQuery {
|
||||
}
|
||||
}
|
||||
|
||||
return { select: [...fields], relations }
|
||||
return { select: [...fields], relations, args }
|
||||
}
|
||||
|
||||
private hasPagination(options: { [attr: string]: unknown }): boolean {
|
||||
@@ -126,6 +131,13 @@ export class RemoteQuery {
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
}> {
|
||||
if (this.customRemoteFetchData) {
|
||||
const resp = await this.customRemoteFetchData(expand, keyField, ids)
|
||||
if (resp !== undefined) {
|
||||
return resp
|
||||
}
|
||||
}
|
||||
|
||||
const serviceConfig = expand.serviceConfig
|
||||
const service = this.modulesMap.get(serviceConfig.serviceName)!
|
||||
|
||||
@@ -196,9 +208,14 @@ export class RemoteQuery {
|
||||
}
|
||||
}
|
||||
|
||||
public async query(query: string, variables: any = {}): Promise<any> {
|
||||
return await this.remoteJoiner.query(
|
||||
RemoteJoiner.parseQuery(query, variables)
|
||||
)
|
||||
public async query(
|
||||
query: string | RemoteJoinerQuery,
|
||||
variables?: Record<string, unknown>
|
||||
): Promise<any> {
|
||||
const finalQuery = isString(query)
|
||||
? RemoteJoiner.parseQuery(query, variables)
|
||||
: query
|
||||
|
||||
return await this.remoteJoiner.query(finalQuery)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.10.2",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/utils": "^1.9.6",
|
||||
|
||||
@@ -274,4 +274,85 @@ describe("RemoteJoiner.parseQuery", () => {
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
it("Nested query with fields and directives", async () => {
|
||||
const graphqlQuery = `
|
||||
query {
|
||||
order(regularArgs: 123) {
|
||||
id
|
||||
number @include(if: "date > '2020-01-01'")
|
||||
date
|
||||
products {
|
||||
product_id
|
||||
variant_id
|
||||
variant @count {
|
||||
name @lowerCase
|
||||
sku @include(if: "name == 'test'")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const parser = new GraphQLParser(graphqlQuery)
|
||||
const rjQuery = parser.parseQuery()
|
||||
|
||||
expect(rjQuery).toEqual({
|
||||
alias: "order",
|
||||
fields: ["id", "number", "date", "products"],
|
||||
expands: [
|
||||
{
|
||||
property: "products",
|
||||
fields: ["product_id", "variant_id", "variant"],
|
||||
directives: {
|
||||
variant: [
|
||||
{
|
||||
name: "count",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
property: "products.variant",
|
||||
fields: ["name", "sku"],
|
||||
directives: {
|
||||
name: [
|
||||
{
|
||||
name: "lowerCase",
|
||||
},
|
||||
],
|
||||
sku: [
|
||||
{
|
||||
name: "include",
|
||||
args: [
|
||||
{
|
||||
name: "if",
|
||||
value: "name == 'test'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
args: [
|
||||
{
|
||||
name: "regularArgs",
|
||||
value: 123,
|
||||
},
|
||||
],
|
||||
directives: {
|
||||
number: [
|
||||
{
|
||||
name: "include",
|
||||
args: [
|
||||
{
|
||||
name: "if",
|
||||
value: "date > '2020-01-01'",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { RemoteJoinerQuery } from "@medusajs/types"
|
||||
import {
|
||||
ArgumentNode,
|
||||
DirectiveNode,
|
||||
DocumentNode,
|
||||
FieldNode,
|
||||
Kind,
|
||||
@@ -15,18 +16,24 @@ interface Argument {
|
||||
value?: unknown
|
||||
}
|
||||
|
||||
interface Directive {
|
||||
name: string
|
||||
args?: Argument[]
|
||||
}
|
||||
|
||||
interface Entity {
|
||||
property: string
|
||||
fields: string[]
|
||||
args?: Argument[]
|
||||
directives?: { [field: string]: Directive[] }
|
||||
}
|
||||
|
||||
class GraphQLParser {
|
||||
private ast: DocumentNode
|
||||
|
||||
constructor(input: string, private variables?: { [key: string]: unknown }) {
|
||||
constructor(input: string, private variables: Record<string, unknown> = {}) {
|
||||
this.ast = parse(input)
|
||||
this.variables = variables || {}
|
||||
this.variables = variables
|
||||
}
|
||||
|
||||
private parseValueNode(valueNode: ValueNode): unknown {
|
||||
@@ -75,6 +82,33 @@ class GraphQLParser {
|
||||
})
|
||||
}
|
||||
|
||||
private parseDirectives(directives: readonly DirectiveNode[]): Directive[] {
|
||||
return directives.map((directive) => ({
|
||||
name: directive.name.value,
|
||||
args: this.parseArguments(directive.arguments || []),
|
||||
}))
|
||||
}
|
||||
|
||||
private createDirectivesMap(selectionSet: SelectionSetNode):
|
||||
| {
|
||||
[field: string]: Directive[]
|
||||
}
|
||||
| undefined {
|
||||
const directivesMap: { [field: string]: Directive[] } = {}
|
||||
let hasDirectives = false
|
||||
selectionSet.selections.forEach((field) => {
|
||||
const fieldName = (field as FieldNode).name.value
|
||||
const fieldDirectives = this.parseDirectives(
|
||||
(field as FieldNode).directives || []
|
||||
)
|
||||
if (fieldDirectives.length > 0) {
|
||||
hasDirectives = true
|
||||
directivesMap[fieldName] = fieldDirectives
|
||||
}
|
||||
})
|
||||
return hasDirectives ? directivesMap : undefined
|
||||
}
|
||||
|
||||
private extractEntities(
|
||||
node: SelectionSetNode,
|
||||
parentName = "",
|
||||
@@ -98,7 +132,8 @@ class GraphQLParser {
|
||||
fields: fieldNode.selectionSet.selections.map(
|
||||
(field) => (field as FieldNode).name.value
|
||||
),
|
||||
args: this.parseArguments(fieldNode.arguments!),
|
||||
args: this.parseArguments(fieldNode.arguments || []),
|
||||
directives: this.createDirectivesMap(fieldNode.selectionSet),
|
||||
}
|
||||
|
||||
entities.push(nestedEntity)
|
||||
@@ -126,8 +161,8 @@ class GraphQLParser {
|
||||
|
||||
const rootFieldNode = queryDefinition.selectionSet
|
||||
.selections[0] as FieldNode
|
||||
|
||||
const propName = rootFieldNode.name.value
|
||||
|
||||
const remoteJoinConfig: RemoteJoinerQuery = {
|
||||
alias: propName,
|
||||
fields: [],
|
||||
@@ -142,7 +177,9 @@ class GraphQLParser {
|
||||
remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map(
|
||||
(field) => (field as FieldNode).name.value
|
||||
)
|
||||
|
||||
remoteJoinConfig.directives = this.createDirectivesMap(
|
||||
rootFieldNode.selectionSet
|
||||
)
|
||||
remoteJoinConfig.expands = this.extractEntities(
|
||||
rootFieldNode.selectionSet,
|
||||
propName,
|
||||
|
||||
@@ -2,14 +2,27 @@ import {
|
||||
JoinerRelationship,
|
||||
JoinerServiceConfig,
|
||||
JoinerServiceConfigAlias,
|
||||
ModuleJoinerConfig,
|
||||
RemoteExpandProperty,
|
||||
RemoteJoinerQuery,
|
||||
RemoteNestedExpands,
|
||||
} from "@medusajs/types"
|
||||
|
||||
import { isDefined } from "@medusajs/utils"
|
||||
import GraphQLParser from "./graphql-ast"
|
||||
|
||||
const BASE_PATH = "_root"
|
||||
|
||||
export type RemoteFetchDataCallback = (
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: any
|
||||
) => Promise<{
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
}>
|
||||
|
||||
export class RemoteJoiner {
|
||||
private serviceConfigCache: Map<string, JoinerServiceConfig> = new Map()
|
||||
|
||||
@@ -18,7 +31,7 @@ export class RemoteJoiner {
|
||||
fields: string[],
|
||||
expands?: RemoteNestedExpands
|
||||
): Record<string, unknown> {
|
||||
if (!fields) {
|
||||
if (!fields || !data) {
|
||||
return data
|
||||
}
|
||||
|
||||
@@ -78,44 +91,29 @@ export class RemoteJoiner {
|
||||
}, {})
|
||||
}
|
||||
|
||||
static parseQuery(graphqlQuery: string, variables?: any): RemoteJoinerQuery {
|
||||
static parseQuery(
|
||||
graphqlQuery: string,
|
||||
variables?: Record<string, unknown>
|
||||
): RemoteJoinerQuery {
|
||||
const parser = new GraphQLParser(graphqlQuery, variables)
|
||||
return parser.parseQuery()
|
||||
}
|
||||
|
||||
constructor(
|
||||
private serviceConfigs: JoinerServiceConfig[],
|
||||
private remoteFetchData: (
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: any
|
||||
) => Promise<{
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
}>
|
||||
private serviceConfigs: ModuleJoinerConfig[],
|
||||
private remoteFetchData: RemoteFetchDataCallback
|
||||
) {
|
||||
this.serviceConfigs = this.buildReferences(serviceConfigs)
|
||||
}
|
||||
|
||||
public setFetchDataCallback(
|
||||
remoteFetchData: (
|
||||
expand: RemoteExpandProperty,
|
||||
keyField: string,
|
||||
ids?: (unknown | unknown[])[],
|
||||
relationship?: any
|
||||
) => Promise<{
|
||||
data: unknown[] | { [path: string]: unknown }
|
||||
path?: string
|
||||
}>
|
||||
): void {
|
||||
public setFetchDataCallback(remoteFetchData: RemoteFetchDataCallback): void {
|
||||
this.remoteFetchData = remoteFetchData
|
||||
}
|
||||
|
||||
private buildReferences(serviceConfigs: JoinerServiceConfig[]) {
|
||||
private buildReferences(serviceConfigs: ModuleJoinerConfig[]) {
|
||||
const expandedRelationships: Map<string, JoinerRelationship[]> = new Map()
|
||||
for (const service of serviceConfigs) {
|
||||
if (this.serviceConfigCache.has(service.serviceName)) {
|
||||
if (this.serviceConfigCache.has(service.serviceName!)) {
|
||||
throw new Error(`Service "${service.serviceName}" is already defined.`)
|
||||
}
|
||||
|
||||
@@ -124,38 +122,42 @@ export class RemoteJoiner {
|
||||
}
|
||||
|
||||
// add aliases
|
||||
if (!service.alias) {
|
||||
service.alias = [{ name: service.serviceName.toLowerCase() }]
|
||||
} else if (!Array.isArray(service.alias)) {
|
||||
service.alias = [service.alias]
|
||||
}
|
||||
|
||||
// self-reference
|
||||
for (const alias of service.alias) {
|
||||
if (this.serviceConfigCache.has(`alias_${alias.name}}`)) {
|
||||
const defined = this.serviceConfigCache.get(`alias_${alias.name}}`)
|
||||
throw new Error(
|
||||
`Cannot add alias "${alias.name}" for "${service.serviceName}". It is already defined for Service "${defined?.serviceName}".`
|
||||
)
|
||||
const isReadOnlyDefinition =
|
||||
service.serviceName === undefined || service.isReadOnlyLink
|
||||
if (!isReadOnlyDefinition) {
|
||||
if (!service.alias) {
|
||||
service.alias = [{ name: service.serviceName!.toLowerCase() }]
|
||||
} else if (!Array.isArray(service.alias)) {
|
||||
service.alias = [service.alias]
|
||||
}
|
||||
|
||||
const args =
|
||||
service.args || alias.args
|
||||
? { ...service.args, ...alias.args }
|
||||
: undefined
|
||||
// self-reference
|
||||
for (const alias of service.alias) {
|
||||
if (this.serviceConfigCache.has(`alias_${alias.name}}`)) {
|
||||
const defined = this.serviceConfigCache.get(`alias_${alias.name}}`)
|
||||
throw new Error(
|
||||
`Cannot add alias "${alias.name}" for "${service.serviceName}". It is already defined for Service "${defined?.serviceName}".`
|
||||
)
|
||||
}
|
||||
|
||||
service.relationships?.push({
|
||||
alias: alias.name,
|
||||
foreignKey: alias.name + "_id",
|
||||
primaryKey: "id",
|
||||
serviceName: service.serviceName,
|
||||
args,
|
||||
})
|
||||
this.cacheServiceConfig(serviceConfigs, undefined, alias.name)
|
||||
const args =
|
||||
service.args || alias.args
|
||||
? { ...service.args, ...alias.args }
|
||||
: undefined
|
||||
|
||||
service.relationships?.push({
|
||||
alias: alias.name,
|
||||
foreignKey: alias.name + "_id",
|
||||
primaryKey: "id",
|
||||
serviceName: service.serviceName!,
|
||||
args,
|
||||
})
|
||||
this.cacheServiceConfig(serviceConfigs, undefined, alias.name)
|
||||
}
|
||||
|
||||
this.cacheServiceConfig(serviceConfigs, service.serviceName)
|
||||
}
|
||||
|
||||
this.cacheServiceConfig(serviceConfigs, service.serviceName)
|
||||
|
||||
if (!service.extends) {
|
||||
continue
|
||||
}
|
||||
@@ -295,7 +297,7 @@ export class RemoteJoiner {
|
||||
Map<string, RemoteExpandProperty>,
|
||||
string,
|
||||
Set<string>
|
||||
][] = [[items, query, parsedExpands, "", new Set()]]
|
||||
][] = [[items, query, parsedExpands, BASE_PATH, new Set()]]
|
||||
|
||||
while (stack.length > 0) {
|
||||
const [
|
||||
@@ -307,9 +309,7 @@ export class RemoteJoiner {
|
||||
] = stack.pop()!
|
||||
|
||||
for (const [expandedPath, expand] of currentParsedExpands.entries()) {
|
||||
const isImmediateChildPath =
|
||||
expandedPath.startsWith(basePath) &&
|
||||
expandedPath.split(".").length === basePath.split(".").length + 1
|
||||
const isImmediateChildPath = basePath === expand.parent
|
||||
|
||||
if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) {
|
||||
continue
|
||||
@@ -433,7 +433,8 @@ export class RemoteJoiner {
|
||||
item[relationship.alias] = item[field]
|
||||
.map((id) => {
|
||||
if (relationship.isList && !Array.isArray(relatedDataMap[id])) {
|
||||
relatedDataMap[id] = [relatedDataMap[id]]
|
||||
relatedDataMap[id] =
|
||||
relatedDataMap[id] !== undefined ? [relatedDataMap[id]] : []
|
||||
}
|
||||
|
||||
return relatedDataMap[id]
|
||||
@@ -441,7 +442,10 @@ export class RemoteJoiner {
|
||||
.filter((relatedItem) => relatedItem !== undefined)
|
||||
} else {
|
||||
if (relationship.isList && !Array.isArray(relatedDataMap[itemKey])) {
|
||||
relatedDataMap[itemKey] = [relatedDataMap[itemKey]]
|
||||
relatedDataMap[itemKey] =
|
||||
relatedDataMap[itemKey] !== undefined
|
||||
? [relatedDataMap[itemKey]]
|
||||
: []
|
||||
}
|
||||
|
||||
item[relationship.alias] = relatedDataMap[itemKey]
|
||||
@@ -539,13 +543,13 @@ export class RemoteJoiner {
|
||||
serviceConfig: currentServiceConfig,
|
||||
fields,
|
||||
args,
|
||||
parent: [BASE_PATH, ...currentPath].join("."),
|
||||
})
|
||||
}
|
||||
|
||||
currentPath.push(prop)
|
||||
}
|
||||
}
|
||||
|
||||
return parsedExpands
|
||||
}
|
||||
|
||||
@@ -566,7 +570,7 @@ export class RemoteJoiner {
|
||||
for (const [path, expand] of sortedParsedExpands.entries()) {
|
||||
const currentServiceName = expand.serviceConfig.serviceName
|
||||
|
||||
let parentPath = path.split(".").slice(0, -1).join(".")
|
||||
let parentPath = expand.parent
|
||||
|
||||
// Check if the parentPath was merged before
|
||||
while (mergedPaths.has(parentPath)) {
|
||||
@@ -580,6 +584,7 @@ export class RemoteJoiner {
|
||||
|
||||
if (parentExpand.serviceConfig.serviceName === currentServiceName) {
|
||||
const nestedKeys = path.split(".").slice(parentPath.split(".").length)
|
||||
|
||||
let targetExpand: any = parentExpand
|
||||
|
||||
for (let key of nestedKeys) {
|
||||
@@ -633,6 +638,7 @@ export class RemoteJoiner {
|
||||
const parsedExpands = this.parseExpands(
|
||||
{
|
||||
property: "",
|
||||
parent: "",
|
||||
serviceConfig: serviceConfig,
|
||||
fields: queryObj.fields,
|
||||
args: otherArgs,
|
||||
|
||||
@@ -1,6 +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.DB_TEMP_NAME = `medusa-pricing-integration-${tempName}`
|
||||
}
|
||||
|
||||
process.env.MEDUSA_PRICING_DB_SCHEMA = "public"
|
||||
|
||||
@@ -13,12 +13,12 @@ import {
|
||||
} from "../../../__fixtures__/product/data"
|
||||
|
||||
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { ProductRepository } from "@repositories"
|
||||
import { ProductService } from "@services"
|
||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||
import { TestDatabase } from "../../../utils"
|
||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||
import { kebabCase } from "@medusajs/utils"
|
||||
import { TestDatabase } from "../../../utils"
|
||||
|
||||
jest.setTimeout(30000)
|
||||
|
||||
@@ -513,7 +513,7 @@ describe("Product Service", () => {
|
||||
const products = await service.create([data])
|
||||
const product = products[0]
|
||||
await service.softDelete([product.id])
|
||||
const restoreProducts = await service.restore([product.id])
|
||||
const [restoreProducts] = await service.restore([product.id])
|
||||
|
||||
expect(restoreProducts).toHaveLength(1)
|
||||
expect(restoreProducts[0].deleted_at).toBeNull()
|
||||
|
||||
@@ -4,14 +4,14 @@ module.exports = {
|
||||
"^@services": "<rootDir>/src/services",
|
||||
"^@repositories": "<rootDir>/src/repositories",
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -39,14 +39,14 @@
|
||||
"@mikro-orm/cli": "5.7.12",
|
||||
"cross-env": "^5.2.1",
|
||||
"faker": "^6.6.6",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"medusa-test-utils": "^1.1.40",
|
||||
"pg-god": "^1.0.12",
|
||||
"rimraf": "^3.0.2",
|
||||
"ts-jest": "^25.5.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "^1.8.6",
|
||||
"typescript": "^4.4.4"
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.9.2",
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import {
|
||||
ExternalModuleDeclaration,
|
||||
InternalModuleDeclaration,
|
||||
MedusaModule,
|
||||
MODULE_PACKAGE_NAMES,
|
||||
MedusaModule,
|
||||
Modules,
|
||||
} from "@medusajs/modules-sdk"
|
||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
|
||||
import { InitializeModuleInjectableDependencies } from "../types"
|
||||
import { moduleDefinition } from "../module-definition"
|
||||
|
||||
export const initialize = async (
|
||||
options?:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { JoinerServiceConfig } from "@medusajs/types"
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
import {
|
||||
Product,
|
||||
ProductCategory,
|
||||
@@ -10,10 +11,9 @@ import {
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import ProductImage from "./models/product-image"
|
||||
import { MapToConfig } from "@medusajs/utils"
|
||||
|
||||
export enum LinkableKeys {
|
||||
PRODUCT_ID = "product_id",
|
||||
PRODUCT_ID = "product_id", // Main service ID must the first
|
||||
PRODUCT_HANDLE = "product_handle",
|
||||
VARIANT_ID = "variant_id",
|
||||
VARIANT_SKU = "variant_sku",
|
||||
@@ -55,9 +55,10 @@ export const entityNameToLinkableKeysMap: MapToConfig = {
|
||||
],
|
||||
}
|
||||
|
||||
export const joinerConfig: JoinerServiceConfig = {
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.PRODUCT,
|
||||
primaryKeys: ["id", "handle"],
|
||||
linkableKeys: Object.values(LinkableKeys),
|
||||
alias: [
|
||||
{
|
||||
name: "product",
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
} from "@services"
|
||||
Context,
|
||||
CreateProductOnlyDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
IEventBusModuleService,
|
||||
InternalModuleDeclaration,
|
||||
ModuleJoinerConfig,
|
||||
ProductTypes,
|
||||
RestoreReturn,
|
||||
SoftDeleteReturn,
|
||||
} from "@medusajs/types"
|
||||
import {
|
||||
Image,
|
||||
Product,
|
||||
@@ -18,15 +21,14 @@ import {
|
||||
ProductVariant,
|
||||
} from "@models"
|
||||
import {
|
||||
Context,
|
||||
CreateProductOnlyDTO,
|
||||
DAL,
|
||||
FindConfig,
|
||||
IEventBusModuleService,
|
||||
InternalModuleDeclaration,
|
||||
JoinerServiceConfig,
|
||||
ProductTypes,
|
||||
} from "@medusajs/types"
|
||||
ProductCategoryService,
|
||||
ProductCollectionService,
|
||||
ProductOptionService,
|
||||
ProductService,
|
||||
ProductTagService,
|
||||
ProductTypeService,
|
||||
ProductVariantService,
|
||||
} from "@services"
|
||||
|
||||
import ProductImageService from "./product-image"
|
||||
|
||||
@@ -136,7 +138,7 @@ export default class ProductModuleService<
|
||||
this.eventBusModuleService_ = eventBusModuleService
|
||||
}
|
||||
|
||||
__joinerConfig(): JoinerServiceConfig {
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
@@ -1071,11 +1073,7 @@ export default class ProductModuleService<
|
||||
>
|
||||
>(
|
||||
productIds: string[],
|
||||
{
|
||||
returnLinkableKeys,
|
||||
}: { returnLinkableKeys?: TReturnableLinkableKeys[] } = {
|
||||
returnLinkableKeys: [],
|
||||
},
|
||||
{ returnLinkableKeys }: SoftDeleteReturn<TReturnableLinkableKeys> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||
const [products, cascadedEntitiesMap] = await this.softDelete_(
|
||||
@@ -1098,10 +1096,12 @@ export default class ProductModuleService<
|
||||
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<
|
||||
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||
pick: returnLinkableKeys as string[],
|
||||
pick: returnLinkableKeys,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1116,22 +1116,39 @@ export default class ProductModuleService<
|
||||
return await this.productService_.softDelete(productIds, sharedContext)
|
||||
}
|
||||
|
||||
async restore(
|
||||
async restore<
|
||||
TReturnableLinkableKeys extends string = Lowercase<
|
||||
keyof typeof LinkableKeys
|
||||
>
|
||||
>(
|
||||
productIds: string[],
|
||||
{ returnLinkableKeys }: RestoreReturn<TReturnableLinkableKeys> = {},
|
||||
sharedContext: Context = {}
|
||||
): Promise<ProductTypes.ProductDTO[]> {
|
||||
const products = await this.restore_(productIds, sharedContext)
|
||||
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||
const [_, cascadedEntitiesMap] = await this.restore_(
|
||||
productIds,
|
||||
sharedContext
|
||||
)
|
||||
|
||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
||||
populate: true,
|
||||
})
|
||||
let mappedCascadedEntitiesMap
|
||||
if (returnLinkableKeys) {
|
||||
// Map internal table/column names to their respective external linkable keys
|
||||
// eg: product.id = product_id, variant.id = variant_id
|
||||
mappedCascadedEntitiesMap = mapObjectTo<
|
||||
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||
pick: returnLinkableKeys,
|
||||
})
|
||||
}
|
||||
|
||||
return mappedCascadedEntitiesMap ? mappedCascadedEntitiesMap : void 0
|
||||
}
|
||||
|
||||
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||
async restore_(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TProduct[]> {
|
||||
): Promise<[TProduct[], Record<string, unknown[]>]> {
|
||||
return await this.productService_.restore(productIds, sharedContext)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { Product } from "@models"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
@@ -9,12 +8,13 @@ import {
|
||||
import {
|
||||
InjectManager,
|
||||
InjectTransactionManager,
|
||||
isDefined,
|
||||
MedusaContext,
|
||||
MedusaError,
|
||||
ModulesSdkUtils,
|
||||
ProductUtils,
|
||||
isDefined,
|
||||
} from "@medusajs/utils"
|
||||
import { Product } from "@models"
|
||||
import { ProductRepository } from "@repositories"
|
||||
|
||||
import { ProductServiceTypes } from "../types/services"
|
||||
@@ -178,7 +178,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
||||
async restore(
|
||||
productIds: string[],
|
||||
@MedusaContext() sharedContext: Context = {}
|
||||
): Promise<TEntity[]> {
|
||||
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||
return await this.productRepository_.restore(productIds, {
|
||||
transactionManager: sharedContext.transactionManager,
|
||||
})
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.spec.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -19,10 +19,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.8.8",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.8.8",
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||
import { Modules } from "@medusajs/modules-sdk"
|
||||
import { JoinerServiceConfig } from "@medusajs/types"
|
||||
|
||||
export const joinerConfig: JoinerServiceConfig = {
|
||||
export const joinerConfig: ModuleJoinerConfig = {
|
||||
serviceName: Modules.STOCK_LOCATION,
|
||||
primaryKeys: ["id"],
|
||||
linkableKeys: ["stock_location_id"],
|
||||
alias: [
|
||||
{
|
||||
name: "stock_location",
|
||||
|
||||
@@ -4,8 +4,8 @@ import {
|
||||
FilterableStockLocationProps,
|
||||
FindConfig,
|
||||
IEventBusService,
|
||||
JoinerServiceConfig,
|
||||
MODULE_RESOURCE_TYPE,
|
||||
ModuleJoinerConfig,
|
||||
SharedContext,
|
||||
StockLocationAddressInput,
|
||||
UpdateStockLocationInput,
|
||||
@@ -50,7 +50,7 @@ export default class StockLocationService {
|
||||
this.eventBusService_ = eventBusService
|
||||
}
|
||||
|
||||
__joinerConfig(): JoinerServiceConfig {
|
||||
__joinerConfig(): ModuleJoinerConfig {
|
||||
return joinerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"ioredis": "^5.2.5",
|
||||
"rimraf": "^5.0.1",
|
||||
"typeorm": "^0.3.16",
|
||||
"typescript": "^4.4.4",
|
||||
"typescript": "^5.1.6",
|
||||
"winston": "^3.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { FindOptions } from "./index"
|
||||
import { RepositoryTransformOptions } from "../common"
|
||||
import { Context } from "../shared-context"
|
||||
import { FindOptions } from "./index"
|
||||
|
||||
/**
|
||||
* Data access layer (DAL) interface to implements for any repository service.
|
||||
@@ -54,7 +54,10 @@ export interface RepositoryService<T = any> extends BaseRepositoryService<T> {
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
|
||||
restore(ids: string[], context?: Context): Promise<T[]>
|
||||
restore(
|
||||
ids: string[],
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
}
|
||||
|
||||
export interface TreeRepositoryService<T = any>
|
||||
@@ -75,3 +78,11 @@ export interface TreeRepositoryService<T = any>
|
||||
|
||||
delete(id: string, context?: Context): Promise<void>
|
||||
}
|
||||
|
||||
export type SoftDeleteReturn<TReturnableLinkableKeys = string> = {
|
||||
returnLinkableKeys?: TReturnableLinkableKeys[]
|
||||
}
|
||||
|
||||
export type RestoreReturn<TReturnableLinkableKeys = string> = {
|
||||
returnLinkableKeys?: TReturnableLinkableKeys[]
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ export * from "./feature-flag"
|
||||
export * from "./file-service"
|
||||
export * from "./inventory"
|
||||
export * from "./joiner"
|
||||
export * from "./link-modules"
|
||||
export * from "./logger"
|
||||
export * from "./modules-sdk"
|
||||
export * from "./pricing"
|
||||
|
||||
@@ -13,11 +13,11 @@ import {
|
||||
} from "./common"
|
||||
|
||||
import { FindConfig } from "../common"
|
||||
import { JoinerServiceConfig } from "../joiner"
|
||||
import { SharedContext } from ".."
|
||||
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||
import { SharedContext } from "../shared-context"
|
||||
|
||||
export interface IInventoryService {
|
||||
__joinerConfig(): JoinerServiceConfig
|
||||
__joinerConfig(): ModuleJoinerConfig
|
||||
listInventoryItems(
|
||||
selector: FilterableInventoryItemProps,
|
||||
config?: FindConfig<InventoryItemDTO>,
|
||||
|
||||
@@ -28,7 +28,11 @@ export interface JoinerServiceConfig {
|
||||
export interface JoinerArgument {
|
||||
name: string
|
||||
value?: any
|
||||
field?: string
|
||||
}
|
||||
|
||||
export interface JoinerDirective {
|
||||
name: string
|
||||
value?: any
|
||||
}
|
||||
|
||||
export interface RemoteJoinerQuery {
|
||||
@@ -38,10 +42,11 @@ export interface RemoteJoinerQuery {
|
||||
property: string
|
||||
fields: string[]
|
||||
args?: JoinerArgument[]
|
||||
relationships?: JoinerRelationship[]
|
||||
directives?: { [field: string]: JoinerDirective[] }
|
||||
}>
|
||||
fields: string[]
|
||||
args?: JoinerArgument[]
|
||||
directives?: { [field: string]: JoinerDirective[] }
|
||||
}
|
||||
|
||||
export interface RemoteNestedExpands {
|
||||
@@ -54,6 +59,7 @@ export interface RemoteNestedExpands {
|
||||
|
||||
export interface RemoteExpandProperty {
|
||||
property: string
|
||||
parent: string
|
||||
serviceConfig: JoinerServiceConfig
|
||||
fields: string[]
|
||||
args?: JoinerArgument[]
|
||||
|
||||
49
packages/types/src/link-modules/index.ts
Normal file
49
packages/types/src/link-modules/index.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { FindConfig } from "../common"
|
||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
|
||||
export interface ILinkModule {
|
||||
__joinerConfig(): ModuleJoinerConfig
|
||||
|
||||
list(
|
||||
filters?: Record<string, unknown>,
|
||||
config?: FindConfig<unknown>,
|
||||
sharedContext?: Context
|
||||
): Promise<unknown[]>
|
||||
|
||||
listAndCount(
|
||||
filters?: Record<string, unknown>,
|
||||
config?: FindConfig<unknown>,
|
||||
sharedContext?: Context
|
||||
): Promise<[unknown[], number]>
|
||||
|
||||
create(
|
||||
primaryKeyOrBulkData:
|
||||
| string
|
||||
| string[]
|
||||
| [string | string[], string, Record<string, unknown>?][],
|
||||
foreignKeyData?: string,
|
||||
sharedContext?: Context
|
||||
): Promise<unknown[]>
|
||||
|
||||
dismiss(
|
||||
primaryKeyOrBulkData: string | string[] | [string | string[], string][],
|
||||
foreignKeyData?: string,
|
||||
sharedContext?: Context
|
||||
): Promise<unknown[]>
|
||||
|
||||
delete(data: unknown | unknown[], sharedContext?: Context): Promise<void>
|
||||
|
||||
softDelete(
|
||||
data: unknown | unknown[],
|
||||
config?: SoftDeleteReturn,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, unknown[]> | void>
|
||||
|
||||
restore(
|
||||
data: unknown | unknown[],
|
||||
config?: RestoreReturn,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, unknown[]> | void>
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { JoinerServiceConfig } from "../joiner"
|
||||
import { Logger } from "../logger"
|
||||
import { JoinerRelationship, JoinerServiceConfig } from "../joiner"
|
||||
|
||||
import { MedusaContainer } from "../common"
|
||||
import { RepositoryService } from "../dal"
|
||||
import { Logger } from "../logger"
|
||||
|
||||
export type Constructor<T> = new (...args: any[]) => T
|
||||
export * from "../common/medusa-container"
|
||||
@@ -30,6 +31,9 @@ export type InternalModuleDeclaration = {
|
||||
scope: MODULE_SCOPE.INTERNAL
|
||||
resources: MODULE_RESOURCE_TYPE
|
||||
dependencies?: string[]
|
||||
/**
|
||||
* @deprecated The property should not be used.
|
||||
*/
|
||||
resolve?: string
|
||||
options?: Record<string, unknown>
|
||||
alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them
|
||||
@@ -43,6 +47,7 @@ export type ExternalModuleDeclaration = {
|
||||
url: string
|
||||
keepAlive: boolean
|
||||
}
|
||||
options?: Record<string, unknown>
|
||||
alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them
|
||||
main?: boolean // If the module is the main module for the key when multiple ones are registered
|
||||
}
|
||||
@@ -61,17 +66,38 @@ export type ModuleDefinition = {
|
||||
registrationName: string
|
||||
defaultPackage: string | false
|
||||
label: string
|
||||
/**
|
||||
* @deprecated property will be removed in future versions
|
||||
*/
|
||||
canOverride?: boolean
|
||||
/**
|
||||
* @deprecated property will be removed in future versions
|
||||
*/
|
||||
isRequired?: boolean
|
||||
isQueryable?: boolean // If the modules should be queryable via Remote Joiner
|
||||
isQueryable?: boolean // If the module is queryable via Remote Joiner
|
||||
dependencies?: string[]
|
||||
defaultModuleDeclaration:
|
||||
| InternalModuleDeclaration
|
||||
| ExternalModuleDeclaration
|
||||
}
|
||||
|
||||
export type LinkModuleDefinition = {
|
||||
key: string
|
||||
registrationName: string
|
||||
label: string
|
||||
dependencies?: string[]
|
||||
defaultModuleDeclaration: InternalModuleDeclaration
|
||||
}
|
||||
|
||||
type ModuleDeclaration = ExternalModuleDeclaration | InternalModuleDeclaration
|
||||
export type ModuleConfig = ModuleDeclaration & {
|
||||
module: string
|
||||
path: string
|
||||
definition: ModuleDefinition
|
||||
}
|
||||
|
||||
export type LoadedModule = unknown & {
|
||||
__joinerConfig: JoinerServiceConfig
|
||||
__joinerConfig: ModuleJoinerConfig
|
||||
__definition: ModuleDefinition
|
||||
}
|
||||
|
||||
@@ -91,6 +117,60 @@ export type ModulesResponse = {
|
||||
resolution: string | false
|
||||
}[]
|
||||
|
||||
export type ModuleJoinerConfig = Omit<
|
||||
JoinerServiceConfig,
|
||||
"serviceName" | "primaryKeys" | "relationships" | "extends"
|
||||
> & {
|
||||
relationships?: ModuleJoinerRelationship[]
|
||||
extends?: {
|
||||
serviceName: string
|
||||
relationship: ModuleJoinerRelationship
|
||||
}[]
|
||||
serviceName?: string
|
||||
primaryKeys?: string[]
|
||||
isLink?: boolean // If the module is a link module
|
||||
linkableKeys?: string[] // Keys that can be used to link to other modules
|
||||
isReadOnlyLink?: boolean // If true it expands a RemoteQuery property but doesn't create a pivot table
|
||||
databaseConfig?: {
|
||||
tableName?: string // Name of the pivot table. If not provided it is auto generated
|
||||
idPrefix?: string // Prefix for the id column. If not provided it is "link"
|
||||
extraFields?: Record<
|
||||
string,
|
||||
{
|
||||
type:
|
||||
| "date"
|
||||
| "time"
|
||||
| "datetime"
|
||||
| "bigint"
|
||||
| "blob"
|
||||
| "uint8array"
|
||||
| "array"
|
||||
| "enumArray"
|
||||
| "enum"
|
||||
| "json"
|
||||
| "integer"
|
||||
| "smallint"
|
||||
| "tinyint"
|
||||
| "mediumint"
|
||||
| "float"
|
||||
| "double"
|
||||
| "boolean"
|
||||
| "decimal"
|
||||
| "string"
|
||||
| "uuid"
|
||||
| "text"
|
||||
defaultValue?: string
|
||||
nullable?: boolean
|
||||
options?: Record<string, unknown> // Mikro-orm options for the column
|
||||
}
|
||||
>
|
||||
}
|
||||
}
|
||||
|
||||
export declare type ModuleJoinerRelationship = JoinerRelationship & {
|
||||
deleteCascade?: boolean // If true, the link joiner will cascade deleting the relationship
|
||||
}
|
||||
|
||||
export type ModuleExports = {
|
||||
service: Constructor<any>
|
||||
loaders?: ModuleLoaderFunction[]
|
||||
@@ -114,6 +194,11 @@ export interface ModuleServiceInitializeOptions {
|
||||
connection?: any
|
||||
clientUrl?: string
|
||||
schema?: string
|
||||
host?: string
|
||||
port?: number
|
||||
user?: string
|
||||
password?: string
|
||||
database?: string
|
||||
driverOptions?: Record<string, unknown>
|
||||
debug?: boolean
|
||||
}
|
||||
|
||||
@@ -27,12 +27,13 @@ import {
|
||||
UpdateProductTypeDTO,
|
||||
} from "./common"
|
||||
|
||||
import { Context } from "../shared-context"
|
||||
import { FindConfig } from "../common"
|
||||
import { JoinerServiceConfig } from "../joiner"
|
||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||
import { Context } from "../shared-context"
|
||||
|
||||
export interface IProductModuleService {
|
||||
__joinerConfig(): JoinerServiceConfig
|
||||
__joinerConfig(): ModuleJoinerConfig
|
||||
|
||||
retrieve(
|
||||
productId: string,
|
||||
@@ -241,9 +242,13 @@ export interface IProductModuleService {
|
||||
|
||||
softDelete<TReturnableLinkableKeys extends string = string>(
|
||||
productIds: string[],
|
||||
config?: { returnLinkableKeys?: TReturnableLinkableKeys[] },
|
||||
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
|
||||
restore(productIds: string[], sharedContext?: Context): Promise<ProductDTO[]>
|
||||
restore<TReturnableLinkableKeys extends string = string>(
|
||||
productIds: string[],
|
||||
config?: RestoreReturn<TReturnableLinkableKeys>,
|
||||
sharedContext?: Context
|
||||
): Promise<Record<string, string[]> | void>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
import { FindConfig } from "../common/common"
|
||||
import { JoinerServiceConfig } from "../joiner"
|
||||
import { SharedContext } from "../shared-context"
|
||||
import {
|
||||
CreateStockLocationInput,
|
||||
FilterableStockLocationProps,
|
||||
@@ -8,8 +5,12 @@ import {
|
||||
UpdateStockLocationInput,
|
||||
} from "./common"
|
||||
|
||||
import { FindConfig } from "../common/common"
|
||||
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||
import { SharedContext } from "../shared-context"
|
||||
|
||||
export interface IStockLocationService {
|
||||
__joinerConfig(): JoinerServiceConfig
|
||||
__joinerConfig(): ModuleJoinerConfig
|
||||
list(
|
||||
selector: FilterableStockLocationProps,
|
||||
config?: FindConfig<StockLocationDTO>,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
"@types/express": "^4.17.17",
|
||||
"cross-env": "^5.2.1",
|
||||
"express": "^4.18.2",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"awilix": "^8.0.1",
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { Context, DAL, RepositoryTransformOptions } from "@medusajs/types"
|
||||
import {
|
||||
Context,
|
||||
DAL,
|
||||
FilterQuery,
|
||||
RepositoryTransformOptions,
|
||||
} from "@medusajs/types"
|
||||
import { isString } from "../../common"
|
||||
import { MedusaContext } from "../../decorators"
|
||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
||||
import { InjectTransactionManager, buildQuery } from "../../modules-sdk"
|
||||
import {
|
||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||
transactionWrapper,
|
||||
@@ -68,11 +74,21 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
||||
|
||||
@InjectTransactionManager()
|
||||
async softDelete(
|
||||
ids: string[],
|
||||
idsOrFilter: string[] | FilterQuery,
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
const entities = await this.find({ where: { id: { $in: ids } } as any })
|
||||
const isArray = Array.isArray(idsOrFilter)
|
||||
const filter =
|
||||
isArray || isString(idsOrFilter)
|
||||
? {
|
||||
id: {
|
||||
$in: isArray ? idsOrFilter : [idsOrFilter],
|
||||
},
|
||||
}
|
||||
: idsOrFilter
|
||||
|
||||
const entities = await this.find({ where: filter as any })
|
||||
const date = new Date()
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
||||
@@ -86,22 +102,34 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
||||
|
||||
@InjectTransactionManager()
|
||||
async restore(
|
||||
ids: string[],
|
||||
idsOrFilter: string[] | FilterQuery,
|
||||
@MedusaContext()
|
||||
{ transactionManager: manager }: Context = {}
|
||||
): Promise<T[]> {
|
||||
const query = buildQuery(
|
||||
{ id: { $in: ids } },
|
||||
{
|
||||
withDeleted: true,
|
||||
}
|
||||
)
|
||||
): Promise<[T[], Record<string, unknown[]>]> {
|
||||
const isArray = Array.isArray(idsOrFilter)
|
||||
const filter =
|
||||
isArray || isString(idsOrFilter)
|
||||
? {
|
||||
id: {
|
||||
$in: isArray ? idsOrFilter : [idsOrFilter],
|
||||
},
|
||||
}
|
||||
: idsOrFilter
|
||||
|
||||
const query = buildQuery(filter, {
|
||||
withDeleted: true,
|
||||
})
|
||||
|
||||
const entities = await this.find(query)
|
||||
|
||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, null)
|
||||
|
||||
return entities
|
||||
const softDeletedEntitiesMap = getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
restored: true,
|
||||
})
|
||||
|
||||
return [entities, softDeletedEntitiesMap]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -54,7 +54,10 @@ export abstract class AbstractBaseRepository<T = any>
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
|
||||
abstract restore(ids: string[], context?: Context): Promise<T[]>
|
||||
abstract restore(
|
||||
ids: string[],
|
||||
context?: Context
|
||||
): Promise<[T[], Record<string, unknown[]>]>
|
||||
|
||||
abstract getFreshManager<TManager = unknown>(): TManager
|
||||
|
||||
|
||||
@@ -47,10 +47,12 @@ export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
entities,
|
||||
deletedEntitiesMap,
|
||||
getEntityName,
|
||||
restored,
|
||||
}: {
|
||||
entities: any[]
|
||||
deletedEntitiesMap?: Map<string, any[]>
|
||||
getEntityName?: (entity: any) => string
|
||||
restored?: boolean
|
||||
}): Record<string, any[]> {
|
||||
deletedEntitiesMap ??= new Map<string, any[]>()
|
||||
getEntityName ??= (entity) => entity.constructor.name
|
||||
@@ -61,7 +63,7 @@ export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
||||
.get(entityName)
|
||||
?.some((e) => e.id === entity.id)
|
||||
|
||||
if (!entity.deleted_at || shouldSkip) {
|
||||
if ((restored ? !!entity.deleted_at : !entity.deleted_at) || shouldSkip) {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -3,15 +3,19 @@ import { loadDatabaseConfig } from "../load-module-database-config"
|
||||
describe("loadDatabaseConfig", function () {
|
||||
afterEach(() => {
|
||||
delete process.env.POSTGRES_URL
|
||||
delete process.env.MEDUSA_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"
|
||||
it("should return the local configuration using the environment variable respecting their precedence", function () {
|
||||
process.env.MEDUSA_POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/product"
|
||||
process.env.POSTGRES_URL = "postgres://localhost:5432/share_db"
|
||||
|
||||
let config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
@@ -21,12 +25,25 @@ describe("loadDatabaseConfig", function () {
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.POSTGRES_URL
|
||||
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
|
||||
delete process.env.PRODUCT_POSTGRES_URL
|
||||
config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||
clientUrl: process.env.MEDUSA_POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
},
|
||||
},
|
||||
debug: false,
|
||||
schema: "",
|
||||
})
|
||||
|
||||
delete process.env.MEDUSA_POSTGRES_URL
|
||||
config = loadDatabaseConfig("product")
|
||||
|
||||
expect(config).toEqual({
|
||||
clientUrl: process.env.POSTGRES_URL,
|
||||
driverOptions: {
|
||||
connection: {
|
||||
ssl: false,
|
||||
@@ -127,7 +144,7 @@ describe("loadDatabaseConfig", function () {
|
||||
}
|
||||
|
||||
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."
|
||||
"No database clientUrl provided. Please provide the clientUrl through the [MODULE]_POSTGRES_URL, MEDUSA_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function."
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { MedusaError } from "../common"
|
||||
import { ModulesSdkTypes } from "@medusajs/types"
|
||||
import { MedusaError } from "../common"
|
||||
|
||||
function getEnv(key: string, moduleName: string): string {
|
||||
const value =
|
||||
process.env[`${moduleName.toUpperCase()}_${key}`] ?? process.env[`${key}`]
|
||||
process.env[`${moduleName.toUpperCase()}_${key}`] ??
|
||||
process.env[`MEDUSA_${key}`] ??
|
||||
process.env[`${key}`]
|
||||
return value ?? ""
|
||||
}
|
||||
|
||||
@@ -39,6 +41,16 @@ function getDefaultDriverOptions(clientUrl: string) {
|
||||
: {}
|
||||
}
|
||||
|
||||
function getDatabaseUrl(
|
||||
config: ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
): string {
|
||||
const { clientUrl, host, port, user, password, database } = config.database!
|
||||
if (clientUrl) {
|
||||
return clientUrl
|
||||
}
|
||||
return `postgres://${user}:${password}@${host}:${port}/${database}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the config for the database connection. The options can be retrieved
|
||||
* e.g through PRODUCT_* (e.g PRODUCT_POSTGRES_URL) or * (e.g POSTGRES_URL) environment variables or the options object.
|
||||
@@ -49,11 +61,14 @@ export function loadDatabaseConfig(
|
||||
moduleName: string,
|
||||
options?: ModulesSdkTypes.ModuleServiceInitializeOptions,
|
||||
silent: boolean = false
|
||||
): ModulesSdkTypes.ModuleServiceInitializeOptions["database"] {
|
||||
): Pick<
|
||||
ModulesSdkTypes.ModuleServiceInitializeOptions["database"],
|
||||
"clientUrl" | "schema" | "driverOptions" | "debug"
|
||||
> {
|
||||
const clientUrl = getEnv("POSTGRES_URL", moduleName)
|
||||
|
||||
const database = {
|
||||
clientUrl: getEnv("POSTGRES_URL", moduleName),
|
||||
clientUrl,
|
||||
schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
|
||||
driverOptions: JSON.parse(
|
||||
getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
|
||||
@@ -63,7 +78,7 @@ export function loadDatabaseConfig(
|
||||
}
|
||||
|
||||
if (isModuleServiceInitializeOptions(options)) {
|
||||
database.clientUrl = options.database!.clientUrl ?? database.clientUrl
|
||||
database.clientUrl = getDatabaseUrl(options)
|
||||
database.schema = options.database!.schema ?? database.schema
|
||||
database.driverOptions =
|
||||
options.database!.driverOptions ??
|
||||
@@ -74,7 +89,7 @@ export function loadDatabaseConfig(
|
||||
if (!database.clientUrl && !silent) {
|
||||
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."
|
||||
"No database clientUrl provided. Please provide the clientUrl through the [MODULE]_POSTGRES_URL, MEDUSA_POSTGRES_URL or POSTGRES_URL environment variable or the options object in the initialize function."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,6 @@ export async function mikroOrmConnectionLoader({
|
||||
const shouldSwallowError = !!(
|
||||
options as ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||
)?.database?.connection
|
||||
|
||||
dbConfig = {
|
||||
...loadDatabaseConfig(
|
||||
"product",
|
||||
@@ -97,7 +96,7 @@ async function loadShared({ container, entities }) {
|
||||
)
|
||||
if (!sharedConnection) {
|
||||
throw new Error(
|
||||
"The module is setup to use a shared resources but no shared connection is present. A new connection will be created"
|
||||
"The module is setup to use a shared resources but no shared connection is present."
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: false,
|
||||
},
|
||||
},
|
||||
transform: {
|
||||
"^.+\\.[jt]s?$": "ts-jest",
|
||||
"^.+\\.[jt]s?$": [
|
||||
"ts-jest",
|
||||
{
|
||||
tsConfig: "tsconfig.json",
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testEnvironment: `node`,
|
||||
moduleFileExtensions: [`js`, `ts`],
|
||||
|
||||
@@ -20,10 +20,10 @@
|
||||
"devDependencies": {
|
||||
"@medusajs/types": "^1.10.2",
|
||||
"cross-env": "^5.2.1",
|
||||
"jest": "^25.5.4",
|
||||
"jest": "^29.6.3",
|
||||
"rimraf": "^5.0.1",
|
||||
"ts-jest": "^25.5.1",
|
||||
"typescript": "^4.4.4"
|
||||
"ts-jest": "^29.1.1",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
"dependencies": {
|
||||
"@medusajs/modules-sdk": "^1.9.2",
|
||||
|
||||
91
yarn.lock
91
yarn.lock
@@ -6196,10 +6196,10 @@ __metadata:
|
||||
"@medusajs/modules-sdk": ^1.8.8
|
||||
"@medusajs/types": ^1.8.8
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6212,10 +6212,10 @@ __metadata:
|
||||
awilix: ^8.0.0
|
||||
cross-env: ^5.2.1
|
||||
ioredis: ^5.3.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6241,10 +6241,10 @@ __metadata:
|
||||
"@medusajs/types": ^1.8.10
|
||||
"@medusajs/utils": ^1.9.2
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.2
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
ulid: ^2.3.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -6260,11 +6260,11 @@ __metadata:
|
||||
bullmq: ^3.5.6
|
||||
cross-env: ^5.2.1
|
||||
ioredis: ^5.2.5
|
||||
jest: ^25.5.2
|
||||
jest: ^29.6.3
|
||||
medusa-test-utils: ^1.1.40
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6310,11 +6310,32 @@ __metadata:
|
||||
"@medusajs/utils": ^1.9.1
|
||||
awilix: ^8.0.0
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
ts-jest: ^29.1.1
|
||||
typeorm: ^0.3.16
|
||||
typescript: ^4.4.4
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@medusajs/link-modules@workspace:packages/link-modules":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@medusajs/link-modules@workspace:packages/link-modules"
|
||||
dependencies:
|
||||
"@medusajs/modules-sdk": ^1.8.8
|
||||
"@medusajs/types": ^1.8.11
|
||||
"@medusajs/utils": ^1.9.2
|
||||
"@mikro-orm/core": 5.7.12
|
||||
"@mikro-orm/postgresql": 5.7.12
|
||||
awilix: ^8.0.0
|
||||
cross-env: ^5.2.1
|
||||
jest: ^29.6.3
|
||||
pg-god: ^1.0.12
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^29.1.1
|
||||
ts-node: ^10.9.1
|
||||
tsc-alias: ^1.8.6
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6496,11 +6517,11 @@ __metadata:
|
||||
"@medusajs/utils": ^1.9.6
|
||||
awilix: ^8.0.0
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
resolve-cwd: ^3.0.0
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6558,10 +6579,10 @@ __metadata:
|
||||
"@medusajs/utils": ^1.9.6
|
||||
cross-env: ^5.2.1
|
||||
graphql: ^16.6.0
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6609,16 +6630,16 @@ __metadata:
|
||||
cross-env: ^5.2.1
|
||||
dotenv: ^16.1.4
|
||||
faker: ^6.6.6
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
knex: 2.4.2
|
||||
lodash: ^4.17.21
|
||||
medusa-test-utils: ^1.1.40
|
||||
pg-god: ^1.0.12
|
||||
rimraf: ^3.0.2
|
||||
ts-jest: ^25.5.1
|
||||
ts-jest: ^29.1.1
|
||||
ts-node: ^10.9.1
|
||||
tsc-alias: ^1.8.6
|
||||
typescript: ^4.4.4
|
||||
typescript: ^5.1.6
|
||||
bin:
|
||||
medusa-product-migrations-down: dist/scripts/bin/run-migration-down.js
|
||||
medusa-product-migrations-up: dist/scripts/bin/run-migration-up.js
|
||||
@@ -6635,11 +6656,11 @@ __metadata:
|
||||
"@medusajs/utils": ^1.9.1
|
||||
awilix: ^8.0.0
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
ts-jest: ^29.1.1
|
||||
typeorm: ^0.3.16
|
||||
typescript: ^4.4.4
|
||||
typescript: ^5.1.6
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -6652,7 +6673,7 @@ __metadata:
|
||||
ioredis: ^5.2.5
|
||||
rimraf: ^5.0.1
|
||||
typeorm: ^0.3.16
|
||||
typescript: ^4.4.4
|
||||
typescript: ^5.1.6
|
||||
winston: ^3.8.2
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -6763,10 +6784,10 @@ __metadata:
|
||||
awilix: ^8.0.1
|
||||
cross-env: ^5.2.1
|
||||
express: ^4.18.2
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
ulid: ^2.3.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -6781,10 +6802,10 @@ __metadata:
|
||||
"@medusajs/utils": ^1.9.6
|
||||
awilix: ^8.0.1
|
||||
cross-env: ^5.2.1
|
||||
jest: ^25.5.4
|
||||
jest: ^29.6.3
|
||||
rimraf: ^5.0.1
|
||||
ts-jest: ^25.5.1
|
||||
typescript: ^4.4.4
|
||||
ts-jest: ^29.1.1
|
||||
typescript: ^5.1.6
|
||||
ulid: ^2.3.0
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
@@ -29262,7 +29283,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jest@npm:25.5.4, jest@npm:^25.5.2, jest@npm:^25.5.4":
|
||||
"jest@npm:25.5.4, jest@npm:^25.5.4":
|
||||
version: 25.5.4
|
||||
resolution: "jest@npm:25.5.4"
|
||||
dependencies:
|
||||
|
||||
Reference in New Issue
Block a user