Платформа электронной коммерции с открытым исходным кодом для многопрофильных торговых площадок

? Содержание этого руководства может быть устаревшим. Вместо этого вы можете ознакомиться с полным кодом этой серии в этом репозитории 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. Этот плагин создан на основе кода из данного руководства и будет обновляться с выходом каждой новой части этой серии.

Предварительные условия

Прежде чем приступить к выполнению данного руководства, убедитесь, что у вас:

  1. Установлен экземпляр сервера Medusa. О том, как это сделать, вы можете узнать из нашего простого руководства по быстрому запуску.
  2. Установлен PostgreSQL и ваш сервер Medusa подключен к нему.
  3. Установлен 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.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *