diff --git a/packages/customer/.gitignore b/packages/customer/.gitignore new file mode 100644 index 0000000000..874c6c69d3 --- /dev/null +++ b/packages/customer/.gitignore @@ -0,0 +1,6 @@ +/dist +node_modules +.DS_store +.env* +.env +*.sql diff --git a/packages/customer/CHANGELOG.md b/packages/customer/CHANGELOG.md new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/customer/README.md b/packages/customer/README.md new file mode 100644 index 0000000000..464bef28ac --- /dev/null +++ b/packages/customer/README.md @@ -0,0 +1,8 @@ +# Customer Module + +Customers represent entities who can make purchases in a store. + +### Features + +- The customer module enables you to store your customers’s contact details. +- The customer module enables you to group your customers. diff --git a/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts new file mode 100644 index 0000000000..bb4719c245 --- /dev/null +++ b/packages/customer/integration-tests/__tests__/services/customer-module/index.spec.ts @@ -0,0 +1,41 @@ +import { ICustomerModuleService } from "@medusajs/types" +import { initialize } from "../../../../src/initialize" +import { DB_URL, MikroOrmWrapper } from "../../../utils" + +jest.setTimeout(30000) + +describe("Customer Module Service", () => { + let service: ICustomerModuleService + + beforeEach(async () => { + await MikroOrmWrapper.setupDatabase() + + service = await initialize({ + database: { + clientUrl: DB_URL, + schema: process.env.MEDUSA_CUSTOMER_DB_SCHEMA, + }, + }) + }) + + afterEach(async () => { + await MikroOrmWrapper.clearDatabase() + }) + + describe("create", () => { + it("should create a customer", async () => { + const customerPromise = service.create({ + first_name: "John", + last_name: "Doe", + }) + + await expect(customerPromise).resolves.toEqual( + expect.objectContaining({ + id: expect.any(String), + first_name: "John", + last_name: "Doe", + }) + ) + }) + }) +}) diff --git a/packages/customer/integration-tests/setup-env.js b/packages/customer/integration-tests/setup-env.js new file mode 100644 index 0000000000..cc51317fb0 --- /dev/null +++ b/packages/customer/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-customer-integration-${tempName}` +} + +process.env.MEDUSA_CUSTOMER_DB_SCHEMA = "public" diff --git a/packages/customer/integration-tests/setup.js b/packages/customer/integration-tests/setup.js new file mode 100644 index 0000000000..43f99aab4a --- /dev/null +++ b/packages/customer/integration-tests/setup.js @@ -0,0 +1,3 @@ +import { JestUtils } from "medusa-test-utils" + +JestUtils.afterAllHookDropDatabase() diff --git a/packages/customer/integration-tests/utils/config.ts b/packages/customer/integration-tests/utils/config.ts new file mode 100644 index 0000000000..d5530a25f8 --- /dev/null +++ b/packages/customer/integration-tests/utils/config.ts @@ -0,0 +1,6 @@ +import { ModuleServiceInitializeOptions } from "@medusajs/types" + +export const databaseOptions: ModuleServiceInitializeOptions["database"] = { + schema: "public", + clientUrl: "medusa-customer-test", +} diff --git a/packages/customer/integration-tests/utils/database.ts b/packages/customer/integration-tests/utils/database.ts new file mode 100644 index 0000000000..118464d4d7 --- /dev/null +++ b/packages/customer/integration-tests/utils/database.ts @@ -0,0 +1,18 @@ +import { TestDatabaseUtils } from "medusa-test-utils" + +import * as Models from "@models" + +const pathToMigrations = "../../src/migrations" +const mikroOrmEntities = Models 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/customer/integration-tests/utils/index.ts b/packages/customer/integration-tests/utils/index.ts new file mode 100644 index 0000000000..5ca5d1bdc0 --- /dev/null +++ b/packages/customer/integration-tests/utils/index.ts @@ -0,0 +1,2 @@ +export * from "./config" +export * from "./database" diff --git a/packages/customer/jest.config.js b/packages/customer/jest.config.js new file mode 100644 index 0000000000..860ba90a49 --- /dev/null +++ b/packages/customer/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/customer/mikro-orm.config.dev.ts b/packages/customer/mikro-orm.config.dev.ts new file mode 100644 index 0000000000..6761df13e8 --- /dev/null +++ b/packages/customer/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-customer", + type: "postgresql", +} diff --git a/packages/customer/package.json b/packages/customer/package.json new file mode 100644 index 0000000000..0ce21007fd --- /dev/null +++ b/packages/customer/package.json @@ -0,0 +1,61 @@ +{ + "name": "@medusajs/customer", + "version": "0.0.1", + "description": "Medusa Customer module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist" + ], + "engines": { + "node": ">=16" + }, + "bin": { + "medusa-customer-seed": "dist/scripts/bin/run-seed.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/medusajs/medusa", + "directory": "packages/customer" + }, + "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": { + "@medusajs/types": "workspace:^", + "@mikro-orm/cli": "5.9.7", + "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/utils": "^1.11.2", + "@mikro-orm/core": "5.9.7", + "@mikro-orm/migrations": "5.9.7", + "@mikro-orm/postgresql": "5.9.7", + "awilix": "^8.0.0", + "dotenv": "^16.1.4", + "knex": "2.4.2" + } +} diff --git a/packages/customer/src/index.ts b/packages/customer/src/index.ts new file mode 100644 index 0000000000..c84e04625b --- /dev/null +++ b/packages/customer/src/index.ts @@ -0,0 +1,24 @@ +import { Modules } from "@medusajs/modules-sdk" +import { ModulesSdkUtils } from "@medusajs/utils" +import * as Models from "@models" + +import { moduleDefinition } from "./module-definition" + +export default moduleDefinition + +const migrationScriptOptions = { + moduleName: Modules.CUSTOMER, + models: Models, + pathToMigrations: __dirname + "/migrations", +} + +export const revertMigration = ModulesSdkUtils.buildRevertMigrationScript( + migrationScriptOptions +) + +export const runMigration = ModulesSdkUtils.buildMigrationScript( + migrationScriptOptions +) + +export * from "./initialize" +export * from "./loaders" diff --git a/packages/customer/src/initialize/index.ts b/packages/customer/src/initialize/index.ts new file mode 100644 index 0000000000..ad262c0587 --- /dev/null +++ b/packages/customer/src/initialize/index.ts @@ -0,0 +1,31 @@ +import { + ExternalModuleDeclaration, + InternalModuleDeclaration, + MedusaModule, + MODULE_PACKAGE_NAMES, + Modules, +} from "@medusajs/modules-sdk" +import { ICustomerModuleService, ModulesSdkTypes } from "@medusajs/types" +import { moduleDefinition } from "../module-definition" +import { InitializeModuleInjectableDependencies } from "../types" + +export const initialize = async ( + options?: + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + | ExternalModuleDeclaration + | InternalModuleDeclaration, + injectedDependencies?: InitializeModuleInjectableDependencies +): Promise => { + const loaded = await MedusaModule.bootstrap({ + moduleKey: Modules.CUSTOMER, + defaultPath: MODULE_PACKAGE_NAMES[Modules.CUSTOMER], + declaration: options as + | InternalModuleDeclaration + | ExternalModuleDeclaration, + injectedDependencies, + moduleExports: moduleDefinition, + }) + + return loaded[Modules.CUSTOMER] +} diff --git a/packages/customer/src/joiner-config.ts b/packages/customer/src/joiner-config.ts new file mode 100644 index 0000000000..4fed5e99df --- /dev/null +++ b/packages/customer/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 { Customer } from "@models" + +export const LinkableKeys = { + customer_id: Customer.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.CUSTOMER, + primaryKeys: ["id"], + linkableKeys: LinkableKeys, + alias: { + name: ["customer", "customers"], + args: { + entity: Customer.name, + }, + }, +} diff --git a/packages/customer/src/loaders/connection.ts b/packages/customer/src/loaders/connection.ts new file mode 100644 index 0000000000..cd0f565fa5 --- /dev/null +++ b/packages/customer/src/loaders/connection.ts @@ -0,0 +1,34 @@ +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 CustomerModels from "../models" + +export default async ( + { + options, + container, + logger, + }: LoaderOptions< + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + >, + moduleDeclaration?: InternalModuleDeclaration +): Promise => { + const entities = Object.values(CustomerModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + await ModulesSdkUtils.mikroOrmConnectionLoader({ + moduleName: Modules.CUSTOMER, + entities, + container, + options, + moduleDeclaration, + logger, + pathToMigrations, + }) +} diff --git a/packages/customer/src/loaders/container.ts b/packages/customer/src/loaders/container.ts new file mode 100644 index 0000000000..facacdd7df --- /dev/null +++ b/packages/customer/src/loaders/container.ts @@ -0,0 +1,52 @@ +import * as defaultRepositories from "@repositories" + +import { LoaderOptions } from "@medusajs/modules-sdk" +import { ModulesSdkTypes } from "@medusajs/types" +import { loadCustomRepositories } from "@medusajs/utils" +import * as defaultServices from "@services" +import { asClass } from "awilix" + +export default async ({ + container, + options, +}: LoaderOptions< + | ModulesSdkTypes.ModuleServiceInitializeOptions + | ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions +>): Promise => { + const customRepositories = ( + options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions + )?.repositories + + container.register({ + customerService: asClass(defaultServices.CustomerService).singleton(), + addressService: asClass(defaultServices.AddressService).singleton(), + customerGroupService: asClass( + defaultServices.CustomerGroupService + ).singleton(), + }) + + if (customRepositories) { + loadCustomRepositories({ + defaultRepositories, + customRepositories, + container, + }) + } else { + loadDefaultRepositories({ container }) + } +} + +function loadDefaultRepositories({ container }) { + container.register({ + baseRepository: asClass(defaultRepositories.BaseRepository).singleton(), + customerRepository: asClass( + defaultRepositories.CustomerRepository + ).singleton(), + addressRepository: asClass( + defaultRepositories.AddressRepository + ).singleton(), + customerGroupRepository: asClass( + defaultRepositories.CustomerGroupRepository + ).singleton(), + }) +} diff --git a/packages/customer/src/loaders/index.ts b/packages/customer/src/loaders/index.ts new file mode 100644 index 0000000000..3614963d8c --- /dev/null +++ b/packages/customer/src/loaders/index.ts @@ -0,0 +1,2 @@ +export * from "./connection" +export * from "./container" diff --git a/packages/customer/src/models/address.ts b/packages/customer/src/models/address.ts new file mode 100644 index 0000000000..ae641cac94 --- /dev/null +++ b/packages/customer/src/models/address.ts @@ -0,0 +1,89 @@ +import { DAL } from "@medusajs/types" +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + OnInit, + OptionalProps, + PrimaryKey, + Property, + ManyToOne, +} from "@mikro-orm/core" +import Customer from "./customer" + +type OptionalAddressProps = DAL.EntityDateColumns // TODO: To be revisited when more clear + +@Entity({ tableName: "customer_address" }) +export default class Address { + [OptionalProps]: OptionalAddressProps + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text" }) + customer_id: string + + @ManyToOne(() => Customer, { + fieldName: "customer_id", + nullable: true, + }) + customer?: Customer + + @Property({ columnType: "text", nullable: true }) + company: string | null = null + + @Property({ columnType: "text", nullable: true }) + first_name: string | null = null + + @Property({ columnType: "text", nullable: true }) + last_name: string | null = null + + @Property({ columnType: "text", nullable: true }) + address_1: string | null = null + + @Property({ columnType: "text", nullable: true }) + address_2: string | null = null + + @Property({ columnType: "text", nullable: true }) + city: string | null = null + + @Property({ columnType: "text", nullable: true }) + country_code: string | null = null + + @Property({ columnType: "text", nullable: true }) + province: string | null = null + + @Property({ columnType: "text", nullable: true }) + postal_code: string | null = null + + @Property({ columnType: "text", nullable: true }) + phone: string | null = null + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "cuaddr") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "cuaddr") + } +} diff --git a/packages/customer/src/models/customer-group-customer.ts b/packages/customer/src/models/customer-group-customer.ts new file mode 100644 index 0000000000..ec6ed1c242 --- /dev/null +++ b/packages/customer/src/models/customer-group-customer.ts @@ -0,0 +1,65 @@ +import { DAL } from "@medusajs/types" +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + ManyToOne, + Entity, + OnInit, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import Customer from "./customer" +import CustomerGroup from "./customer-group" + +type OptionalGroupProps = DAL.EntityDateColumns // TODO: To be revisited when more clear + +@Entity({ tableName: "customer_group_customer" }) +export default class CustomerGroupCustomer { + [OptionalProps]: OptionalGroupProps + + @PrimaryKey({ columnType: "text" }) + id!: string + + @ManyToOne({ + entity: () => Customer, + fieldName: "customer__id", + index: "IDX_customer_group_customer_customer_id", + }) + customer: Customer + + @ManyToOne({ + entity: () => CustomerGroup, + fieldName: "customer_group_id", + index: "IDX_customer_group_customer_group_id", + }) + customer_group: CustomerGroup + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "cusgc") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "cusgc") + } +} diff --git a/packages/customer/src/models/customer-group.ts b/packages/customer/src/models/customer-group.ts new file mode 100644 index 0000000000..9a6e70695f --- /dev/null +++ b/packages/customer/src/models/customer-group.ts @@ -0,0 +1,61 @@ +import { DAL } from "@medusajs/types" +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Entity, + OnInit, + OptionalProps, + PrimaryKey, + Property, + ManyToMany, + Collection, +} from "@mikro-orm/core" +import Customer from "./customer" +import CustomerGroupCustomer from "./customer-group-customer" + +type OptionalGroupProps = DAL.EntityDateColumns // TODO: To be revisited when more clear + +@Entity({ tableName: "customer_group" }) +export default class CustomerGroup { + [OptionalProps]: OptionalGroupProps + + @PrimaryKey({ columnType: "text" }) + id!: string + + @Property({ columnType: "text", nullable: true }) + name: string | null = null + + @ManyToMany({ + entity: () => Customer, + pivotEntity: () => CustomerGroupCustomer, + }) + customers = new Collection(this) + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "cusgroup") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "cusgroup") + } +} diff --git a/packages/customer/src/models/customer.ts b/packages/customer/src/models/customer.ts new file mode 100644 index 0000000000..150d9fccc4 --- /dev/null +++ b/packages/customer/src/models/customer.ts @@ -0,0 +1,115 @@ +import { DAL } from "@medusajs/types" +import { generateEntityId } from "@medusajs/utils" +import { + BeforeCreate, + Cascade, + Collection, + Entity, + Index, + ManyToMany, + ManyToOne, + OnInit, + OneToMany, + OptionalProps, + PrimaryKey, + Property, +} from "@mikro-orm/core" +import CustomerGroup from "./customer-group" +import CustomerGroupCustomer from "./customer-group-customer" +import Address from "./address" + +type OptionalCustomerProps = + | "groups" + | "addresses" + | "default_shipping_address" + | "default_billing_address" + | DAL.EntityDateColumns + +@Entity({ tableName: "customer" }) +export default class Customer { + [OptionalProps]?: OptionalCustomerProps + + @PrimaryKey({ columnType: "text" }) + id: string + + @Property({ columnType: "text", nullable: true }) + company_name: string | null = null + + @Property({ columnType: "text", nullable: true }) + first_name: string | null = null + + @Property({ columnType: "text", nullable: true }) + last_name: string | null = null + + @Property({ columnType: "text", nullable: true }) + email: string | null = null + + @Property({ columnType: "text", nullable: true }) + phone: string | null = null + + @Index({ name: "IDX_customer_default_shipping_address_id" }) + @Property({ columnType: "text", nullable: true }) + default_shipping_address_id: string | null = null + + @ManyToOne(() => Address, { + fieldName: "default_shipping_address_id", + nullable: true, + }) + default_shipping_address: Address | null + + @Index({ name: "IDX_customer_default_billing_address_id" }) + @Property({ columnType: "text", nullable: true }) + default_billing_address_id: string | null = null + + @ManyToOne(() => Address, { + fieldName: "default_billing_address_id", + nullable: true, + }) + default_billing_address: Address | null + + @Property({ columnType: "jsonb", nullable: true }) + metadata: Record | null = null + + @ManyToMany({ + inversedBy: (group) => group.customers, + entity: () => CustomerGroup, + pivotEntity: () => CustomerGroupCustomer, + }) + groups = new Collection(this) + + @OneToMany(() => Address, (address) => address.customer, { + cascade: [Cascade.REMOVE], + }) + addresses = new Collection
(this) + + @Property({ + onCreate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + created_at: Date + + @Property({ + onCreate: () => new Date(), + onUpdate: () => new Date(), + columnType: "timestamptz", + defaultRaw: "now()", + }) + updated_at: Date + + @Property({ columnType: "timestamptz", nullable: true }) + deleted_at: Date | null = null + + @Property({ columnType: "text", nullable: true }) + created_by: string | null = null + + @BeforeCreate() + onCreate() { + this.id = generateEntityId(this.id, "cus") + } + + @OnInit() + onInit() { + this.id = generateEntityId(this.id, "cus") + } +} diff --git a/packages/customer/src/models/index.ts b/packages/customer/src/models/index.ts new file mode 100644 index 0000000000..c2b6c076f9 --- /dev/null +++ b/packages/customer/src/models/index.ts @@ -0,0 +1,4 @@ +export { default as Address } from "./address" +export { default as Customer } from "./customer" +export { default as CustomerGroup } from "./customer-group" +export { default as CustomerGroupCustomer } from "./customer-group-customer" diff --git a/packages/customer/src/module-definition.ts b/packages/customer/src/module-definition.ts new file mode 100644 index 0000000000..d3e28a4c78 --- /dev/null +++ b/packages/customer/src/module-definition.ts @@ -0,0 +1,12 @@ +import { ModuleExports } from "@medusajs/types" +import { CustomerModuleService } from "@services" +import loadConnection from "./loaders/connection" +import loadContainer from "./loaders/container" + +const service = CustomerModuleService +const loaders = [loadContainer, loadConnection] as any + +export const moduleDefinition: ModuleExports = { + service, + loaders, +} diff --git a/packages/customer/src/repositories/address.ts b/packages/customer/src/repositories/address.ts new file mode 100644 index 0000000000..7d4871bd4a --- /dev/null +++ b/packages/customer/src/repositories/address.ts @@ -0,0 +1,11 @@ +import { DALUtils } from "@medusajs/utils" +import { Address } from "@models" +import { CreateAddressDTO, UpdateAddressDTO } from "@types" + +export class AddressRepository extends DALUtils.mikroOrmBaseRepositoryFactory< + Address, + { + create: CreateAddressDTO + update: UpdateAddressDTO + } +>(Address) {} diff --git a/packages/customer/src/repositories/customer-group.ts b/packages/customer/src/repositories/customer-group.ts new file mode 100644 index 0000000000..b6d167c164 --- /dev/null +++ b/packages/customer/src/repositories/customer-group.ts @@ -0,0 +1,11 @@ +import { DALUtils } from "@medusajs/utils" +import { CustomerGroup } from "@models" +import { CreateCustomerGroupDTO, UpdateCustomerGroupDTO } from "@types" + +export class CustomerGroupRepository extends DALUtils.mikroOrmBaseRepositoryFactory< + CustomerGroup, + { + create: CreateCustomerGroupDTO + update: UpdateCustomerGroupDTO + } +>(CustomerGroup) {} diff --git a/packages/customer/src/repositories/customer.ts b/packages/customer/src/repositories/customer.ts new file mode 100644 index 0000000000..e228d8549a --- /dev/null +++ b/packages/customer/src/repositories/customer.ts @@ -0,0 +1,11 @@ +import { DALUtils } from "@medusajs/utils" +import { Customer } from "@models" +import { CreateCustomerDTO, UpdateCustomerDTO } from "@medusajs/types" + +export class CustomerRepository extends DALUtils.mikroOrmBaseRepositoryFactory< + Customer, + { + create: CreateCustomerDTO + update: UpdateCustomerDTO + } +>(Customer) {} diff --git a/packages/customer/src/repositories/index.ts b/packages/customer/src/repositories/index.ts new file mode 100644 index 0000000000..106e6d94ec --- /dev/null +++ b/packages/customer/src/repositories/index.ts @@ -0,0 +1,4 @@ +export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils" +export * from "./address" +export * from "./customer" +export * from "./customer-group" diff --git a/packages/customer/src/scripts/bin/run-seed.ts b/packages/customer/src/scripts/bin/run-seed.ts new file mode 100644 index 0000000000..21f41d64b5 --- /dev/null +++ b/packages/customer/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-cart-seed ` + ) + } + + await run({ path }) +})() diff --git a/packages/customer/src/scripts/seed.ts b/packages/customer/src/scripts/seed.ts new file mode 100644 index 0000000000..103e1bff4b --- /dev/null +++ b/packages/customer/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 CustomerModels 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 { customerData } = 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: customerData.${EOL}${e}` + ) + throw e + } + ) + + const dbData = ModulesSdkUtils.loadDatabaseConfig(Modules.CUSTOMER, options)! + const entities = Object.values(CustomerModels) as unknown as EntitySchema[] + const pathToMigrations = __dirname + "/../migrations" + + const orm = await DALUtils.mikroOrmCreateConnection( + dbData, + entities, + pathToMigrations + ) + + const manager = orm.em.fork() + + try { + logger.info("Seeding customer data..") + + // TODO: implement customer seed data + // await createCustomers(manager, customersData) + } 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/customer/src/services/__tests__/index.spec.ts b/packages/customer/src/services/__tests__/index.spec.ts new file mode 100644 index 0000000000..728f6245c6 --- /dev/null +++ b/packages/customer/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/customer/src/services/address.ts b/packages/customer/src/services/address.ts new file mode 100644 index 0000000000..383a07707b --- /dev/null +++ b/packages/customer/src/services/address.ts @@ -0,0 +1,23 @@ +import { DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { Address } from "@models" +import { CreateAddressDTO, UpdateAddressDTO } from "@types" + +type InjectedDependencies = { + addressRepository: DAL.RepositoryService +} + +export default class AddressService< + TEntity extends Address = Address +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreateAddressDTO + update: UpdateAddressDTO + } +>(Address) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/customer/src/services/customer-group.ts b/packages/customer/src/services/customer-group.ts new file mode 100644 index 0000000000..8b5e4851ec --- /dev/null +++ b/packages/customer/src/services/customer-group.ts @@ -0,0 +1,23 @@ +import { DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { CustomerGroup } from "@models" +import { CreateCustomerGroupDTO, UpdateCustomerGroupDTO } from "@types" + +type InjectedDependencies = { + customerGroupRepository: DAL.RepositoryService +} + +export default class CustomerGroupService< + TEntity extends CustomerGroup = CustomerGroup +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { + create: CreateCustomerGroupDTO + update: UpdateCustomerGroupDTO + } +>(CustomerGroup) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/customer/src/services/customer-module.ts b/packages/customer/src/services/customer-module.ts new file mode 100644 index 0000000000..96d0fde771 --- /dev/null +++ b/packages/customer/src/services/customer-module.ts @@ -0,0 +1,81 @@ +import { + Context, + DAL, + FindConfig, + ICustomerModuleService, + InternalModuleDeclaration, + ModuleJoinerConfig, + CustomerTypes, +} from "@medusajs/types" + +import { InjectManager, MedusaContext } from "@medusajs/utils" +import { joinerConfig } from "../joiner-config" +import * as services from "../services" + +type InjectedDependencies = { + baseRepository: DAL.RepositoryService + customerService: services.CustomerService + addressService: services.AddressService + customerGroupService: services.CustomerGroupService +} + +export default class CustomerModuleService implements ICustomerModuleService { + protected baseRepository_: DAL.RepositoryService + protected customerService_: services.CustomerService + protected addressService_: services.AddressService + protected customerGroupService_: services.CustomerGroupService + + constructor( + { + baseRepository, + customerService, + addressService, + customerGroupService, + }: InjectedDependencies, + protected readonly moduleDeclaration: InternalModuleDeclaration + ) { + this.baseRepository_ = baseRepository + this.customerService_ = customerService + this.addressService_ = addressService + this.customerGroupService_ = customerGroupService + } + + __joinerConfig(): ModuleJoinerConfig { + return joinerConfig + } + + @InjectManager("baseRepository_") + async retrieve( + id: string, + config: FindConfig = {}, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const customer = await this.customerService_.retrieve( + id, + config, + sharedContext + ) + + return await this.baseRepository_.serialize( + customer, + { + populate: true, + } + ) + } + + @InjectManager("baseRepository_") + async create( + data: CustomerTypes.CreateCustomerDTO, + @MedusaContext() sharedContext: Context = {} + ): Promise { + const [customer] = await this.customerService_.create([data], sharedContext) + + return await this.baseRepository_.serialize( + customer, + { + populate: true, + } + ) + } +} diff --git a/packages/customer/src/services/customer.ts b/packages/customer/src/services/customer.ts new file mode 100644 index 0000000000..03523ee1b8 --- /dev/null +++ b/packages/customer/src/services/customer.ts @@ -0,0 +1,19 @@ +import { CreateCustomerDTO, DAL } from "@medusajs/types" +import { ModulesSdkUtils } from "@medusajs/utils" +import { Customer } from "@models" + +type InjectedDependencies = { + cartRepository: DAL.RepositoryService +} + +export default class CustomerService< + TEntity extends Customer = Customer +> extends ModulesSdkUtils.abstractServiceFactory< + InjectedDependencies, + { create: CreateCustomerDTO } +>(Customer) { + constructor(container: InjectedDependencies) { + // @ts-ignore + super(...arguments) + } +} diff --git a/packages/customer/src/services/index.ts b/packages/customer/src/services/index.ts new file mode 100644 index 0000000000..f77c4c1554 --- /dev/null +++ b/packages/customer/src/services/index.ts @@ -0,0 +1,4 @@ +export { default as AddressService } from "./address" +export { default as CustomerGroupService } from "./customer-group" +export { default as CustomerService } from "./customer" +export { default as CustomerModuleService } from "./customer-module" diff --git a/packages/customer/src/types/address.ts b/packages/customer/src/types/address.ts new file mode 100644 index 0000000000..02059b6d4b --- /dev/null +++ b/packages/customer/src/types/address.ts @@ -0,0 +1,28 @@ +export type CreateAddressDTO = { + customer_id: string + company?: string | null + first_name?: string | null + last_name?: string | null + address_1?: string | null + address_2?: string | null + city?: string | null + country_code?: string | null + province?: string | null + postal_code?: string | null + phone?: string | null + metadata?: Record | null +} + +export type UpdateAddressDTO = { + company?: string | null + first_name?: string | null + last_name?: string | null + address_1?: string | null + address_2?: string | null + city?: string | null + country_code?: string | null + province?: string | null + postal_code?: string | null + phone?: string | null + metadata?: Record | null +} diff --git a/packages/customer/src/types/customer-group.ts b/packages/customer/src/types/customer-group.ts new file mode 100644 index 0000000000..0245273b03 --- /dev/null +++ b/packages/customer/src/types/customer-group.ts @@ -0,0 +1,11 @@ +export type CreateCustomerGroupDTO = { + name: string + customer_ids?: string[] + metadata?: Record | null +} + +export type UpdateCustomerGroupDTO = { + name?: string + customer_ids?: string[] + metadata?: Record | null +} diff --git a/packages/customer/src/types/index.ts b/packages/customer/src/types/index.ts new file mode 100644 index 0000000000..8a40d6c8e7 --- /dev/null +++ b/packages/customer/src/types/index.ts @@ -0,0 +1,7 @@ +import { Logger } from "@medusajs/types" +export * from "./address" +export * from "./customer-group" + +export type InitializeModuleInjectableDependencies = { + logger?: Logger +} diff --git a/packages/customer/tsconfig.json b/packages/customer/tsconfig.json new file mode 100644 index 0000000000..6143fb1ef3 --- /dev/null +++ b/packages/customer/tsconfig.json @@ -0,0 +1,37 @@ +{ + "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"], + "@types": ["./src/types"], + } + }, + "include": ["src"], + "exclude": [ + "dist", + "./src/**/__tests__", + "./src/**/__mocks__", + "./src/**/__fixtures__", + "node_modules" + ] +} diff --git a/packages/customer/tsconfig.spec.json b/packages/customer/tsconfig.spec.json new file mode 100644 index 0000000000..48e47e8cbb --- /dev/null +++ b/packages/customer/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/modules-sdk/src/definitions.ts b/packages/modules-sdk/src/definitions.ts index a70b6f1b93..503ad6163f 100644 --- a/packages/modules-sdk/src/definitions.ts +++ b/packages/modules-sdk/src/definitions.ts @@ -16,6 +16,7 @@ export enum Modules { PROMOTION = "promotion", AUTHENTICATION = "authentication", CART = "cart", + CUSTOMER = "customer", PAYMENT = "payment", } @@ -29,6 +30,7 @@ export enum ModuleRegistrationName { PROMOTION = "promotionModuleService", AUTHENTICATION = "authenticationModuleService", CART = "cartModuleService", + CUSTOMER = "customerModuleService", PAYMENT = "paymentModuleService", } @@ -42,6 +44,7 @@ export const MODULE_PACKAGE_NAMES = { [Modules.PROMOTION]: "@medusajs/promotion", [Modules.AUTHENTICATION]: "@medusajs/authentication", [Modules.CART]: "@medusajs/cart", + [Modules.CUSTOMER]: "@medusajs/customer", [Modules.PAYMENT]: "@medusajs/payment", } @@ -174,6 +177,20 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } = resources: MODULE_RESOURCE_TYPE.SHARED, }, }, + [Modules.CUSTOMER]: { + key: Modules.CUSTOMER, + registrationName: ModuleRegistrationName.CUSTOMER, + defaultPackage: false, + label: upperCaseFirst(ModuleRegistrationName.CUSTOMER), + isRequired: false, + canOverride: true, + isQueryable: true, + dependencies: ["logger"], + defaultModuleDeclaration: { + scope: MODULE_SCOPE.INTERNAL, + resources: MODULE_RESOURCE_TYPE.SHARED, + }, + }, [Modules.PAYMENT]: { key: Modules.PAYMENT, registrationName: ModuleRegistrationName.PAYMENT, diff --git a/packages/types/src/customer/common.ts b/packages/types/src/customer/common.ts index 1aa6c14f3d..a59c00870b 100644 --- a/packages/types/src/customer/common.ts +++ b/packages/types/src/customer/common.ts @@ -1,6 +1,25 @@ import { AddressDTO } from "../address" export interface CustomerDTO { + id: string + email: string + default_billing_address_id?: string | null + default_shipping_address_id?: string | null + company_name?: string | null + first_name?: string | null + last_name?: string | null + default_billing_address?: AddressDTO + default_shipping_address?: AddressDTO + addresses?: AddressDTO[] + phone?: string | null + groups?: { id: string }[] + metadata?: Record + deleted_at?: Date | string + created_at?: Date | string + updated_at?: Date | string +} + +export type legacy_CustomerDTO = { id: string email: string billing_address_id?: string | null diff --git a/packages/types/src/customer/index.ts b/packages/types/src/customer/index.ts index 488a94fdff..cfbdfc76b8 100644 --- a/packages/types/src/customer/index.ts +++ b/packages/types/src/customer/index.ts @@ -1 +1,3 @@ export * from "./common" +export * from "./service" +export * from "./mutations" diff --git a/packages/types/src/customer/mutations.ts b/packages/types/src/customer/mutations.ts new file mode 100644 index 0000000000..c7127bf270 --- /dev/null +++ b/packages/types/src/customer/mutations.ts @@ -0,0 +1,17 @@ +export interface CreateCustomerDTO { + company_name?: string + first_name?: string + last_name?: string + email?: string + phone?: string + metadata?: Record +} + +export interface UpdateCustomerDTO { + company_name?: string + first_name?: string + last_name?: string + email?: string + phone?: string + metadata?: Record +} diff --git a/packages/types/src/customer/service.ts b/packages/types/src/customer/service.ts new file mode 100644 index 0000000000..82796e6d0f --- /dev/null +++ b/packages/types/src/customer/service.ts @@ -0,0 +1,15 @@ +import { FindConfig } from "../common" +import { IModuleService } from "../modules-sdk" +import { Context } from "../shared-context" +import { CustomerDTO } from "./common" +import { CreateCustomerDTO } from "./mutations" + +export interface ICustomerModuleService extends IModuleService { + retrieve( + customerId: string, + config?: FindConfig, + sharedContext?: Context + ): Promise + + create(data: CreateCustomerDTO, sharedContext?: Context): Promise +} diff --git a/yarn.lock b/yarn.lock index 69fb14775c..c945955f06 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7960,6 +7960,33 @@ __metadata: languageName: unknown linkType: soft +"@medusajs/customer@workspace:packages/customer": + version: 0.0.0-use.local + resolution: "@medusajs/customer@workspace:packages/customer" + dependencies: + "@medusajs/modules-sdk": ^1.12.5 + "@medusajs/types": "workspace:^" + "@medusajs/utils": ^1.11.2 + "@mikro-orm/cli": 5.9.7 + "@mikro-orm/core": 5.9.7 + "@mikro-orm/migrations": 5.9.7 + "@mikro-orm/postgresql": 5.9.7 + 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-customer-seed: dist/scripts/bin/run-seed.js + languageName: unknown + linkType: soft + "@medusajs/dashboard@workspace:packages/admin-next/dashboard": version: 0.0.0-use.local resolution: "@medusajs/dashboard@workspace:packages/admin-next/dashboard"