diff --git a/.changeset/itchy-knives-stare.md b/.changeset/itchy-knives-stare.md new file mode 100644 index 0000000000..b46b5528a8 --- /dev/null +++ b/.changeset/itchy-knives-stare.md @@ -0,0 +1,6 @@ +--- +"@medusajs/modules-sdk": patch +"@medusajs/types": patch +--- + +feat(types,modules-sdk): basic module setup for promotions diff --git a/packages/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index 78b00d0e9c..60b19f4905 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -4,6 +4,8 @@ import { ModuleDefinition, } from "@medusajs/types" +import { upperCaseFirst } from "@medusajs/utils" + export enum Modules { EVENT_BUS = "eventBus", STOCK_LOCATION = "stockLocationService", @@ -11,6 +13,7 @@ export enum Modules { CACHE = "cacheService", PRODUCT = "productService", PRICING = "pricingService", + PROMOTION = "promotion", } export enum ModuleRegistrationName { @@ -20,6 +23,7 @@ export enum ModuleRegistrationName { CACHE = "cacheService", PRODUCT = "productModuleService", PRICING = "pricingModuleService", + PROMOTION = "promotionModuleService", } export const MODULE_PACKAGE_NAMES = { @@ -29,6 +33,7 @@ export const MODULE_PACKAGE_NAMES = { [Modules.INVENTORY]: "@medusajs/inventory", [Modules.CACHE]: "@medusajs/cache-inmemory", [Modules.PRICING]: "@medusajs/pricing", + [Modules.PROMOTION]: "@medusajs/promotion", } export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = @@ -38,7 +43,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = isLegacy: true, registrationName: ModuleRegistrationName.EVENT_BUS, defaultPackage: MODULE_PACKAGE_NAMES[Modules.EVENT_BUS], - label: "EventBusModuleService", + label: upperCaseFirst(ModuleRegistrationName.EVENT_BUS), canOverride: true, isRequired: true, dependencies: ["logger"], @@ -52,7 +57,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = isLegacy: true, registrationName: ModuleRegistrationName.STOCK_LOCATION, defaultPackage: false, - label: "StockLocationService", + label: upperCaseFirst(ModuleRegistrationName.STOCK_LOCATION), isRequired: false, canOverride: true, isQueryable: true, @@ -67,7 +72,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = isLegacy: true, registrationName: ModuleRegistrationName.INVENTORY, defaultPackage: false, - label: "InventoryService", + label: upperCaseFirst(ModuleRegistrationName.INVENTORY), isRequired: false, canOverride: true, isQueryable: true, @@ -82,7 +87,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = isLegacy: true, registrationName: ModuleRegistrationName.CACHE, defaultPackage: MODULE_PACKAGE_NAMES[Modules.CACHE], - label: "CacheService", + label: upperCaseFirst(ModuleRegistrationName.CACHE), isRequired: true, canOverride: true, defaultModuleDeclaration: { @@ -94,7 +99,7 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = key: Modules.PRODUCT, registrationName: ModuleRegistrationName.PRODUCT, defaultPackage: false, - label: "ProductModuleService", + label: upperCaseFirst(ModuleRegistrationName.PRODUCT), isRequired: false, canOverride: true, isQueryable: true, @@ -108,7 +113,21 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = key: Modules.PRICING, registrationName: ModuleRegistrationName.PRICING, defaultPackage: false, - label: "PricingModuleService", + label: upperCaseFirst(ModuleRegistrationName.PRICING), + isRequired: false, + canOverride: true, + isQueryable: true, + dependencies: ["logger"], + defaultModuleDeclaration: { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.SHARED, + }, + }, + [Modules.PROMOTION]: { + key: Modules.PROMOTION, + registrationName: ModuleRegistrationName.PROMOTION, + defaultPackage: false, + label: upperCaseFirst(ModuleRegistrationName.PROMOTION), isRequired: false, canOverride: true, isQueryable: true, diff --git a/packages/modules-sdk/src/medusa-module.ts b/packages/modules-sdk/src/medusa-module.ts index 6b2a803341..19c036071e 100644 --- a/packages/modules-sdk/src/medusa-module.ts +++ b/packages/modules-sdk/src/medusa-module.ts @@ -3,9 +3,10 @@ import { InternalModuleDeclaration, LinkModuleDefinition, LoadedModule, - MedusaContainer, MODULE_RESOURCE_TYPE, MODULE_SCOPE, + MedusaContainer, + ModuleBootstrapDeclaration, ModuleDefinition, ModuleExports, ModuleJoinerConfig, @@ -52,7 +53,7 @@ type ModuleAlias = { export type ModuleBootstrapOptions = { moduleKey: string defaultPath: string - declaration?: InternalModuleDeclaration | ExternalModuleDeclaration + declaration?: ModuleBootstrapDeclaration moduleExports?: ModuleExports sharedContainer?: MedusaContainer moduleDefinition?: ModuleDefinition diff --git a/packages/promotion/.gitignore b/packages/promotion/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/promotion/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/promotion/CHANGELOG.md b/packages/promotion/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/promotion/README.md b/packages/promotion/README.md new file mode 100644 index 0000000000..ec2dbe6eec --- /dev/null +++ b/packages/promotion/README.md @@ -0,0 +1,3 @@ +# Promotion Module + +The PromotionModule is Medusa’s promotion engine. It offers functionality to discount carts through coupon codes with a given set of rules governing when and how the code should be applied. diff --git a/packages/promotion/integration-tests/__tests__/index.spec.ts b/packages/promotion/integration-tests/__tests__/index.spec.ts new file mode 100644 index 0000000000..728f6245c6 --- /dev/null +++ b/packages/promotion/integration-tests/__tests__/index.spec.ts @@ -0,0 +1,5 @@ +describe("Noop test", () => { + it("noop check", async () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/promotion/integration-tests/setup-env.js b/packages/promotion/integration-tests/setup-env.js new file mode 100644 index 0000000000..dc7c561c75 --- /dev/null +++ b/packages/promotion/integration-tests/setup-env.js @@ -0,0 +1,6 @@ +if (typeof process.env.DB_TEMP_NAME === "undefined") { + const tempName = parseInt(process.env.JEST_WORKER_ID || "1") + process.env.DB_TEMP_NAME = `medusa-promotion-integration-${tempName}` +} + +process.env.MEDUSA_PROMOTION_DB_SCHEMA = "public" diff --git a/packages/promotion/integration-tests/setup.js b/packages/promotion/integration-tests/setup.js new file mode 100644 index 0000000000..43f99aab4a --- /dev/null +++ b/packages/promotion/integration-tests/setup.js @@ -0,0 +1,3 @@ +import { JestUtils } from "medusa-test-utils" + +JestUtils.afterAllHookDropDatabase() diff --git a/packages/promotion/integration-tests/utils/config.ts b/packages/promotion/integration-tests/utils/config.ts new file mode 100644 index 0000000000..83730cc610 --- /dev/null +++ b/packages/promotion/integration-tests/utils/config.ts @@ -0,0 +1,6 @@ +import { ModuleServiceInitializeOptions } from "@medusajs/types" + +export const databaseOptions: ModuleServiceInitializeOptions["database"] = { + schema: "public", + clientUrl: "medusa-promotion-test", +} diff --git a/packages/promotion/integration-tests/utils/database.ts b/packages/promotion/integration-tests/utils/database.ts new file mode 100644 index 0000000000..8069e5a72f --- /dev/null +++ b/packages/promotion/integration-tests/utils/database.ts @@ -0,0 +1,18 @@ +import { TestDatabaseUtils } from "medusa-test-utils" + +import * as PromotionModels from "@models" + +const pathToMigrations = "../../src/migrations" +const mikroOrmEntities = PromotionModels as unknown as any[] + +export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper( + mikroOrmEntities, + pathToMigrations +) + +export const MikroOrmConfig = TestDatabaseUtils.getMikroOrmConfig( + mikroOrmEntities, + pathToMigrations +) + +export const DB_URL = TestDatabaseUtils.getDatabaseURL() diff --git a/packages/promotion/integration-tests/utils/index.ts b/packages/promotion/integration-tests/utils/index.ts new file mode 100644 index 0000000000..5ca5d1bdc0 --- /dev/null +++ b/packages/promotion/integration-tests/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./config" +export * from "./database" diff --git a/packages/promotion/jest.config.js b/packages/promotion/jest.config.js new file mode 100644 index 0000000000..860ba90a49 --- /dev/null +++ b/packages/promotion/jest.config.js @@ -0,0 +1,21 @@ +module.exports = { + moduleNameMapper: { + "^@models": "/src/models", + "^@services": "/src/services", + "^@repositories": "/src/repositories", + }, + transform: { + "^.+\\.[jt]s?$": [ + "ts-jest", + { + tsConfig: "tsconfig.spec.json", + isolatedModules: true, + }, + ], + }, + testEnvironment: `node`, + moduleFileExtensions: [`js`, `ts`], + modulePathIgnorePatterns: ["dist/"], + setupFiles: ["/integration-tests/setup-env.js"], + setupFilesAfterEnv: ["/integration-tests/setup.js"], +} diff --git a/packages/promotion/mikro-orm.config.dev.ts b/packages/promotion/mikro-orm.config.dev.ts new file mode 100644 index 0000000000..a07c8806f3 --- /dev/null +++ b/packages/promotion/mikro-orm.config.dev.ts @@ -0,0 +1,8 @@ +import * as entities from "./src/models" + +module.exports = { + entities: Object.values(entities), + schema: "public", + clientUrl: "postgres://postgres@localhost/medusa-promotion", + type: "postgresql", +} diff --git a/packages/promotion/package.json b/packages/promotion/package.json new file mode 100644 index 0000000000..10c86f3989 --- /dev/null +++ b/packages/promotion/package.json @@ -0,0 +1,63 @@ +{ + "name": "@medusajs/promotion", + "version": "0.0.1", + "description": "Medusa Promotion module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "engines": { + "node": ">=16" + }, + "bin": { + "medusa-promotion-migrations-down": "dist/scripts/bin/run-migration-down.js", + "medusa-promotion-migrations-up": "dist/scripts/bin/run-migration-up.js", + "medusa-promotion-seed": "dist/scripts/bin/run-seed.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/promotion" + }, + "publishConfig": { + "access": "public" + }, + "author": "Medusa", + "license": "MIT", + "scripts": { + "watch": "tsc --build --watch", + "watch:test": "tsc --build tsconfig.spec.json --watch", + "prepublishOnly": "cross-env NODE_ENV=production tsc --build && tsc-alias -p tsconfig.json", + "build": "rimraf dist && tsc --build && tsc-alias -p tsconfig.json", + "test": "jest --runInBand --bail --forceExit -- src/**/__tests__/**/*.ts", + "test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.ts", + "migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:generate", + "migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create --initial", + "migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:create", + "migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm migration:up", + "orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts mikro-orm cache:clear" + }, + "devDependencies": { + "@mikro-orm/cli": "5.7.12", + "cross-env": "^5.2.1", + "jest": "^29.6.3", + "medusa-test-utils": "^1.1.40", + "rimraf": "^3.0.2", + "ts-jest": "^29.1.1", + "ts-node": "^10.9.1", + "tsc-alias": "^1.8.6", + "typescript": "^5.1.6" + }, + "dependencies": { + "@medusajs/modules-sdk": "^1.12.5", + "@medusajs/types": "^1.11.9", + "@medusajs/utils": "^1.11.2", + "@mikro-orm/core": "5.7.12", + "@mikro-orm/migrations": "5.7.12", + "@mikro-orm/postgresql": "5.7.12", + "awilix": "^8.0.0", + "dotenv": "^16.1.4", + "knex": "2.4.2" + } +} diff --git a/packages/promotion/src/index.ts b/packages/promotion/src/index.ts new file mode 100644 index 0000000000..4ea0e7c3e3 --- /dev/null +++ b/packages/promotion/src/index.ts @@ -0,0 +1,7 @@ +import { moduleDefinition } from "./module-definition" + +export default moduleDefinition + +export * from "./initialize" +export * from "./loaders" +export * from "./scripts" diff --git a/packages/promotion/src/initialize/index.ts b/packages/promotion/src/initialize/index.ts new file mode 100644 index 0000000000..5a7caa6bf9 --- /dev/null +++ b/packages/promotion/src/initialize/index.ts @@ -0,0 +1,27 @@ +import { + ExternalModuleDeclaration, + InternalModuleDeclaration, + MODULE_PACKAGE_NAMES, + MedusaModule, + Modules, +} from "@medusajs/modules-sdk" +import { IPromotionModuleService, ModulesSdkTypes } from "@medusajs/types" +import { moduleDefinition } from "../module-definition" +import { InitializeModuleInjectableDependencies } from "../types" + +export const initialize = async ( + options?: ModulesSdkTypes.ModuleBootstrapDeclaration, + injectedDependencies?: InitializeModuleInjectableDependencies +): Promise => { + const loaded = await MedusaModule.bootstrap({ + moduleKey: Modules.PROMOTION, + defaultPath: MODULE_PACKAGE_NAMES[Modules.PROMOTION], + declaration: options as + | InternalModuleDeclaration + | ExternalModuleDeclaration, + injectedDependencies, + moduleExports: moduleDefinition, + }) + + return loaded[Modules.PROMOTION] +} diff --git a/packages/promotion/src/joiner-config.ts b/packages/promotion/src/joiner-config.ts new file mode 100644 index 0000000000..d7e301d7f2 --- /dev/null +++ b/packages/promotion/src/joiner-config.ts @@ -0,0 +1,31 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModuleJoinerConfig } from "@medusajs/types" +import { MapToConfig } from "@medusajs/utils" +import { Promotion } from "@models" + +export const LinkableKeys = { + promotion_id: Promotion.name, +} + +const entityLinkableKeysMap: MapToConfig = {} +Object.entries(LinkableKeys).forEach(([key, value]) => { + entityLinkableKeysMap[value] ??= [] + entityLinkableKeysMap[value].push({ + mapTo: key, + valueFrom: key.split("_").pop()!, + }) +}) + +export const entityNameToLinkableKeysMap: MapToConfig = entityLinkableKeysMap + +export const joinerConfig: ModuleJoinerConfig = { + serviceName: Modules.PROMOTION, + primaryKeys: ["id"], + linkableKeys: LinkableKeys, + alias: { + name: ["promotion", "promotions"], + args: { + entity: Promotion.name, + }, + }, +} diff --git a/packages/promotion/src/loaders/connection.ts b/packages/promotion/src/loaders/connection.ts new file mode 100644 index 0000000000..a1b7cf9790 --- /dev/null +++ b/packages/promotion/src/loaders/connection.ts @@ -0,0 +1,30 @@ +import { InternalModuleDeclaration, LoaderOptions, Modules } from "@medusajs/modules-sdk" +import { ModulesSdkTypes } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { EntitySchema } from "@mikro-orm/core" +import * as PromotionModels from "../models" + +export default async ( + { + options, + container, + logger, + }: LoaderOptions< + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + >, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + const entities = Object.values(PromotionModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + await ModulesSdkUtils.mikroOrmConnectionLoader({ + moduleName: Modules.PROMOTION, + entities, + container, + options, + moduleDeclaration, + logger, + pathToMigrations, + }) +} diff --git a/packages/promotion/src/loaders/container.ts b/packages/promotion/src/loaders/container.ts new file mode 100644 index 0000000000..ac06bd7677 --- /dev/null +++ b/packages/promotion/src/loaders/container.ts @@ -0,0 +1,38 @@ +import * as defaultRepositories from "@repositories" + +import { LoaderOptions } from "@medusajs/modules-sdk" +import { ModulesSdkTypes } from "@medusajs/types" +import { loadCustomRepositories } from "@medusajs/utils" +import { asClass } from "awilix" + +export default async ({ + container, + options, +}: LoaderOptions< + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions +>): Promise => { + const customRepositories = ( + options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + )?.repositories + + container.register({ + // promotionService: asClass(defaultServices.PromotionService).singleton(), + }) + + if (customRepositories) { + loadCustomRepositories({ + defaultRepositories, + customRepositories, + container, + }) + } else { + loadDefaultRepositories({ container }) + } +} + +function loadDefaultRepositories({ container }) { + container.register({ + baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), + }) +} diff --git a/packages/promotion/src/loaders/index.ts b/packages/promotion/src/loaders/index.ts new file mode 100644 index 0000000000..3614963d8c --- /dev/null +++ b/packages/promotion/src/loaders/index.ts @@ -0,0 +1,2 @@ +export * from "./connection" +export * from "./container" diff --git a/packages/promotion/src/migrations/.snapshot-medusa-promotions.json b/packages/promotion/src/migrations/.snapshot-medusa-promotions.json new file mode 100644 index 0000000000..aed248bc5e --- /dev/null +++ b/packages/promotion/src/migrations/.snapshot-medusa-promotions.json @@ -0,0 +1,36 @@ +{ + "namespaces": [ + "public" + ], + "name": "public", + "tables": [ + { + "columns": { + "id": { + "name": "id", + "type": "text", + "unsigned": false, + "autoincrement": false, + "primary": false, + "nullable": false, + "mappedType": "text" + } + }, + "name": "promotion", + "schema": "public", + "indexes": [ + { + "keyName": "promotion_pkey", + "columnNames": [ + "id" + ], + "composite": false, + "primary": true, + "unique": true + } + ], + "checks": [], + "foreignKeys": {} + } + ] +} diff --git a/packages/promotion/src/migrations/Migration20231218142613.ts b/packages/promotion/src/migrations/Migration20231218142613.ts new file mode 100644 index 0000000000..ee0c83775d --- /dev/null +++ b/packages/promotion/src/migrations/Migration20231218142613.ts @@ -0,0 +1,9 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20231218142613 extends Migration { + + async up(): Promise { + this.addSql('create table "promotion" ("id" text not null, constraint "promotion_pkey" primary key ("id"));'); + } + +} diff --git a/packages/promotion/src/models/index.ts b/packages/promotion/src/models/index.ts new file mode 100644 index 0000000000..a5c0052922 --- /dev/null +++ b/packages/promotion/src/models/index.ts @@ -0,0 +1 @@ +export { default as Promotion } from "./promotion" diff --git a/packages/promotion/src/models/promotion.ts b/packages/promotion/src/models/promotion.ts new file mode 100644 index 0000000000..ad99440f91 --- /dev/null +++ b/packages/promotion/src/models/promotion.ts @@ -0,0 +1,18 @@ +import { generateEntityId } from "@medusajs/utils" +import { BeforeCreate, Entity, PrimaryKey, OnInit } from "@mikro-orm/core" + +@Entity() +export default class Promotion { + @PrimaryKey({ columnType: "text" }) + id!: string + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "promo") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "promo") + } +} diff --git a/packages/promotion/src/module-definition.ts b/packages/promotion/src/module-definition.ts new file mode 100644 index 0000000000..db87261647 --- /dev/null +++ b/packages/promotion/src/module-definition.ts @@ -0,0 +1,12 @@ +import { ModuleExports } from "@medusajs/types" +import { PromotionModuleService } from "@services" +import loadConnection from "./loaders/connection" +import loadContainer from "./loaders/container" + +const service = PromotionModuleService +const loaders = [loadContainer, loadConnection] as any + +export const moduleDefinition: ModuleExports = { + service, + loaders, +} diff --git a/packages/promotion/src/repositories/index.ts b/packages/promotion/src/repositories/index.ts new file mode 100644 index 0000000000..147c9cc259 --- /dev/null +++ b/packages/promotion/src/repositories/index.ts @@ -0,0 +1 @@ +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" diff --git a/packages/promotion/src/scripts/bin/run-migration-down.ts b/packages/promotion/src/scripts/bin/run-migration-down.ts new file mode 100644 index 0000000000..e352048fd4 --- /dev/null +++ b/packages/promotion/src/scripts/bin/run-migration-down.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +export default (async () => { + const { revertMigration } = await import("../migration-down") + const { config } = await import("dotenv") + config() + await revertMigration() +})() diff --git a/packages/promotion/src/scripts/bin/run-migration-up.ts b/packages/promotion/src/scripts/bin/run-migration-up.ts new file mode 100644 index 0000000000..35f3bee853 --- /dev/null +++ b/packages/promotion/src/scripts/bin/run-migration-up.ts @@ -0,0 +1,8 @@ +#!/usr/bin/env node + +export default (async () => { + const { runMigrations } = await import("../migration-up") + const { config } = await import("dotenv") + config() + await runMigrations() +})() diff --git a/packages/promotion/src/scripts/bin/run-seed.ts b/packages/promotion/src/scripts/bin/run-seed.ts new file mode 100644 index 0000000000..823708ccf4 --- /dev/null +++ b/packages/promotion/src/scripts/bin/run-seed.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node + +import { EOL } from "os" +import { run } from "../seed" + +const args = process.argv +const path = args.pop() as string + +export default (async () => { + const { config } = await import("dotenv") + config() + if (!path) { + throw new Error( + `filePath is required.${EOL}Example: medusa-promotion-seed ` + ) + } + + await run({ path }) +})() diff --git a/packages/promotion/src/scripts/index.ts b/packages/promotion/src/scripts/index.ts new file mode 100644 index 0000000000..cfa5c5ddf5 --- /dev/null +++ b/packages/promotion/src/scripts/index.ts @@ -0,0 +1,2 @@ +export * from "./migration-up" +export * from "./migration-down" diff --git a/packages/promotion/src/scripts/migration-down.ts b/packages/promotion/src/scripts/migration-down.ts new file mode 100644 index 0000000000..9272bb3685 --- /dev/null +++ b/packages/promotion/src/scripts/migration-down.ts @@ -0,0 +1,43 @@ +import { Modules } from "@medusajs/modules-sdk" +import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" +import { DALUtils, ModulesSdkUtils } from "@medusajs/utils" +import { EntitySchema } from "@mikro-orm/core" +import * as PromotionModels from "@models" + +/** + * This script is only valid for mikro orm managers. If a user provide a custom manager + * he is in charge of reverting the migrations. + * @param options + * @param logger + * @param moduleDeclaration + */ +export async function revertMigration({ + options, + logger, +}: Pick< + LoaderOptions, + "options" | "logger" +> = {}) { + logger ??= console as unknown as Logger + + const dbData = ModulesSdkUtils.loadDatabaseConfig(Modules.PROMOTION, options)! + const entities = Object.values(PromotionModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + const orm = await DALUtils.mikroOrmCreateConnection( + dbData, + entities, + pathToMigrations + ) + + try { + const migrator = orm.getMigrator() + await migrator.down() + + logger?.info("Promotion module migration executed") + } catch (error) { + logger?.error(`Promotion module migration failed to run - Error: ${error}`) + } + + await orm.close() +} diff --git a/packages/promotion/src/scripts/migration-up.ts b/packages/promotion/src/scripts/migration-up.ts new file mode 100644 index 0000000000..704086e02b --- /dev/null +++ b/packages/promotion/src/scripts/migration-up.ts @@ -0,0 +1,55 @@ +import { Modules } from "@medusajs/modules-sdk" +import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" +import { DALUtils, ModulesSdkUtils } from "@medusajs/utils" +import { EntitySchema } from "@mikro-orm/core" +import * as PromotionModels from "@models" + +/** + * This script is only valid for mikro orm managers. If a user provide a custom manager + * he is in charge of running the migrations. + * @param options + * @param logger + * @param moduleDeclaration + */ +export async function runMigrations({ + options, + logger, +}: Pick< + LoaderOptions, + "options" | "logger" +> = {}) { + logger ??= console as unknown as Logger + + const dbData = ModulesSdkUtils.loadDatabaseConfig(Modules.PROMOTION, options)! + const entities = Object.values(PromotionModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + const orm = await DALUtils.mikroOrmCreateConnection( + dbData, + entities, + pathToMigrations + ) + + try { + const migrator = orm.getMigrator() + + const pendingMigrations = await migrator.getPendingMigrations() + logger.info( + `Running pending migrations: ${JSON.stringify( + pendingMigrations, + null, + 2 + )}` + ) + + await migrator.up({ + migrations: pendingMigrations.map((m) => m.name), + }) + + logger.info("Promotion module migration executed") + } catch (error) { + logger.error(`Promotion module migration failed to run - Error: ${error}`) + } + + await orm.close() +} diff --git a/packages/promotion/src/scripts/seed.ts b/packages/promotion/src/scripts/seed.ts new file mode 100644 index 0000000000..eb51fa0095 --- /dev/null +++ b/packages/promotion/src/scripts/seed.ts @@ -0,0 +1,58 @@ +import { Modules } from "@medusajs/modules-sdk" +import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types" +import { DALUtils, ModulesSdkUtils } from "@medusajs/utils" +import { EntitySchema } from "@mikro-orm/core" +import * as PromotionModels from "@models" +import { EOL } from "os" +import { resolve } from "path" + +export async function run({ + options, + logger, + path, +}: Partial< + Pick< + LoaderOptions, + "options" | "logger" + > +> & { + path: string +}) { + logger ??= console as unknown as Logger + + logger.info(`Loading seed data from ${path}...`) + + const { promotionsData } = await import(resolve(process.cwd(), path)).catch( + (e) => { + logger?.error( + `Failed to load seed data from ${path}. Please, provide a relative path and check that you export the following: promotionsData.${EOL}${e}` + ) + throw e + } + ) + + const dbData = ModulesSdkUtils.loadDatabaseConfig(Modules.PROMOTION, options)! + const entities = Object.values(PromotionModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + const orm = await DALUtils.mikroOrmCreateConnection( + dbData, + entities, + pathToMigrations + ) + + const manager = orm.em.fork() + + try { + logger.info("Inserting promotions..") + + // TODO: implement promotions seed data + // await createPromotions(manager, promotionsData) + } catch (e) { + logger.error( + `Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}` + ) + } + + await orm.close(true) +} diff --git a/packages/promotion/src/services/__tests__/index.spec.ts b/packages/promotion/src/services/__tests__/index.spec.ts new file mode 100644 index 0000000000..728f6245c6 --- /dev/null +++ b/packages/promotion/src/services/__tests__/index.spec.ts @@ -0,0 +1,5 @@ +describe("Noop test", () => { + it("noop check", async () => { + expect(true).toBe(true) + }) +}) diff --git a/packages/promotion/src/services/index.ts b/packages/promotion/src/services/index.ts new file mode 100644 index 0000000000..c127ea20f4 --- /dev/null +++ b/packages/promotion/src/services/index.ts @@ -0,0 +1 @@ +export { default as PromotionModuleService } from "./promotion-module" diff --git a/packages/promotion/src/services/promotion-module.ts b/packages/promotion/src/services/promotion-module.ts new file mode 100644 index 0000000000..468a140f3f --- /dev/null +++ b/packages/promotion/src/services/promotion-module.ts @@ -0,0 +1,32 @@ +import { + DAL, + InternalModuleDeclaration, + ModuleJoinerConfig, + PromotionTypes, +} from "@medusajs/types" + +import { Promotion } from "@models" + +import { joinerConfig } from "../joiner-config" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService +} + +export default class PromotionModuleService< + TPromotion extends Promotion = Promotion +> implements PromotionTypes.IPromotionModuleService +{ + protected baseRepository_: DAL.RepositoryService + + constructor( + { baseRepository }: InjectedDependencies, + protected readonly moduleDeclaration: InternalModuleDeclaration + ) { + this.baseRepository_ = baseRepository + } + + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig + } +} diff --git a/packages/promotion/src/types/index.ts b/packages/promotion/src/types/index.ts new file mode 100644 index 0000000000..0f252977b0 --- /dev/null +++ b/packages/promotion/src/types/index.ts @@ -0,0 +1,5 @@ +import { Logger } from "@medusajs/types" + +export type InitializeModuleInjectableDependencies = { + logger?: Logger +} diff --git a/packages/promotion/tsconfig.json b/packages/promotion/tsconfig.json new file mode 100644 index 0000000000..213e38fc55 --- /dev/null +++ b/packages/promotion/tsconfig.json @@ -0,0 +1,36 @@ +{ + "compilerOptions": { + "lib": ["es2020"], + "target": "es2020", + "outDir": "./dist", + "esModuleInterop": true, + "declaration": true, + "module": "commonjs", + "moduleResolution": "node", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "sourceMap": false, + "noImplicitReturns": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "allowJs": true, + "skipLibCheck": true, + "downlevelIteration": true, // to use ES5 specific tooling + "baseUrl": ".", + "resolveJsonModule": true, + "paths": { + "@models": ["./src/models"], + "@services": ["./src/services"], + "@repositories": ["./src/repositories"] + } + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/promotion/tsconfig.spec.json b/packages/promotion/tsconfig.spec.json new file mode 100644 index 0000000000..48e47e8cbb --- /dev/null +++ b/packages/promotion/tsconfig.spec.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "include": ["src", "integration-tests"], + "exclude": ["node_modules", "dist"], + "compilerOptions": { + "sourceMap": true + } +} diff --git a/packages/types/src/bundles.ts b/packages/types/src/bundles.ts index f928d021cc..198bb2684d 100644 --- a/packages/types/src/bundles.ts +++ b/packages/types/src/bundles.ts @@ -9,6 +9,7 @@ export * as LoggerTypes from "./logger" export * as ModulesSdkTypes from "./modules-sdk" export * as PricingTypes from "./pricing" export * as ProductTypes from "./product" +export * as PromotionTypes from "./promotion" export * as RegionTypes from "./region" export * as SalesChannelTypes from "./sales-channel" export * as SearchTypes from "./search" diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 85df67d803..829c57461b 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -16,6 +16,7 @@ export * from "./modules-sdk" export * from "./pricing" export * from "./product" export * from "./product-category" +export * from "./promotion" export * from "./region" export * from "./sales-channel" export * from "./search" diff --git a/packages/types/src/modules-sdk/index.ts b/packages/types/src/modules-sdk/index.ts index 43ff5622a8..1d52937aaa 100644 --- a/packages/types/src/modules-sdk/index.ts +++ b/packages/types/src/modules-sdk/index.ts @@ -1,8 +1,12 @@ -import { JoinerRelationship, JoinerServiceConfig, RemoteJoinerQuery } from "../joiner" +import { + JoinerRelationship, + JoinerServiceConfig, + RemoteJoinerQuery, +} from "../joiner" -import { Logger } from "../logger" import { MedusaContainer } from "../common" import { RepositoryService } from "../dal" +import { Logger } from "../logger" export type Constructor = new (...args: any[]) => T export * from "../common/medusa-container" @@ -261,7 +265,22 @@ export type ModuleServiceInitializeCustomDataLayerOptions = { } } +export type ModuleBootstrapDeclaration = + | InternalModuleDeclaration + | ExternalModuleDeclaration +// TODO: These should be added back when the chain of types are fixed +// | ModuleServiceInitializeOptions +// | ModuleServiceInitializeCustomDataLayerOptions + export type RemoteQueryFunction = ( query: string | RemoteJoinerQuery | object, variables?: Record -) => Promise | null \ No newline at end of file +) => Promise | null + +export interface IModuleService { + __joinerConfig?(): ModuleJoinerConfig + + __hooks?: { + onApplicationStart?: () => Promise + } +} diff --git a/packages/types/src/promotion/index.ts b/packages/types/src/promotion/index.ts new file mode 100644 index 0000000000..9376fea807 --- /dev/null +++ b/packages/types/src/promotion/index.ts @@ -0,0 +1 @@ +export * from "./service" diff --git a/packages/types/src/promotion/service.ts b/packages/types/src/promotion/service.ts new file mode 100644 index 0000000000..6a12a02aaa --- /dev/null +++ b/packages/types/src/promotion/service.ts @@ -0,0 +1,3 @@ +import { IModuleService } from "../modules-sdk" + +export interface IPromotionModuleService extends IModuleService {} diff --git a/yarn.lock b/yarn.lock index 48d14724f8..bab445b0c9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7904,6 +7904,35 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/promotion@workspace:packages/promotion": + version: 0.0.0-use.local + resolution: "@medusajs/promotion@workspace:packages/promotion" + dependencies: + "@medusajs/modules-sdk": ^1.12.5 + "@medusajs/types": ^1.11.9 + "@medusajs/utils": ^1.11.2 + "@mikro-orm/cli": 5.7.12 + "@mikro-orm/core": 5.7.12 + "@mikro-orm/migrations": 5.7.12 + "@mikro-orm/postgresql": 5.7.12 + awilix: ^8.0.0 + cross-env: ^5.2.1 + dotenv: ^16.1.4 + jest: ^29.6.3 + knex: 2.4.2 + medusa-test-utils: ^1.1.40 + rimraf: ^3.0.2 + ts-jest: ^29.1.1 + ts-node: ^10.9.1 + tsc-alias: ^1.8.6 + typescript: ^5.1.6 + bin: + medusa-promotion-migrations-down: dist/scripts/bin/run-migration-down.js + medusa-promotion-migrations-up: dist/scripts/bin/run-migration-up.js + medusa-promotion-seed: dist/scripts/bin/run-seed.js + languageName: unknown + linkType: soft + "@medusajs/stock-location@workspace:packages/stock-location": version: 0.0.0-use.local resolution: "@medusajs/stock-location@workspace:packages/stock-location"