feat(cart): Add cart module package (#5982)

* feat: Add CartModule foundation

* remove migration

* fix ts issue

* Create hot-dingos-pay.md
This commit is contained in:
Oli Juhl
2024-01-02 15:59:23 +01:00
committed by GitHub
parent 487067fb48
commit 925feea04a
44 changed files with 679 additions and 3 deletions

View File

@@ -0,0 +1,7 @@
---
"@medusajs/medusa-react": patch
"@medusajs/types": patch
"@medusajs/modules-sdk": patch
---
feat(cart): Add cart module package

6
packages/cart/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
/dist
node_modules
.DS_store
.env*
.env
*.sql

View File

3
packages/cart/README.md Normal file
View File

@@ -0,0 +1,3 @@
# Cart Module
A Cart is a collection of items that a customer intends to purchase. The Cart also stores where the items should be shipped, how they should be shipped, how the goods will be paid for and who the customer is. The Cart facilitates calculations of totals and validation of purchase flows.

View File

@@ -0,0 +1,5 @@
describe("Noop test", () => {
it("noop check", async () => {
expect(true).toBe(true)
})
})

View File

@@ -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-cart-integration-${tempName}`
}
process.env.MEDUSA_CART_DB_SCHEMA = "public"

View File

@@ -0,0 +1,3 @@
import { JestUtils } from "medusa-test-utils"
JestUtils.afterAllHookDropDatabase()

View File

@@ -0,0 +1,6 @@
import { ModuleServiceInitializeOptions } from "@medusajs/types"
export const databaseOptions: ModuleServiceInitializeOptions["database"] = {
schema: "public",
clientUrl: "medusa-cart-test",
}

View File

@@ -0,0 +1,18 @@
import { TestDatabaseUtils } from "medusa-test-utils"
import * as CartModels from "@models"
const pathToMigrations = "../../src/migrations"
const mikroOrmEntities = CartModels as unknown as any[]
export const MikroOrmWrapper = TestDatabaseUtils.getMikroOrmWrapper(
mikroOrmEntities,
pathToMigrations
)
export const MikroOrmConfig = TestDatabaseUtils.getMikroOrmConfig(
mikroOrmEntities,
pathToMigrations
)
export const DB_URL = TestDatabaseUtils.getDatabaseURL()

View File

@@ -0,0 +1,2 @@
export * from "./config"
export * from "./database"

View File

@@ -0,0 +1,21 @@
module.exports = {
moduleNameMapper: {
"^@models": "<rootDir>/src/models",
"^@services": "<rootDir>/src/services",
"^@repositories": "<rootDir>/src/repositories",
},
transform: {
"^.+\\.[jt]s?$": [
"ts-jest",
{
tsConfig: "tsconfig.spec.json",
isolatedModules: true,
},
],
},
testEnvironment: `node`,
moduleFileExtensions: [`js`, `ts`],
modulePathIgnorePatterns: ["dist/"],
setupFiles: ["<rootDir>/integration-tests/setup-env.js"],
setupFilesAfterEnv: ["<rootDir>/integration-tests/setup.js"],
}

View File

@@ -0,0 +1,8 @@
import * as entities from "./src/models"
module.exports = {
entities: Object.values(entities),
schema: "public",
clientUrl: "postgres://postgres@localhost/medusa-cart",
type: "postgresql",
}

View File

@@ -0,0 +1,63 @@
{
"name": "@medusajs/cart",
"version": "0.0.1",
"description": "Medusa Cart module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"dist"
],
"engines": {
"node": ">=16"
},
"bin": {
"medusa-cart-migrations-down": "dist/scripts/bin/run-migration-down.js",
"medusa-cart-migrations-up": "dist/scripts/bin/run-migration-up.js",
"medusa-cart-seed": "dist/scripts/bin/run-seed.js"
},
"repository": {
"type": "git",
"url": "https://github.com/medusajs/medusa",
"directory": "packages/cart"
},
"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"
}
}

View File

@@ -0,0 +1,7 @@
import { moduleDefinition } from "./module-definition"
export default moduleDefinition
export * from "./initialize"
export * from "./loaders"
export * from "./scripts"

View File

@@ -0,0 +1,27 @@
import {
ExternalModuleDeclaration,
InternalModuleDeclaration,
MedusaModule,
MODULE_PACKAGE_NAMES,
Modules,
} from "@medusajs/modules-sdk"
import { ICartModuleService, ModulesSdkTypes } from "@medusajs/types"
import { moduleDefinition } from "../module-definition"
import { InitializeModuleInjectableDependencies } from "../types"
export const initialize = async (
options?: ModulesSdkTypes.ModuleBootstrapDeclaration,
injectedDependencies?: InitializeModuleInjectableDependencies
): Promise<ICartModuleService> => {
const loaded = await MedusaModule.bootstrap<ICartModuleService>({
moduleKey: Modules.CART,
defaultPath: MODULE_PACKAGE_NAMES[Modules.CART],
declaration: options as
| InternalModuleDeclaration
| ExternalModuleDeclaration,
injectedDependencies,
moduleExports: moduleDefinition,
})
return loaded[Modules.CART]
}

View File

@@ -0,0 +1,31 @@
import { Modules } from "@medusajs/modules-sdk"
import { ModuleJoinerConfig } from "@medusajs/types"
import { MapToConfig } from "@medusajs/utils"
import { Cart } from "@models"
export const LinkableKeys = {
cart_id: Cart.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.CART,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: {
name: ["cart", "carts"],
args: {
entity: Cart.name,
},
},
}

View File

@@ -0,0 +1,36 @@
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 CartModels from "../models"
export default async (
{
options,
container,
logger,
}: LoaderOptions<
| ModulesSdkTypes.ModuleServiceInitializeOptions
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
>,
moduleDeclaration?: InternalModuleDeclaration
): Promise<void> => {
const entities = Object.values(
CartModels
) as unknown as EntitySchema[]
const pathToMigrations = __dirname + "/../migrations"
await ModulesSdkUtils.mikroOrmConnectionLoader({
moduleName: Modules.CART,
entities,
container,
options,
moduleDeclaration,
logger,
pathToMigrations,
})
}

View File

@@ -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<void> => {
const customRepositories = (
options as ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
)?.repositories
container.register({
// cartService: asClass(defaultServices.CartService).singleton(),
})
if (customRepositories) {
loadCustomRepositories({
defaultRepositories,
customRepositories,
container,
})
} else {
loadDefaultRepositories({ container })
}
}
function loadDefaultRepositories({ container }) {
container.register({
baseRepository: asClass(defaultRepositories.BaseRepository).singleton(),
})
}

View File

@@ -0,0 +1,2 @@
export * from "./connection"
export * from "./container"

View File

@@ -0,0 +1,18 @@
import { generateEntityId } from "@medusajs/utils"
import { BeforeCreate, Entity, OnInit, PrimaryKey } from "@mikro-orm/core"
@Entity()
export default class Cart {
@PrimaryKey({ columnType: "text" })
id!: string
@BeforeCreate()
onCreate() {
this.id = generateEntityId(this.id, "cart")
}
@OnInit()
onInit() {
this.id = generateEntityId(this.id, "cart")
}
}

View File

@@ -0,0 +1,2 @@
export { default as Cart } from "./cart";

View File

@@ -0,0 +1,12 @@
import { ModuleExports } from "@medusajs/types"
import { CartModuleService } from "@services"
import loadConnection from "./loaders/connection"
import loadContainer from "./loaders/container"
const service = CartModuleService
const loaders = [loadContainer, loadConnection] as any
export const moduleDefinition: ModuleExports = {
service,
loaders,
}

View File

@@ -0,0 +1 @@
export { MikroOrmBaseRepository as BaseRepository } from "@medusajs/utils"

View File

@@ -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()
})()

View File

@@ -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()
})()

View File

@@ -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 <filePath>`
)
}
await run({ path })
})()

View File

@@ -0,0 +1,2 @@
export * from "./migration-up"
export * from "./migration-down"

View File

@@ -0,0 +1,50 @@
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 CartModels 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<ModulesSdkTypes.ModuleServiceInitializeOptions>,
"options" | "logger"
> = {}) {
logger ??= console as unknown as Logger
const dbData = ModulesSdkUtils.loadDatabaseConfig(
Modules.CART,
options
)!
const entities = Object.values(
CartModels
) 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("Cart module migration executed")
} catch (error) {
logger?.error(
`Cart module migration failed to run - Error: ${error}`
)
}
await orm.close()
}

View File

@@ -0,0 +1,62 @@
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 CartModels 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<ModulesSdkTypes.ModuleServiceInitializeOptions>,
"options" | "logger"
> = {}) {
logger ??= console as unknown as Logger
const dbData = ModulesSdkUtils.loadDatabaseConfig(
Modules.CART,
options
)!
const entities = Object.values(
CartModels
) 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("Cart module migration executed")
} catch (error) {
logger.error(
`Cart module migration failed to run - Error: ${error}`
)
}
await orm.close()
}

View File

@@ -0,0 +1,63 @@
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 CartModels from "@models"
import { EOL } from "os"
import { resolve } from "path"
export async function run({
options,
logger,
path,
}: Partial<
Pick<
LoaderOptions<ModulesSdkTypes.ModuleServiceInitializeOptions>,
"options" | "logger"
>
> & {
path: string
}) {
logger ??= console as unknown as Logger
logger.info(`Loading seed data from ${path}...`)
const { cartData } = 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: cartData.${EOL}${e}`
)
throw e
})
const dbData = ModulesSdkUtils.loadDatabaseConfig(
Modules.CART,
options
)!
const entities = Object.values(
CartModels
) 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 cart data..")
// TODO: implement cart seed data
// await createCarts(manager, cartsData)
} catch (e) {
logger.error(
`Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}`
)
}
await orm.close(true)
}

View File

@@ -0,0 +1,5 @@
describe("Noop test", () => {
it("noop check", async () => {
expect(true).toBe(true)
})
})

View File

@@ -0,0 +1,32 @@
import {
CartTypes,
DAL,
InternalModuleDeclaration,
ModuleJoinerConfig,
} from "@medusajs/types"
import { Cart } from "@models"
import { joinerConfig } from "../joiner-config"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
}
export default class CartModuleService<
TCart extends Cart = Cart
> implements CartTypes.ICartModuleService
{
protected baseRepository_: DAL.RepositoryService
constructor(
{ baseRepository }: InjectedDependencies,
protected readonly moduleDeclaration: InternalModuleDeclaration
) {
this.baseRepository_ = baseRepository
}
__joinerConfig(): ModuleJoinerConfig {
return joinerConfig
}
}

View File

@@ -0,0 +1,2 @@
export { default as CartModuleService } from "./cart-module";

View File

@@ -0,0 +1,5 @@
import { Logger } from "@medusajs/types"
export type InitializeModuleInjectableDependencies = {
logger?: Logger
}

View File

@@ -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"
]
}

View File

@@ -0,0 +1,8 @@
{
"extends": "./tsconfig.json",
"include": ["src", "integration-tests"],
"exclude": ["node_modules", "dist"],
"compilerOptions": {
"sourceMap": true
}
}

View File

@@ -86,9 +86,7 @@ export const buildCustomOptions = <
})
}
if (options?.onSuccess) {
return options.onSuccess(...args)
}
return options?.onSuccess?.(...args)
},
}
}

View File

@@ -15,6 +15,7 @@ export enum Modules {
PRICING = "pricingService",
PROMOTION = "promotion",
AUTHENTICATION = "authentication",
CART = "cart",
}
export enum ModuleRegistrationName {
@@ -26,6 +27,7 @@ export enum ModuleRegistrationName {
PRICING = "pricingModuleService",
PROMOTION = "promotionModuleService",
AUTHENTICATION = "authenticationModuleService",
CART = "cartModuleService",
}
export const MODULE_PACKAGE_NAMES = {
@@ -37,6 +39,7 @@ export const MODULE_PACKAGE_NAMES = {
[Modules.PRICING]: "@medusajs/pricing",
[Modules.PROMOTION]: "@medusajs/promotion",
[Modules.AUTHENTICATION]: "@medusajs/authentication",
[Modules.CART]: "@medusajs/cart",
}
export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } =
@@ -154,6 +157,20 @@ export const ModulesDefinition: { [key: string | Modules]: ModuleDefinition } =
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
[Modules.CART]: {
key: Modules.CART,
registrationName: ModuleRegistrationName.CART,
defaultPackage: false,
label: upperCaseFirst(ModuleRegistrationName.CART),
isRequired: false,
canOverride: true,
isQueryable: true,
dependencies: ["logger"],
defaultModuleDeclaration: {
scope: MODULE_SCOPE.INTERNAL,
resources: MODULE_RESOURCE_TYPE.SHARED,
},
},
}
export const MODULE_DEFINITIONS: ModuleDefinition[] =

View File

@@ -5,3 +5,4 @@ export * from "./medusa-app"
export * from "./medusa-module"
export * from "./remote-link"
export * from "./remote-query"

View File

@@ -1,5 +1,6 @@
export * as AuthenticationTypes from "./authentication"
export * as CacheTypes from "./cache"
export * as CartTypes from "./cart"
export * as CommonTypes from "./common"
export * as CustomerTypes from "./customer"
export * as DAL from "./dal"
@@ -17,3 +18,4 @@ export * as SearchTypes from "./search"
export * as StockLocationTypes from "./stock-location"
export * as TransactionBaseTypes from "./transaction-base"
export * as WorkflowTypes from "./workflow"

View File

@@ -1 +1,3 @@
export * from "./common"
export * from "./service"

View File

@@ -0,0 +1,4 @@
import { IModuleService } from "../modules-sdk";
export interface ICartModuleService extends IModuleService {
}

View File

@@ -25,3 +25,4 @@ export * from "./shared-context"
export * from "./stock-location"
export * from "./transaction-base"
export * from "./workflow"

View File

@@ -7464,6 +7464,35 @@ __metadata:
languageName: unknown
linkType: soft
"@medusajs/cart@workspace:packages/cart":
version: 0.0.0-use.local
resolution: "@medusajs/cart@workspace:packages/cart"
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-cart-migrations-down: dist/scripts/bin/run-migration-down.js
medusa-cart-migrations-up: dist/scripts/bin/run-migration-up.js
medusa-cart-seed: dist/scripts/bin/run-seed.js
languageName: unknown
linkType: soft
"@medusajs/client-types@workspace:packages/generated/client-types":
version: 0.0.0-use.local
resolution: "@medusajs/client-types@workspace:packages/generated/client-types"