diff --git a/integration-tests/environment-helpers/use-db.js b/integration-tests/environment-helpers/use-db.js index 261ccfac95..6c88804cc6 100644 --- a/integration-tests/environment-helpers/use-db.js +++ b/integration-tests/environment-helpers/use-db.js @@ -11,6 +11,7 @@ const { dropDatabase } = require("pg-god") const { DataSource } = require("typeorm") const dbFactory = require("./use-template-db") const { ContainerRegistrationKeys } = require("@medusajs/utils") +const { migrateMedusaApp } = require("@medusajs/medusa/dist/loaders/medusa-app") const DB_HOST = process.env.DB_HOST const DB_USERNAME = process.env.DB_USERNAME @@ -155,35 +156,26 @@ module.exports = { const featureFlagLoader = require("@medusajs/medusa/dist/loaders/feature-flags").default - const medusaAppLoader = - require("@medusajs/medusa/dist/loaders/medusa-app").default - const container = createMedusaContainer() const featureFlagRouter = await featureFlagLoader(configModule) + const pgConnection = await pgConnectionLoader({ configModule, container }) + container.register({ [ContainerRegistrationKeys.CONFIG_MODULE]: asValue(configModule), [ContainerRegistrationKeys.LOGGER]: asValue(console), [ContainerRegistrationKeys.MANAGER]: asValue(dbDataSource.manager), + [ContainerRegistrationKeys.PG_CONNECTION]: asValue(pgConnection), featureFlagRouter: asValue(featureFlagRouter), }) - const pgConnection = await pgConnectionLoader({ configModule, container }) instance.setPgConnection(pgConnection) - const { runMigrations } = await medusaAppLoader( + await migrateMedusaApp( { configModule, container }, { registerInContainer: false } ) - - const options = { - database: { - clientUrl: DB_URL, - connection: pgConnection, - }, - } - await runMigrations(options) } return dbDataSource diff --git a/integration-tests/plugins/__tests__/customer/store/update-customer-address.spec.ts b/integration-tests/plugins/__tests__/customer/store/update-customer-address.spec.ts index 3401650f20..255af7e6bd 100644 --- a/integration-tests/plugins/__tests__/customer/store/update-customer-address.spec.ts +++ b/integration-tests/plugins/__tests__/customer/store/update-customer-address.spec.ts @@ -1,12 +1,14 @@ import { initDb, useDb } from "../../../../environment-helpers/use-db" -import { ICustomerModuleService } from "@medusajs/types" import { ModuleRegistrationName } from "@medusajs/modules-sdk" -import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" -import { getContainer } from "../../../../environment-helpers/use-container" +import { ICustomerModuleService } from "@medusajs/types" import path from "path" import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { createAuthenticatedCustomer } from "../../../helpers/create-authenticated-customer" + +jest.setTimeout(50000) const env = { MEDUSA_FF_MEDUSA_V2: true } diff --git a/integration-tests/plugins/__tests__/regions/admin/get-region.spec.ts b/integration-tests/plugins/__tests__/regions/admin/get-region.spec.ts new file mode 100644 index 0000000000..5269654761 --- /dev/null +++ b/integration-tests/plugins/__tests__/regions/admin/get-region.spec.ts @@ -0,0 +1,66 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IRegionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/regions/:id", () => { + let dbConnection + let appContainer + let shutdownServer + let regionModuleService: IRegionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get a region", async () => { + const [region] = await regionModuleService.create([ + { + name: "Test", + currency_code: "usd", + }, + ]) + + const api = useApi() as any + const response = await api.get(`/admin/regions/${region.id}`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.region).toEqual( + expect.objectContaining({ + id: region.id, + name: "Test", + currency_code: "usd", + }) + ) + }) +}) diff --git a/integration-tests/plugins/__tests__/regions/admin/list-regions.spec.ts b/integration-tests/plugins/__tests__/regions/admin/list-regions.spec.ts new file mode 100644 index 0000000000..14683ce621 --- /dev/null +++ b/integration-tests/plugins/__tests__/regions/admin/list-regions.spec.ts @@ -0,0 +1,66 @@ +import { ModuleRegistrationName } from "@medusajs/modules-sdk" +import { IRegionModuleService } from "@medusajs/types" +import path from "path" +import { startBootstrapApp } from "../../../../environment-helpers/bootstrap-app" +import { useApi } from "../../../../environment-helpers/use-api" +import { getContainer } from "../../../../environment-helpers/use-container" +import { initDb, useDb } from "../../../../environment-helpers/use-db" +import adminSeeder from "../../../../helpers/admin-seeder" + +jest.setTimeout(50000) + +const env = { MEDUSA_FF_MEDUSA_V2: true } +const adminHeaders = { + headers: { "x-medusa-access-token": "test_token" }, +} + +describe("GET /admin/regions", () => { + let dbConnection + let appContainer + let shutdownServer + let regionModuleService: IRegionModuleService + + beforeAll(async () => { + const cwd = path.resolve(path.join(__dirname, "..", "..", "..")) + dbConnection = await initDb({ cwd, env } as any) + shutdownServer = await startBootstrapApp({ cwd, env }) + appContainer = getContainer() + regionModuleService = appContainer.resolve(ModuleRegistrationName.REGION) + }) + + afterAll(async () => { + const db = useDb() + await db.shutdown() + await shutdownServer() + }) + + beforeEach(async () => { + await adminSeeder(dbConnection) + }) + + afterEach(async () => { + const db = useDb() + await db.teardown() + }) + + it("should get all regions and count", async () => { + await regionModuleService.create([ + { + name: "Test", + currency_code: "usd", + }, + ]) + + const api = useApi() as any + const response = await api.get(`/admin/regions`, adminHeaders) + + expect(response.status).toEqual(200) + expect(response.data.regions).toEqual([ + expect.objectContaining({ + id: expect.any(String), + name: "Test", + currency_code: "usd", + }), + ]) + }) +}) diff --git a/integration-tests/plugins/medusa-config.js b/integration-tests/plugins/medusa-config.js index fdfad225df..4eba0580ad 100644 --- a/integration-tests/plugins/medusa-config.js +++ b/integration-tests/plugins/medusa-config.js @@ -91,5 +91,10 @@ module.exports = { resources: "shared", resolve: "@medusajs/cart", }, + [Modules.REGION]: { + scope: "internal", + resources: "shared", + resolve: "@medusajs/region", + }, }, } diff --git a/integration-tests/plugins/package.json b/integration-tests/plugins/package.json index 17c355023e..50db400844 100644 --- a/integration-tests/plugins/package.json +++ b/integration-tests/plugins/package.json @@ -19,6 +19,7 @@ "@medusajs/pricing": "workspace:^", "@medusajs/product": "workspace:^", "@medusajs/promotion": "workspace:^", + "@medusajs/region": "workspace:^", "@medusajs/utils": "workspace:^", "faker": "^5.5.3", "medusa-fulfillment-webshipper": "workspace:*", diff --git a/packages/medusa/src/api-v2/admin/regions/[id]/route.ts b/packages/medusa/src/api-v2/admin/regions/[id]/route.ts new file mode 100644 index 0000000000..e8886d1013 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/regions/[id]/route.ts @@ -0,0 +1,19 @@ +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../../types/routing" +import { defaultAdminRegionFields } from "../query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const variables = { id: req.params.id } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "region", + variables, + fields: defaultAdminRegionFields, + }) + + const [region] = await remoteQuery(queryObject) + + res.status(200).json({ region }) +} diff --git a/packages/medusa/src/api-v2/admin/regions/middlewares.ts b/packages/medusa/src/api-v2/admin/regions/middlewares.ts new file mode 100644 index 0000000000..df15958d80 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/regions/middlewares.ts @@ -0,0 +1,30 @@ +import { transformQuery } from "../../../api/middlewares" +import { MiddlewareRoute } from "../../../loaders/helpers/routing/types" +import * as QueryConfig from "./query-config" +import { + AdminGetRegionsParams, + AdminGetRegionsRegionParams, +} from "./validators" + +export const adminRegionRoutesMiddlewares: MiddlewareRoute[] = [ + { + method: ["GET"], + matcher: "/admin/regions", + middlewares: [ + transformQuery( + AdminGetRegionsParams, + QueryConfig.listTransformQueryConfig + ), + ], + }, + { + method: ["GET"], + matcher: "/admin/regions/:id", + middlewares: [ + transformQuery( + AdminGetRegionsRegionParams, + QueryConfig.retrieveTransformQueryConfig + ), + ], + }, +] diff --git a/packages/medusa/src/api-v2/admin/regions/query-config.ts b/packages/medusa/src/api-v2/admin/regions/query-config.ts new file mode 100644 index 0000000000..e0256ffe51 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/regions/query-config.ts @@ -0,0 +1,27 @@ +export const defaultAdminRegionFields = [ + "id", + "name", + "currency_code", + "created_at", + "updated_at", + "deleted_at", + "metadata", + "countries.id", + "countries.iso_2", + "countries.iso_3", + "countries.num_code", + "countries.name", + "currency.code", + "currency.symbol", + "currency.symbol_native", + "currency.name", +] + +export const retrieveTransformQueryConfig = { + isList: false, +} + +export const listTransformQueryConfig = { + defaultLimit: 20, + isList: true, +} diff --git a/packages/medusa/src/api-v2/admin/regions/route.ts b/packages/medusa/src/api-v2/admin/regions/route.ts new file mode 100644 index 0000000000..bf2a9d4e45 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/regions/route.ts @@ -0,0 +1,20 @@ +import { remoteQueryObjectFromString } from "@medusajs/utils" +import { MedusaRequest, MedusaResponse } from "../../../types/routing" +import { defaultAdminRegionFields } from "./query-config" + +export const GET = async (req: MedusaRequest, res: MedusaResponse) => { + const remoteQuery = req.scope.resolve("remoteQuery") + + const variables = { filters: req.filterableFields } + + const queryObject = remoteQueryObjectFromString({ + entryPoint: "region", + variables, + fields: defaultAdminRegionFields, + }) + + // TODO: Add count, offset, limit + const regions = await remoteQuery(queryObject) + + res.json({ regions }) +} diff --git a/packages/medusa/src/api-v2/admin/regions/validators.ts b/packages/medusa/src/api-v2/admin/regions/validators.ts new file mode 100644 index 0000000000..31f75f9679 --- /dev/null +++ b/packages/medusa/src/api-v2/admin/regions/validators.ts @@ -0,0 +1,71 @@ +import { OperatorMap } from "@medusajs/types" +import { Type } from "class-transformer" +import { IsOptional, IsString, ValidateNested } from "class-validator" +import { FindParams, extendedFindParamsMixin } from "../../../types/common" +import { OperatorMapValidator } from "../../../types/validators/operator-map" + +export class AdminGetRegionsRegionParams extends FindParams {} + +/** + * Parameters used to filter and configure the pagination of the retrieved regions. + */ +export class AdminGetRegionsParams extends extendedFindParamsMixin({ + limit: 50, + offset: 0, +}) { + /** + * Search parameter for regions. + */ + @IsString({ each: true }) + @IsOptional() + id?: string | string[] + + /** + * Filter by currency code + */ + @IsString({ each: true }) + @IsOptional() + code?: string | string[] + + /** + * Filter by region name + */ + @IsString({ each: true }) + @IsOptional() + name?: string | string[] + + /** + * Date filters to apply on the regions' `created_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + created_at?: OperatorMap + + /** + * Date filters to apply on the regions' `updated_at` date. + */ + @IsOptional() + @ValidateNested() + @Type(() => OperatorMapValidator) + updated_at?: OperatorMap + + /** + * Date filters to apply on the regions' `deleted_at` date. + */ + @ValidateNested() + @IsOptional() + @Type(() => OperatorMapValidator) + deleted_at?: OperatorMap + + // Additional filters from BaseFilterable + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetRegionsParams) + $and?: AdminGetRegionsParams[] + + @IsOptional() + @ValidateNested({ each: true }) + @Type(() => AdminGetRegionsParams) + $or?: AdminGetRegionsParams[] +} diff --git a/packages/medusa/src/api-v2/middlewares.ts b/packages/medusa/src/api-v2/middlewares.ts index e7e440d096..4fc7aad0b3 100644 --- a/packages/medusa/src/api-v2/middlewares.ts +++ b/packages/medusa/src/api-v2/middlewares.ts @@ -1,11 +1,12 @@ import { MiddlewaresConfig } from "../loaders/helpers/routing/types" import { adminCampaignRoutesMiddlewares } from "./admin/campaigns/middlewares" import { adminCustomerGroupRoutesMiddlewares } from "./admin/customer-groups/middlewares" -import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" import { adminCustomerRoutesMiddlewares } from "./admin/customers/middlewares" import { adminPromotionRoutesMiddlewares } from "./admin/promotions/middlewares" -import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" +import { adminRegionRoutesMiddlewares } from "./admin/regions/middlewares" import { authRoutesMiddlewares } from "./auth/middlewares" +import { storeCartRoutesMiddlewares } from "./store/carts/middlewares" +import { storeCustomerRoutesMiddlewares } from "./store/customers/middlewares" export const config: MiddlewaresConfig = { routes: [ @@ -16,5 +17,6 @@ export const config: MiddlewaresConfig = { ...storeCustomerRoutesMiddlewares, ...storeCartRoutesMiddlewares, ...authRoutesMiddlewares, + ...adminRegionRoutesMiddlewares, ], } diff --git a/packages/medusa/src/loaders/index.ts b/packages/medusa/src/loaders/index.ts index f3310f8a8e..36e46ede97 100644 --- a/packages/medusa/src/loaders/index.ts +++ b/packages/medusa/src/loaders/index.ts @@ -8,7 +8,10 @@ import { Express, NextFunction, Request, Response } from "express" import databaseLoader, { dataSource } from "./database" import pluginsLoader, { registerPluginModels } from "./plugins" -import { ContainerRegistrationKeys, isString } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + isString +} from "@medusajs/utils" import { asValue } from "awilix" import { createMedusaContainer } from "medusa-core-utils" import { track } from "medusa-telemetry" diff --git a/packages/medusa/src/loaders/medusa-app.ts b/packages/medusa/src/loaders/medusa-app.ts index 2552bb01a3..af8cae1ccb 100644 --- a/packages/medusa/src/loaders/medusa-app.ts +++ b/packages/medusa/src/loaders/medusa-app.ts @@ -1,8 +1,9 @@ import { - MODULE_PACKAGE_NAMES, MedusaApp, + MedusaAppMigrateUp, MedusaAppOutput, MedusaModule, + MODULE_PACKAGE_NAMES, Modules, ModulesDefinition, } from "@medusajs/modules-sdk" @@ -12,9 +13,12 @@ import { MedusaContainer, ModuleDefinition, } from "@medusajs/types" -import { FlagRouter, MedusaV2Flag } from "@medusajs/utils" - -import { ContainerRegistrationKeys, isObject } from "@medusajs/utils" +import { + ContainerRegistrationKeys, + FlagRouter, + isObject, + MedusaV2Flag, +} from "@medusajs/utils" import { asValue } from "awilix" import { remoteQueryFetchData } from ".." import { joinerConfig } from "../joiner-config" @@ -37,6 +41,67 @@ export function mergeDefaultModules( return configModules } +export async function migrateMedusaApp( + { + configModule, + container, + }: { + configModule: { + modules?: CommonTypes.ConfigModule["modules"] + projectConfig: CommonTypes.ConfigModule["projectConfig"] + } + container: MedusaContainer + }, + config = { registerInContainer: true } +): Promise { + const featureFlagRouter = container.resolve("featureFlagRouter") + const isMedusaV2Enabled = featureFlagRouter.isFeatureEnabled(MedusaV2Flag.key) + const injectedDependencies = { + [ContainerRegistrationKeys.PG_CONNECTION]: container.resolve( + ContainerRegistrationKeys.PG_CONNECTION + ), + } + + const sharedResourcesConfig = { + database: { + clientUrl: configModule.projectConfig.database_url, + driverOptions: configModule.projectConfig.database_extra, + }, + } + + const configModules = mergeDefaultModules(configModule.modules) + + // Apply default options to legacy modules + for (const moduleKey of Object.keys(configModules)) { + if (!ModulesDefinition[moduleKey].isLegacy) { + continue + } + + if (isObject(configModules[moduleKey])) { + ;( + configModules[moduleKey] as Partial + ).options ??= { + database: { + type: "postgres", + url: configModule.projectConfig.database_url, + extra: configModule.projectConfig.database_extra, + schema: configModule.projectConfig.database_schema, + logging: configModule.projectConfig.database_logging, + }, + } + } + } + + await MedusaAppMigrateUp({ + modulesConfig: configModules, + servicesConfig: joinerConfig, + remoteFetchData: remoteQueryFetchData(container), + sharedContainer: container, + sharedResourcesConfig, + injectedDependencies, + }) +} + export const loadMedusaApp = async ( { configModule, diff --git a/packages/modules-sdk/src/loaders/module-loader.ts b/packages/modules-sdk/src/loaders/module-loader.ts index e590c881be..ed11bfdda2 100644 --- a/packages/modules-sdk/src/loaders/module-loader.ts +++ b/packages/modules-sdk/src/loaders/module-loader.ts @@ -13,13 +13,20 @@ export const moduleLoader = async ({ container, moduleResolutions, logger, + migrationOnly, }: { container: MedusaContainer moduleResolutions: Record logger: Logger + migrationOnly?: boolean }): Promise => { for (const resolution of Object.values(moduleResolutions ?? {})) { - const registrationResult = await loadModule(container, resolution, logger!) + const registrationResult = await loadModule( + container, + resolution, + logger!, + migrationOnly + ) if (registrationResult?.error) { const { error } = registrationResult @@ -40,7 +47,8 @@ export const moduleLoader = async ({ async function loadModule( container: MedusaContainer, resolution: ModuleResolution, - logger: Logger + logger: Logger, + migrationOnly?: boolean ): Promise<{ error?: Error } | void> { const modDefinition = resolution.definition const registrationName = modDefinition.registrationName @@ -77,5 +85,5 @@ async function loadModule( return } - return await loadInternalModule(container, resolution, logger) + return await loadInternalModule(container, resolution, logger, migrationOnly) } diff --git a/packages/modules-sdk/src/loaders/utils/load-internal.ts b/packages/modules-sdk/src/loaders/utils/load-internal.ts index 0d71bcf07f..e8f8bdb524 100644 --- a/packages/modules-sdk/src/loaders/utils/load-internal.ts +++ b/packages/modules-sdk/src/loaders/utils/load-internal.ts @@ -16,7 +16,8 @@ import { asFunction, asValue } from "awilix" export async function loadInternalModule( container: MedusaContainer, resolution: ModuleResolution, - logger: Logger + logger: Logger, + migrationOnly?: boolean ): Promise<{ error?: Error } | void> { const registrationName = resolution.definition.registrationName @@ -64,6 +65,17 @@ export async function loadInternalModule( } } + if (migrationOnly) { + // Partially loaded module, only register the service __joinerConfig function to be able to resolve it later + const moduleService = { + __joinerConfig: loadedModule.service.prototype.__joinerConfig, + } + container.register({ + [registrationName]: asValue(moduleService), + }) + return + } + const localContainer = createMedusaContainer() const dependencies = resolution?.dependencies ?? [] diff --git a/packages/modules-sdk/src/medusa-app.ts b/packages/modules-sdk/src/medusa-app.ts index d96ecf5d5d..ce372e6c49 100644 --- a/packages/modules-sdk/src/medusa-app.ts +++ b/packages/modules-sdk/src/medusa-app.ts @@ -67,7 +67,11 @@ export type SharedResources = { } } -async function loadModules(modulesConfig, sharedContainer) { +async function loadModules( + modulesConfig, + sharedContainer, + migrationOnly = false +) { const allModules = {} await Promise.all( @@ -106,6 +110,7 @@ async function loadModules(modulesConfig, sharedContainer) { sharedContainer, moduleDefinition: definition as ModuleDefinition, moduleExports, + migrationOnly, })) as LoadedModule const service = loaded[moduleName] @@ -190,18 +195,7 @@ export type MedusaAppOutput = { runMigrations: RunMigrationFn } -export async function MedusaApp({ - sharedContainer, - sharedResourcesConfig, - servicesConfig, - modulesConfigPath, - modulesConfigFileName, - modulesConfig, - linkModules, - remoteFetchData, - injectedDependencies, - onApplicationStartCb, -}: { +export type MedusaAppOptions = { sharedContainer?: MedusaContainer sharedResourcesConfig?: SharedResources loadedModules?: LoadedModule[] @@ -213,7 +207,21 @@ export async function MedusaApp({ remoteFetchData?: RemoteFetchDataCallback injectedDependencies?: any onApplicationStartCb?: () => void -} = {}): Promise<{ +} + +async function MedusaApp_({ + sharedContainer, + sharedResourcesConfig, + servicesConfig, + modulesConfigPath, + modulesConfigFileName, + modulesConfig, + linkModules, + remoteFetchData, + injectedDependencies = {}, + onApplicationStartCb, + migrationOnly = false, +}: MedusaAppOptions & { migrationOnly?: boolean } = {}): Promise<{ modules: Record link: RemoteLink | undefined query: ( @@ -224,8 +232,6 @@ export async function MedusaApp({ notFound?: Record> runMigrations: RunMigrationFn }> { - injectedDependencies ??= {} - const sharedContainer_ = createMedusaContainer({}, sharedContainer) const modules: MedusaModuleConfig = @@ -279,7 +285,7 @@ export async function MedusaApp({ }) } - const allModules = await loadModules(modules, sharedContainer_) + const allModules = await loadModules(modules, sharedContainer_, migrationOnly) // Share Event bus with link modules injectedDependencies[ModuleRegistrationName.EVENT_BUS] = @@ -346,16 +352,35 @@ export async function MedusaApp({ })) } - try { - return { - modules: allModules, - link: remoteLink, - query, - entitiesMap: schema.getTypeMap(), - notFound, - runMigrations, - } - } finally { - MedusaModule.onApplicationStart(onApplicationStartCb) + return { + modules: allModules, + link: remoteLink, + query, + entitiesMap: schema.getTypeMap(), + notFound, + runMigrations, } } + +export async function MedusaApp( + options: MedusaAppOptions = {} +): Promise { + try { + return await MedusaApp_(options) + } finally { + MedusaModule.onApplicationStart(options.onApplicationStartCb) + } +} + +export async function MedusaAppMigrateUp( + options: MedusaAppOptions = {} +): Promise { + const migrationOnly = true + + const { runMigrations } = await MedusaApp_({ + ...options, + migrationOnly, + }) + + await runMigrations().finally(MedusaModule.clearInstances) +} diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index f5629f02df..1d2cdd1e80 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -4,9 +4,9 @@ import { InternalModuleDeclaration, LinkModuleDefinition, LoadedModule, + MedusaContainer, MODULE_RESOURCE_TYPE, MODULE_SCOPE, - MedusaContainer, ModuleBootstrapDeclaration, ModuleDefinition, ModuleExports, @@ -59,6 +59,11 @@ export type ModuleBootstrapOptions = { sharedContainer?: MedusaContainer moduleDefinition?: ModuleDefinition injectedDependencies?: Record + /** + * In this mode, all instances are partially loaded, meaning that the module will not be fully loaded and the services will not be available. + * Don't forget to clear the instances (MedusaModule.clearInstances()) after the migration are done. + */ + migrationOnly?: boolean } export type LinkModuleBootstrapOptions = { @@ -213,6 +218,7 @@ export class MedusaModule { sharedContainer, moduleDefinition, injectedDependencies, + migrationOnly, }: ModuleBootstrapOptions): Promise<{ [key: string]: T }> { @@ -283,6 +289,7 @@ export class MedusaModule { container, moduleResolutions, logger, + migrationOnly, }) } catch (err) { errorLoading(err) diff --git a/packages/region/integration-tests/setup-env.js b/packages/region/integration-tests/setup-env.js index 10e738a799..7eff9a200b 100644 --- a/packages/region/integration-tests/setup-env.js +++ b/packages/region/integration-tests/setup-env.js @@ -4,3 +4,5 @@ if (typeof process.env.DB_TEMP_NAME === "undefined") { } process.env.MEDUSA_REGION_DB_SCHEMA = "public" +// TODO: Remove this when all modules are migrated to v2 +process.env.MEDUSA_FF_MEDUSA_V2 = true diff --git a/packages/region/src/index.ts b/packages/region/src/index.ts index f679a83abf..d449f73544 100644 --- a/packages/region/src/index.ts +++ b/packages/region/src/index.ts @@ -1,28 +1,10 @@ -import { Modules } from "@medusajs/modules-sdk" -import { ModulesSdkUtils } from "@medusajs/utils" - -import * as RegionModels from "@models" - -import { moduleDefinition } from "./module-definition" +import { + moduleDefinition, + revertMigration, + runMigrations, +} from "./module-definition" export default moduleDefinition - -const migrationScriptOptions = { - moduleName: Modules.REGION, - models: RegionModels, - pathToMigrations: __dirname + "/migrations", -} - -export const runMigrations = ModulesSdkUtils.buildMigrationScript( - migrationScriptOptions -) -export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( - migrationScriptOptions -) +export { revertMigration, runMigrations } export * from "./initialize" -export * from "./loaders" -export * from "./models" -export * from "./services" -export * from "./types" - diff --git a/packages/region/src/loaders/defaults.ts b/packages/region/src/loaders/defaults.ts index 4f842f984f..22e0207959 100644 --- a/packages/region/src/loaders/defaults.ts +++ b/packages/region/src/loaders/defaults.ts @@ -2,6 +2,12 @@ import { ModuleRegistrationName } from "@medusajs/modules-sdk" import { IRegionModuleService, LoaderOptions } from "@medusajs/types" export default async ({ container }: LoaderOptions): Promise => { - const service: IRegionModuleService = container.resolve(ModuleRegistrationName.REGION) - await service.createDefaultCountriesAndCurrencies() + const service: IRegionModuleService = container.resolve( + ModuleRegistrationName.REGION + ) + + // TODO: Remove when legacy modules have been migrated + if (!!process.env.MEDUSA_FF_MEDUSA_V2) { + await service.createDefaultCountriesAndCurrencies() + } } diff --git a/packages/region/src/migrations/RegionModuleSetup20240205173216.ts b/packages/region/src/migrations/RegionModuleSetup20240205173216.ts index 601180c149..c60010a279 100644 --- a/packages/region/src/migrations/RegionModuleSetup20240205173216.ts +++ b/packages/region/src/migrations/RegionModuleSetup20240205173216.ts @@ -1,3 +1,4 @@ +import { generatePostgresAlterColummnIfExistStatement } from "@medusajs/utils" import { Migration } from "@mikro-orm/migrations" export class RegionModuleSetup20240205173216 extends Migration { @@ -27,6 +28,13 @@ CREATE TABLE IF NOT EXISTS "region_currency" ( -- Adjust "region" table ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_3bdd5896ec93be2f1c62a3309a5"; ALTER TABLE "region" DROP CONSTRAINT IF EXISTS "FK_91f88052197680f9790272aaf5b"; + +${generatePostgresAlterColummnIfExistStatement( + "region", + ["tax_rate", "gift_cards_taxable", "automatic_taxes", "includes_tax"], + "DROP NOT NULL" +)} + ALTER TABLE "region" ADD CONSTRAINT "region_currency_code_foreign" FOREIGN KEY ("currency_code") REFERENCES "region_currency" ("code") ON UPDATE CASCADE; CREATE INDEX IF NOT EXISTS "IDX_region_currency_code" ON "region" ("currency_code"); diff --git a/packages/region/src/module-definition.ts b/packages/region/src/module-definition.ts index 24d9c5b777..cc36daff1d 100644 --- a/packages/region/src/module-definition.ts +++ b/packages/region/src/module-definition.ts @@ -1,14 +1,32 @@ import { ModuleExports } from "@medusajs/types" import { RegionModuleService } from "@services" +import { Modules } from "@medusajs/modules-sdk" +import { ModulesSdkUtils } from "@medusajs/utils" +import * as RegionModels from "@models" import loadConnection from "./loaders/connection" import loadContainer from "./loaders/container" import loadDefaults from "./loaders/defaults" +const migrationScriptOptions = { + moduleName: Modules.REGION, + models: RegionModels, + pathToMigrations: __dirname + "/migrations", +} + +export const runMigrations = ModulesSdkUtils.buildMigrationScript( + migrationScriptOptions +) +export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( + migrationScriptOptions +) + const service = RegionModuleService const loaders = [loadContainer, loadConnection, loadDefaults] as any export const moduleDefinition: ModuleExports = { service, loaders, + runMigrations, + revertMigration, } diff --git a/packages/region/src/services/region-module.ts b/packages/region/src/services/region-module.ts index ccda59e755..d39c18745d 100644 --- a/packages/region/src/services/region-module.ts +++ b/packages/region/src/services/region-module.ts @@ -172,7 +172,7 @@ export default class RegionModuleService< ): Promise { const [countries, count] = await this.countryService_.listAndCount( {}, - { select: ["id", "iso_2"], take: COUNTRIES_LIMIT }, + { select: ["iso_2"], take: COUNTRIES_LIMIT }, sharedContext ) diff --git a/packages/types/src/region/common.ts b/packages/types/src/region/common.ts index bb15ed6e5c..d05e514691 100644 --- a/packages/types/src/region/common.ts +++ b/packages/types/src/region/common.ts @@ -1,4 +1,4 @@ -import { BaseFilterable } from "../dal" +import { BaseFilterable, OperatorMap } from "../dal" export interface RegionDTO { id: string @@ -6,6 +6,9 @@ export interface RegionDTO { currency_code: string currency: RegionCurrencyDTO countries: CountryDTO[] + metadata?: Record + created_at: string + updated_at: string } export interface CountryDTO { @@ -19,8 +22,12 @@ export interface CountryDTO { export interface FilterableRegionProps extends BaseFilterable { - id?: string[] - name?: string[] + id?: string[] | string + name?: string | OperatorMap + currency_code?: string | OperatorMap + metadata?: Record | OperatorMap> + created_at?: OperatorMap + updated_at?: OperatorMap } export interface RegionCountryDTO { diff --git a/packages/utils/src/common/alter-columns-helper.ts b/packages/utils/src/common/alter-columns-helper.ts new file mode 100644 index 0000000000..49c284347e --- /dev/null +++ b/packages/utils/src/common/alter-columns-helper.ts @@ -0,0 +1,32 @@ +export function generatePostgresAlterColummnIfExistStatement( + tableName: string, + columns: string[], + alterExpression: string +) { + let script = ` + DO $$ + DECLARE + current_column text; + BEGIN` + + columns.forEach((column) => { + script += ` + current_column := '${column}'; + IF EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = '${tableName}' + AND column_name = current_column + ) THEN + EXECUTE format('ALTER TABLE %I ALTER COLUMN %I ${alterExpression}', '${tableName}', current_column); + ELSE + RAISE NOTICE 'Column % does not exist or alteration condition not met.', current_column; + END IF;` + }) + + script += ` + END$$; + ` + + return script +} diff --git a/packages/utils/src/common/index.ts b/packages/utils/src/common/index.ts index 85e47879ee..e654f2eb46 100644 --- a/packages/utils/src/common/index.ts +++ b/packages/utils/src/common/index.ts @@ -1,3 +1,4 @@ +export * from "./alter-columns-helper" export * from "./array-difference" export * from "./build-query" export * from "./camel-to-snake-case" @@ -45,3 +46,4 @@ export * from "./to-pascal-case" export * from "./transaction" export * from "./upper-case-first" export * from "./wrap-handler" + diff --git a/yarn.lock b/yarn.lock index 698f1efd27..73d0800a2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8605,7 +8605,7 @@ __metadata: languageName: unknown linkType: soft -"@medusajs/region@workspace:packages/region": +"@medusajs/region@workspace:^, @medusajs/region@workspace:packages/region": version: 0.0.0-use.local resolution: "@medusajs/region@workspace:packages/region" dependencies: @@ -31573,6 +31573,7 @@ __metadata: "@medusajs/pricing": "workspace:^" "@medusajs/product": "workspace:^" "@medusajs/promotion": "workspace:^" + "@medusajs/region": "workspace:^" "@medusajs/types": "workspace:^" "@medusajs/utils": "workspace:^" babel-preset-medusa-package: "*"