breaking: rework how links database migrations are managed (#8162)
This commit is contained in:
Vendored
-2
@@ -1,2 +0,0 @@
|
|||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,16 @@
|
|||||||
import { initialize, runMigrations } from "@medusajs/link-modules"
|
import { getMigrationPlanner, initialize } from "@medusajs/link-modules"
|
||||||
import { MedusaModule, ModuleJoinerConfig } from "@medusajs/modules-sdk"
|
import { MedusaModule, ModuleJoinerConfig } from "@medusajs/modules-sdk"
|
||||||
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
import { medusaIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
|
||||||
jest.setTimeout(5000000)
|
jest.setTimeout(5000000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ getContainer, dbConfig: { clientUrl } }) => {
|
testSuite: ({ dbConfig: { clientUrl } }) => {
|
||||||
let DB_URL
|
let DB_URL
|
||||||
let container
|
|
||||||
let links
|
let links
|
||||||
|
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
DB_URL = clientUrl
|
DB_URL = clientUrl
|
||||||
container = getContainer()
|
|
||||||
|
|
||||||
const linkDefinition: ModuleJoinerConfig[] = [
|
const linkDefinition: ModuleJoinerConfig[] = [
|
||||||
{
|
{
|
||||||
@@ -73,7 +71,8 @@ medusaIntegrationTestRunner({
|
|||||||
]
|
]
|
||||||
}) as any)
|
}) as any)
|
||||||
|
|
||||||
await runMigrations({ options: dbConfig }, linkDefinition)
|
const planner = getMigrationPlanner(dbConfig, linkDefinition)
|
||||||
|
await planner.executePlan(await planner.createPlan())
|
||||||
links = await initialize(dbConfig, linkDefinition)
|
links = await initialize(dbConfig, linkDefinition)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
try {
|
try {
|
||||||
require('ts-node').register({})
|
require("ts-node").register({})
|
||||||
} catch {}
|
} catch {}
|
||||||
require("dotenv").config()
|
require("dotenv").config()
|
||||||
require("./dist/index.js")
|
require("./dist/index.js")
|
||||||
|
|||||||
@@ -190,6 +190,23 @@ function buildLocalCommands(cli, isLocalProject) {
|
|||||||
})
|
})
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
|
.command({
|
||||||
|
command: `links [action]`,
|
||||||
|
desc: `Manage migrations for the links from the core, your project and packages`,
|
||||||
|
builder: {
|
||||||
|
action: {
|
||||||
|
demand: true,
|
||||||
|
description: "The action to perform on links",
|
||||||
|
choices: ["sync"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
handler: handlerP(
|
||||||
|
getCommandHandler(`links`, (args, cmd) => {
|
||||||
|
process.env.NODE_ENV = process.env.NODE_ENV || `development`
|
||||||
|
return cmd(args)
|
||||||
|
})
|
||||||
|
),
|
||||||
|
})
|
||||||
.command({
|
.command({
|
||||||
command: `develop`,
|
command: `develop`,
|
||||||
desc: `Start development server. Watches file and rebuilds when something changes`,
|
desc: `Start development server. Watches file and rebuilds when something changes`,
|
||||||
|
|||||||
@@ -40,8 +40,25 @@ export async function initDb({
|
|||||||
try {
|
try {
|
||||||
const {
|
const {
|
||||||
runMedusaAppMigrations,
|
runMedusaAppMigrations,
|
||||||
|
getLinksExecutionPlanner,
|
||||||
} = require("@medusajs/medusa/dist/loaders/medusa-app")
|
} = require("@medusajs/medusa/dist/loaders/medusa-app")
|
||||||
await runMedusaAppMigrations({ configModule, container })
|
await runMedusaAppMigrations({ configModule, container })
|
||||||
|
const planner = await getLinksExecutionPlanner({
|
||||||
|
configModule,
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
|
||||||
|
const actionPlan = await planner.createPlan()
|
||||||
|
await planner.executePlan(actionPlan)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* cleanup temporary created resources for the migrations
|
||||||
|
* @internal I didnt find a god place to put that, should we eventually add a close function
|
||||||
|
* to the planner to handle that part? so that you would do planner.close() and it will handle the cleanup
|
||||||
|
* automatically just like we usually do for the classic migrations actions
|
||||||
|
*/
|
||||||
|
const { MedusaModule } = require("@medusajs/modules-sdk")
|
||||||
|
MedusaModule.clearInstances()
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Something went wrong while running the migrations")
|
console.error("Something went wrong while running the migrations")
|
||||||
throw err
|
throw err
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { mergeTypeDefs } from "@graphql-tools/merge"
|
import { mergeTypeDefs } from "@graphql-tools/merge"
|
||||||
import { makeExecutableSchema } from "@graphql-tools/schema"
|
import { makeExecutableSchema } from "@graphql-tools/schema"
|
||||||
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
import { RemoteFetchDataCallback } from "@medusajs/orchestration"
|
||||||
import type {
|
import {
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
ExternalModuleDeclaration,
|
ExternalModuleDeclaration,
|
||||||
|
ILinkMigrationsPlanner,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
LoadedModule,
|
LoadedModule,
|
||||||
Logger,
|
Logger,
|
||||||
@@ -38,8 +39,8 @@ import {
|
|||||||
} from "./medusa-module"
|
} from "./medusa-module"
|
||||||
import { RemoteLink } from "./remote-link"
|
import { RemoteLink } from "./remote-link"
|
||||||
import { RemoteQuery } from "./remote-query"
|
import { RemoteQuery } from "./remote-query"
|
||||||
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
|
|
||||||
import { cleanGraphQLSchema } from "./utils"
|
import { cleanGraphQLSchema } from "./utils"
|
||||||
|
import { MODULE_RESOURCE_TYPE, MODULE_SCOPE } from "./types"
|
||||||
|
|
||||||
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
|
const LinkModulePackage = MODULE_PACKAGE_NAMES[Modules.LINK]
|
||||||
|
|
||||||
@@ -56,6 +57,7 @@ declare module "@medusajs/types" {
|
|||||||
export type RunMigrationFn = () => Promise<void>
|
export type RunMigrationFn = () => Promise<void>
|
||||||
export type RevertMigrationFn = (moduleNames: string[]) => Promise<void>
|
export type RevertMigrationFn = (moduleNames: string[]) => Promise<void>
|
||||||
export type GenerateMigrations = (moduleNames: string[]) => Promise<void>
|
export type GenerateMigrations = (moduleNames: string[]) => Promise<void>
|
||||||
|
export type GetLinkExecutionPlanner = () => ILinkMigrationsPlanner
|
||||||
|
|
||||||
export type MedusaModuleConfig = {
|
export type MedusaModuleConfig = {
|
||||||
[key: string | Modules]:
|
[key: string | Modules]:
|
||||||
@@ -164,7 +166,7 @@ async function initializeLinks({
|
|||||||
moduleExports,
|
moduleExports,
|
||||||
}) {
|
}) {
|
||||||
try {
|
try {
|
||||||
const { initialize, runMigrations, revertMigrations } =
|
const { initialize, getMigrationPlanner } =
|
||||||
moduleExports ?? (await import(LinkModulePackage))
|
moduleExports ?? (await import(LinkModulePackage))
|
||||||
|
|
||||||
const linkResolution = await initialize(
|
const linkResolution = await initialize(
|
||||||
@@ -176,16 +178,14 @@ async function initializeLinks({
|
|||||||
return {
|
return {
|
||||||
remoteLink: new RemoteLink(),
|
remoteLink: new RemoteLink(),
|
||||||
linkResolution,
|
linkResolution,
|
||||||
runMigrations,
|
getMigrationPlanner,
|
||||||
revertMigrations,
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.warn("Error initializing link modules.", err)
|
console.warn("Error initializing link modules.", err)
|
||||||
return {
|
return {
|
||||||
remoteLink: undefined,
|
remoteLink: undefined,
|
||||||
linkResolution: undefined,
|
linkResolution: undefined,
|
||||||
runMigrations: () => void 0,
|
getMigrationPlanner: () => void 0,
|
||||||
revertMigrations: () => void 0,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -231,6 +231,7 @@ export type MedusaAppOutput = {
|
|||||||
runMigrations: RunMigrationFn
|
runMigrations: RunMigrationFn
|
||||||
revertMigrations: RevertMigrationFn
|
revertMigrations: RevertMigrationFn
|
||||||
generateMigrations: GenerateMigrations
|
generateMigrations: GenerateMigrations
|
||||||
|
linkMigrationExecutionPlanner: GetLinkExecutionPlanner
|
||||||
onApplicationShutdown: () => Promise<void>
|
onApplicationShutdown: () => Promise<void>
|
||||||
onApplicationPrepareShutdown: () => Promise<void>
|
onApplicationPrepareShutdown: () => Promise<void>
|
||||||
onApplicationStart: () => Promise<void>
|
onApplicationStart: () => Promise<void>
|
||||||
@@ -369,6 +370,11 @@ async function MedusaApp_({
|
|||||||
generateMigrations: async () => {
|
generateMigrations: async () => {
|
||||||
throw new Error("Generate migrations not allowed in loaderOnly mode")
|
throw new Error("Generate migrations not allowed in loaderOnly mode")
|
||||||
},
|
},
|
||||||
|
linkMigrationExecutionPlanner: () => {
|
||||||
|
throw new Error(
|
||||||
|
"Migrations planner is not avaibable in loaderOnly mode"
|
||||||
|
)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,11 +398,7 @@ async function MedusaApp_({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const { remoteLink, getMigrationPlanner } = await initializeLinks({
|
||||||
remoteLink,
|
|
||||||
runMigrations: linkModuleMigration,
|
|
||||||
revertMigrations: revertLinkModuleMigration,
|
|
||||||
} = await initializeLinks({
|
|
||||||
config: linkModuleOrOptions,
|
config: linkModuleOrOptions,
|
||||||
linkModules,
|
linkModules,
|
||||||
injectedDependencies,
|
injectedDependencies,
|
||||||
@@ -479,7 +481,27 @@ async function MedusaApp_({
|
|||||||
await applyMigration({
|
await applyMigration({
|
||||||
modulesNames: Object.keys(allModules),
|
modulesNames: Object.keys(allModules),
|
||||||
})
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const revertMigrations: RevertMigrationFn = async (
|
||||||
|
modulesNames
|
||||||
|
): Promise<void> => {
|
||||||
|
await applyMigration({
|
||||||
|
modulesNames,
|
||||||
|
action: "revert",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const generateMigrations: GenerateMigrations = async (
|
||||||
|
modulesNames
|
||||||
|
): Promise<void> => {
|
||||||
|
await applyMigration({
|
||||||
|
modulesNames,
|
||||||
|
action: "generate",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const getMigrationPlannerFn = () => {
|
||||||
const options: Partial<ModuleServiceInitializeOptions> =
|
const options: Partial<ModuleServiceInitializeOptions> =
|
||||||
"scope" in linkModuleOrOptions
|
"scope" in linkModuleOrOptions
|
||||||
? { ...linkModuleOrOptions.options }
|
? { ...linkModuleOrOptions.options }
|
||||||
@@ -491,52 +513,7 @@ async function MedusaApp_({
|
|||||||
...sharedResourcesConfig?.database,
|
...sharedResourcesConfig?.database,
|
||||||
}
|
}
|
||||||
|
|
||||||
await linkModuleMigration(
|
return getMigrationPlanner(options, linkModules)
|
||||||
{
|
|
||||||
options,
|
|
||||||
injectedDependencies,
|
|
||||||
},
|
|
||||||
linkModules
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const revertMigrations: RevertMigrationFn = async (
|
|
||||||
modulesNames
|
|
||||||
): Promise<void> => {
|
|
||||||
await applyMigration({
|
|
||||||
modulesNames,
|
|
||||||
action: "revert",
|
|
||||||
})
|
|
||||||
|
|
||||||
// TODO: Temporarely disabling this part until we discussed a more appropriate approach to sync the link
|
|
||||||
// Currently it would revert all link as soon as the revert is run
|
|
||||||
/*const options: Partial<ModuleServiceInitializeOptions> =
|
|
||||||
"scope" in linkModuleOrOptions
|
|
||||||
? { ...linkModuleOrOptions.options }
|
|
||||||
: {
|
|
||||||
...(linkModuleOrOptions as Partial<ModuleServiceInitializeOptions>),
|
|
||||||
}
|
|
||||||
|
|
||||||
options.database ??= {
|
|
||||||
...sharedResourcesConfig?.database,
|
|
||||||
}
|
|
||||||
|
|
||||||
await revertLinkModuleMigration(
|
|
||||||
{
|
|
||||||
options,
|
|
||||||
injectedDependencies,
|
|
||||||
},
|
|
||||||
linkModules
|
|
||||||
)*/
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateMigrations: GenerateMigrations = async (
|
|
||||||
modulesNames
|
|
||||||
): Promise<void> => {
|
|
||||||
await applyMigration({
|
|
||||||
modulesNames,
|
|
||||||
action: "generate",
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -551,6 +528,7 @@ async function MedusaApp_({
|
|||||||
runMigrations,
|
runMigrations,
|
||||||
revertMigrations,
|
revertMigrations,
|
||||||
generateMigrations,
|
generateMigrations,
|
||||||
|
linkMigrationExecutionPlanner: getMigrationPlannerFn,
|
||||||
sharedContainer: sharedContainer_,
|
sharedContainer: sharedContainer_,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -601,3 +579,16 @@ export async function MedusaAppMigrateGenerate(
|
|||||||
|
|
||||||
await generateMigrations(moduleNames).finally(MedusaModule.clearInstances)
|
await generateMigrations(moduleNames).finally(MedusaModule.clearInstances)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function MedusaAppGetLinksExecutionPlanner(
|
||||||
|
options: MedusaAppOptions = {}
|
||||||
|
): Promise<ILinkMigrationsPlanner> {
|
||||||
|
const migrationOnly = true
|
||||||
|
|
||||||
|
const { linkMigrationExecutionPlanner } = await MedusaApp_({
|
||||||
|
...options,
|
||||||
|
migrationOnly,
|
||||||
|
})
|
||||||
|
|
||||||
|
return linkMigrationExecutionPlanner()
|
||||||
|
}
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ class MedusaModule {
|
|||||||
MedusaModule.modules_.clear()
|
MedusaModule.modules_.clear()
|
||||||
MedusaModule.joinerConfig_.clear()
|
MedusaModule.joinerConfig_.clear()
|
||||||
MedusaModule.moduleResolutions_.clear()
|
MedusaModule.moduleResolutions_.clear()
|
||||||
|
MedusaModule.customLinks_.length = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public static isInstalled(moduleKey: string, alias?: string): boolean {
|
public static isInstalled(moduleKey: string, alias?: string): boolean {
|
||||||
|
|||||||
@@ -1,47 +1,2 @@
|
|||||||
import { FindConfig } from "../common"
|
export * from "./service"
|
||||||
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
export * from "./migrations"
|
||||||
import { IModuleService } from "../modules-sdk"
|
|
||||||
import { Context } from "../shared-context"
|
|
||||||
|
|
||||||
export interface ILinkModule extends IModuleService {
|
|
||||||
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>
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
/**
|
||||||
|
* Link descriptor containing metadata about the link's
|
||||||
|
* modules and models.
|
||||||
|
*/
|
||||||
|
export type PlannerActionLinkDescriptor = {
|
||||||
|
fromModule: string
|
||||||
|
toModule: string
|
||||||
|
fromModel: string
|
||||||
|
toModel: string
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list of actions prepared and executed by
|
||||||
|
* the "ILinkMigrationsPlanner".
|
||||||
|
*/
|
||||||
|
export type LinkMigrationsPlannerAction =
|
||||||
|
| {
|
||||||
|
action: "create" | "update" | "notify"
|
||||||
|
linkDescriptor: PlannerActionLinkDescriptor
|
||||||
|
sql: string
|
||||||
|
tableName: string
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "noop"
|
||||||
|
tableName: string
|
||||||
|
linkDescriptor: PlannerActionLinkDescriptor
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
action: "delete"
|
||||||
|
linkDescriptor: PlannerActionLinkDescriptor
|
||||||
|
tableName: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILinkMigrationsPlanner {
|
||||||
|
createPlan(): Promise<LinkMigrationsPlannerAction[]>
|
||||||
|
executePlan(actions: LinkMigrationsPlannerAction[]): Promise<void>
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { FindConfig } from "../common"
|
||||||
|
import { RestoreReturn, SoftDeleteReturn } from "../dal"
|
||||||
|
import { IModuleService } from "../modules-sdk"
|
||||||
|
import { Context } from "../shared-context"
|
||||||
|
|
||||||
|
export interface ILinkModule extends IModuleService {
|
||||||
|
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>
|
||||||
|
}
|
||||||
@@ -34,7 +34,7 @@ type ExtraOptions = {
|
|||||||
[key: string]: string
|
[key: string]: string
|
||||||
}
|
}
|
||||||
database?: {
|
database?: {
|
||||||
table: string
|
table?: string
|
||||||
idPrefix?: string
|
idPrefix?: string
|
||||||
extraColumns?: LinkModulesExtraFields
|
extraColumns?: LinkModulesExtraFields
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ export type ModelsConfigTemplate = { [key: string]: ModelDTOConfig }
|
|||||||
|
|
||||||
export type ModelConfigurationsToConfigTemplate<T extends ModelEntries> = {
|
export type ModelConfigurationsToConfigTemplate<T extends ModelEntries> = {
|
||||||
[Key in keyof T]: {
|
[Key in keyof T]: {
|
||||||
dto: T[Key] extends IDmlEntity<any, any>
|
dto: T[Key] extends DmlEntity<any, any>
|
||||||
? InferEntityType<T[Key]>
|
? InferEntityType<T[Key]>
|
||||||
: T[Key] extends Constructor<any>
|
: T[Key] extends Constructor<any>
|
||||||
? InstanceType<T[Key]>
|
? InstanceType<T[Key]>
|
||||||
@@ -261,7 +261,8 @@ type InferModelFromConfig<T> = {
|
|||||||
: never
|
: never
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MedusaServiceReturnType<ModelsConfig extends Record<any, any>> = {
|
export type MedusaServiceReturnType<ModelsConfig extends Record<string, any>> =
|
||||||
new (...args: any[]): AbstractModuleService<ModelsConfig>
|
{
|
||||||
$modelObjects: InferModelFromConfig<ModelsConfig>
|
new (...args: any[]): AbstractModuleService<ModelsConfig>
|
||||||
}
|
$modelObjects: InferModelFromConfig<ModelsConfig>
|
||||||
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"test": "jest --silent --bail --maxWorkers=50% --forceExit"
|
"test": "jest --silent --bail --maxWorkers=50% --forceExit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@inquirer/checkbox": "^2.3.11",
|
||||||
"@medusajs/admin-sdk": "0.0.1",
|
"@medusajs/admin-sdk": "0.0.1",
|
||||||
"@medusajs/core-flows": "^0.0.9",
|
"@medusajs/core-flows": "^0.0.9",
|
||||||
"@medusajs/link-modules": "^0.2.11",
|
"@medusajs/link-modules": "^0.2.11",
|
||||||
@@ -55,6 +56,7 @@
|
|||||||
"awilix": "^8.0.0",
|
"awilix": "^8.0.0",
|
||||||
"body-parser": "^1.19.0",
|
"body-parser": "^1.19.0",
|
||||||
"boxen": "^5.0.1",
|
"boxen": "^5.0.1",
|
||||||
|
"chalk": "^4.0.0",
|
||||||
"chokidar": "^3.4.2",
|
"chokidar": "^3.4.2",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.7.4",
|
||||||
"connect-redis": "^5.0.0",
|
"connect-redis": "^5.0.0",
|
||||||
|
|||||||
@@ -0,0 +1,176 @@
|
|||||||
|
import boxen from "boxen"
|
||||||
|
import chalk from "chalk"
|
||||||
|
import checkbox from "@inquirer/checkbox"
|
||||||
|
|
||||||
|
import Logger from "../loaders/logger"
|
||||||
|
import { initializeContainer } from "../loaders"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
import { getResolvedPlugins } from "../loaders/helpers/resolve-plugins"
|
||||||
|
import { resolvePluginsLinks } from "../loaders/helpers/resolve-plugins-links"
|
||||||
|
import { getLinksExecutionPlanner } from "../loaders/medusa-app"
|
||||||
|
import { LinkMigrationsPlannerAction } from "@medusajs/types"
|
||||||
|
|
||||||
|
type Action = "sync"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Groups action tables by their "action" property
|
||||||
|
* @param actionPlan LinkMigrationsPlannerAction
|
||||||
|
*/
|
||||||
|
function groupByActionPlan(actionPlan: LinkMigrationsPlannerAction[]) {
|
||||||
|
return actionPlan.reduce((acc, action) => {
|
||||||
|
acc[action.action] ??= []
|
||||||
|
acc[action.action].push(action)
|
||||||
|
return acc
|
||||||
|
}, {} as Record<"noop" | "notify" | "create" | "update" | "delete", LinkMigrationsPlannerAction[]>)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the link description for printing it to the
|
||||||
|
* console
|
||||||
|
*
|
||||||
|
* @param action: LinkMigrationsPlannerAction
|
||||||
|
*/
|
||||||
|
function buildLinkDescription(action: LinkMigrationsPlannerAction) {
|
||||||
|
const { linkDescriptor } = action
|
||||||
|
const from = chalk.yellow(
|
||||||
|
`${linkDescriptor.fromModule}.${linkDescriptor.fromModel}`
|
||||||
|
)
|
||||||
|
const to = chalk.yellow(
|
||||||
|
`${linkDescriptor.toModule}.${linkDescriptor.toModel}`
|
||||||
|
)
|
||||||
|
const table = chalk.dim(`(${action.tableName})`)
|
||||||
|
|
||||||
|
return `${from} <> ${to} ${table}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logs the actions of a given action type with a nice border and
|
||||||
|
* a title
|
||||||
|
*/
|
||||||
|
function logActions(
|
||||||
|
title: string,
|
||||||
|
actionsOrContext: LinkMigrationsPlannerAction[]
|
||||||
|
) {
|
||||||
|
const actionsList = actionsOrContext
|
||||||
|
.map((action) => ` - ${buildLinkDescription(action)}`)
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
|
console.log(boxen(`${title}\n${actionsList}`, { padding: 1 }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays a prompt to select tables that must be impacted with
|
||||||
|
* action
|
||||||
|
*/
|
||||||
|
async function askForLinkActionsToPerform(
|
||||||
|
message: string,
|
||||||
|
actions: LinkMigrationsPlannerAction[]
|
||||||
|
) {
|
||||||
|
console.log(boxen(message, { borderColor: "red", padding: 1 }))
|
||||||
|
|
||||||
|
return await checkbox({
|
||||||
|
message: "Select tables to act upon",
|
||||||
|
instructions: chalk.dim(
|
||||||
|
" <space> select, <a> select all, <i> inverse, <enter> submit"
|
||||||
|
),
|
||||||
|
choices: actions.map((action) => {
|
||||||
|
return {
|
||||||
|
name: buildLinkDescription(action),
|
||||||
|
value: action,
|
||||||
|
checked: false,
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const main = async function ({ directory }) {
|
||||||
|
const args = process.argv
|
||||||
|
args.shift()
|
||||||
|
args.shift()
|
||||||
|
args.shift()
|
||||||
|
|
||||||
|
const action = args[0] as Action
|
||||||
|
|
||||||
|
if (action !== "sync") {
|
||||||
|
return process.exit()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const container = await initializeContainer(directory)
|
||||||
|
|
||||||
|
const configModule = container.resolve(
|
||||||
|
ContainerRegistrationKeys.CONFIG_MODULE
|
||||||
|
)
|
||||||
|
|
||||||
|
const plugins = getResolvedPlugins(directory, configModule, true) || []
|
||||||
|
const pluginLinks = await resolvePluginsLinks(plugins, container)
|
||||||
|
|
||||||
|
const planner = await getLinksExecutionPlanner({
|
||||||
|
configModule,
|
||||||
|
linkModules: pluginLinks,
|
||||||
|
container,
|
||||||
|
})
|
||||||
|
|
||||||
|
Logger.info("Syncing links...")
|
||||||
|
|
||||||
|
const actionPlan = await planner.createPlan()
|
||||||
|
const groupActionPlan = groupByActionPlan(actionPlan)
|
||||||
|
|
||||||
|
if (groupActionPlan.delete?.length) {
|
||||||
|
groupActionPlan.delete = await askForLinkActionsToPerform(
|
||||||
|
`Select the tables to ${chalk.red(
|
||||||
|
"DELETE"
|
||||||
|
)}. The following links have been removed`,
|
||||||
|
groupActionPlan.delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (groupActionPlan.notify?.length) {
|
||||||
|
const answer = await askForLinkActionsToPerform(
|
||||||
|
`Select the tables to ${chalk.red(
|
||||||
|
"UPDATE"
|
||||||
|
)}. The following links have been updated`,
|
||||||
|
groupActionPlan.notify
|
||||||
|
)
|
||||||
|
|
||||||
|
groupActionPlan.update ??= []
|
||||||
|
groupActionPlan.update.push(
|
||||||
|
...answer.map((action) => {
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
action: "update",
|
||||||
|
} as LinkMigrationsPlannerAction
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const toCreate = groupActionPlan.create ?? []
|
||||||
|
const toUpdate = groupActionPlan.update ?? []
|
||||||
|
const toDelete = groupActionPlan.delete ?? []
|
||||||
|
const actionsToExecute = [...toCreate, ...toUpdate, ...toDelete]
|
||||||
|
|
||||||
|
await planner.executePlan(actionsToExecute)
|
||||||
|
|
||||||
|
if (toCreate.length) {
|
||||||
|
logActions("Created following links tables", toCreate)
|
||||||
|
}
|
||||||
|
if (toUpdate.length) {
|
||||||
|
logActions("Updated following links tables", toUpdate)
|
||||||
|
}
|
||||||
|
if (toDelete.length) {
|
||||||
|
logActions("Deleted following links tables", toDelete)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (actionsToExecute.length) {
|
||||||
|
Logger.info("Links sync completed")
|
||||||
|
} else {
|
||||||
|
Logger.info("Database already up-to-date")
|
||||||
|
}
|
||||||
|
process.exit()
|
||||||
|
} catch (e) {
|
||||||
|
Logger.error(e)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default main
|
||||||
@@ -40,7 +40,7 @@ const main = async function ({ directory }) {
|
|||||||
args.shift()
|
args.shift()
|
||||||
args.shift()
|
args.shift()
|
||||||
|
|
||||||
const action = args[0] as "run" | "revert" | "generate" | "show"
|
const action = args[0] as Action
|
||||||
const modules = args.splice(1)
|
const modules = args.splice(1)
|
||||||
|
|
||||||
validateInputArgs({ action, modules })
|
validateInputArgs({ action, modules })
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
MedusaApp,
|
MedusaApp,
|
||||||
|
MedusaAppGetLinksExecutionPlanner,
|
||||||
MedusaAppMigrateDown,
|
MedusaAppMigrateDown,
|
||||||
MedusaAppMigrateGenerate,
|
MedusaAppMigrateGenerate,
|
||||||
MedusaAppMigrateUp,
|
MedusaAppMigrateUp,
|
||||||
@@ -10,6 +11,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
CommonTypes,
|
CommonTypes,
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
|
ILinkMigrationsPlanner,
|
||||||
InternalModuleDeclaration,
|
InternalModuleDeclaration,
|
||||||
LoadedModule,
|
LoadedModule,
|
||||||
MedusaContainer,
|
MedusaContainer,
|
||||||
@@ -134,6 +136,57 @@ export async function runMedusaAppMigrations({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an instance of the link module migration planner.
|
||||||
|
*
|
||||||
|
* @param configModule
|
||||||
|
* @param container
|
||||||
|
* @param linkModules
|
||||||
|
*/
|
||||||
|
export async function getLinksExecutionPlanner({
|
||||||
|
configModule,
|
||||||
|
container,
|
||||||
|
linkModules,
|
||||||
|
}: {
|
||||||
|
configModule: {
|
||||||
|
modules?: CommonTypes.ConfigModule["modules"]
|
||||||
|
projectConfig: CommonTypes.ConfigModule["projectConfig"]
|
||||||
|
}
|
||||||
|
linkModules?: MedusaAppOptions["linkModules"]
|
||||||
|
container: MedusaContainer
|
||||||
|
}): Promise<ILinkMigrationsPlanner> {
|
||||||
|
const injectedDependencies = {
|
||||||
|
[ContainerRegistrationKeys.PG_CONNECTION]: container.resolve(
|
||||||
|
ContainerRegistrationKeys.PG_CONNECTION
|
||||||
|
),
|
||||||
|
[ContainerRegistrationKeys.LOGGER]: container.resolve(
|
||||||
|
ContainerRegistrationKeys.LOGGER
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
const sharedResourcesConfig = {
|
||||||
|
database: {
|
||||||
|
clientUrl:
|
||||||
|
injectedDependencies[ContainerRegistrationKeys.PG_CONNECTION]?.client
|
||||||
|
?.config?.connection?.connectionString ??
|
||||||
|
configModule.projectConfig.databaseUrl,
|
||||||
|
driverOptions: configModule.projectConfig.databaseDriverOptions,
|
||||||
|
debug: !!(configModule.projectConfig.databaseLogging ?? false),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
const configModules = mergeDefaultModules(configModule.modules)
|
||||||
|
|
||||||
|
const migrationOptions = {
|
||||||
|
modulesConfig: configModules,
|
||||||
|
sharedContainer: container,
|
||||||
|
linkModules,
|
||||||
|
sharedResourcesConfig,
|
||||||
|
injectedDependencies,
|
||||||
|
}
|
||||||
|
|
||||||
|
return await MedusaAppGetLinksExecutionPlanner(migrationOptions)
|
||||||
|
}
|
||||||
|
|
||||||
export const loadMedusaApp = async (
|
export const loadMedusaApp = async (
|
||||||
{
|
{
|
||||||
container,
|
container,
|
||||||
@@ -200,7 +253,7 @@ export const loadMedusaApp = async (
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register all unresolved modules as undefined to be present in the container with undefined value by defaul
|
// Register all unresolved modules as undefined to be present in the container with undefined value by default
|
||||||
// but still resolvable
|
// but still resolvable
|
||||||
for (const moduleDefinition of Object.values(ModulesDefinition)) {
|
for (const moduleDefinition of Object.values(ModulesDefinition)) {
|
||||||
if (!container.hasRegistration(moduleDefinition.registrationName)) {
|
if (!container.hasRegistration(moduleDefinition.registrationName)) {
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import {
|
||||||
|
defineJoinerConfig,
|
||||||
|
MedusaService,
|
||||||
|
model,
|
||||||
|
Module,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
|
export const User = model.define("user", {
|
||||||
|
id: model.id().primaryKey(),
|
||||||
|
name: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const Car = model.define("car", {
|
||||||
|
id: model.id().primaryKey(),
|
||||||
|
name: model.text(),
|
||||||
|
})
|
||||||
|
|
||||||
|
export const userJoinerConfig = defineJoinerConfig("User", {
|
||||||
|
models: [User],
|
||||||
|
})
|
||||||
|
|
||||||
|
export const carJoinerConfig = defineJoinerConfig("Car", {
|
||||||
|
models: [Car],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class UserService extends MedusaService({ User }) {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class CarService extends MedusaService({ Car }) {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UserModule = Module("User", {
|
||||||
|
service: UserService,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const CarModule = Module("Car", {
|
||||||
|
service: CarService,
|
||||||
|
})
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { MigrationsExecutionPlanner } from "../../src"
|
||||||
|
import { MedusaModule, ModuleJoinerConfig } from "@medusajs/modules-sdk"
|
||||||
|
import {
|
||||||
|
Car,
|
||||||
|
carJoinerConfig,
|
||||||
|
CarModule,
|
||||||
|
User,
|
||||||
|
userJoinerConfig,
|
||||||
|
UserModule,
|
||||||
|
} from "../__fixtures__/migrations"
|
||||||
|
import { defineLink, isObject, Modules } from "@medusajs/utils"
|
||||||
|
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { ILinkModule } from "@medusajs/types"
|
||||||
|
|
||||||
|
jest.setTimeout(30000)
|
||||||
|
|
||||||
|
MedusaModule.setJoinerConfig(userJoinerConfig.serviceName, userJoinerConfig)
|
||||||
|
MedusaModule.setJoinerConfig(carJoinerConfig.serviceName, carJoinerConfig)
|
||||||
|
|
||||||
|
moduleIntegrationTestRunner<ILinkModule>({
|
||||||
|
moduleName: Modules.LINK,
|
||||||
|
moduleModels: [User, Car],
|
||||||
|
testSuite: ({ dbConfig }) => {
|
||||||
|
describe("MigrationsExecutionPlanner", () => {
|
||||||
|
test("should generate an execution plan", async () => {
|
||||||
|
defineLink(UserModule.linkable.user, CarModule.linkable.car)
|
||||||
|
|
||||||
|
MedusaModule.getCustomLinks().forEach((linkDefinition: any) => {
|
||||||
|
MedusaModule.setCustomLink(
|
||||||
|
linkDefinition(MedusaModule.getAllJoinerConfigs())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect a create plan
|
||||||
|
*/
|
||||||
|
|
||||||
|
let joinerConfigs = MedusaModule.getCustomLinks().filter(
|
||||||
|
(link): link is ModuleJoinerConfig => isObject(link)
|
||||||
|
)
|
||||||
|
|
||||||
|
let planner = new MigrationsExecutionPlanner(joinerConfigs, {
|
||||||
|
database: dbConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
let actionPlan = await planner.createPlan()
|
||||||
|
await planner.executePlan(actionPlan)
|
||||||
|
|
||||||
|
expect(actionPlan).toHaveLength(1)
|
||||||
|
expect(actionPlan[0]).toEqual({
|
||||||
|
action: "create",
|
||||||
|
linkDescriptor: {
|
||||||
|
fromModule: "User",
|
||||||
|
toModule: "Car",
|
||||||
|
fromModel: "user",
|
||||||
|
toModel: "car",
|
||||||
|
},
|
||||||
|
tableName: "User_user_Car_car",
|
||||||
|
sql: 'create table "User_user_Car_car" ("user_id" varchar(255) not null, "car_id" varchar(255) not null, "id" varchar(255) not null, "created_at" timestamptz(0) not null default CURRENT_TIMESTAMP, "updated_at" timestamptz(0) not null default CURRENT_TIMESTAMP, "deleted_at" timestamptz(0) null, constraint "User_user_Car_car_pkey" primary key ("user_id", "car_id"));\ncreate index "IDX_car_id_-8c9667b4" on "User_user_Car_car" ("car_id");\ncreate index "IDX_id_-8c9667b4" on "User_user_Car_car" ("id");\ncreate index "IDX_user_id_-8c9667b4" on "User_user_Car_car" ("user_id");\ncreate index "IDX_deleted_at_-8c9667b4" on "User_user_Car_car" ("deleted_at");\n\n',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect an update plan
|
||||||
|
*/
|
||||||
|
;(MedusaModule as any).customLinks_.length = 0
|
||||||
|
|
||||||
|
defineLink(UserModule.linkable.user, CarModule.linkable.car, {
|
||||||
|
database: {
|
||||||
|
extraColumns: {
|
||||||
|
data: {
|
||||||
|
type: "json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
MedusaModule.getCustomLinks().forEach((linkDefinition: any) => {
|
||||||
|
MedusaModule.setCustomLink(
|
||||||
|
linkDefinition(MedusaModule.getAllJoinerConfigs())
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
joinerConfigs = MedusaModule.getCustomLinks().filter(
|
||||||
|
(link): link is ModuleJoinerConfig => isObject(link)
|
||||||
|
)
|
||||||
|
|
||||||
|
planner = new MigrationsExecutionPlanner(joinerConfigs, {
|
||||||
|
database: dbConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
actionPlan = await planner.createPlan()
|
||||||
|
await planner.executePlan(actionPlan)
|
||||||
|
|
||||||
|
expect(actionPlan).toHaveLength(1)
|
||||||
|
expect(actionPlan[0]).toEqual({
|
||||||
|
action: "update",
|
||||||
|
linkDescriptor: {
|
||||||
|
fromModule: "User",
|
||||||
|
toModule: "Car",
|
||||||
|
fromModel: "user",
|
||||||
|
toModel: "car",
|
||||||
|
},
|
||||||
|
tableName: "User_user_Car_car",
|
||||||
|
sql: 'alter table "User_user_Car_car" add column "data" jsonb not null;\n\n',
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect a noop plan
|
||||||
|
*/
|
||||||
|
|
||||||
|
actionPlan = await planner.createPlan()
|
||||||
|
await planner.executePlan(actionPlan)
|
||||||
|
|
||||||
|
expect(actionPlan).toHaveLength(1)
|
||||||
|
expect(actionPlan[0]).toEqual({
|
||||||
|
action: "noop",
|
||||||
|
linkDescriptor: {
|
||||||
|
fromModule: "User",
|
||||||
|
toModule: "Car",
|
||||||
|
fromModel: "user",
|
||||||
|
toModel: "car",
|
||||||
|
},
|
||||||
|
tableName: "User_user_Car_car",
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Expect a delete plan
|
||||||
|
*/
|
||||||
|
|
||||||
|
joinerConfigs = []
|
||||||
|
|
||||||
|
planner = new MigrationsExecutionPlanner(joinerConfigs, {
|
||||||
|
database: dbConfig,
|
||||||
|
})
|
||||||
|
|
||||||
|
actionPlan = await planner.createPlan()
|
||||||
|
|
||||||
|
expect(actionPlan).toHaveLength(1)
|
||||||
|
expect(actionPlan[0]).toEqual({
|
||||||
|
action: "delete",
|
||||||
|
tableName: "User_user_Car_car",
|
||||||
|
linkDescriptor: {
|
||||||
|
toModel: "car",
|
||||||
|
toModule: "Car",
|
||||||
|
fromModel: "user",
|
||||||
|
fromModule: "User",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,4 +1,10 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@models": "<rootDir>/src/models",
|
||||||
|
"^@services": "<rootDir>/src/services",
|
||||||
|
"^@repositories": "<rootDir>/src/repositories",
|
||||||
|
"^@types": "<rootDir>/src/types",
|
||||||
|
},
|
||||||
transform: {
|
transform: {
|
||||||
"^.+\\.[jt]s$": [
|
"^.+\\.[jt]s$": [
|
||||||
"@swc/jest",
|
"@swc/jest",
|
||||||
@@ -12,4 +18,5 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
testEnvironment: `node`,
|
testEnvironment: `node`,
|
||||||
moduleFileExtensions: [`js`, `ts`],
|
moduleFileExtensions: [`js`, `ts`],
|
||||||
|
modulePathIgnorePatterns: ["dist/"],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
"prepare": "cross-env NODE_ENV=production yarn run build",
|
"prepare": "cross-env NODE_ENV=production yarn run build",
|
||||||
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
|
"build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json",
|
||||||
"test": "jest --passWithNoTests --runInBand --bail --forceExit -- src",
|
"test": "jest --passWithNoTests --runInBand --bail --forceExit -- src",
|
||||||
"test:integration": "jest --passWithNoTests"
|
"test:integration": "jest --forceExit -- integration-tests/**/__tests__/**/*.ts"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@medusajs/types": "^1.11.16",
|
"@medusajs/types": "^1.11.16",
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
"tsc-alias": "^1.8.6",
|
"tsc-alias": "^1.8.6",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@medusajs/modules-sdk": "^1.12.11",
|
"@medusajs/modules-sdk": "^1.12.11",
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
export * from "./initialize"
|
export * from "./initialize"
|
||||||
|
export * from "./migration"
|
||||||
export * from "./types"
|
export * from "./types"
|
||||||
export * from "./loaders"
|
export * from "./loaders"
|
||||||
export * from "./services"
|
export * from "./services"
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
ExternalModuleDeclaration,
|
ExternalModuleDeclaration,
|
||||||
ILinkModule,
|
ILinkModule,
|
||||||
LinkModuleDefinition,
|
LinkModuleDefinition,
|
||||||
LoaderOptions,
|
|
||||||
ModuleExports,
|
ModuleExports,
|
||||||
ModuleJoinerConfig,
|
ModuleJoinerConfig,
|
||||||
ModuleServiceInitializeCustomDataLayerOptions,
|
ModuleServiceInitializeCustomDataLayerOptions,
|
||||||
@@ -23,7 +22,7 @@ import {
|
|||||||
toPascalCase,
|
toPascalCase,
|
||||||
} from "@medusajs/utils"
|
} from "@medusajs/utils"
|
||||||
import * as linkDefinitions from "../definitions"
|
import * as linkDefinitions from "../definitions"
|
||||||
import { getMigration, getRevertMigration } from "../migration"
|
import { MigrationsExecutionPlanner } from "../migration"
|
||||||
import { InitializeModuleInjectableDependencies } from "../types"
|
import { InitializeModuleInjectableDependencies } from "../types"
|
||||||
import {
|
import {
|
||||||
composeLinkName,
|
composeLinkName,
|
||||||
@@ -38,7 +37,7 @@ export const initialize = async (
|
|||||||
| ModuleServiceInitializeCustomDataLayerOptions
|
| ModuleServiceInitializeCustomDataLayerOptions
|
||||||
| ExternalModuleDeclaration
|
| ExternalModuleDeclaration
|
||||||
| InternalModuleDeclaration,
|
| InternalModuleDeclaration,
|
||||||
modulesDefinition?: ModuleJoinerConfig[],
|
pluginLinksDefinitions?: ModuleJoinerConfig[],
|
||||||
injectedDependencies?: InitializeModuleInjectableDependencies
|
injectedDependencies?: InitializeModuleInjectableDependencies
|
||||||
): Promise<{ [link: string]: ILinkModule }> => {
|
): Promise<{ [link: string]: ILinkModule }> => {
|
||||||
const allLinks = {}
|
const allLinks = {}
|
||||||
@@ -47,7 +46,7 @@ export const initialize = async (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||||
modulesDefinition ?? []
|
pluginLinksDefinitions ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
for (const linkDefinition of allLinksToLoad) {
|
for (const linkDefinition of allLinksToLoad) {
|
||||||
@@ -186,20 +185,24 @@ export const initialize = async (
|
|||||||
return allLinks
|
return allLinks
|
||||||
}
|
}
|
||||||
|
|
||||||
async function applyMigrationUpOrDown(
|
/**
|
||||||
{
|
* Prepare an execution plan and run the migrations accordingly.
|
||||||
options,
|
* It includes creating, updating, deleting the tables according to the execution plan.
|
||||||
logger,
|
* If any unsafe sql is identified then we will notify the user to act manually.
|
||||||
}: Omit<LoaderOptions<ModuleServiceInitializeOptions>, "container">,
|
*
|
||||||
modulesDefinition?: ModuleJoinerConfig[],
|
* @param options
|
||||||
revert = false
|
* @param pluginLinksDefinition
|
||||||
|
*/
|
||||||
|
export function getMigrationPlanner(
|
||||||
|
options: ModuleServiceInitializeOptions,
|
||||||
|
pluginLinksDefinition?: ModuleJoinerConfig[]
|
||||||
) {
|
) {
|
||||||
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
const modulesLoadedKeys = MedusaModule.getLoadedModules().map(
|
||||||
(mod) => Object.keys(mod)[0]
|
(mod) => Object.keys(mod)[0]
|
||||||
)
|
)
|
||||||
|
|
||||||
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
const allLinksToLoad = Object.values(linkDefinitions).concat(
|
||||||
modulesDefinition ?? []
|
pluginLinksDefinition ?? []
|
||||||
)
|
)
|
||||||
|
|
||||||
const allLinks = new Set<string>()
|
const allLinks = new Set<string>()
|
||||||
@@ -237,30 +240,7 @@ async function applyMigrationUpOrDown(
|
|||||||
) {
|
) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const migrate = revert
|
|
||||||
? getRevertMigration(definition, serviceKey, primary, foreign)
|
|
||||||
: getMigration(definition, serviceKey, primary, foreign)
|
|
||||||
await migrate({ options, logger })
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
export async function runMigrations(
|
return new MigrationsExecutionPlanner(allLinksToLoad, options)
|
||||||
{
|
|
||||||
options,
|
|
||||||
logger,
|
|
||||||
}: Omit<LoaderOptions<ModuleServiceInitializeOptions>, "container">,
|
|
||||||
modulesDefinition?: ModuleJoinerConfig[]
|
|
||||||
) {
|
|
||||||
await applyMigrationUpOrDown({ options, logger }, modulesDefinition)
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function revertMigrations(
|
|
||||||
{
|
|
||||||
options,
|
|
||||||
logger,
|
|
||||||
}: Omit<LoaderOptions<ModuleServiceInitializeOptions>, "container">,
|
|
||||||
modulesDefinition?: ModuleJoinerConfig[]
|
|
||||||
) {
|
|
||||||
await applyMigrationUpOrDown({ options, logger }, modulesDefinition, true)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,134 +1,394 @@
|
|||||||
import {
|
import {
|
||||||
JoinerRelationship,
|
ILinkMigrationsPlanner,
|
||||||
LoaderOptions,
|
LinkMigrationsPlannerAction,
|
||||||
Logger,
|
|
||||||
ModuleJoinerConfig,
|
ModuleJoinerConfig,
|
||||||
ModuleServiceInitializeOptions,
|
ModuleServiceInitializeOptions,
|
||||||
|
PlannerActionLinkDescriptor,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
|
||||||
import { generateEntity } from "../utils"
|
import { generateEntity } from "../utils"
|
||||||
|
import { EntitySchema, MikroORM } from "@mikro-orm/core"
|
||||||
|
import { DatabaseSchema, PostgreSqlDriver } from "@mikro-orm/postgresql"
|
||||||
|
import {
|
||||||
|
arrayDifference,
|
||||||
|
DALUtils,
|
||||||
|
ModulesSdkUtils,
|
||||||
|
promiseAll,
|
||||||
|
} from "@medusajs/utils"
|
||||||
|
|
||||||
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
|
/**
|
||||||
|
* The migrations execution planner creates a plan of SQL queries
|
||||||
|
* to be executed to keep link modules database state in sync
|
||||||
|
* with the links defined inside the user application.
|
||||||
|
*/
|
||||||
|
export class MigrationsExecutionPlanner implements ILinkMigrationsPlanner {
|
||||||
|
/**
|
||||||
|
* Database options for the module service
|
||||||
|
*/
|
||||||
|
#dbConfig: ReturnType<typeof ModulesSdkUtils.loadDatabaseConfig>
|
||||||
|
|
||||||
export function getMigration(
|
/**
|
||||||
joinerConfig: ModuleJoinerConfig,
|
* The set of commands that are unsafe to execute automatically when
|
||||||
serviceName: string,
|
* performing "alter table"
|
||||||
primary: JoinerRelationship,
|
*/
|
||||||
foreign: JoinerRelationship
|
#unsafeSQLCommands = ["alter column", "drop column"]
|
||||||
) {
|
|
||||||
return async function runMigrations(
|
/**
|
||||||
{
|
* On-the-fly computed set of entities for the user provided joinerConfig and the link it is coming from
|
||||||
options,
|
*/
|
||||||
logger,
|
#linksEntities: {
|
||||||
}: Pick<
|
linkDescriptor: PlannerActionLinkDescriptor
|
||||||
LoaderOptions<ModuleServiceInitializeOptions>,
|
entity: EntitySchema
|
||||||
"options" | "logger"
|
}[]
|
||||||
> = {} as any
|
|
||||||
|
/**
|
||||||
|
* The table that keeps a track of tables generated by the link
|
||||||
|
* module.
|
||||||
|
*/
|
||||||
|
protected tableName = "link_module_migrations"
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
joinerConfig: ModuleJoinerConfig[],
|
||||||
|
options?: ModuleServiceInitializeOptions
|
||||||
) {
|
) {
|
||||||
logger ??= console as unknown as Logger
|
this.#dbConfig = ModulesSdkUtils.loadDatabaseConfig("link_modules", options)
|
||||||
|
this.#linksEntities = joinerConfig
|
||||||
|
.map((config) => {
|
||||||
|
if (config.isReadOnlyLink) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const dbData = ModulesSdkUtils.loadDatabaseConfig("link_modules", options)
|
const [primary, foreign] = config.relationships ?? []
|
||||||
const entity = generateEntity(joinerConfig, primary, foreign)
|
const linkDescriptor: PlannerActionLinkDescriptor = {
|
||||||
const pathToMigrations = __dirname + "/../migrations"
|
fromModule: primary.serviceName,
|
||||||
|
toModule: foreign.serviceName,
|
||||||
|
fromModel: primary.alias,
|
||||||
|
toModel: foreign.alias,
|
||||||
|
}
|
||||||
|
|
||||||
const orm = await DALUtils.mikroOrmCreateConnection(
|
return {
|
||||||
dbData,
|
entity: generateEntity(config, primary, foreign),
|
||||||
[entity],
|
linkDescriptor,
|
||||||
pathToMigrations
|
}
|
||||||
)
|
})
|
||||||
|
.filter((item) => !!item)
|
||||||
|
}
|
||||||
|
|
||||||
const tableName = entity.meta.collection
|
/**
|
||||||
|
* Initializes the ORM using the normalized dbConfig and set
|
||||||
|
* of provided entities
|
||||||
|
*/
|
||||||
|
protected async createORM(entities: EntitySchema[] = []) {
|
||||||
|
return await DALUtils.mikroOrmCreateConnection(this.#dbConfig, entities, "")
|
||||||
|
}
|
||||||
|
|
||||||
let hasTable = false
|
/**
|
||||||
try {
|
* Ensure the table to track link modules migrations
|
||||||
|
* exists.
|
||||||
|
*
|
||||||
|
* @param orm MikroORM
|
||||||
|
*/
|
||||||
|
protected async ensureMigrationsTable(
|
||||||
|
orm: MikroORM<PostgreSqlDriver>
|
||||||
|
): Promise<void> {
|
||||||
|
await orm.em.getDriver().getConnection().execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS "${this.tableName}" (
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
table_name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
link_descriptor JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||||
|
)
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the migrations table is in sync
|
||||||
|
*
|
||||||
|
* @param orm
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async ensureMigrationsTableUpToDate(
|
||||||
|
orm: MikroORM<PostgreSqlDriver>
|
||||||
|
) {
|
||||||
|
const existingTables: string[] = (
|
||||||
await orm.em
|
await orm.em
|
||||||
|
.getDriver()
|
||||||
.getConnection()
|
.getConnection()
|
||||||
.execute(`SELECT 1 FROM "${tableName}" LIMIT 0`)
|
.execute<
|
||||||
hasTable = true
|
{
|
||||||
} catch {}
|
table_name: string
|
||||||
|
}[]
|
||||||
|
>(
|
||||||
|
`
|
||||||
|
SELECT table_name
|
||||||
|
FROM information_schema.tables;
|
||||||
|
`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.map(({ table_name }) => table_name)
|
||||||
|
.filter((tableName) =>
|
||||||
|
this.#linksEntities.some(
|
||||||
|
({ entity }) => entity.meta.collection === tableName
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!existingTables.length) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const orderedDescriptors = existingTables.map((tableName) => {
|
||||||
|
return this.#linksEntities.find(
|
||||||
|
({ entity }) => entity.meta.collection === tableName
|
||||||
|
)!.linkDescriptor
|
||||||
|
})
|
||||||
|
|
||||||
|
const positionalArgs = new Array(existingTables.length)
|
||||||
|
.fill("(?, ?)")
|
||||||
|
.join(", ")
|
||||||
|
await orm.em
|
||||||
|
.getDriver()
|
||||||
|
.getConnection()
|
||||||
|
.execute(
|
||||||
|
`
|
||||||
|
INSERT INTO ${this.tableName} (table_name, link_descriptor) VALUES ${positionalArgs} ON CONFLICT DO NOTHING;
|
||||||
|
`,
|
||||||
|
existingTables.flatMap((tableName, index) => [
|
||||||
|
tableName,
|
||||||
|
JSON.stringify(orderedDescriptors[index]),
|
||||||
|
])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert tuple to the migrations table and create the link table
|
||||||
|
*
|
||||||
|
* @param orm
|
||||||
|
* @param action
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected async createLinkTable(
|
||||||
|
orm: MikroORM<PostgreSqlDriver>,
|
||||||
|
action: LinkMigrationsPlannerAction & {
|
||||||
|
linkDescriptor: PlannerActionLinkDescriptor
|
||||||
|
sql: string
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
const { tableName, linkDescriptor, sql } = action
|
||||||
|
|
||||||
|
await orm.em
|
||||||
|
.getDriver()
|
||||||
|
.getConnection()
|
||||||
|
.execute(
|
||||||
|
`
|
||||||
|
INSERT INTO "${this.tableName}" (table_name, link_descriptor) VALUES (?, ?);
|
||||||
|
${sql}
|
||||||
|
`,
|
||||||
|
[tableName, linkDescriptor]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drops the link table and untracks it from the "link_modules_migrations"
|
||||||
|
* table.
|
||||||
|
*/
|
||||||
|
protected async dropLinkTable(
|
||||||
|
orm: MikroORM<PostgreSqlDriver>,
|
||||||
|
tableName: string
|
||||||
|
) {
|
||||||
|
await orm.em.getDriver().getConnection().execute(`
|
||||||
|
DROP TABLE IF EXISTS "${tableName}";
|
||||||
|
DELETE FROM "${this.tableName}" WHERE table_name = '${tableName}';
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of table names that have been tracked during
|
||||||
|
* the last run. In short, these tables were created by the
|
||||||
|
* link modules migrations runner.
|
||||||
|
*
|
||||||
|
* @param orm MikroORM
|
||||||
|
*/
|
||||||
|
protected async getTrackedLinksTables(
|
||||||
|
orm: MikroORM<PostgreSqlDriver>
|
||||||
|
): Promise<
|
||||||
|
{ table_name: string; link_descriptor: PlannerActionLinkDescriptor }[]
|
||||||
|
> {
|
||||||
|
const results = await orm.em.getDriver().getConnection().execute<
|
||||||
|
{
|
||||||
|
table_name: string
|
||||||
|
link_descriptor: PlannerActionLinkDescriptor
|
||||||
|
}[]
|
||||||
|
>(`
|
||||||
|
SELECT table_name, link_descriptor from "${this.tableName}"
|
||||||
|
`)
|
||||||
|
|
||||||
|
return results.map((tuple) => ({
|
||||||
|
table_name: tuple.table_name,
|
||||||
|
link_descriptor: tuple.link_descriptor,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the migration plan for a specific link entity.
|
||||||
|
*/
|
||||||
|
protected async getEntityMigrationPlan(
|
||||||
|
linkDescriptor: PlannerActionLinkDescriptor,
|
||||||
|
entity: EntitySchema,
|
||||||
|
trackedLinksTables: string[]
|
||||||
|
): Promise<LinkMigrationsPlannerAction> {
|
||||||
|
const tableName = entity.meta.collection
|
||||||
|
const orm = await this.createORM([entity])
|
||||||
|
|
||||||
const generator = orm.getSchemaGenerator()
|
const generator = orm.getSchemaGenerator()
|
||||||
if (hasTable) {
|
const platform = orm.em.getPlatform()
|
||||||
/* const updateSql = await generator.getUpdateSchemaSQL()
|
const connection = orm.em.getConnection()
|
||||||
const entityUpdates = updateSql
|
const schemaName = this.#dbConfig.schema || "public"
|
||||||
.split(";")
|
|
||||||
.map((sql) => sql.trim())
|
|
||||||
.filter((sql) =>
|
|
||||||
sql.toLowerCase().includes(`alter table "${tableName.toLowerCase()}"`)
|
|
||||||
)
|
|
||||||
|
|
||||||
if (entityUpdates.length > 0) {
|
/**
|
||||||
try {
|
* If the table name for the entity has not been
|
||||||
await generator.execute(entityUpdates.join(";"))
|
* managed by us earlier, then we should create
|
||||||
logger.info(`Link module "${serviceName}" migration executed`)
|
* it.
|
||||||
} catch (error) {
|
*/
|
||||||
logger.error(
|
if (!trackedLinksTables.includes(tableName)) {
|
||||||
`Link module "${serviceName}" migration failed to run - Error: ${error.errros ?? error}`
|
return {
|
||||||
)
|
action: "create",
|
||||||
}
|
linkDescriptor,
|
||||||
} else {
|
tableName,
|
||||||
logger.info(`Skipping "${tableName}" migration.`)
|
sql: await generator.getCreateSchemaSQL(),
|
||||||
}*/
|
|
||||||
// Note: Temporarily skipping this for handling no logs on the CI. Bring this back if necessary.
|
|
||||||
// logger.info(
|
|
||||||
// `Link module('${serviceName}'): Table already exists. Write your own migration if needed.`
|
|
||||||
// )
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await generator.createSchema()
|
|
||||||
|
|
||||||
logger.info(`Link module('${serviceName}'): Migration executed`)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Link module('${serviceName}'): Migration failed - Error: ${
|
|
||||||
error.errros ?? error
|
|
||||||
}`
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await orm.close()
|
/**
|
||||||
}
|
* Pre-fetching information schema from the database and using that
|
||||||
}
|
* as the way to compute the update diff.
|
||||||
|
*
|
||||||
|
* @note
|
||||||
|
* The "loadInformationSchema" mutates the "dbSchema" argument provided
|
||||||
|
* to it as the first argument.
|
||||||
|
*/
|
||||||
|
const dbSchema = new DatabaseSchema(platform, schemaName)
|
||||||
|
await platform
|
||||||
|
.getSchemaHelper?.()
|
||||||
|
?.loadInformationSchema(dbSchema, connection, [
|
||||||
|
{
|
||||||
|
table_name: tableName,
|
||||||
|
schema_name: schemaName,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
export function getRevertMigration(
|
const updateSQL = await generator.getUpdateSchemaSQL({
|
||||||
joinerConfig: ModuleJoinerConfig,
|
fromSchema: dbSchema,
|
||||||
serviceName: string,
|
})
|
||||||
primary: JoinerRelationship,
|
|
||||||
foreign: JoinerRelationship
|
|
||||||
) {
|
|
||||||
return async function revertMigrations(
|
|
||||||
{
|
|
||||||
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)
|
* Entity is upto-date and hence we do not have to perform
|
||||||
const pathToMigrations = __dirname + "/../migrations"
|
* any updates on it.
|
||||||
|
*/
|
||||||
|
if (!updateSQL.length) {
|
||||||
|
return {
|
||||||
|
action: "noop",
|
||||||
|
linkDescriptor,
|
||||||
|
tableName,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const orm = await DALUtils.mikroOrmCreateConnection(
|
const usesUnsafeCommands = this.#unsafeSQLCommands.some((fragment) => {
|
||||||
dbData,
|
return updateSQL.match(new RegExp(`${fragment}`, "ig"))
|
||||||
[entity],
|
})
|
||||||
pathToMigrations
|
|
||||||
)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const migrator = orm.getMigrator()
|
return {
|
||||||
await migrator.down()
|
action: usesUnsafeCommands ? "notify" : "update",
|
||||||
logger.info(`Link module "${serviceName}" migration executed`)
|
linkDescriptor,
|
||||||
} catch (error) {
|
tableName,
|
||||||
logger.error(
|
sql: updateSQL,
|
||||||
`Link module "${serviceName}" migration failed to run - Error: ${
|
}
|
||||||
error.errros ?? error
|
} finally {
|
||||||
}`
|
await orm.close(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a plan to executed in order to keep the database state in
|
||||||
|
* sync with the user-defined links.
|
||||||
|
*
|
||||||
|
* This method only creates a plan and does not change the database
|
||||||
|
* state. You must call the "executePlan" method for that.
|
||||||
|
*/
|
||||||
|
async createPlan() {
|
||||||
|
const orm = await this.createORM()
|
||||||
|
await this.ensureMigrationsTable(orm)
|
||||||
|
|
||||||
|
const executionActions: LinkMigrationsPlannerAction[] = []
|
||||||
|
|
||||||
|
await this.ensureMigrationsTableUpToDate(orm)
|
||||||
|
|
||||||
|
const trackedTables = await this.getTrackedLinksTables(orm)
|
||||||
|
const trackedTablesNames = trackedTables.map(({ table_name }) => table_name)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Looping through the new set of entities and generating
|
||||||
|
* execution plan for them
|
||||||
|
*/
|
||||||
|
for (let { entity, linkDescriptor } of this.#linksEntities) {
|
||||||
|
executionActions.push(
|
||||||
|
await this.getEntityMigrationPlan(
|
||||||
|
linkDescriptor,
|
||||||
|
entity,
|
||||||
|
trackedTablesNames
|
||||||
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
await orm.close()
|
const linksTableNames = this.#linksEntities.map(
|
||||||
|
({ entity }) => entity.meta.collection
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finding the tables to be removed
|
||||||
|
*/
|
||||||
|
const tablesToRemove = arrayDifference(trackedTablesNames, linksTableNames)
|
||||||
|
tablesToRemove.forEach((tableToRemove) => {
|
||||||
|
executionActions.push({
|
||||||
|
action: "delete",
|
||||||
|
tableName: tableToRemove,
|
||||||
|
linkDescriptor: trackedTables.find(
|
||||||
|
({ table_name }) => tableToRemove === table_name
|
||||||
|
)!.link_descriptor,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
return executionActions
|
||||||
|
} finally {
|
||||||
|
await orm.close(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the actionsPlan actions where the action is one of 'create' | 'update' | 'delete'.
|
||||||
|
* 'noop' and 'notify' actions are implicitly ignored. If a notify action needs to be
|
||||||
|
* executed, you can mutate its action to 'update', in that scenario it means that an unsafe
|
||||||
|
* update sql (from our point of view) will be executed and some data could be lost.
|
||||||
|
*
|
||||||
|
* @param actionPlan
|
||||||
|
*/
|
||||||
|
async executePlan(actionPlan: LinkMigrationsPlannerAction[]): Promise<void> {
|
||||||
|
const orm = await this.createORM()
|
||||||
|
|
||||||
|
await promiseAll(
|
||||||
|
actionPlan.map(async (action) => {
|
||||||
|
switch (action.action) {
|
||||||
|
case "delete":
|
||||||
|
return await this.dropLinkTable(orm, action.tableName)
|
||||||
|
case "create":
|
||||||
|
return await this.createLinkTable(orm, action)
|
||||||
|
case "update":
|
||||||
|
return await orm.em.getDriver().getConnection().execute(action.sql)
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
).finally(() => orm.close(true))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3625,6 +3625,19 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@inquirer/checkbox@npm:^2.3.11":
|
||||||
|
version: 2.3.11
|
||||||
|
resolution: "@inquirer/checkbox@npm:2.3.11"
|
||||||
|
dependencies:
|
||||||
|
"@inquirer/core": ^9.0.3
|
||||||
|
"@inquirer/figures": ^1.0.4
|
||||||
|
"@inquirer/type": ^1.5.0
|
||||||
|
ansi-escapes: ^4.3.2
|
||||||
|
yoctocolors-cjs: ^2.1.2
|
||||||
|
checksum: 688b011a80e156a35fb51a31d9ed114216abcad78e3d0a490fe98154a7293fa8f7e80315cad390c75fc10e6f7e2910fbd353dd6b07dd39f7b2e9694eb42bad86
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@inquirer/confirm@npm:^3.0.0":
|
"@inquirer/confirm@npm:^3.0.0":
|
||||||
version: 3.1.7
|
version: 3.1.7
|
||||||
resolution: "@inquirer/confirm@npm:3.1.7"
|
resolution: "@inquirer/confirm@npm:3.1.7"
|
||||||
@@ -3656,6 +3669,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@inquirer/core@npm:^9.0.3":
|
||||||
|
version: 9.0.3
|
||||||
|
resolution: "@inquirer/core@npm:9.0.3"
|
||||||
|
dependencies:
|
||||||
|
"@inquirer/figures": ^1.0.4
|
||||||
|
"@inquirer/type": ^1.5.0
|
||||||
|
"@types/mute-stream": ^0.0.4
|
||||||
|
"@types/node": ^20.14.11
|
||||||
|
"@types/wrap-ansi": ^3.0.0
|
||||||
|
ansi-escapes: ^4.3.2
|
||||||
|
cli-spinners: ^2.9.2
|
||||||
|
cli-width: ^4.1.0
|
||||||
|
mute-stream: ^1.0.0
|
||||||
|
signal-exit: ^4.1.0
|
||||||
|
strip-ansi: ^6.0.1
|
||||||
|
wrap-ansi: ^6.2.0
|
||||||
|
yoctocolors-cjs: ^2.1.2
|
||||||
|
checksum: 1929e2df237dd2384bb023cc51b4f5aae2732b99a78a76bf404ff7e0d500587ced2e02114071114ad28f7f57b0dfcc534e4f5fbaf8adecac825798450a57daec
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@inquirer/figures@npm:^1.0.1":
|
"@inquirer/figures@npm:^1.0.1":
|
||||||
version: 1.0.1
|
version: 1.0.1
|
||||||
resolution: "@inquirer/figures@npm:1.0.1"
|
resolution: "@inquirer/figures@npm:1.0.1"
|
||||||
@@ -3663,6 +3697,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@inquirer/figures@npm:^1.0.4":
|
||||||
|
version: 1.0.4
|
||||||
|
resolution: "@inquirer/figures@npm:1.0.4"
|
||||||
|
checksum: f3d8ade38f4895eb6cfc61e14e7bfaa25b2ff95ce9195587e161d89c05e1beeb8666d2115d900d5ba5e652325fff14ad3a7b973f36c1e8796653068ef3c01a23
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@inquirer/type@npm:^1.3.1":
|
"@inquirer/type@npm:^1.3.1":
|
||||||
version: 1.3.1
|
version: 1.3.1
|
||||||
resolution: "@inquirer/type@npm:1.3.1"
|
resolution: "@inquirer/type@npm:1.3.1"
|
||||||
@@ -3670,6 +3711,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@inquirer/type@npm:^1.5.0":
|
||||||
|
version: 1.5.0
|
||||||
|
resolution: "@inquirer/type@npm:1.5.0"
|
||||||
|
dependencies:
|
||||||
|
mute-stream: ^1.0.0
|
||||||
|
checksum: 6a2379af9ca7227ae577f952c29f6736142b2925c2460d110504002633902f70c145e6df04783c32d22f475e464ad203465234f5e2ef33f85eb719bd573032fb
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@internationalized/date@npm:^3.5.4":
|
"@internationalized/date@npm:^3.5.4":
|
||||||
version: 3.5.4
|
version: 3.5.4
|
||||||
resolution: "@internationalized/date@npm:3.5.4"
|
resolution: "@internationalized/date@npm:3.5.4"
|
||||||
@@ -4689,7 +4739,7 @@ __metadata:
|
|||||||
rimraf: ^5.0.1
|
rimraf: ^5.0.1
|
||||||
ts-node: ^10.9.1
|
ts-node: ^10.9.1
|
||||||
tsc-alias: ^1.8.6
|
tsc-alias: ^1.8.6
|
||||||
typescript: ^5.1.6
|
typescript: ^5.5.0
|
||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
@@ -4769,6 +4819,7 @@ __metadata:
|
|||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/medusa@workspace:packages/medusa"
|
resolution: "@medusajs/medusa@workspace:packages/medusa"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@inquirer/checkbox": ^2.3.11
|
||||||
"@medusajs/admin-sdk": 0.0.1
|
"@medusajs/admin-sdk": 0.0.1
|
||||||
"@medusajs/core-flows": ^0.0.9
|
"@medusajs/core-flows": ^0.0.9
|
||||||
"@medusajs/link-modules": ^0.2.11
|
"@medusajs/link-modules": ^0.2.11
|
||||||
@@ -4789,6 +4840,7 @@ __metadata:
|
|||||||
awilix: ^8.0.0
|
awilix: ^8.0.0
|
||||||
body-parser: ^1.19.0
|
body-parser: ^1.19.0
|
||||||
boxen: ^5.0.1
|
boxen: ^5.0.1
|
||||||
|
chalk: ^4.0.0
|
||||||
chokidar: ^3.4.2
|
chokidar: ^3.4.2
|
||||||
compression: ^1.7.4
|
compression: ^1.7.4
|
||||||
connect-redis: ^5.0.0
|
connect-redis: ^5.0.0
|
||||||
@@ -12094,6 +12146,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/node@npm:^20.14.11":
|
||||||
|
version: 20.14.11
|
||||||
|
resolution: "@types/node@npm:20.14.11"
|
||||||
|
dependencies:
|
||||||
|
undici-types: ~5.26.4
|
||||||
|
checksum: 5306becc0ff41d81b1e31524bd376e958d0741d1ce892dffd586b9ae0cb6553c62b0d62abd16da8bea6b9a2c17572d360450535d7c073794b0cef9cb4e39691e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/normalize-package-data@npm:^2.4.0":
|
"@types/normalize-package-data@npm:^2.4.0":
|
||||||
version: 2.4.4
|
version: 2.4.4
|
||||||
resolution: "@types/normalize-package-data@npm:2.4.4"
|
resolution: "@types/normalize-package-data@npm:2.4.4"
|
||||||
@@ -30026,6 +30087,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"typescript@npm:^5.5.0":
|
||||||
|
version: 5.5.3
|
||||||
|
resolution: "typescript@npm:5.5.3"
|
||||||
|
bin:
|
||||||
|
tsc: bin/tsc
|
||||||
|
tsserver: bin/tsserver
|
||||||
|
checksum: f52c71ccbc7080b034b9d3b72051d563601a4815bf3e39ded188e6ce60813f75dbedf11ad15dd4d32a12996a9ed8c7155b46c93a9b9c9bad1049766fe614bbdd
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"typescript@patch:typescript@4.9.5#~builtin<compat/typescript>, typescript@patch:typescript@^4.1.3#~builtin<compat/typescript>":
|
"typescript@patch:typescript@4.9.5#~builtin<compat/typescript>, typescript@patch:typescript@^4.1.3#~builtin<compat/typescript>":
|
||||||
version: 4.9.5
|
version: 4.9.5
|
||||||
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=7ad353"
|
resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin<compat/typescript>::version=4.9.5&hash=7ad353"
|
||||||
@@ -30076,6 +30147,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"typescript@patch:typescript@^5.5.0#~builtin<compat/typescript>":
|
||||||
|
version: 5.5.3
|
||||||
|
resolution: "typescript@patch:typescript@npm%3A5.5.3#~builtin<compat/typescript>::version=5.5.3&hash=7ad353"
|
||||||
|
bin:
|
||||||
|
tsc: bin/tsc
|
||||||
|
tsserver: bin/tsserver
|
||||||
|
checksum: 5a437c416251334deeaf29897157032311f3f126547cfdc4b133768b606cb0e62bcee733bb97cf74c42fe7268801aea1392d8e40988cdef112e9546eba4c03c5
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ua-parser-js@npm:^1.0.35":
|
"ua-parser-js@npm:^1.0.35":
|
||||||
version: 1.0.37
|
version: 1.0.37
|
||||||
resolution: "ua-parser-js@npm:1.0.37"
|
resolution: "ua-parser-js@npm:1.0.37"
|
||||||
@@ -31476,6 +31557,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"yoctocolors-cjs@npm:^2.1.2":
|
||||||
|
version: 2.1.2
|
||||||
|
resolution: "yoctocolors-cjs@npm:2.1.2"
|
||||||
|
checksum: a0e36eb88fea2c7981eab22d1ba45e15d8d268626e6c4143305e2c1628fa17ebfaa40cd306161a8ce04c0a60ee0262058eab12567493d5eb1409780853454c6f
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"z-schema@npm:~5.0.2":
|
"z-schema@npm:~5.0.2":
|
||||||
version: 5.0.5
|
version: 5.0.5
|
||||||
resolution: "z-schema@npm:5.0.5"
|
resolution: "z-schema@npm:5.0.5"
|
||||||
|
|||||||
Reference in New Issue
Block a user