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:
1 parent
bc4c9e0d32
commit
4d16acf5f0
97 files changed
+3540
-424
No files matched your search
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/link-modules": patch
|
||||||
|
"@medusajs/modules-sdk": patch
|
||||||
|
"@medusajs/types": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Add extra fields to link modules
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/orchestration": minor
|
||||||
|
"@medusajs/link-modules": minor
|
||||||
|
"@medusajs/modules-sdk": minor
|
||||||
|
"@medusajs/types": minor
|
||||||
|
"@medusajs/utils": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Medusa App Loader
|
||||||
@@ -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
|
||||||
@@ -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 = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.8",
|
"@medusajs/types": "^1.8.8",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.8",
|
"@medusajs/types": "^1.8.8",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
}
|
}
|
||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.10",
|
"@medusajs/types": "^1.8.10",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.2",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `jsx`, `ts`, `tsx`, `json`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
}
|
}
|
||||||
@@ -19,11 +19,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.10",
|
"@medusajs/types": "^1.8.10",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.2",
|
"jest": "^29.6.3",
|
||||||
"medusa-test-utils": "^1.1.40",
|
"medusa-test-utils": "^1.1.40",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"watch": "tsc --build --watch",
|
"watch": "tsc --build --watch",
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.11",
|
"@medusajs/types": "^1.8.11",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/modules-sdk": "^1.8.8",
|
"@medusajs/modules-sdk": "^1.8.8",
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
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,
|
serviceName: Modules.INVENTORY,
|
||||||
primaryKeys: ["id"],
|
primaryKeys: ["id"],
|
||||||
|
linkableKeys: [
|
||||||
|
"inventory_item_id",
|
||||||
|
"inventory_level_id",
|
||||||
|
"reservation_item_id",
|
||||||
|
],
|
||||||
alias: [
|
alias: [
|
||||||
{
|
{
|
||||||
name: "inventory_items",
|
name: "inventory_items",
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import {
|
|||||||
IInventoryService,
|
IInventoryService,
|
||||||
InventoryItemDTO,
|
InventoryItemDTO,
|
||||||
InventoryLevelDTO,
|
InventoryLevelDTO,
|
||||||
JoinerServiceConfig,
|
|
||||||
MODULE_RESOURCE_TYPE,
|
MODULE_RESOURCE_TYPE,
|
||||||
|
ModuleJoinerConfig,
|
||||||
ReservationItemDTO,
|
ReservationItemDTO,
|
||||||
SharedContext,
|
SharedContext,
|
||||||
UpdateInventoryLevelInput,
|
UpdateInventoryLevelInput,
|
||||||
@@ -59,7 +59,7 @@ export default class InventoryService implements IInventoryService {
|
|||||||
this.reservationItemService_ = reservationItemService
|
this.reservationItemService_ = reservationItemService
|
||||||
}
|
}
|
||||||
|
|
||||||
__joinerConfig(): JoinerServiceConfig {
|
__joinerConfig(): ModuleJoinerConfig {
|
||||||
return joinerConfig
|
return joinerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
module.exports = {
|
||||||
|
transform: {
|
||||||
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
testEnvironment: `node`,
|
||||||
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export * from "./initialize"
|
||||||
|
export * from "./types"
|
||||||
|
export * from "./loaders"
|
||||||
|
export * from "./services"
|
||||||
@@ -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 })
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
),
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)]
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"
|
||||||
|
export { getLinkRepository } from "./link"
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
export * from "./dynamic-service-class"
|
||||||
|
export { default as LinkService } from "./link"
|
||||||
|
export { default as LinkModuleService } from "./link-module-service"
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { Logger } from "@medusajs/types"
|
||||||
|
|
||||||
|
export type InitializeModuleInjectableDependencies = {
|
||||||
|
logger?: Logger
|
||||||
|
}
|
||||||
@@ -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("_")
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"include": ["src", "integration-tests"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
import { stringifyNullProperties } from "../../src/utils"
|
const { stringifyNullProperties } = require("../../utils")
|
||||||
|
|
||||||
describe("stringifyNullProperties", () => {
|
describe("stringifyNullProperties", () => {
|
||||||
test("returns empty object on no props", () => {
|
test("returns empty object on no props", () => {
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
{
|
{
|
||||||
"extends": "./tsconfig.json",
|
"extends": "./tsconfig.json",
|
||||||
"include": ["test"],
|
"include": [
|
||||||
|
"./src/**/*",
|
||||||
|
"index.d.ts",
|
||||||
|
"./src/**/__tests__",
|
||||||
|
"./src/**/__mocks__"
|
||||||
|
],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/orchestration": "^0.2.0",
|
"@medusajs/orchestration": "^0.2.0",
|
||||||
|
|||||||
@@ -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(() => {}),
|
||||||
|
}
|
||||||
@@ -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(() => {}),
|
||||||
|
}
|
||||||
@@ -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(() => {}),
|
||||||
|
}
|
||||||
@@ -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(() => {}),
|
||||||
|
}
|
||||||
@@ -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 "@medusajs/types/dist/modules-sdk"
|
||||||
export * from "./definitions"
|
export * from "./definitions"
|
||||||
export * from "./loaders"
|
export * from "./loaders"
|
||||||
|
export * from "./medusa-app"
|
||||||
export * from "./medusa-module"
|
export * from "./medusa-module"
|
||||||
export * from "./module-helper"
|
export * from "./module-helper"
|
||||||
|
export * from "./remote-link"
|
||||||
export * from "./remote-query"
|
export * from "./remote-query"
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
Logger,
|
Logger,
|
||||||
MedusaContainer,
|
|
||||||
MODULE_SCOPE,
|
MODULE_SCOPE,
|
||||||
|
MedusaContainer,
|
||||||
ModuleResolution,
|
ModuleResolution,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { ModulesHelper } from "../module-helper"
|
import { ModulesHelper } from "../module-helper"
|
||||||
@@ -11,53 +12,6 @@ import { loadInternalModule } from "./utils"
|
|||||||
|
|
||||||
export const moduleHelper = new ModulesHelper()
|
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 ({
|
export const moduleLoader = async ({
|
||||||
container,
|
container,
|
||||||
moduleResolutions,
|
moduleResolutions,
|
||||||
@@ -94,7 +48,48 @@ export const moduleLoader = async ({
|
|||||||
}, {})
|
}, {})
|
||||||
)
|
)
|
||||||
|
|
||||||
container.register({
|
container.register("modulesHelper", asValue(moduleHelper))
|
||||||
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,
|
ModuleExports,
|
||||||
ModuleResolution,
|
ModuleResolution,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import { isObject } from "@medusajs/utils"
|
import { isObject } from "@medusajs/utils"
|
||||||
import resolveCwd from "resolve-cwd"
|
import resolveCwd from "resolve-cwd"
|
||||||
import MODULE_DEFINITIONS from "../definitions"
|
import { MODULE_DEFINITIONS, ModulesDefinition } from "../definitions"
|
||||||
|
|
||||||
export const registerModules = (
|
export const registerModules = (
|
||||||
modules?: Record<
|
modules?: Record<
|
||||||
@@ -46,34 +47,57 @@ export const registerModules = (
|
|||||||
|
|
||||||
export const registerMedusaModule = (
|
export const registerMedusaModule = (
|
||||||
moduleKey: string,
|
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
|
moduleExports?: ModuleExports
|
||||||
): Record<string, ModuleResolution> => {
|
): Record<string, ModuleResolution> => {
|
||||||
const moduleResolutions = {} as Record<string, ModuleResolution>
|
const moduleResolutions = {} as Record<string, ModuleResolution>
|
||||||
|
|
||||||
for (const definition of MODULE_DEFINITIONS) {
|
moduleResolutions[definition.key] = getInternalModuleResolution(
|
||||||
if (definition.key !== moduleKey) {
|
definition,
|
||||||
continue
|
moduleDeclaration as InternalModuleDeclaration,
|
||||||
}
|
moduleExports
|
||||||
|
)
|
||||||
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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return moduleResolutions
|
return moduleResolutions
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInternalModuleResolution(
|
function getInternalModuleResolution(
|
||||||
definition: ModuleDefinition,
|
definition: ModuleDefinition,
|
||||||
moduleConfig: InternalModuleDeclaration | false | string,
|
moduleConfig: InternalModuleDeclaration | string | false,
|
||||||
moduleExports?: ModuleExports
|
moduleExports?: ModuleExports
|
||||||
): ModuleResolution {
|
): ModuleResolution {
|
||||||
if (typeof moduleConfig === "boolean") {
|
if (typeof moduleConfig === "boolean") {
|
||||||
@@ -116,7 +140,7 @@ function getInternalModuleResolution(
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
moduleDeclaration: {
|
moduleDeclaration: {
|
||||||
...definition.defaultModuleDeclaration,
|
...(definition.defaultModuleDeclaration ?? {}),
|
||||||
...moduleDeclaration,
|
...moduleDeclaration,
|
||||||
},
|
},
|
||||||
moduleExports,
|
moduleExports,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import {
|
|||||||
Constructor,
|
Constructor,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
Logger,
|
Logger,
|
||||||
MedusaContainer,
|
|
||||||
MODULE_RESOURCE_TYPE,
|
MODULE_RESOURCE_TYPE,
|
||||||
MODULE_SCOPE,
|
MODULE_SCOPE,
|
||||||
|
MedusaContainer,
|
||||||
ModuleExports,
|
ModuleExports,
|
||||||
ModuleResolution,
|
ModuleResolution,
|
||||||
} from "@medusajs/types"
|
} 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
|
// 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
|
// does not need to import the package that is currently being loaded as it would create a
|
||||||
// circular reference.
|
// circular reference.
|
||||||
loadedModule =
|
const path = resolution.resolutionPath as string
|
||||||
resolution.moduleExports ??
|
|
||||||
(await import(resolution.resolutionPath as string)).default
|
if (resolution.moduleExports) {
|
||||||
|
loadedModule = resolution.moduleExports
|
||||||
|
} else {
|
||||||
|
loadedModule = await import(path)
|
||||||
|
loadedModule = (loadedModule as any).default
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (
|
if (
|
||||||
resolution.definition.isRequired &&
|
resolution.definition.isRequired &&
|
||||||
|
|||||||
@@ -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 {
|
import {
|
||||||
ExternalModuleDeclaration,
|
ExternalModuleDeclaration,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
|
LinkModuleDefinition,
|
||||||
LoadedModule,
|
LoadedModule,
|
||||||
MODULE_RESOURCE_TYPE,
|
MODULE_RESOURCE_TYPE,
|
||||||
MODULE_SCOPE,
|
MODULE_SCOPE,
|
||||||
|
ModuleDefinition,
|
||||||
ModuleExports,
|
ModuleExports,
|
||||||
|
ModuleJoinerConfig,
|
||||||
ModuleResolution,
|
ModuleResolution,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
@@ -12,7 +15,11 @@ import {
|
|||||||
simpleHash,
|
simpleHash,
|
||||||
stringifyCircular,
|
stringifyCircular,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import { moduleLoader, registerMedusaModule } from "./loaders"
|
import {
|
||||||
|
moduleLoader,
|
||||||
|
registerMedusaLinkModule,
|
||||||
|
registerMedusaModule,
|
||||||
|
} from "./loaders"
|
||||||
|
|
||||||
import { asValue } from "awilix"
|
import { asValue } from "awilix"
|
||||||
import { loadModuleMigrations } from "./loaders/utils"
|
import { loadModuleMigrations } from "./loaders/utils"
|
||||||
@@ -36,6 +43,7 @@ declare global {
|
|||||||
type ModuleAlias = {
|
type ModuleAlias = {
|
||||||
key: string
|
key: string
|
||||||
hash: string
|
hash: string
|
||||||
|
isLink: boolean
|
||||||
alias?: string
|
alias?: string
|
||||||
main?: boolean
|
main?: boolean
|
||||||
}
|
}
|
||||||
@@ -125,7 +133,8 @@ export class MedusaModule {
|
|||||||
defaultPath: string,
|
defaultPath: string,
|
||||||
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
|
declaration?: InternalModuleDeclaration | ExternalModuleDeclaration,
|
||||||
moduleExports?: ModuleExports,
|
moduleExports?: ModuleExports,
|
||||||
injectedDependencies?: Record<string, any>
|
injectedDependencies?: Record<string, any>,
|
||||||
|
moduleDefinition?: ModuleDefinition
|
||||||
): Promise<{
|
): Promise<{
|
||||||
[key: string]: T
|
[key: string]: T
|
||||||
}> {
|
}> {
|
||||||
@@ -157,10 +166,10 @@ export class MedusaModule {
|
|||||||
|
|
||||||
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
|
if (declaration?.scope !== MODULE_SCOPE.EXTERNAL) {
|
||||||
modDeclaration = {
|
modDeclaration = {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: declaration?.scope || MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.ISOLATED,
|
resources: declaration?.resources || MODULE_RESOURCE_TYPE.ISOLATED,
|
||||||
resolve: defaultPath,
|
resolve: defaultPath,
|
||||||
options: declaration,
|
options: declaration?.options ?? declaration,
|
||||||
alias: declaration?.alias,
|
alias: declaration?.alias,
|
||||||
main: declaration?.main,
|
main: declaration?.main,
|
||||||
}
|
}
|
||||||
@@ -177,6 +186,118 @@ export class MedusaModule {
|
|||||||
const moduleResolutions = registerMedusaModule(
|
const moduleResolutions = registerMedusaModule(
|
||||||
moduleKey,
|
moduleKey,
|
||||||
modDeclaration!,
|
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
|
moduleExports
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -203,9 +324,17 @@ export class MedusaModule {
|
|||||||
services[keyName].__definition = resolution.definition
|
services[keyName].__definition = resolution.definition
|
||||||
|
|
||||||
if (resolution.definition.isQueryable) {
|
if (resolution.definition.isQueryable) {
|
||||||
services[keyName].__joinerConfig = await services[
|
const joinerConfig: ModuleJoinerConfig = await services[
|
||||||
keyName
|
keyName
|
||||||
].__joinerConfig()
|
].__joinerConfig()
|
||||||
|
|
||||||
|
services[keyName].__joinerConfig = joinerConfig
|
||||||
|
|
||||||
|
if (!joinerConfig.isLink) {
|
||||||
|
throw new Error(
|
||||||
|
"MedusaModule.bootstrapLink must be used only for Link Modules"
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MedusaModule.registerModule(keyName, {
|
MedusaModule.registerModule(keyName, {
|
||||||
@@ -213,6 +342,7 @@ export class MedusaModule {
|
|||||||
hash: hashKey,
|
hash: hashKey,
|
||||||
alias: modDeclaration.alias ?? hashKey,
|
alias: modDeclaration.alias ?? hashKey,
|
||||||
main: !!modDeclaration.main,
|
main: !!modDeclaration.main,
|
||||||
|
isLink: true,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
JoinerRelationship,
|
||||||
JoinerServiceConfig,
|
JoinerServiceConfig,
|
||||||
LoadedModule,
|
LoadedModule,
|
||||||
|
ModuleJoinerConfig,
|
||||||
RemoteExpandProperty,
|
RemoteExpandProperty,
|
||||||
|
RemoteJoinerQuery,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
|
import { RemoteFetchDataCallback, RemoteJoiner } from "@medusajs/orchestration"
|
||||||
|
import { isString, toPascalCase } from "@medusajs/utils"
|
||||||
import { MedusaModule } from "./medusa-module"
|
import { MedusaModule } from "./medusa-module"
|
||||||
import { RemoteJoiner } from "@medusajs/orchestration"
|
|
||||||
import { toPascalCase } from "@medusajs/utils"
|
|
||||||
|
|
||||||
export class RemoteQuery {
|
export class RemoteQuery {
|
||||||
private remoteJoiner: RemoteJoiner
|
private remoteJoiner: RemoteJoiner
|
||||||
private modulesMap: Map<string, LoadedModule> = new Map()
|
private modulesMap: Map<string, LoadedModule> = new Map()
|
||||||
|
private customRemoteFetchData?: RemoteFetchDataCallback
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
modulesLoaded?: LoadedModule[],
|
modulesLoaded?: LoadedModule[],
|
||||||
remoteFetchData?: (
|
customRemoteFetchData?: RemoteFetchDataCallback
|
||||||
expand: RemoteExpandProperty,
|
|
||||||
keyField: string,
|
|
||||||
ids?: (unknown | unknown[])[],
|
|
||||||
relationship?: JoinerRelationship
|
|
||||||
) => Promise<{
|
|
||||||
data: unknown[] | { [path: string]: unknown[] }
|
|
||||||
path?: string
|
|
||||||
}>
|
|
||||||
) {
|
) {
|
||||||
if (!modulesLoaded?.length) {
|
if (!modulesLoaded?.length) {
|
||||||
modulesLoaded = MedusaModule.getLoadedModules().map(
|
modulesLoaded = MedusaModule.getLoadedModules().map(
|
||||||
@@ -31,25 +26,28 @@ export class RemoteQuery {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const servicesConfig: JoinerServiceConfig[] = []
|
const servicesConfig: ModuleJoinerConfig[] = []
|
||||||
for (const mod of modulesLoaded) {
|
for (const mod of modulesLoaded) {
|
||||||
if (!mod.__definition.isQueryable) {
|
if (!mod.__definition.isQueryable) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.modulesMap.has(mod.__definition.key)) {
|
const serviceName = mod.__definition.key
|
||||||
|
|
||||||
|
if (this.modulesMap.has(serviceName)) {
|
||||||
throw new Error(
|
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)
|
servicesConfig.push(mod.__joinerConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.customRemoteFetchData = customRemoteFetchData
|
||||||
this.remoteJoiner = new RemoteJoiner(
|
this.remoteJoiner = new RemoteJoiner(
|
||||||
servicesConfig,
|
servicesConfig as JoinerServiceConfig[],
|
||||||
remoteFetchData ?? this.remoteFetchData.bind(this)
|
this.remoteFetchData.bind(this)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,14 +67,20 @@ export class RemoteQuery {
|
|||||||
|
|
||||||
private static getAllFieldsAndRelations(
|
private static getAllFieldsAndRelations(
|
||||||
data: any,
|
data: any,
|
||||||
prefix = ""
|
prefix = "",
|
||||||
): { select: string[]; relations: string[] } {
|
args: Record<string, unknown[]> = {}
|
||||||
|
): {
|
||||||
|
select: string[]
|
||||||
|
relations: string[]
|
||||||
|
args: Record<string, unknown[]>
|
||||||
|
} {
|
||||||
let fields: Set<string> = new Set()
|
let fields: Set<string> = new Set()
|
||||||
let relations: string[] = []
|
let relations: string[] = []
|
||||||
|
|
||||||
data.fields?.forEach((field: string) => {
|
data.fields?.forEach((field: string) => {
|
||||||
fields.add(prefix ? `${prefix}.${field}` : field)
|
fields.add(prefix ? `${prefix}.${field}` : field)
|
||||||
})
|
})
|
||||||
|
args[prefix] = data.args
|
||||||
|
|
||||||
if (data.expands) {
|
if (data.expands) {
|
||||||
for (const property in data.expands) {
|
for (const property in data.expands) {
|
||||||
@@ -87,7 +91,8 @@ export class RemoteQuery {
|
|||||||
|
|
||||||
const result = RemoteQuery.getAllFieldsAndRelations(
|
const result = RemoteQuery.getAllFieldsAndRelations(
|
||||||
data.expands[property],
|
data.expands[property],
|
||||||
newPrefix
|
newPrefix,
|
||||||
|
args
|
||||||
)
|
)
|
||||||
|
|
||||||
result.select.forEach(fields.add, fields)
|
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 {
|
private hasPagination(options: { [attr: string]: unknown }): boolean {
|
||||||
@@ -126,6 +131,13 @@ export class RemoteQuery {
|
|||||||
data: unknown[] | { [path: string]: unknown }
|
data: unknown[] | { [path: string]: unknown }
|
||||||
path?: string
|
path?: string
|
||||||
}> {
|
}> {
|
||||||
|
if (this.customRemoteFetchData) {
|
||||||
|
const resp = await this.customRemoteFetchData(expand, keyField, ids)
|
||||||
|
if (resp !== undefined) {
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const serviceConfig = expand.serviceConfig
|
const serviceConfig = expand.serviceConfig
|
||||||
const service = this.modulesMap.get(serviceConfig.serviceName)!
|
const service = this.modulesMap.get(serviceConfig.serviceName)!
|
||||||
|
|
||||||
@@ -196,9 +208,14 @@ export class RemoteQuery {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async query(query: string, variables: any = {}): Promise<any> {
|
public async query(
|
||||||
return await this.remoteJoiner.query(
|
query: string | RemoteJoinerQuery,
|
||||||
RemoteJoiner.parseQuery(query, variables)
|
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 = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.10.2",
|
"@medusajs/types": "^1.10.2",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/utils": "^1.9.6",
|
"@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 { RemoteJoinerQuery } from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
ArgumentNode,
|
ArgumentNode,
|
||||||
|
DirectiveNode,
|
||||||
DocumentNode,
|
DocumentNode,
|
||||||
FieldNode,
|
FieldNode,
|
||||||
Kind,
|
Kind,
|
||||||
@@ -15,18 +16,24 @@ interface Argument {
|
|||||||
value?: unknown
|
value?: unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface Directive {
|
||||||
|
name: string
|
||||||
|
args?: Argument[]
|
||||||
|
}
|
||||||
|
|
||||||
interface Entity {
|
interface Entity {
|
||||||
property: string
|
property: string
|
||||||
fields: string[]
|
fields: string[]
|
||||||
args?: Argument[]
|
args?: Argument[]
|
||||||
|
directives?: { [field: string]: Directive[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
class GraphQLParser {
|
class GraphQLParser {
|
||||||
private ast: DocumentNode
|
private ast: DocumentNode
|
||||||
|
|
||||||
constructor(input: string, private variables?: { [key: string]: unknown }) {
|
constructor(input: string, private variables: Record<string, unknown> = {}) {
|
||||||
this.ast = parse(input)
|
this.ast = parse(input)
|
||||||
this.variables = variables || {}
|
this.variables = variables
|
||||||
}
|
}
|
||||||
|
|
||||||
private parseValueNode(valueNode: ValueNode): unknown {
|
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(
|
private extractEntities(
|
||||||
node: SelectionSetNode,
|
node: SelectionSetNode,
|
||||||
parentName = "",
|
parentName = "",
|
||||||
@@ -98,7 +132,8 @@ class GraphQLParser {
|
|||||||
fields: fieldNode.selectionSet.selections.map(
|
fields: fieldNode.selectionSet.selections.map(
|
||||||
(field) => (field as FieldNode).name.value
|
(field) => (field as FieldNode).name.value
|
||||||
),
|
),
|
||||||
args: this.parseArguments(fieldNode.arguments!),
|
args: this.parseArguments(fieldNode.arguments || []),
|
||||||
|
directives: this.createDirectivesMap(fieldNode.selectionSet),
|
||||||
}
|
}
|
||||||
|
|
||||||
entities.push(nestedEntity)
|
entities.push(nestedEntity)
|
||||||
@@ -126,8 +161,8 @@ class GraphQLParser {
|
|||||||
|
|
||||||
const rootFieldNode = queryDefinition.selectionSet
|
const rootFieldNode = queryDefinition.selectionSet
|
||||||
.selections[0] as FieldNode
|
.selections[0] as FieldNode
|
||||||
|
|
||||||
const propName = rootFieldNode.name.value
|
const propName = rootFieldNode.name.value
|
||||||
|
|
||||||
const remoteJoinConfig: RemoteJoinerQuery = {
|
const remoteJoinConfig: RemoteJoinerQuery = {
|
||||||
alias: propName,
|
alias: propName,
|
||||||
fields: [],
|
fields: [],
|
||||||
@@ -142,7 +177,9 @@ class GraphQLParser {
|
|||||||
remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map(
|
remoteJoinConfig.fields = rootFieldNode.selectionSet.selections.map(
|
||||||
(field) => (field as FieldNode).name.value
|
(field) => (field as FieldNode).name.value
|
||||||
)
|
)
|
||||||
|
remoteJoinConfig.directives = this.createDirectivesMap(
|
||||||
|
rootFieldNode.selectionSet
|
||||||
|
)
|
||||||
remoteJoinConfig.expands = this.extractEntities(
|
remoteJoinConfig.expands = this.extractEntities(
|
||||||
rootFieldNode.selectionSet,
|
rootFieldNode.selectionSet,
|
||||||
propName,
|
propName,
|
||||||
|
|||||||
@@ -2,14 +2,27 @@ import {
|
|||||||
JoinerRelationship,
|
JoinerRelationship,
|
||||||
JoinerServiceConfig,
|
JoinerServiceConfig,
|
||||||
JoinerServiceConfigAlias,
|
JoinerServiceConfigAlias,
|
||||||
|
ModuleJoinerConfig,
|
||||||
RemoteExpandProperty,
|
RemoteExpandProperty,
|
||||||
RemoteJoinerQuery,
|
RemoteJoinerQuery,
|
||||||
RemoteNestedExpands,
|
RemoteNestedExpands,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import { isDefined } from "@medusajs/utils"
|
import { isDefined } from "@medusajs/utils"
|
||||||
import GraphQLParser from "./graphql-ast"
|
import GraphQLParser from "./graphql-ast"
|
||||||
|
|
||||||
const BASE_PATH = "_root"
|
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 {
|
export class RemoteJoiner {
|
||||||
private serviceConfigCache: Map<string, JoinerServiceConfig> = new Map()
|
private serviceConfigCache: Map<string, JoinerServiceConfig> = new Map()
|
||||||
|
|
||||||
@@ -18,7 +31,7 @@ export class RemoteJoiner {
|
|||||||
fields: string[],
|
fields: string[],
|
||||||
expands?: RemoteNestedExpands
|
expands?: RemoteNestedExpands
|
||||||
): Record<string, unknown> {
|
): Record<string, unknown> {
|
||||||
if (!fields) {
|
if (!fields || !data) {
|
||||||
return 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)
|
const parser = new GraphQLParser(graphqlQuery, variables)
|
||||||
return parser.parseQuery()
|
return parser.parseQuery()
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private serviceConfigs: JoinerServiceConfig[],
|
private serviceConfigs: ModuleJoinerConfig[],
|
||||||
private remoteFetchData: (
|
private remoteFetchData: RemoteFetchDataCallback
|
||||||
expand: RemoteExpandProperty,
|
|
||||||
keyField: string,
|
|
||||||
ids?: (unknown | unknown[])[],
|
|
||||||
relationship?: any
|
|
||||||
) => Promise<{
|
|
||||||
data: unknown[] | { [path: string]: unknown }
|
|
||||||
path?: string
|
|
||||||
}>
|
|
||||||
) {
|
) {
|
||||||
this.serviceConfigs = this.buildReferences(serviceConfigs)
|
this.serviceConfigs = this.buildReferences(serviceConfigs)
|
||||||
}
|
}
|
||||||
|
|
||||||
public setFetchDataCallback(
|
public setFetchDataCallback(remoteFetchData: RemoteFetchDataCallback): void {
|
||||||
remoteFetchData: (
|
|
||||||
expand: RemoteExpandProperty,
|
|
||||||
keyField: string,
|
|
||||||
ids?: (unknown | unknown[])[],
|
|
||||||
relationship?: any
|
|
||||||
) => Promise<{
|
|
||||||
data: unknown[] | { [path: string]: unknown }
|
|
||||||
path?: string
|
|
||||||
}>
|
|
||||||
): void {
|
|
||||||
this.remoteFetchData = remoteFetchData
|
this.remoteFetchData = remoteFetchData
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildReferences(serviceConfigs: JoinerServiceConfig[]) {
|
private buildReferences(serviceConfigs: ModuleJoinerConfig[]) {
|
||||||
const expandedRelationships: Map<string, JoinerRelationship[]> = new Map()
|
const expandedRelationships: Map<string, JoinerRelationship[]> = new Map()
|
||||||
for (const service of serviceConfigs) {
|
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.`)
|
throw new Error(`Service "${service.serviceName}" is already defined.`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,38 +122,42 @@ export class RemoteJoiner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// add aliases
|
// add aliases
|
||||||
if (!service.alias) {
|
const isReadOnlyDefinition =
|
||||||
service.alias = [{ name: service.serviceName.toLowerCase() }]
|
service.serviceName === undefined || service.isReadOnlyLink
|
||||||
} else if (!Array.isArray(service.alias)) {
|
if (!isReadOnlyDefinition) {
|
||||||
service.alias = [service.alias]
|
if (!service.alias) {
|
||||||
}
|
service.alias = [{ name: service.serviceName!.toLowerCase() }]
|
||||||
|
} else if (!Array.isArray(service.alias)) {
|
||||||
// self-reference
|
service.alias = [service.alias]
|
||||||
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 args =
|
// self-reference
|
||||||
service.args || alias.args
|
for (const alias of service.alias) {
|
||||||
? { ...service.args, ...alias.args }
|
if (this.serviceConfigCache.has(`alias_${alias.name}}`)) {
|
||||||
: undefined
|
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({
|
const args =
|
||||||
alias: alias.name,
|
service.args || alias.args
|
||||||
foreignKey: alias.name + "_id",
|
? { ...service.args, ...alias.args }
|
||||||
primaryKey: "id",
|
: undefined
|
||||||
serviceName: service.serviceName,
|
|
||||||
args,
|
service.relationships?.push({
|
||||||
})
|
alias: alias.name,
|
||||||
this.cacheServiceConfig(serviceConfigs, undefined, 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) {
|
if (!service.extends) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -295,7 +297,7 @@ export class RemoteJoiner {
|
|||||||
Map<string, RemoteExpandProperty>,
|
Map<string, RemoteExpandProperty>,
|
||||||
string,
|
string,
|
||||||
Set<string>
|
Set<string>
|
||||||
][] = [[items, query, parsedExpands, "", new Set()]]
|
][] = [[items, query, parsedExpands, BASE_PATH, new Set()]]
|
||||||
|
|
||||||
while (stack.length > 0) {
|
while (stack.length > 0) {
|
||||||
const [
|
const [
|
||||||
@@ -307,9 +309,7 @@ export class RemoteJoiner {
|
|||||||
] = stack.pop()!
|
] = stack.pop()!
|
||||||
|
|
||||||
for (const [expandedPath, expand] of currentParsedExpands.entries()) {
|
for (const [expandedPath, expand] of currentParsedExpands.entries()) {
|
||||||
const isImmediateChildPath =
|
const isImmediateChildPath = basePath === expand.parent
|
||||||
expandedPath.startsWith(basePath) &&
|
|
||||||
expandedPath.split(".").length === basePath.split(".").length + 1
|
|
||||||
|
|
||||||
if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) {
|
if (!isImmediateChildPath || resolvedPaths.has(expandedPath)) {
|
||||||
continue
|
continue
|
||||||
@@ -433,7 +433,8 @@ export class RemoteJoiner {
|
|||||||
item[relationship.alias] = item[field]
|
item[relationship.alias] = item[field]
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
if (relationship.isList && !Array.isArray(relatedDataMap[id])) {
|
if (relationship.isList && !Array.isArray(relatedDataMap[id])) {
|
||||||
relatedDataMap[id] = [relatedDataMap[id]]
|
relatedDataMap[id] =
|
||||||
|
relatedDataMap[id] !== undefined ? [relatedDataMap[id]] : []
|
||||||
}
|
}
|
||||||
|
|
||||||
return relatedDataMap[id]
|
return relatedDataMap[id]
|
||||||
@@ -441,7 +442,10 @@ export class RemoteJoiner {
|
|||||||
.filter((relatedItem) => relatedItem !== undefined)
|
.filter((relatedItem) => relatedItem !== undefined)
|
||||||
} else {
|
} else {
|
||||||
if (relationship.isList && !Array.isArray(relatedDataMap[itemKey])) {
|
if (relationship.isList && !Array.isArray(relatedDataMap[itemKey])) {
|
||||||
relatedDataMap[itemKey] = [relatedDataMap[itemKey]]
|
relatedDataMap[itemKey] =
|
||||||
|
relatedDataMap[itemKey] !== undefined
|
||||||
|
? [relatedDataMap[itemKey]]
|
||||||
|
: []
|
||||||
}
|
}
|
||||||
|
|
||||||
item[relationship.alias] = relatedDataMap[itemKey]
|
item[relationship.alias] = relatedDataMap[itemKey]
|
||||||
@@ -539,13 +543,13 @@ export class RemoteJoiner {
|
|||||||
serviceConfig: currentServiceConfig,
|
serviceConfig: currentServiceConfig,
|
||||||
fields,
|
fields,
|
||||||
args,
|
args,
|
||||||
|
parent: [BASE_PATH, ...currentPath].join("."),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
currentPath.push(prop)
|
currentPath.push(prop)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return parsedExpands
|
return parsedExpands
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -566,7 +570,7 @@ export class RemoteJoiner {
|
|||||||
for (const [path, expand] of sortedParsedExpands.entries()) {
|
for (const [path, expand] of sortedParsedExpands.entries()) {
|
||||||
const currentServiceName = expand.serviceConfig.serviceName
|
const currentServiceName = expand.serviceConfig.serviceName
|
||||||
|
|
||||||
let parentPath = path.split(".").slice(0, -1).join(".")
|
let parentPath = expand.parent
|
||||||
|
|
||||||
// Check if the parentPath was merged before
|
// Check if the parentPath was merged before
|
||||||
while (mergedPaths.has(parentPath)) {
|
while (mergedPaths.has(parentPath)) {
|
||||||
@@ -580,6 +584,7 @@ export class RemoteJoiner {
|
|||||||
|
|
||||||
if (parentExpand.serviceConfig.serviceName === currentServiceName) {
|
if (parentExpand.serviceConfig.serviceName === currentServiceName) {
|
||||||
const nestedKeys = path.split(".").slice(parentPath.split(".").length)
|
const nestedKeys = path.split(".").slice(parentPath.split(".").length)
|
||||||
|
|
||||||
let targetExpand: any = parentExpand
|
let targetExpand: any = parentExpand
|
||||||
|
|
||||||
for (let key of nestedKeys) {
|
for (let key of nestedKeys) {
|
||||||
@@ -633,6 +638,7 @@ export class RemoteJoiner {
|
|||||||
const parsedExpands = this.parseExpands(
|
const parsedExpands = this.parseExpands(
|
||||||
{
|
{
|
||||||
property: "",
|
property: "",
|
||||||
|
parent: "",
|
||||||
serviceConfig: serviceConfig,
|
serviceConfig: serviceConfig,
|
||||||
fields: queryObj.fields,
|
fields: queryObj.fields,
|
||||||
args: otherArgs,
|
args: otherArgs,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
if (typeof process.env.DB_TEMP_NAME === "undefined") {
|
if (typeof process.env.DB_TEMP_NAME === "undefined") {
|
||||||
const tempName = parseInt(process.env.JEST_WORKER_ID || "1")
|
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"
|
process.env.MEDUSA_PRICING_DB_SCHEMA = "public"
|
||||||
@@ -13,12 +13,12 @@ import {
|
|||||||
} from "../../../__fixtures__/product/data"
|
} from "../../../__fixtures__/product/data"
|
||||||
|
|
||||||
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
import { ProductDTO, ProductTypes } from "@medusajs/types"
|
||||||
|
import { kebabCase } from "@medusajs/utils"
|
||||||
|
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
||||||
import { ProductRepository } from "@repositories"
|
import { ProductRepository } from "@repositories"
|
||||||
import { ProductService } from "@services"
|
import { ProductService } from "@services"
|
||||||
import { SqlEntityManager } from "@mikro-orm/postgresql"
|
|
||||||
import { TestDatabase } from "../../../utils"
|
|
||||||
import { createProductCategories } from "../../../__fixtures__/product-category"
|
import { createProductCategories } from "../../../__fixtures__/product-category"
|
||||||
import { kebabCase } from "@medusajs/utils"
|
import { TestDatabase } from "../../../utils"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
@@ -513,7 +513,7 @@ describe("Product Service", () => {
|
|||||||
const products = await service.create([data])
|
const products = await service.create([data])
|
||||||
const product = products[0]
|
const product = products[0]
|
||||||
await service.softDelete([product.id])
|
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).toHaveLength(1)
|
||||||
expect(restoreProducts[0].deleted_at).toBeNull()
|
expect(restoreProducts[0].deleted_at).toBeNull()
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ module.exports = {
|
|||||||
"^@services": "<rootDir>/src/services",
|
"^@services": "<rootDir>/src/services",
|
||||||
"^@repositories": "<rootDir>/src/repositories",
|
"^@repositories": "<rootDir>/src/repositories",
|
||||||
},
|
},
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.spec.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -39,14 +39,14 @@
|
|||||||
"@mikro-orm/cli": "5.7.12",
|
"@mikro-orm/cli": "5.7.12",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"faker": "^6.6.6",
|
"faker": "^6.6.6",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"medusa-test-utils": "^1.1.40",
|
"medusa-test-utils": "^1.1.40",
|
||||||
"pg-god": "^1.0.12",
|
"pg-god": "^1.0.12",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsc-alias": "^1.8.6",
|
"tsc-alias": "^1.8.6",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/modules-sdk": "^1.9.2",
|
"@medusajs/modules-sdk": "^1.9.2",
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import {
|
import {
|
||||||
ExternalModuleDeclaration,
|
ExternalModuleDeclaration,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
MedusaModule,
|
|
||||||
MODULE_PACKAGE_NAMES,
|
MODULE_PACKAGE_NAMES,
|
||||||
|
MedusaModule,
|
||||||
Modules,
|
Modules,
|
||||||
} from "@medusajs/modules-sdk"
|
} from "@medusajs/modules-sdk"
|
||||||
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
import { IProductModuleService, ModulesSdkTypes } from "@medusajs/types"
|
||||||
import { moduleDefinition } from "../module-definition"
|
|
||||||
import { InitializeModuleInjectableDependencies } from "../types"
|
import { InitializeModuleInjectableDependencies } from "../types"
|
||||||
|
import { moduleDefinition } from "../module-definition"
|
||||||
|
|
||||||
export const initialize = async (
|
export const initialize = async (
|
||||||
options?:
|
options?:
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { JoinerServiceConfig } from "@medusajs/types"
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
|
import { MapToConfig } from "@medusajs/utils"
|
||||||
import {
|
import {
|
||||||
Product,
|
Product,
|
||||||
ProductCategory,
|
ProductCategory,
|
||||||
@@ -10,10 +11,9 @@ import {
|
|||||||
ProductVariant,
|
ProductVariant,
|
||||||
} from "@models"
|
} from "@models"
|
||||||
import ProductImage from "./models/product-image"
|
import ProductImage from "./models/product-image"
|
||||||
import { MapToConfig } from "@medusajs/utils"
|
|
||||||
|
|
||||||
export enum LinkableKeys {
|
export enum LinkableKeys {
|
||||||
PRODUCT_ID = "product_id",
|
PRODUCT_ID = "product_id", // Main service ID must the first
|
||||||
PRODUCT_HANDLE = "product_handle",
|
PRODUCT_HANDLE = "product_handle",
|
||||||
VARIANT_ID = "variant_id",
|
VARIANT_ID = "variant_id",
|
||||||
VARIANT_SKU = "variant_sku",
|
VARIANT_SKU = "variant_sku",
|
||||||
@@ -55,9 +55,10 @@ export const entityNameToLinkableKeysMap: MapToConfig = {
|
|||||||
],
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
export const joinerConfig: JoinerServiceConfig = {
|
export const joinerConfig: ModuleJoinerConfig = {
|
||||||
serviceName: Modules.PRODUCT,
|
serviceName: Modules.PRODUCT,
|
||||||
primaryKeys: ["id", "handle"],
|
primaryKeys: ["id", "handle"],
|
||||||
|
linkableKeys: Object.values(LinkableKeys),
|
||||||
alias: [
|
alias: [
|
||||||
{
|
{
|
||||||
name: "product",
|
name: "product",
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ProductCategoryService,
|
Context,
|
||||||
ProductCollectionService,
|
CreateProductOnlyDTO,
|
||||||
ProductOptionService,
|
DAL,
|
||||||
ProductService,
|
FindConfig,
|
||||||
ProductTagService,
|
IEventBusModuleService,
|
||||||
ProductTypeService,
|
InternalModuleDeclaration,
|
||||||
ProductVariantService,
|
ModuleJoinerConfig,
|
||||||
} from "@services"
|
ProductTypes,
|
||||||
|
RestoreReturn,
|
||||||
|
SoftDeleteReturn,
|
||||||
|
} from "@medusajs/types"
|
||||||
import {
|
import {
|
||||||
Image,
|
Image,
|
||||||
Product,
|
Product,
|
||||||
@@ -18,15 +21,14 @@ import {
|
|||||||
ProductVariant,
|
ProductVariant,
|
||||||
} from "@models"
|
} from "@models"
|
||||||
import {
|
import {
|
||||||
Context,
|
ProductCategoryService,
|
||||||
CreateProductOnlyDTO,
|
ProductCollectionService,
|
||||||
DAL,
|
ProductOptionService,
|
||||||
FindConfig,
|
ProductService,
|
||||||
IEventBusModuleService,
|
ProductTagService,
|
||||||
InternalModuleDeclaration,
|
ProductTypeService,
|
||||||
JoinerServiceConfig,
|
ProductVariantService,
|
||||||
ProductTypes,
|
} from "@services"
|
||||||
} from "@medusajs/types"
|
|
||||||
|
|
||||||
import ProductImageService from "./product-image"
|
import ProductImageService from "./product-image"
|
||||||
|
|
||||||
@@ -136,7 +138,7 @@ export default class ProductModuleService<
|
|||||||
this.eventBusModuleService_ = eventBusModuleService
|
this.eventBusModuleService_ = eventBusModuleService
|
||||||
}
|
}
|
||||||
|
|
||||||
__joinerConfig(): JoinerServiceConfig {
|
__joinerConfig(): ModuleJoinerConfig {
|
||||||
return joinerConfig
|
return joinerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1071,11 +1073,7 @@ export default class ProductModuleService<
|
|||||||
>
|
>
|
||||||
>(
|
>(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
{
|
{ returnLinkableKeys }: SoftDeleteReturn<TReturnableLinkableKeys> = {},
|
||||||
returnLinkableKeys,
|
|
||||||
}: { returnLinkableKeys?: TReturnableLinkableKeys[] } = {
|
|
||||||
returnLinkableKeys: [],
|
|
||||||
},
|
|
||||||
sharedContext: Context = {}
|
sharedContext: Context = {}
|
||||||
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||||
const [products, cascadedEntitiesMap] = await this.softDelete_(
|
const [products, cascadedEntitiesMap] = await this.softDelete_(
|
||||||
@@ -1098,10 +1096,12 @@ export default class ProductModuleService<
|
|||||||
|
|
||||||
let mappedCascadedEntitiesMap
|
let mappedCascadedEntitiesMap
|
||||||
if (returnLinkableKeys) {
|
if (returnLinkableKeys) {
|
||||||
|
// Map internal table/column names to their respective external linkable keys
|
||||||
|
// eg: product.id = product_id, variant.id = variant_id
|
||||||
mappedCascadedEntitiesMap = mapObjectTo<
|
mappedCascadedEntitiesMap = mapObjectTo<
|
||||||
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
Record<Lowercase<keyof typeof LinkableKeys>, string[]>
|
||||||
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
>(cascadedEntitiesMap, entityNameToLinkableKeysMap, {
|
||||||
pick: returnLinkableKeys as string[],
|
pick: returnLinkableKeys,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1116,22 +1116,39 @@ export default class ProductModuleService<
|
|||||||
return await this.productService_.softDelete(productIds, sharedContext)
|
return await this.productService_.softDelete(productIds, sharedContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
async restore(
|
async restore<
|
||||||
|
TReturnableLinkableKeys extends string = Lowercase<
|
||||||
|
keyof typeof LinkableKeys
|
||||||
|
>
|
||||||
|
>(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
|
{ returnLinkableKeys }: RestoreReturn<TReturnableLinkableKeys> = {},
|
||||||
sharedContext: Context = {}
|
sharedContext: Context = {}
|
||||||
): Promise<ProductTypes.ProductDTO[]> {
|
): Promise<Record<Lowercase<keyof typeof LinkableKeys>, string[]> | void> {
|
||||||
const products = await this.restore_(productIds, sharedContext)
|
const [_, cascadedEntitiesMap] = await this.restore_(
|
||||||
|
productIds,
|
||||||
|
sharedContext
|
||||||
|
)
|
||||||
|
|
||||||
return this.baseRepository_.serialize<ProductTypes.ProductDTO[]>(products, {
|
let mappedCascadedEntitiesMap
|
||||||
populate: true,
|
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_")
|
@InjectTransactionManager(shouldForceTransaction, "baseRepository_")
|
||||||
async restore_(
|
async restore_(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TProduct[]> {
|
): Promise<[TProduct[], Record<string, unknown[]>]> {
|
||||||
return await this.productService_.restore(productIds, sharedContext)
|
return await this.productService_.restore(productIds, sharedContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Product } from "@models"
|
|
||||||
import {
|
import {
|
||||||
Context,
|
Context,
|
||||||
DAL,
|
DAL,
|
||||||
@@ -9,12 +8,13 @@ import {
|
|||||||
import {
|
import {
|
||||||
InjectManager,
|
InjectManager,
|
||||||
InjectTransactionManager,
|
InjectTransactionManager,
|
||||||
isDefined,
|
|
||||||
MedusaContext,
|
MedusaContext,
|
||||||
MedusaError,
|
MedusaError,
|
||||||
ModulesSdkUtils,
|
ModulesSdkUtils,
|
||||||
ProductUtils,
|
ProductUtils,
|
||||||
|
isDefined,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
|
import { Product } from "@models"
|
||||||
import { ProductRepository } from "@repositories"
|
import { ProductRepository } from "@repositories"
|
||||||
|
|
||||||
import { ProductServiceTypes } from "../types/services"
|
import { ProductServiceTypes } from "../types/services"
|
||||||
@@ -178,7 +178,7 @@ export default class ProductService<TEntity extends Product = Product> {
|
|||||||
async restore(
|
async restore(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
@MedusaContext() sharedContext: Context = {}
|
@MedusaContext() sharedContext: Context = {}
|
||||||
): Promise<TEntity[]> {
|
): Promise<[TEntity[], Record<string, unknown[]>]> {
|
||||||
return await this.productRepository_.restore(productIds, {
|
return await this.productRepository_.restore(productIds, {
|
||||||
transactionManager: sharedContext.transactionManager,
|
transactionManager: sharedContext.transactionManager,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.spec.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.spec.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -19,10 +19,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.8.8",
|
"@medusajs/types": "^1.8.8",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/modules-sdk": "^1.8.8",
|
"@medusajs/modules-sdk": "^1.8.8",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
|
import { ModuleJoinerConfig } from "@medusajs/types"
|
||||||
import { Modules } from "@medusajs/modules-sdk"
|
import { Modules } from "@medusajs/modules-sdk"
|
||||||
import { JoinerServiceConfig } from "@medusajs/types"
|
|
||||||
|
|
||||||
export const joinerConfig: JoinerServiceConfig = {
|
export const joinerConfig: ModuleJoinerConfig = {
|
||||||
serviceName: Modules.STOCK_LOCATION,
|
serviceName: Modules.STOCK_LOCATION,
|
||||||
primaryKeys: ["id"],
|
primaryKeys: ["id"],
|
||||||
|
linkableKeys: ["stock_location_id"],
|
||||||
alias: [
|
alias: [
|
||||||
{
|
{
|
||||||
name: "stock_location",
|
name: "stock_location",
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import {
|
|||||||
FilterableStockLocationProps,
|
FilterableStockLocationProps,
|
||||||
FindConfig,
|
FindConfig,
|
||||||
IEventBusService,
|
IEventBusService,
|
||||||
JoinerServiceConfig,
|
|
||||||
MODULE_RESOURCE_TYPE,
|
MODULE_RESOURCE_TYPE,
|
||||||
|
ModuleJoinerConfig,
|
||||||
SharedContext,
|
SharedContext,
|
||||||
StockLocationAddressInput,
|
StockLocationAddressInput,
|
||||||
UpdateStockLocationInput,
|
UpdateStockLocationInput,
|
||||||
@@ -50,7 +50,7 @@ export default class StockLocationService {
|
|||||||
this.eventBusService_ = eventBusService
|
this.eventBusService_ = eventBusService
|
||||||
}
|
}
|
||||||
|
|
||||||
__joinerConfig(): JoinerServiceConfig {
|
__joinerConfig(): ModuleJoinerConfig {
|
||||||
return joinerConfig
|
return joinerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
"ioredis": "^5.2.5",
|
"ioredis": "^5.2.5",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"typeorm": "^0.3.16",
|
"typeorm": "^0.3.16",
|
||||||
"typescript": "^4.4.4",
|
"typescript": "^5.1.6",
|
||||||
"winston": "^3.8.2"
|
"winston": "^3.8.2"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { FindOptions } from "./index"
|
|
||||||
import { RepositoryTransformOptions } from "../common"
|
import { RepositoryTransformOptions } from "../common"
|
||||||
import { Context } from "../shared-context"
|
import { Context } from "../shared-context"
|
||||||
|
import { FindOptions } from "./index"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Data access layer (DAL) interface to implements for any repository service.
|
* 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
|
context?: Context
|
||||||
): Promise<[T[], Record<string, unknown[]>]>
|
): 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>
|
export interface TreeRepositoryService<T = any>
|
||||||
@@ -75,3 +78,11 @@ export interface TreeRepositoryService<T = any>
|
|||||||
|
|
||||||
delete(id: string, context?: Context): Promise<void>
|
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 "./file-service"
|
||||||
export * from "./inventory"
|
export * from "./inventory"
|
||||||
export * from "./joiner"
|
export * from "./joiner"
|
||||||
|
export * from "./link-modules"
|
||||||
export * from "./logger"
|
export * from "./logger"
|
||||||
export * from "./modules-sdk"
|
export * from "./modules-sdk"
|
||||||
export * from "./pricing"
|
export * from "./pricing"
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import {
|
|||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
import { FindConfig } from "../common"
|
import { FindConfig } from "../common"
|
||||||
import { JoinerServiceConfig } from "../joiner"
|
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||||
import { SharedContext } from ".."
|
import { SharedContext } from "../shared-context"
|
||||||
|
|
||||||
export interface IInventoryService {
|
export interface IInventoryService {
|
||||||
__joinerConfig(): JoinerServiceConfig
|
__joinerConfig(): ModuleJoinerConfig
|
||||||
listInventoryItems(
|
listInventoryItems(
|
||||||
selector: FilterableInventoryItemProps,
|
selector: FilterableInventoryItemProps,
|
||||||
config?: FindConfig<InventoryItemDTO>,
|
config?: FindConfig<InventoryItemDTO>,
|
||||||
|
|||||||
@@ -28,7 +28,11 @@ export interface JoinerServiceConfig {
|
|||||||
export interface JoinerArgument {
|
export interface JoinerArgument {
|
||||||
name: string
|
name: string
|
||||||
value?: any
|
value?: any
|
||||||
field?: string
|
}
|
||||||
|
|
||||||
|
export interface JoinerDirective {
|
||||||
|
name: string
|
||||||
|
value?: any
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteJoinerQuery {
|
export interface RemoteJoinerQuery {
|
||||||
@@ -38,10 +42,11 @@ export interface RemoteJoinerQuery {
|
|||||||
property: string
|
property: string
|
||||||
fields: string[]
|
fields: string[]
|
||||||
args?: JoinerArgument[]
|
args?: JoinerArgument[]
|
||||||
relationships?: JoinerRelationship[]
|
directives?: { [field: string]: JoinerDirective[] }
|
||||||
}>
|
}>
|
||||||
fields: string[]
|
fields: string[]
|
||||||
args?: JoinerArgument[]
|
args?: JoinerArgument[]
|
||||||
|
directives?: { [field: string]: JoinerDirective[] }
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RemoteNestedExpands {
|
export interface RemoteNestedExpands {
|
||||||
@@ -54,6 +59,7 @@ export interface RemoteNestedExpands {
|
|||||||
|
|
||||||
export interface RemoteExpandProperty {
|
export interface RemoteExpandProperty {
|
||||||
property: string
|
property: string
|
||||||
|
parent: string
|
||||||
serviceConfig: JoinerServiceConfig
|
serviceConfig: JoinerServiceConfig
|
||||||
fields: string[]
|
fields: string[]
|
||||||
args?: JoinerArgument[]
|
args?: JoinerArgument[]
|
||||||
|
|||||||
@@ -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 { JoinerRelationship, JoinerServiceConfig } from "../joiner"
|
||||||
import { Logger } from "../logger"
|
|
||||||
import { MedusaContainer } from "../common"
|
import { MedusaContainer } from "../common"
|
||||||
import { RepositoryService } from "../dal"
|
import { RepositoryService } from "../dal"
|
||||||
|
import { Logger } from "../logger"
|
||||||
|
|
||||||
export type Constructor<T> = new (...args: any[]) => T
|
export type Constructor<T> = new (...args: any[]) => T
|
||||||
export * from "../common/medusa-container"
|
export * from "../common/medusa-container"
|
||||||
@@ -30,6 +31,9 @@ export type InternalModuleDeclaration = {
|
|||||||
scope: MODULE_SCOPE.INTERNAL
|
scope: MODULE_SCOPE.INTERNAL
|
||||||
resources: MODULE_RESOURCE_TYPE
|
resources: MODULE_RESOURCE_TYPE
|
||||||
dependencies?: string[]
|
dependencies?: string[]
|
||||||
|
/**
|
||||||
|
* @deprecated The property should not be used.
|
||||||
|
*/
|
||||||
resolve?: string
|
resolve?: string
|
||||||
options?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
alias?: string // If multiple modules are registered with the same key, the alias can be used to differentiate them
|
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
|
url: string
|
||||||
keepAlive: boolean
|
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
|
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
|
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
|
registrationName: string
|
||||||
defaultPackage: string | false
|
defaultPackage: string | false
|
||||||
label: string
|
label: string
|
||||||
|
/**
|
||||||
|
* @deprecated property will be removed in future versions
|
||||||
|
*/
|
||||||
canOverride?: boolean
|
canOverride?: boolean
|
||||||
|
/**
|
||||||
|
* @deprecated property will be removed in future versions
|
||||||
|
*/
|
||||||
isRequired?: boolean
|
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[]
|
dependencies?: string[]
|
||||||
defaultModuleDeclaration:
|
defaultModuleDeclaration:
|
||||||
| InternalModuleDeclaration
|
| InternalModuleDeclaration
|
||||||
| ExternalModuleDeclaration
|
| 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 & {
|
export type LoadedModule = unknown & {
|
||||||
__joinerConfig: JoinerServiceConfig
|
__joinerConfig: ModuleJoinerConfig
|
||||||
__definition: ModuleDefinition
|
__definition: ModuleDefinition
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,6 +117,60 @@ export type ModulesResponse = {
|
|||||||
resolution: string | false
|
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 = {
|
export type ModuleExports = {
|
||||||
service: Constructor<any>
|
service: Constructor<any>
|
||||||
loaders?: ModuleLoaderFunction[]
|
loaders?: ModuleLoaderFunction[]
|
||||||
@@ -114,6 +194,11 @@ export interface ModuleServiceInitializeOptions {
|
|||||||
connection?: any
|
connection?: any
|
||||||
clientUrl?: string
|
clientUrl?: string
|
||||||
schema?: string
|
schema?: string
|
||||||
|
host?: string
|
||||||
|
port?: number
|
||||||
|
user?: string
|
||||||
|
password?: string
|
||||||
|
database?: string
|
||||||
driverOptions?: Record<string, unknown>
|
driverOptions?: Record<string, unknown>
|
||||||
debug?: boolean
|
debug?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ import {
|
|||||||
UpdateProductTypeDTO,
|
UpdateProductTypeDTO,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
import { Context } from "../shared-context"
|
|
||||||
import { FindConfig } from "../common"
|
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 {
|
export interface IProductModuleService {
|
||||||
__joinerConfig(): JoinerServiceConfig
|
__joinerConfig(): ModuleJoinerConfig
|
||||||
|
|
||||||
retrieve(
|
retrieve(
|
||||||
productId: string,
|
productId: string,
|
||||||
@@ -241,9 +242,13 @@ export interface IProductModuleService {
|
|||||||
|
|
||||||
softDelete<TReturnableLinkableKeys extends string = string>(
|
softDelete<TReturnableLinkableKeys extends string = string>(
|
||||||
productIds: string[],
|
productIds: string[],
|
||||||
config?: { returnLinkableKeys?: TReturnableLinkableKeys[] },
|
config?: SoftDeleteReturn<TReturnableLinkableKeys>,
|
||||||
sharedContext?: Context
|
sharedContext?: Context
|
||||||
): Promise<Record<string, string[]> | void>
|
): 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 {
|
import {
|
||||||
CreateStockLocationInput,
|
CreateStockLocationInput,
|
||||||
FilterableStockLocationProps,
|
FilterableStockLocationProps,
|
||||||
@@ -8,8 +5,12 @@ import {
|
|||||||
UpdateStockLocationInput,
|
UpdateStockLocationInput,
|
||||||
} from "./common"
|
} from "./common"
|
||||||
|
|
||||||
|
import { FindConfig } from "../common/common"
|
||||||
|
import { ModuleJoinerConfig } from "../modules-sdk"
|
||||||
|
import { SharedContext } from "../shared-context"
|
||||||
|
|
||||||
export interface IStockLocationService {
|
export interface IStockLocationService {
|
||||||
__joinerConfig(): JoinerServiceConfig
|
__joinerConfig(): ModuleJoinerConfig
|
||||||
list(
|
list(
|
||||||
selector: FilterableStockLocationProps,
|
selector: FilterableStockLocationProps,
|
||||||
config?: FindConfig<StockLocationDTO>,
|
config?: FindConfig<StockLocationDTO>,
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"awilix": "^8.0.1",
|
"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 { MedusaContext } from "../../decorators"
|
||||||
import { buildQuery, InjectTransactionManager } from "../../modules-sdk"
|
import { InjectTransactionManager, buildQuery } from "../../modules-sdk"
|
||||||
import {
|
import {
|
||||||
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
getSoftDeletedCascadedEntitiesIdsMappedBy,
|
||||||
transactionWrapper,
|
transactionWrapper,
|
||||||
@@ -68,11 +74,21 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
|||||||
|
|
||||||
@InjectTransactionManager()
|
@InjectTransactionManager()
|
||||||
async softDelete(
|
async softDelete(
|
||||||
ids: string[],
|
idsOrFilter: string[] | FilterQuery,
|
||||||
@MedusaContext()
|
@MedusaContext()
|
||||||
{ transactionManager: manager }: Context = {}
|
{ transactionManager: manager }: Context = {}
|
||||||
): Promise<[T[], Record<string, unknown[]>]> {
|
): 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()
|
const date = new Date()
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
await mikroOrmUpdateDeletedAtRecursively(manager, entities, date)
|
||||||
@@ -86,22 +102,34 @@ export abstract class MikroOrmAbstractBaseRepository<T = any>
|
|||||||
|
|
||||||
@InjectTransactionManager()
|
@InjectTransactionManager()
|
||||||
async restore(
|
async restore(
|
||||||
ids: string[],
|
idsOrFilter: string[] | FilterQuery,
|
||||||
@MedusaContext()
|
@MedusaContext()
|
||||||
{ transactionManager: manager }: Context = {}
|
{ transactionManager: manager }: Context = {}
|
||||||
): Promise<T[]> {
|
): Promise<[T[], Record<string, unknown[]>]> {
|
||||||
const query = buildQuery(
|
const isArray = Array.isArray(idsOrFilter)
|
||||||
{ id: { $in: ids } },
|
const filter =
|
||||||
{
|
isArray || isString(idsOrFilter)
|
||||||
withDeleted: true,
|
? {
|
||||||
}
|
id: {
|
||||||
)
|
$in: isArray ? idsOrFilter : [idsOrFilter],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: idsOrFilter
|
||||||
|
|
||||||
|
const query = buildQuery(filter, {
|
||||||
|
withDeleted: true,
|
||||||
|
})
|
||||||
|
|
||||||
const entities = await this.find(query)
|
const entities = await this.find(query)
|
||||||
|
|
||||||
await mikroOrmUpdateDeletedAtRecursively(manager, entities, null)
|
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
|
context?: Context
|
||||||
): Promise<[T[], Record<string, unknown[]>]>
|
): 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
|
abstract getFreshManager<TManager = unknown>(): TManager
|
||||||
|
|
||||||
|
|||||||
@@ -47,10 +47,12 @@ export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
|||||||
entities,
|
entities,
|
||||||
deletedEntitiesMap,
|
deletedEntitiesMap,
|
||||||
getEntityName,
|
getEntityName,
|
||||||
|
restored,
|
||||||
}: {
|
}: {
|
||||||
entities: any[]
|
entities: any[]
|
||||||
deletedEntitiesMap?: Map<string, any[]>
|
deletedEntitiesMap?: Map<string, any[]>
|
||||||
getEntityName?: (entity: any) => string
|
getEntityName?: (entity: any) => string
|
||||||
|
restored?: boolean
|
||||||
}): Record<string, any[]> {
|
}): Record<string, any[]> {
|
||||||
deletedEntitiesMap ??= new Map<string, any[]>()
|
deletedEntitiesMap ??= new Map<string, any[]>()
|
||||||
getEntityName ??= (entity) => entity.constructor.name
|
getEntityName ??= (entity) => entity.constructor.name
|
||||||
@@ -61,7 +63,7 @@ export function getSoftDeletedCascadedEntitiesIdsMappedBy({
|
|||||||
.get(entityName)
|
.get(entityName)
|
||||||
?.some((e) => e.id === entity.id)
|
?.some((e) => e.id === entity.id)
|
||||||
|
|
||||||
if (!entity.deleted_at || shouldSkip) {
|
if ((restored ? !!entity.deleted_at : !entity.deleted_at) || shouldSkip) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,15 +3,19 @@ import { loadDatabaseConfig } from "../load-module-database-config"
|
|||||||
describe("loadDatabaseConfig", function () {
|
describe("loadDatabaseConfig", function () {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete process.env.POSTGRES_URL
|
delete process.env.POSTGRES_URL
|
||||||
|
delete process.env.MEDUSA_POSTGRES_URL
|
||||||
delete process.env.PRODUCT_POSTGRES_URL
|
delete process.env.PRODUCT_POSTGRES_URL
|
||||||
})
|
})
|
||||||
|
|
||||||
it("should return the local configuration using the environment variable", function () {
|
it("should return the local configuration using the environment variable respecting their precedence", function () {
|
||||||
process.env.POSTGRES_URL = "postgres://localhost:5432/medusa"
|
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")
|
let config = loadDatabaseConfig("product")
|
||||||
|
|
||||||
expect(config).toEqual({
|
expect(config).toEqual({
|
||||||
clientUrl: process.env.POSTGRES_URL,
|
clientUrl: process.env.PRODUCT_POSTGRES_URL,
|
||||||
driverOptions: {
|
driverOptions: {
|
||||||
connection: {
|
connection: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
@@ -21,12 +25,25 @@ describe("loadDatabaseConfig", function () {
|
|||||||
schema: "",
|
schema: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
delete process.env.POSTGRES_URL
|
delete process.env.PRODUCT_POSTGRES_URL
|
||||||
process.env.PRODUCT_POSTGRES_URL = "postgres://localhost:5432/medusa"
|
|
||||||
config = loadDatabaseConfig("product")
|
config = loadDatabaseConfig("product")
|
||||||
|
|
||||||
expect(config).toEqual({
|
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: {
|
driverOptions: {
|
||||||
connection: {
|
connection: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
@@ -127,7 +144,7 @@ describe("loadDatabaseConfig", function () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
expect(error.message).toEqual(
|
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 { ModulesSdkTypes } from "@medusajs/types"
|
||||||
|
import { MedusaError } from "../common"
|
||||||
|
|
||||||
function getEnv(key: string, moduleName: string): string {
|
function getEnv(key: string, moduleName: string): string {
|
||||||
const value =
|
const value =
|
||||||
process.env[`${moduleName.toUpperCase()}_${key}`] ?? process.env[`${key}`]
|
process.env[`${moduleName.toUpperCase()}_${key}`] ??
|
||||||
|
process.env[`MEDUSA_${key}`] ??
|
||||||
|
process.env[`${key}`]
|
||||||
return value ?? ""
|
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
|
* 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.
|
* 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,
|
moduleName: string,
|
||||||
options?: ModulesSdkTypes.ModuleServiceInitializeOptions,
|
options?: ModulesSdkTypes.ModuleServiceInitializeOptions,
|
||||||
silent: boolean = false
|
silent: boolean = false
|
||||||
): ModulesSdkTypes.ModuleServiceInitializeOptions["database"] {
|
): Pick<
|
||||||
|
ModulesSdkTypes.ModuleServiceInitializeOptions["database"],
|
||||||
|
"clientUrl" | "schema" | "driverOptions" | "debug"
|
||||||
|
> {
|
||||||
const clientUrl = getEnv("POSTGRES_URL", moduleName)
|
const clientUrl = getEnv("POSTGRES_URL", moduleName)
|
||||||
|
|
||||||
const database = {
|
const database = {
|
||||||
clientUrl: getEnv("POSTGRES_URL", moduleName),
|
clientUrl,
|
||||||
schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
|
schema: getEnv("POSTGRES_SCHEMA", moduleName) ?? "public",
|
||||||
driverOptions: JSON.parse(
|
driverOptions: JSON.parse(
|
||||||
getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
|
getEnv("POSTGRES_DRIVER_OPTIONS", moduleName) ||
|
||||||
@@ -63,7 +78,7 @@ export function loadDatabaseConfig(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isModuleServiceInitializeOptions(options)) {
|
if (isModuleServiceInitializeOptions(options)) {
|
||||||
database.clientUrl = options.database!.clientUrl ?? database.clientUrl
|
database.clientUrl = getDatabaseUrl(options)
|
||||||
database.schema = options.database!.schema ?? database.schema
|
database.schema = options.database!.schema ?? database.schema
|
||||||
database.driverOptions =
|
database.driverOptions =
|
||||||
options.database!.driverOptions ??
|
options.database!.driverOptions ??
|
||||||
@@ -74,7 +89,7 @@ export function loadDatabaseConfig(
|
|||||||
if (!database.clientUrl && !silent) {
|
if (!database.clientUrl && !silent) {
|
||||||
throw new MedusaError(
|
throw new MedusaError(
|
||||||
MedusaError.Types.INVALID_ARGUMENT,
|
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 = !!(
|
const shouldSwallowError = !!(
|
||||||
options as ModulesSdkTypes.ModuleServiceInitializeOptions
|
options as ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||||
)?.database?.connection
|
)?.database?.connection
|
||||||
|
|
||||||
dbConfig = {
|
dbConfig = {
|
||||||
...loadDatabaseConfig(
|
...loadDatabaseConfig(
|
||||||
"product",
|
"product",
|
||||||
@@ -97,7 +96,7 @@ async function loadShared({ container, entities }) {
|
|||||||
)
|
)
|
||||||
if (!sharedConnection) {
|
if (!sharedConnection) {
|
||||||
throw new Error(
|
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 = {
|
module.exports = {
|
||||||
globals: {
|
|
||||||
"ts-jest": {
|
|
||||||
tsConfig: "tsconfig.json",
|
|
||||||
isolatedModules: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s?$": "ts-jest",
|
"^.+\\.[jt]s?$": [
|
||||||
|
"ts-jest",
|
||||||
|
{
|
||||||
|
tsConfig: "tsconfig.json",
|
||||||
|
isolatedModules: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
|||||||
@@ -20,10 +20,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.10.2",
|
"@medusajs/types": "^1.10.2",
|
||||||
"cross-env": "^5.2.1",
|
"cross-env": "^5.2.1",
|
||||||
"jest": "^25.5.4",
|
"jest": "^29.6.3",
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-jest": "^25.5.1",
|
"ts-jest": "^29.1.1",
|
||||||
"typescript": "^4.4.4"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/modules-sdk": "^1.9.2",
|
"@medusajs/modules-sdk": "^1.9.2",
|
||||||
|
|||||||
@@ -6196,10 +6196,10 @@ __metadata:
|
|||||||
"@medusajs/modules-sdk": ^1.8.8
|
"@medusajs/modules-sdk": ^1.8.8
|
||||||
"@medusajs/types": ^1.8.8
|
"@medusajs/types": ^1.8.8
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6212,10 +6212,10 @@ __metadata:
|
|||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
ioredis: ^5.3.1
|
ioredis: ^5.3.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6241,10 +6241,10 @@ __metadata:
|
|||||||
"@medusajs/types": ^1.8.10
|
"@medusajs/types": ^1.8.10
|
||||||
"@medusajs/utils": ^1.9.2
|
"@medusajs/utils": ^1.9.2
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.2
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
ulid: ^2.3.0
|
ulid: ^2.3.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@@ -6260,11 +6260,11 @@ __metadata:
|
|||||||
bullmq: ^3.5.6
|
bullmq: ^3.5.6
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
ioredis: ^5.2.5
|
ioredis: ^5.2.5
|
||||||
jest: ^25.5.2
|
jest: ^29.6.3
|
||||||
medusa-test-utils: ^1.1.40
|
medusa-test-utils: ^1.1.40
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6310,11 +6310,32 @@ __metadata:
|
|||||||
"@medusajs/utils": ^1.9.1
|
"@medusajs/utils": ^1.9.1
|
||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typeorm: ^0.3.16
|
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
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6496,11 +6517,11 @@ __metadata:
|
|||||||
"@medusajs/utils": ^1.9.6
|
"@medusajs/utils": ^1.9.6
|
||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
resolve-cwd: ^3.0.0
|
resolve-cwd: ^3.0.0
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6558,10 +6579,10 @@ __metadata:
|
|||||||
"@medusajs/utils": ^1.9.6
|
"@medusajs/utils": ^1.9.6
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
graphql: ^16.6.0
|
graphql: ^16.6.0
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6609,16 +6630,16 @@ __metadata:
|
|||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
dotenv: ^16.1.4
|
dotenv: ^16.1.4
|
||||||
faker: ^6.6.6
|
faker: ^6.6.6
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
knex: 2.4.2
|
knex: 2.4.2
|
||||||
lodash: ^4.17.21
|
lodash: ^4.17.21
|
||||||
medusa-test-utils: ^1.1.40
|
medusa-test-utils: ^1.1.40
|
||||||
pg-god: ^1.0.12
|
pg-god: ^1.0.12
|
||||||
rimraf: ^3.0.2
|
rimraf: ^3.0.2
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
tsc-alias: ^1.8.6
|
tsc-alias: ^1.8.6
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
bin:
|
bin:
|
||||||
medusa-product-migrations-down: dist/scripts/bin/run-migration-down.js
|
medusa-product-migrations-down: dist/scripts/bin/run-migration-down.js
|
||||||
medusa-product-migrations-up: dist/scripts/bin/run-migration-up.js
|
medusa-product-migrations-up: dist/scripts/bin/run-migration-up.js
|
||||||
@@ -6635,11 +6656,11 @@ __metadata:
|
|||||||
"@medusajs/utils": ^1.9.1
|
"@medusajs/utils": ^1.9.1
|
||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typeorm: ^0.3.16
|
typeorm: ^0.3.16
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -6652,7 +6673,7 @@ __metadata:
|
|||||||
ioredis: ^5.2.5
|
ioredis: ^5.2.5
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
typeorm: ^0.3.16
|
typeorm: ^0.3.16
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
winston: ^3.8.2
|
winston: ^3.8.2
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@@ -6763,10 +6784,10 @@ __metadata:
|
|||||||
awilix: ^8.0.1
|
awilix: ^8.0.1
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
express: ^4.18.2
|
express: ^4.18.2
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
ulid: ^2.3.0
|
ulid: ^2.3.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@@ -6781,10 +6802,10 @@ __metadata:
|
|||||||
"@medusajs/utils": ^1.9.6
|
"@medusajs/utils": ^1.9.6
|
||||||
awilix: ^8.0.1
|
awilix: ^8.0.1
|
||||||
cross-env: ^5.2.1
|
cross-env: ^5.2.1
|
||||||
jest: ^25.5.4
|
jest: ^29.6.3
|
||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-jest: ^25.5.1
|
ts-jest: ^29.1.1
|
||||||
typescript: ^4.4.4
|
typescript: ^5.1.6
|
||||||
ulid: ^2.3.0
|
ulid: ^2.3.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
@@ -29262,7 +29283,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
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
|
version: 25.5.4
|
||||||
resolution: "jest@npm:25.5.4"
|
resolution: "jest@npm:25.5.4"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
Reference in new issue
Block a user