Chore/rm main entity concept (#7709)

**What**
Update the `MedusaService` class, factory and types to remove the concept of main modules. The idea being that all method will be explicitly named and suffixes to represent the object you are trying to manipulate.
This pr also includes various fixes in different modules

Co-authored-by: Stevche Radevski <4820812+sradevski@users.noreply.github.com>
Co-authored-by: Oli Juhl <59018053+olivermrbl@users.noreply.github.com>
This commit is contained in:
Adrien de Peretti
2024-06-19 15:02:16 +02:00
committed by GitHub
parent 2895ccfba8
commit 48963f55ef
533 changed files with 6469 additions and 9769 deletions

View File

@@ -1,3 +1,7 @@
import { moduleDefinition } from "./module-definition"
import { UserModuleService } from "@services"
import { ModuleExports } from "@medusajs/types"
const moduleDefinition: ModuleExports = {
service: UserModuleService,
}
export default moduleDefinition

View File

@@ -1,41 +1,11 @@
import { Invite, User } from "@models"
import { MapToConfig } from "@medusajs/utils"
import { ModuleJoinerConfig } from "@medusajs/types"
import {
buildEntitiesNameToLinkableKeysMap,
defineJoinerConfig,
MapToConfig,
} from "@medusajs/utils"
import { Modules } from "@medusajs/modules-sdk"
export const LinkableKeys = {
user_id: User.name,
invite_id: Invite.name,
}
export const joinerConfig = defineJoinerConfig(Modules.USER)
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.USER,
primaryKeys: ["id"],
linkableKeys: LinkableKeys,
alias: [
{
name: ["user", "users"],
args: {
entity: User.name,
},
},
{
name: ["invite", "invites"],
args: {
entity: Invite.name,
methodSuffix: "Invites",
},
},
],
}
export const entityNameToLinkableKeysMap: MapToConfig =
buildEntitiesNameToLinkableKeysMap(joinerConfig.linkableKeys)

View File

@@ -1,8 +0,0 @@
import { UserModuleService } from "@services"
import { ModuleExports } from "@medusajs/types"
const service = UserModuleService
export const moduleDefinition: ModuleExports = {
service,
}

View File

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

View File

@@ -1,19 +0,0 @@
#!/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-user-seed <filePath>`
)
}
await run({ path })
})()

View File

@@ -1,58 +0,0 @@
import * as UserModels from "@models"
import { DALUtils, ModulesSdkUtils } from "@medusajs/utils"
import { LoaderOptions, Logger, ModulesSdkTypes } from "@medusajs/types"
import { EOL } from "os"
import { EntitySchema } from "@mikro-orm/core"
import { Modules } from "@medusajs/modules-sdk"
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 { userData } = 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: userData.${EOL}${e}`
)
throw e
})
const dbData = ModulesSdkUtils.loadDatabaseConfig(Modules.USER, options)!
const entities = Object.values(UserModels) 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 user data..")
// TODO: implement user seed data
// await createUsers(manager, usersData)
} catch (e) {
logger.error(
`Failed to insert the seed data in the PostgreSQL database ${dbData.clientUrl}.${EOL}${e}`
)
}
await orm.close(true)
}

View File

@@ -1,2 +1 @@
export { default as UserModuleService } from "./user-module"
export { default as InviteService } from "./invite"

View File

@@ -1,184 +0,0 @@
import * as crypto from "crypto"
import { Context, DAL } from "@medusajs/types"
import {
arrayDifference,
InjectTransactionManager,
MedusaError,
ModulesSdkUtils,
} from "@medusajs/utils"
import jwt, { JwtPayload } from "jsonwebtoken"
import { Invite } from "@models"
import { InviteServiceTypes } from "@types"
type InjectedDependencies = {
inviteRepository: DAL.RepositoryService
}
// 1 day
const DEFAULT_VALID_INVITE_DURATION = 60 * 60 * 24 * 1000
export default class InviteService<
TEntity extends Invite = Invite
> extends ModulesSdkUtils.MedusaInternalService<InjectedDependencies>(
Invite
)<TEntity> {
// eslint-disable-next-line max-len
protected readonly inviteRepository_: DAL.RepositoryService<TEntity>
protected options_: { jwt_secret: string; valid_duration: number } | undefined
constructor(container: InjectedDependencies) {
super(container)
this.inviteRepository_ = container.inviteRepository
}
public withModuleOptions(options: any) {
const service = new InviteService<TEntity>(this.__container__)
service.options_ = options
return service
}
private getOption(key: string) {
if (!this.options_) {
throw new MedusaError(
MedusaError.Types.UNEXPECTED_STATE,
`Options are not configured for InviteService, call "withModuleOptions" and provide options`
)
}
return this.options_[key]
}
create(
data: InviteServiceTypes.CreateInviteDTO,
context?: Context
): Promise<TEntity>
create(
data: InviteServiceTypes.CreateInviteDTO[],
context?: Context
): Promise<TEntity[]>
@InjectTransactionManager("inviteRepository_")
async create(
data:
| InviteServiceTypes.CreateInviteDTO
| InviteServiceTypes.CreateInviteDTO[],
context: Context = {}
): Promise<TEntity | TEntity[]> {
const data_ = Array.isArray(data) ? data : [data]
const invites = await super.create(data_, context)
const expiresIn: number = this.getValidDuration()
const updates = invites.map((invite) => {
return {
id: invite.id,
expires_at: new Date().setMilliseconds(
new Date().getMilliseconds() + expiresIn
),
token: this.generateToken({ id: invite.id }),
}
})
return await super.update(updates, context)
}
@InjectTransactionManager("inviteRepository_")
async refreshInviteTokens(
inviteIds: string[],
context: Context = {}
): Promise<TEntity[]> {
const [invites, count] = await super.listAndCount(
{ id: inviteIds },
{},
context
)
if (count !== inviteIds.length) {
const missing = arrayDifference(
inviteIds,
invites.map((invite) => invite.id)
)
if (missing.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The following invites do not exist: ${missing.join(", ")}`
)
}
}
const expiresIn: number = this.getValidDuration()
const updates = invites.map((invite) => {
return {
id: invite.id,
expires_at: new Date().setMilliseconds(
new Date().getMilliseconds() + expiresIn
),
token: this.generateToken({ id: invite.id }),
}
})
return await super.update(updates, context)
}
@InjectTransactionManager("inviteRepository_")
async validateInviteToken(
token: string,
context?: Context
): Promise<TEntity> {
const decoded = this.validateToken(token)
const invite = await super.retrieve(decoded.payload.id, {}, context)
if (invite.expires_at < new Date()) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The invite has expired"
)
}
return invite
}
private generateToken(data: any): string {
const jwtSecret: string = this.getOption("jwt_secret")
const expiresIn: number = this.getValidDuration() / 1000
if (!jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
return jwt.sign(data, jwtSecret, {
jwtid: crypto.randomUUID(),
expiresIn,
})
}
private getValidDuration(): number {
return (
parseInt(this.getOption("valid_duration")) ||
DEFAULT_VALID_INVITE_DURATION
)
}
private validateToken(data: any): JwtPayload {
const jwtSecret = this.getOption("jwt_secret")
if (!jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
return jwt.verify(data, jwtSecret, { complete: true })
}
}

View File

@@ -8,40 +8,40 @@ import {
UserTypes,
} from "@medusajs/types"
import {
arrayDifference,
CommonEvents,
EmitEvents,
InjectManager,
InjectTransactionManager,
MedusaContext,
ModulesSdkUtils,
MedusaError,
MedusaService,
UserEvents,
} from "@medusajs/utils"
import jwt, { JwtPayload } from "jsonwebtoken"
import { entityNameToLinkableKeysMap, joinerConfig } from "../joiner-config"
import crypto from "node:crypto"
import { Invite, User } from "@models"
import InviteService from "./invite"
type InjectedDependencies = {
baseRepository: DAL.RepositoryService
userService: ModulesSdkTypes.IMedusaInternalService<any>
inviteService: InviteService<any>
inviteService: ModulesSdkTypes.IMedusaInternalService<any>
eventBusModuleService: IEventBusModuleService
}
const generateMethodForModels = { Invite }
export default class UserModuleService<
TUser extends User = User,
TInvite extends Invite = Invite
>
extends ModulesSdkUtils.MedusaService<
UserTypes.UserDTO,
{
Invite: {
dto: UserTypes.InviteDTO
}
// 1 day
const DEFAULT_VALID_INVITE_DURATION = 60 * 60 * 24 * 1000
export default class UserModuleService
extends MedusaService<{
User: {
dto: UserTypes.UserDTO
}
>(User, generateMethodForModels, entityNameToLinkableKeysMap)
Invite: {
dto: UserTypes.InviteDTO
}
}>({ User, Invite }, entityNameToLinkableKeysMap)
implements UserTypes.IUserModuleService
{
__joinerConfig(): ModuleJoinerConfig {
@@ -50,8 +50,9 @@ export default class UserModuleService<
protected baseRepository_: DAL.RepositoryService
protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<TUser>
protected readonly inviteService_: InviteService<TInvite>
protected readonly userService_: ModulesSdkTypes.IMedusaInternalService<User>
protected readonly inviteService_: ModulesSdkTypes.IMedusaInternalService<Invite>
protected readonly config: { jwtSecret: string; expiresIn: number }
constructor(
{ userService, inviteService, baseRepository }: InjectedDependencies,
@@ -62,9 +63,20 @@ export default class UserModuleService<
this.baseRepository_ = baseRepository
this.userService_ = userService
this.inviteService_ = inviteService.withModuleOptions(
this.moduleDeclaration
)
this.inviteService_ = inviteService
this.config = {
jwtSecret: moduleDeclaration["jwt_secret"],
expiresIn:
parseInt(moduleDeclaration["valid_duration"]) ||
DEFAULT_VALID_INVITE_DURATION,
}
if (!this.config.jwtSecret) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"No jwt_secret was provided in the UserModule's options. Please add one."
)
}
}
@InjectTransactionManager("baseRepository_")
@@ -72,11 +84,22 @@ export default class UserModuleService<
token: string,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.InviteDTO> {
const invite = await this.inviteService_.validateInviteToken(
token,
const jwtSecret = this.moduleDeclaration["jwt_secret"]
const decoded: JwtPayload = jwt.verify(token, jwtSecret, { complete: true })
const invite = await this.inviteService_.retrieve(
decoded.payload.id,
{},
sharedContext
)
if (invite.expires_at < new Date()) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
"The invite has expired"
)
}
return await this.baseRepository_.serialize<UserTypes.InviteDTO>(invite, {
populate: true,
})
@@ -114,24 +137,52 @@ export default class UserModuleService<
inviteIds: string[],
@MedusaContext() sharedContext: Context = {}
) {
return await this.inviteService_.refreshInviteTokens(
inviteIds,
const [invites, count] = await this.inviteService_.listAndCount(
{ id: inviteIds },
{},
sharedContext
)
if (count !== inviteIds.length) {
const missing = arrayDifference(
inviteIds,
invites.map((invite) => invite.id)
)
if (missing.length > 0) {
throw new MedusaError(
MedusaError.Types.INVALID_DATA,
`The following invites do not exist: ${missing.join(", ")}`
)
}
}
const updates = invites.map((invite) => {
return {
id: invite.id,
expires_at: new Date().setMilliseconds(
new Date().getMilliseconds() + this.config.expiresIn
),
token: this.generateToken({ id: invite.id }),
}
})
return await this.inviteService_.update(updates, sharedContext)
}
create(
// @ts-expect-error
createUsers(
data: UserTypes.CreateUserDTO[],
sharedContext?: Context
): Promise<UserTypes.UserDTO[]>
create(
createUsers(
data: UserTypes.CreateUserDTO,
sharedContext?: Context
): Promise<UserTypes.UserDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async create(
async createUsers(
data: UserTypes.CreateUserDTO[] | UserTypes.CreateUserDTO,
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.UserDTO | UserTypes.UserDTO[]> {
@@ -159,18 +210,19 @@ export default class UserModuleService<
return Array.isArray(data) ? serializedUsers : serializedUsers[0]
}
update(
// @ts-expect-error
updateUsers(
data: UserTypes.UpdateUserDTO[],
sharedContext?: Context
): Promise<UserTypes.UserDTO[]>
update(
updateUsers(
data: UserTypes.UpdateUserDTO,
sharedContext?: Context
): Promise<UserTypes.UserDTO>
@InjectManager("baseRepository_")
@EmitEvents()
async update(
async updateUsers(
data: UserTypes.UpdateUserDTO | UserTypes.UpdateUserDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<UserTypes.UserDTO | UserTypes.UserDTO[]> {
@@ -253,7 +305,7 @@ export default class UserModuleService<
private async createInvites_(
data: UserTypes.CreateInviteDTO[],
@MedusaContext() sharedContext: Context = {}
): Promise<TInvite[]> {
): Promise<Invite[]> {
const toCreate = data.map((invite) => {
return {
...invite,
@@ -262,7 +314,19 @@ export default class UserModuleService<
}
})
return await this.inviteService_.create(toCreate, sharedContext)
const created = await this.inviteService_.create(toCreate, sharedContext)
const updates = created.map((invite) => {
return {
id: invite.id,
expires_at: new Date().setMilliseconds(
new Date().getMilliseconds() + this.config.expiresIn
),
token: this.generateToken({ id: invite.id }),
}
})
return await this.inviteService_.update(updates, sharedContext)
}
// @ts-ignore
@@ -307,4 +371,12 @@ export default class UserModuleService<
return Array.isArray(data) ? serializedInvites : serializedInvites[0]
}
private generateToken(data: any): string {
const jwtSecret: string = this.moduleDeclaration["jwt_secret"]
return jwt.sign(data, jwtSecret, {
jwtid: crypto.randomUUID(),
expiresIn: this.config.expiresIn,
})
}
}