? Содержание этого руководства может быть устаревшим. Вместо этого вы можете ознакомиться с полным кодом этой серии в этом репозитории GitHub.?.
Medusa — это коммерческая платформа без головы с открытым исходным кодом, которая позволяет вам создать свой собственный магазин за несколько минут. Частью того, что делает Medusa хорошим выбором для вашего магазина электронной коммерции, является ее расширяемость. Теперь с помощью Medusa можно создавать торговые площадки с несколькими поставщиками.
Чтобы облегчить работу нашего сообщества разработчиков, Адриан де Перетти, один из наших замечательных участников, создал модуль Medusa, который позволяет расширять все, что угодно.
«Я искал решение для электронной коммерции, которое могло бы предоставить мне некоторые основные функции и при этом быть полностью настраиваемым… После некоторых исследований, в ходе которых я обнаружил, что ни одно из существующих решений не может предоставить то, что мне нужно, я выбрал Medusa, поскольку она предоставляла мне многие из необходимых функций и при этом легко расширялась. В итоге мне понравилась атмосфера сообщества, особенно близость с командой, и я помогаю тем, кто ищет похожее полностью настраиваемое решение, делясь частью своего частного проекта. Так родился medusa-extender». — Адриан де Перетти
В этом руководстве вы узнаете, как установить и настроить модуль Medusa Extender на вашем сервере Medusa. Затем вы узнаете, как использовать его возможности настройки для создания торговой площадки в вашем магазине! Рынок будет состоять из нескольких магазинов или продавцов, и каждый из этих магазинов сможет добавлять свои собственные товары. Этот учебник будет первой частью серии, в которой будут рассмотрены все аспекты создания торговой площадки.
Что такое Medusa Extender
Medusa Extender — это пакет NPM, который вы можете добавить в свой магазин Medusa для расширения или настройки его функциональности. В сферу его применения входят сущности, репозитории, сервисы и многое другое.
Medusa Extender имеет множество вариантов использования, помимо функциональности рынка. Его можно использовать во многих других случаях, например, для добавления пользовательских полей, прослушивания событий для выполнения определенных действий, таких как отправка электронной почты, настройка проверки параметров запроса в Medusa и многое другое.
Что вы будете создавать
В этой статье и следующих частях цикла вы узнаете, как создать торговую площадку с помощью Medusa и Medusa Extender. Торговая площадка — это интернет-магазин, который позволяет нескольким продавцам добавлять свои товары и продавать их.
Торговая площадка имеет множество функций, включая управление заказами и настройками продавца. В этой части учебника будет показано, как создавать магазины для каждого пользователя и прикреплять созданные им товары к этому магазину.
Код для этого урока
Если вы хотите проследить за развитием событий, вы можете найти код для этого урока в этом репозитории.
В качестве альтернативы, если вы хотите установить торговую площадку в существующий магазин Medusa, вы можете установить плагин Medusa Marketplace. Этот плагин создан на основе кода из данного руководства и будет обновляться с выходом каждой новой части этой серии.
Предварительные условия
Прежде чем приступить к выполнению данного руководства, убедитесь, что у вас:
- Установлен экземпляр сервера Medusa. О том, как это сделать, вы можете узнать из нашего простого руководства по быстрому запуску.
- Установлен PostgreSQL и ваш сервер Medusa подключен к нему.
- Установлен Redis и к нему подключен ваш сервер Medusa.
Создание торговой площадки
Настройка проекта
В директории, где находится ваш сервер Medusa, начните с установки Medusa Extender с помощью NPM:
npm i medusa-extender
Чтобы получить все преимущества Medusa-Extender, рекомендуется использовать TypeScript в вашем проекте. Для этого создайте файл tsconfig.json
в корне проекта Medusa со следующим содержимым:
{
"compilerOptions": {
"module": "CommonJS",
"declaration": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"moduleResolution": "node",
"target": "es2017",
"sourceMap": true,
"skipLibCheck": true,
"allowJs": true,
"outDir": "dist",
"rootDir": ".",
"esModuleInterop": true
},
"include": ["src", "medusa-config.js"],
"exclude": ["dist", "node_modules", "**/*.spec.ts"]
}
Затем обновите ключ scripts
в файле package.json
со следующим содержанием:
"scripts": {
"seed": "medusa seed -f ./data/seed.json",
"build": "rm -rf dist && tsc",
"start": "npm run build && node dist/src/main.js",
},
Эти сценарии обеспечат транспонирование файлов TypeScript перед запуском Medusa.
Затем создайте файл main.ts
в каталоге src
со следующим содержимым:
import { Medusa } from 'medusa-extender';
import express = require('express');
async function bootstrap() {
const expressInstance = express();
await new Medusa(__dirname + '/../', expressInstance).load([]);
expressInstance.listen(9000, () => {
console.info('Server successfully started on port 9000');
});
}
bootstrap();
Этот файл обязательно загрузит все настройки, которые вы добавите при следующем запуске сервера Medusa.
Теперь Medusa Extender полностью интегрирован в ваш экземпляр Medusa, и вы можете приступить к созданию торговой площадки.
Настройка сущности магазина
Начните с настройки сущности Store. Она понадобится вам в дальнейшем для добавления отношений между сущностью магазина и сущностями пользователей и товаров.
По традиции, настройки с помощью Medusa Extender организованы в структуру, подобную модулю. Однако это совершенно необязательно.
В каталоге src
создайте каталог modules
, в котором вы будете хранить все настройки.
Затем создайте каталог store
внутри каталога modules
. В директории store
будут храниться все настройки, связанные с магазином.
Создание сущности магазина
Создайте файл src/modules/store/entities/store.entity.ts
со следующим содержимым:
import { Store as MedusaStore } from '@medusajs/medusa/dist';
import { Entity, JoinColumn, OneToMany } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
@MedusaEntity({ override: MedusaStore })
@Entity()
export class Store extends MedusaStore {
//TODO add relations
}
Здесь используется декоратор @Entity
из medusa-extender
для настройки сущности Medusa Store
. Вы создаете класс Store
, который расширяет сущность Medusa Store (импортированную как MedusaStore
).
Позже вы отредактируете эту сущность, чтобы добавить отношения между магазином, пользователями и товарами.
Создание хранилища магазина
Далее вам нужно переопределить репозиторий Medusa StoreRepository
. Этот репозиторий будет возвращать сущность Medusa Store
. Поэтому вам нужно переопределить его, чтобы убедиться, что он возвращает вашу сущность Store
, которую вы только что создали.
Создайте файл src/modules/store/repositories/store.repository.ts
со следующим содержимым:
import { EntityRepository } from 'typeorm';
import { StoreRepository as MedusaStoreRepository } from '@medusajs/medusa/dist/repositories/store';
import { Repository as MedusaRepository, Utils } from 'medusa-extender';
import { Store } from '../entities/store.entity';
@MedusaRepository({ override: MedusaStoreRepository })
@EntityRepository(Store)
export default class StoreRepository extends Utils.repositoryMixin<Store, MedusaStoreRepository>(MedusaStoreRepository) {
}
Создание модуля Store
На данный момент это единственные файлы, которые вы добавите для магазина. Вы можете создать модуль Store с помощью этих файлов.
Создайте файл src/modules/store/store.module.ts
со следующим содержимым:
import { Module } from 'medusa-extender';
import { Store } from './entities/store.entity';
import StoreRepository from './repositories/store.repository';
@Module({
imports: [Store, StoreRepository],
})
export class StoreModule {}
Здесь используется декоратор @Module
из medusa-extender
и импортируются 2 класса, которые вы создали.
Осталось импортировать этот модуль и использовать его с Medusa. В src/main.ts
импортируйте StoreModule
в начале файла:
import { StoreModule } from './modules/store/store.module';
Затем добавьте StoreModule
в массив, переданный в качестве параметра в Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
StoreModule
]);
Это все, что вы пока будете делать в модуле Store. В следующих разделах вы будете добавлять в него классы по мере необходимости.
Настройка сущности пользователя
В этом разделе вы настроите сущность пользователя, главным образом для того, чтобы связать пользователя с магазином.
Создание пользовательской сущности
Создайте каталог user
внутри каталога modules
и создайте файл src/modules/user/entities/user.entity.ts
со следующим содержимым:
import { User as MedusaUser } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaUser })
@Entity()
export class User extends MedusaUser {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id' })
store: Store;
}
Этот класс добавит дополнительный столбец store_id
типа string и добавит отношение к сущности Store
.
Чтобы добавить новый столбец в таблицу user
в базе данных, необходимо создать файл Migration. Создайте файл src/modules/user/user.migration.ts
со следующим содержимым:
import { Migration } from 'medusa-extender';
import { MigrationInterface, QueryRunner } from 'typeorm';
@Migration()
export default class addStoreIdToUser1644946220401 implements MigrationInterface {
name = 'addStoreIdToUser1644946220401';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."user" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
Миграция создается с помощью декоратора @Migration
из medusa-extender
. Обратите внимание, что имя миграции должно заканчиваться временной меткой JavaScript в соответствии с соглашениями typeorm
.
Метод up
выполняется, если миграция не была запущена ранее. Он добавит столбец store_id
в таблицу user
, если он не существует.
Вам также нужно будет добавить связь между сущностями Store и User в src/modules/store/entities/store.entity.ts
. Замените //TODO
на следующее:
@OneToMany(() => User, (user) => user.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
members: User[];
Обязательно импортируйте сущность User
в начало файла:
import { User } from '../../user/entities/user.entity';
Создание хранилища пользователей
Далее необходимо переопределить UserRepository
Medusa. Создайте файл src/modules/user/repositories/user.repository.ts
со следующим содержимым:
import { UserRepository as MedusaUserRepository } from "@medusajs/medusa/dist/repositories/user";
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { User } from "../entities/user.entity";
@MedusaRepository({ override: MedusaUserRepository })
@EntityRepository(User)
export default class UserRepository extends Utils.repositoryMixin<User, MedusaUserRepository>(MedusaUserRepository) {
}
Создание службы пользователя
Далее необходимо переопределить класс UserService
Medusa. Создайте файл src/modules/user/services/user.service.ts
со следующим содержимым:
import { Service } from 'medusa-extender';
import { EntityManager } from 'typeorm';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import { FindConfig } from '@medusajs/medusa/dist/types/common';
import { UserService as MedusaUserService } from '@medusajs/medusa/dist/services';
import { User } from '../entities/user.entity';
import UserRepository from '../repositories/user.repository';
import { MedusaError } from 'medusa-core-utils';
type ConstructorParams = {
manager: EntityManager;
userRepository: typeof UserRepository;
eventBusService: EventBusService;
};
@Service({ override: MedusaUserService })
export default class UserService extends MedusaUserService {
private readonly manager: EntityManager;
private readonly userRepository: typeof UserRepository;
private readonly eventBus: EventBusService;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.userRepository = container.userRepository;
this.eventBus = container.eventBusService;
}
public async retrieve(userId: string, config?: FindConfig<User>): Promise<User> {
const userRepo = this.manager.getCustomRepository(this.userRepository);
const validatedId = this.validateId_(userId);
const query = this.buildQuery_({ id: validatedId }, config);
const user = await userRepo.findOne(query);
if (!user) {
throw new MedusaError(MedusaError.Types.NOT_FOUND, `User with id: ${userId} was not found`);
}
return user as User;
}
}
Здесь используется декоратор @Service
из medusa-extender
для переопределения UserService
Medusa. Класс, который вы создадите для переопределения, будет расширять UserService
.
Этот новый класс переопределяет метод retrieve
, чтобы гарантировать, что возвращаемый пользователь — это новый класс сущности User, который вы создали ранее.
Создайте пользовательское среднее ПО
Функция loggedInUser
недоступна в Medusa. Вам нужно будет создать Middleware, которое при аутентификации запроса регистрирует вошедшего в систему пользователя в пределах области видимости.
Создайте файл src/modules/user/middlewares/loggedInUser.middleware.ts
со следующим содержимым:
import { MedusaAuthenticatedRequest, MedusaMiddleware, Middleware } from 'medusa-extender';
import { NextFunction, Response } from 'express';
import UserService from '../../user/services/user.service';
@Middleware({ requireAuth: true, routes: [{ method: "all", path: '*' }] })
export class LoggedInUserMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
if (req.user && req.user.userId) {
const userService = req.scope.resolve('userService') as UserService;
const loggedInUser = await userService.retrieve(req.user.userId, {
select: ['id', 'store_id'],
});
req.scope.register({
loggedInUser: {
resolve: () => loggedInUser,
},
});
}
next();
}
}
Вы можете использовать декоратор @Middleware
из medusa-extender
для создания Middleware, которое запускается на определенные запросы. Эта Middleware запускается при получении запроса от аутентифицированного пользователя, и она запускается для всех путей (обратите внимание на использование path: '*'
) и для всех типов запросов (обратите внимание на использование method: "all"
).
Внутри промежуточного ПО вы получаете ID текущего пользователя из запроса, затем получаете модель пользователя и регистрируете ее в области видимости, чтобы к ней можно было обращаться из сервисов.
Этот подход упрощен для целей данного учебника. Однако более разумно включить это промежуточное ПО в отдельный модуль
auth
. Включение этого промежуточного ПО в модульuser
или в модульauth
не повлияет на его функциональность.
Создание службы Store для обработки событий вставки пользователя
Вам необходимо убедиться, что при создании пользователя с ним ассоциируется магазин. Это можно сделать, прослушав событие User-created и создав новый магазин для этого пользователя. Вы добавите обработчик этого события в StoreService
.
Создайте файл src/modules/store/services/store.service.ts
со следующим содержимым:
import { StoreService as MedusaStoreService } from '@medusajs/medusa/dist/services';
import { EntityManager } from 'typeorm';
import { CurrencyRepository } from '@medusajs/medusa/dist/repositories/currency';
import { Store } from '../entities/store.entity';
import { EntityEventType, Service, MedusaEventHandlerParams, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../../user/entities/user.entity';
import EventBusService from '@medusajs/medusa/dist/services/event-bus';
import StoreRepository from '../repositories/store.repository';
interface ConstructorParams {
loggedInUser: User;
manager: EntityManager;
storeRepository: typeof StoreRepository;
currencyRepository: typeof CurrencyRepository;
eventBusService: EventBusService;
}
@Service({ override: MedusaStoreService, scope: 'SCOPED' })
export default class StoreService extends MedusaStoreService {
private readonly manager: EntityManager;
private readonly storeRepository: typeof StoreRepository;
constructor(private readonly container: ConstructorParams) {
super(container);
this.manager = container.manager;
this.storeRepository = container.storeRepository;
}
withTransaction(transactionManager: EntityManager): StoreService {
if (!transactionManager) {
return this;
}
const cloned = new StoreService({
...this.container,
manager: transactionManager,
});
cloned.transactionManager_ = transactionManager;
return cloned;
}
@OnMedusaEntityEvent.Before.Insert(User, { async: true })
public async createStoreForNewUser(
params: MedusaEventHandlerParams<User, 'Insert'>
): Promise<EntityEventType<User, 'Insert'>> {
const { event } = params;
const createdStore = await this.withTransaction(event.manager).createForUser(event.entity);
if (!!createdStore) {
event.entity.store_id = createdStore.id;
}
return event;
}
public async createForUser(user: User): Promise<Store | void> {
if (user.store_id) {
return;
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = storeRepo.create() as Store;
return storeRepo.save(store);
}
public async retrieve(relations: string[] = []) {
if (!this.container.loggedInUser) {
return super.retrieve(relations);
}
const storeRepo = this.manager.getCustomRepository(this.storeRepository);
const store = await storeRepo.findOne({
relations,
join: { alias: 'store', innerJoin: { members: 'store.members' } },
where: (qb) => {
qb.where('members.id = :memberId', { memberId: this.container.loggedInUser.id });
},
});
if (!store) {
throw new Error('Unable to find the user store');
}
return store;
}
}
@OnMedusaEntityEvent.Before.Insert
используется для добавления слушателя события вставки на сущность, которая в данном случае является сущностью User
. Внутри слушателя вы создаете пользователя с помощью метода createForUser
. Этот метод просто использует StoreRepository
для создания хранилища.
Вы также добавляете вспомогательное событие retrieve
для получения хранилища, принадлежащего текущему вошедшему пользователю.
Обратите внимание на использование scope: 'SCOPED'
в декораторе @Service
. Это позволит вам получить доступ к вошедшему пользователю, которого вы зарегистрировали ранее в области видимости.
Вам нужно будет импортировать этот новый класс в StoreModule
. В src/modules/store/store.module.ts
добавьте следующий импорт в начало:
import StoreService from './services/store.service';
Затем добавьте StoreService
в массив imports
, переданный в @Module
:
imports: [Store, StoreRepository, StoreService],
Создание подписчика пользователя
Чтобы слушатель событий работал, необходимо сначала издать это событие в подписчике. Событие будет выдаваться до того, как будет вставлен User
. Создайте файл src/modules/user/subscribers/user.subscriber.ts
со следующим содержимым:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { eventEmitter, Utils as MedusaUtils, OnMedusaEntityEvent } from 'medusa-extender';
import { User } from '../entities/user.entity';
@EventSubscriber()
export default class UserSubscriber implements EntitySubscriberInterface<User> {
static attachTo(connection: Connection): void {
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
}
public listenTo(): typeof User {
return User;
}
public async beforeInsert(event: InsertEvent<User>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(User), {
event,
transactionalEntityManager: event.manager,
});
}
}
Это создаст подписчика, используя декоратор EventSubscriber
из typeorm
. Затем, перед вставкой пользователя будет испущено событие OnMedusaEntityEvent.Before.InsertEvent
из medusa-extender
, которое вызовет создание магазина.
Чтобы зарегистрировать подписчика, необходимо создать промежуточное ПО, которое его регистрирует. Создайте файл src/modules/user/middlewares/userSubscriber.middleware.ts
со следующим содержимым:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Response } from 'express';
import { Connection } from 'typeorm';
import UserSubscriber from '../subscribers/user.subscriber';
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }] })
export class AttachUserSubscriberMiddleware implements MedusaMiddleware {
public async consume(req: MedusaAuthenticatedRequest, res: Response, next: NextFunction): Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, UserSubscriber);
return next();
}
}
Это зарегистрирует подписчика, когда запрос POST
будет отправлен в /admin/users
, который создает нового пользователя.
Создание маршрутизатора пользователей
Последняя оставшаяся настройка является необязательной. По умолчанию конечная точка Medusa для создания пользователя требует аутентификации в качестве администратора. В случае использования на рынке вы можете захотеть, чтобы пользователи регистрировались самостоятельно и создавали свои собственные магазины. Если для вас это не так, вы можете пропустить создание следующего класса.
Medusa Extender позволяет вам также переопределять маршруты в Medusa. В данном случае вы добавите маршрут /admin/create-user
для приема неаутентифицированных запросов.
Создайте файл src/modules/user/routers/user.router.ts
и добавьте следующее содержимое:
import { Router } from 'medusa-extender';
import createUserHandler from '@medusajs/medusa/dist/api/routes/admin/users/create-user';
import wrapHandler from '@medusajs/medusa/dist/api/middlewares/await-middleware';
@Router({
routes: [
{
requiredAuth: false,
path: '/admin/create-user',
method: 'post',
handlers: [wrapHandler(createUserHandler)],
},
],
})
export class UserRouter {
}
Вы используете декоратор @Router
из medusa-extender
для создания маршрутизатора. Этот маршрутизатор примет массив routes
, который будет либо добавлен, либо переопределит существующие маршруты на вашем сервере Medusa. В данном случае вы переопределяете маршрут /admin/create-user
и устанавливаете requiredAuth
в false.
Чтобы убедиться, что AttachUserSubscriberMiddleware
также запускается для этого нового маршрута (чтобы обработчики событий перед вставкой пользователя запускались для этого нового маршрута), убедитесь, что добавили новую запись в массив routes
:
@Middleware({ requireAuth: false, routes: [{ method: "post", path: '/admin/users' }, { method: "post", path: '/admin/create-user' }] })
Создание пользовательского модуля
Вы добавили все настройки, необходимые для того, чтобы связать пользователя с его собственным магазином. Теперь вы можете создать модуль User, используя эти файлы.
Создайте файл src/modules/user/user.module.ts
со следующим содержимым:
import { AttachUserSubscriberMiddleware } from './middlewares/userSubscriber.middleware';
import { LoggedInUserMiddleware } from "./middlewares/loggedInUser.middleware";
import { Module } from 'medusa-extender';
import { User } from './entities/user.entity';
import UserRepository from './repositories/user.repository';
import { UserRouter } from "./routers/user.router";
import UserService from './services/user.service';
import addStoreIdToUser1644946220401 from './user.migration';
@Module({
imports: [
User,
UserService,
UserRepository,
addStoreIdToUser1644946220401,
UserRouter,
LoggedInUserMiddleware,
AttachUserSubscriberMiddleware
]
})
export class UserModule {}
Если вы не создали
UserRouter
в предыдущем шаге, то убедитесь, что вы удалили его из массиваimports
.
Осталось последнее — импортировать этот модуль. В src/main.ts
импортируйте UserModule
в начале файла:
import { UserModule } from './modules/user/user.module';
Затем добавьте UserModule
в массив, переданный в качестве параметра в Medusa.load
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
StoreModule
]);
Протестируйте
Теперь вы готовы протестировать эту настройку! В терминале запустите сервер Medusa:
npm start
Или используя CLI Medusa:
medusa develop
После запуска сервера вам необходимо использовать такой инструмент, как Postman, чтобы легко отправлять запросы на ваш сервер.
Если вы не добавили UserRouter
, вам сначала нужно войти в систему как администратор, чтобы иметь возможность добавлять пользователей. Это можно сделать, отправив запрос POST
на адрес localhost:9000/admin/auth
. В теле запроса вы должны указать адрес электронной почты и пароль. Если вы используете свежую установку Medusa, вы можете использовать следующие учетные данные:
{
"email": "admin@medusa-test.com",
"password": "supersecret"
}
После этого запроса вы можете отправлять аутентифицированные запросы администратору.
Отправьте запрос POST
на [localhost:9000/admin/users](http://localhost:9000/admin/users)
для создания нового пользователя. В теле запроса необходимо передать email и пароль нового пользователя:
{
"email": "example@gmail.com",
"password": "supersecret"
}
Запрос вернет объект user с данными нового пользователя:
Обратите внимание, что теперь есть поле store_id
. Если вы попытаетесь создать несколько пользователей, вы увидите, что store_id
будет каждый раз разным.
Настройка сущности Products
Аналогично тому, как вы только что настроили сущность User
, вам нужно настроить сущность Product
, чтобы она также хранила store_id
с отношениями. Затем вы настроите ProductService
, а также другие классы, чтобы убедиться, что когда создается продукт, к нему привязывается идентификатор магазина пользователя, создающего его. Вы также убедитесь, что при получении списка продуктов возвращаются только те продукты, которые принадлежат магазину текущего пользователя.
Создание сущности продукта
Создайте файл src/modules/product/entities/product.entity.ts
со следующим содержимым:
import { Product as MedusaProduct } from '@medusajs/medusa/dist';
import { Column, Entity, Index, JoinColumn, ManyToOne } from 'typeorm';
import { Entity as MedusaEntity } from 'medusa-extender';
import { Store } from '../../store/entities/store.entity';
@MedusaEntity({ override: MedusaProduct })
@Entity()
export class Product extends MedusaProduct {
@Index()
@Column({ nullable: false })
store_id: string;
@ManyToOne(() => Store, (store) => store.members)
@JoinColumn({ name: 'store_id', referencedColumnName: 'id' })
store: Store;
}
Это переопределит сущность Product
Medusa, чтобы добавить поле store_id
и отношение к сущности Store
.
Вам также необходимо отразить это отношение в сущности Store
, поэтому в src/modules/store/entities/store.entity.ts
добавьте следующий код ниже отношения с сущностью User
, которую вы добавили ранее:
@OneToMany(() => Product, (product) => product.store)
@JoinColumn({ name: 'id', referencedColumnName: 'store_id' })
products: Product[];
Убедитесь, что сущность Product
импортирована в начале файла:
import { Product } from '../../product/entities/product.entity';
Создание миграции продукта
Далее создайте файл src/modules/product/product.migration.ts
со следующим содержимым:
import { MigrationInterface, QueryRunner } from 'typeorm';
import { Migration } from 'medusa-extender';
@Migration()
export default class addStoreIdToProduct1645034402086 implements MigrationInterface {
name = 'addStoreIdToProduct1645034402086';
public async up(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" ADD COLUMN IF NOT EXISTS "store_id" text;`;
await queryRunner.query(query);
}
public async down(queryRunner: QueryRunner): Promise<void> {
const query = `ALTER TABLE public."product" DROP COLUMN "store_id";`;
await queryRunner.query(query);
}
}
Это добавит миграцию, которая добавит колонку store_id
в таблицу product
.
Создание хранилища продуктов
Далее создайте файл src/modules/repositories/product.repository.ts
со следующим содержимым:
import { Repository as MedusaRepository, Utils } from "medusa-extender";
import { EntityRepository } from "typeorm";
import { ProductRepository as MedusaProductRepository } from "@medusajs/medusa/dist/repositories/product";
import { Product } from '../entities/product.entity';
@MedusaRepository({ override: MedusaProductRepository })
@EntityRepository(Product)
export default class ProductRepository extends Utils.repositoryMixin<Product, MedusaProductRepository>(MedusaProductRepository) {
}
Это переопределит ProductRepository
Medusa, чтобы вернуть вашу новую сущность Product
.
Создание службы продукта
Теперь добавьте настройку, чтобы при отправке запроса возвращались только те продукты, которые принадлежат вошедшему в систему пользователю.
Поскольку вы ранее создали LoggedInUserMiddleware
, вы можете иметь доступ к зарегистрированному пользователю из любой службы через объект container
, переданный в конструктор службы.
Создайте файл src/modules/product/services/product.service.ts
со следующим содержимым:
import { EntityEventType, MedusaEventHandlerParams, OnMedusaEntityEvent, Service } from 'medusa-extender';
import { EntityManager } from "typeorm";
import { ProductService as MedusaProductService } from '@medusajs/medusa/dist/services';
import { Product } from '../entities/product.entity';
import { User } from '../../user/entities/user.entity';
import UserService from '../../user/services/user.service';
type ConstructorParams = {
manager: any;
loggedInUser: User;
productRepository: any;
productVariantRepository: any;
productOptionRepository: any;
eventBusService: any;
productVariantService: any;
productCollectionService: any;
productTypeRepository: any;
productTagRepository: any;
imageRepository: any;
searchService: any;
userService: UserService;
}
@Service({ scope: 'SCOPED', override: MedusaProductService })
export class ProductService extends MedusaProductService {
readonly #manager: EntityManager;
constructor(private readonly container: ConstructorParams) {
super(container);
this.#manager = container.manager;
}
prepareListQuery_(selector: object, config: object): object {
const loggedInUser = this.container.loggedInUser
if (loggedInUser) {
selector['store_id'] = loggedInUser.store_id
}
return super.prepareListQuery_(selector, config);
}
}
Это переопределит метод prepareListQuery
в Medusa’s ProductService
, который расширяет этот новый класс, чтобы получить зарегистрированного пользователя. Затем, если пользователь успешно получен, ключ store_id
добавляется в объект selector
для фильтрации продуктов по store_id
пользователя.
Создание модуля продукта
Вот и вся настройка, которую вы сделаете на данный момент. Вам просто нужно импортировать все эти файлы в модуль Product.
Создайте src/modules/product/product.module.ts
со следующим содержанием:
import { Module } from 'medusa-extender';
import { Product } from './entities/product.entity';
import ProductRepository from './repositories/product.repository';
import { ProductService } from './services/product.service';
import addStoreIdToProduct1645034402086 from './product.migration';
@Module({
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
]
})
export class ProductModule {}
Наконец, импортируйте ProductModule
в начало src/main.ts
:
import { ProductModule } from './modules/product/product.module';
И добавьте ProductModule
в массив, переданный в load
вместе с UserModule
:
await new Medusa(__dirname + '/../', expressInstance).load([
UserModule,
ProductModule,
StoreModule
]);
Протестируйте
Теперь вы можете приступить к тестированию. Запустите сервер, если он еще не запущен, и войдите в систему под пользователем, которого вы создали ранее, отправив учетные данные по адресу localhost:9000/admin/auth
.
После этого отправьте запрос GET
по адресу localhost:9000/admin/products
. Вы получите пустой массив продуктов, так как у текущего пользователя еще нет продуктов.
Создание подписчика на продукты
Теперь вы добавите необходимые настройки, чтобы прикрепить идентификатор магазина к вновь созданному продукту.
Для прослушивания события создания товара создайте файл src/modules/product/subscribers/product.subscriber.ts
со следующим содержимым:
import { Connection, EntitySubscriberInterface, EventSubscriber, InsertEvent } from 'typeorm';
import { OnMedusaEntityEvent, Utils, eventEmitter } from 'medusa-extender';
import { Product } from '../entities/product.entity';
@EventSubscriber()
export default class ProductSubscriber implements EntitySubscriberInterface<Product> {
static attachTo(connection: Connection): void {
Utils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
}
public listenTo(): typeof Product {
return Product;
}
public async beforeInsert(event: InsertEvent<Product>): Promise<void> {
return await eventEmitter.emitAsync(OnMedusaEntityEvent.Before.InsertEvent(Product), {
event,
transactionalEntityManager: event.manager,
});
}
}
Затем необходимо зарегистрировать этого подписчика с помощью Middleware. Создайте файл src/modules/product/middlewares/product.middleware.ts
со следующим содержимым:
import {
MEDUSA_RESOLVER_KEYS,
MedusaAuthenticatedRequest,
MedusaMiddleware,
Utils as MedusaUtils,
Middleware
} from 'medusa-extender';
import { NextFunction, Request, Response } from 'express';
import { Connection } from 'typeorm';
import ProductSubscriber from '../subscribers/product.subscriber';
@Middleware({ requireAuth: true, routes: [{ method: 'post', path: '/admin/products' }] })
export default class AttachProductSubscribersMiddleware implements MedusaMiddleware {
public consume(req: MedusaAuthenticatedRequest | Request, res: Response, next: NextFunction): void | Promise<void> {
const { connection } = req.scope.resolve(MEDUSA_RESOLVER_KEYS.manager) as { connection: Connection };
MedusaUtils.attachOrReplaceEntitySubscriber(connection, ProductSubscriber);
return next();
}
}
Это зарегистрирует подписчика, когда запрос POST
будет отправлен в /admin/products
, который создает новый продукт.
Добавление слушателя событий в службу продукта
Далее, в src/modules/product/services/product.service.ts
добавьте следующее внутри класса:
@OnMedusaEntityEvent.Before.Insert(Product, { async: true })
public async attachStoreToProduct(
params: MedusaEventHandlerParams<Product, 'Insert'>
): Promise<EntityEventType<Product, 'Insert'>> {
const { event } = params;
const loggedInUser = this.container.loggedInUser;
event.entity.store_id = loggedInUser.store_id;
return event;
}
Это будет слушать событие Insert, используя декоратор @OnMedusaEntityEvent
из medusa-extender
. Затем он будет использовать вошедшего в систему пользователя и прикрепит его store_id
к вновь созданному продукту.
Добавление промежуточного программного обеспечения в модуль продукта
Наконец, не забудьте импортировать новое промежуточное ПО в начало src/modules/product/product.module.ts
:
import AttachProductSubscribersMiddleware from './middlewares/product.middleware';
Затем добавьте его в массив imports
, переданный в @Module
:
imports: [
Product,
ProductRepository,
ProductService,
addStoreIdToProduct1645034402086,
AttachProductSubscribersMiddleware
]
Теперь вы готовы добавлять товары в магазин! Запустите сервер, если он еще не запущен, и убедитесь, что вы вошли в систему под пользователем, созданным ранее. Затем отправьте запрос POST
на адрес [localhost:9000/admin/products](http://localhost:9000/admin/products)
со следующим телом:
{
"title": "my product",
"options": []
}
Это минимальная структура продукта. Вы можете переименовать заголовок в любой другой.
После отправки запроса вы должны получить объект Product, где видно, что store_id
установлен на тот же store_id
пользователя, под которым вы вошли в систему.
Теперь попробуйте отправить запрос GET
на [localhost:9000/admin/products](http://localhost:9000/admin/products)
, как вы делали ранее. Вместо пустого массива вы увидите только что добавленный продукт.
Проверка с помощью администратора Medusa
Если у вас также установлен экземпляр Medusa Admin, вы также можете проверить это. Войдите в систему под пользователем, которого вы создали ранее, и вы увидите, что вы можете видеть только добавленный им продукт.
Заключение
В этом руководстве вы узнали первые шаги по созданию торговой площадки с помощью Medusa и Medusa Extender! В последующих пунктах вы узнаете о том, как добавлять настройки, управлять заказами и многое другое!
Не забудьте поддержать Medusa Extender и проверить репозиторий для получения более подробной информации!
Если у вас возникнут какие-либо проблемы или вопросы, связанные с Medusa, обращайтесь к команде Medusa через Discord. Вы также можете связаться с Адрианом @adrien2p
для получения более подробной информации или помощи относительно Medusa Extender.