Search for a command to run...
В этом руководстве вы узнаете, как мигрировать существующий провайдер Medusa на управление своими настройками через Модуль интеграций.
Модуль интеграций позволяет любому плагину описывать свои параметры в виде схемы, а администраторам магазина настраивать их в Admin, без правок и без повторного развёртывания приложения Medusa. Он генерирует UI, предоставляет CRUD API и валидацию, поэтому вам не нужно писать свои модели данных, роуты, формы или воркфлоу.
Когда параметры провайдера заданы в , смена токена, пароля или любой другой настройки означает правку кода, обновление переменных окружения и повторное развёртывание Medusa. Модуль интеграций избавляет от этой рутины. Он строит UI для управления настройками провайдера по простому декларативному описанию их схемы, и администратор магазина управляет ими в Admin, без правок и без повторного развёртывания.
Если вы уже написали UI для своего провайдера, то миграция на Модуль интеграций позволит вам:
В этом руководстве примеры кода взяты из реальной миграции нашего провайдера модуля фулфилмента ApiShip.
До миграции настройки ApiShip хранились в : токен, подключения к курьерским службам, адрес отправителя по умолчанию, габариты товара и налоговые настройки. Эти данные обслуживал собственный Admin UI, и на каждую CRUD-операцию был отдельный API-роут. Модуль интеграций сводит всё это в один раздел Настройки → Интеграции.
Список параметров провайдера одноуровневый, и каждый параметр соответствует одному элементу в генерируемой UI-форме. Настройки становятся параметрами дескриптора, а в собственный UI переносится только то, для чего в каталоге нет подходящего элемента, например, для параметров с типом . У провайдера ApiShip в собственный UI попадает только список подключений к курьерским службам, потому что он представляет собой массив объектов JSON.
Рядом с существующим провайдером доставки создайте новый провайдер Модуля интеграций в файле и опишите в нём дескриптор. Это тот же список настроек, что и раньше, но каждое поле теперь объявлено явно: тип, обязательность, значение по умолчанию и способ отображения в UI-форме.
Structure1src/providers/2├── integration-apiship/ # новый: дескриптор настроек для Модуля интеграций3└── fulfillment-apiship/ # существующий: провайдер модуля фулфилмента, который читает настройки через Модуль интеграций
Директория содержит дескриптор настроек. остаётся существующим провайдером модуля фулфилмента.
providers/integration-apiship/services/apiship-integration.ts1import { AbstractIntegrationProvider, defineIntegration, z } from "@gorgo/medusa-integration"2import { ProviderKeys } from "../../../types"3import { APISHIP_ICON } from "../icon"45const descriptor = defineIntegration({6 category: "fulfillment",7 displayName: "apiship.name",8 description: "apiship.description",9 icon: APISHIP_ICON,10 preferredLayoutId: "core:two-column",11 supportsMultipleInstances: true,1213 options: {14 token: {15 type: "string",16 required: true,17 secret: true,18 control: "secret",19 label: "apiship.fields.token"20 },21 is_test: {22 type: "boolean",23 default: false,24 control: "switch",25 label: "apiship.fields.isTest"26 },27 is_cod: {28 type: "boolean",29 default: false,30 control: "switch",31 label: "apiship.fields.isCod"32 },33 delivery_cost_vat: {34 type: "enum",35 // Значения однородные: здесь числа, поэтому ставка остаётся числом на всём пути.36 values: [-1, 0, 5, 10, 20 /* ... */],37 default: -1,38 control: "select",39 label: "apiship.fields.deliveryCostVat",40 // Уходит в ApiShip только вместе с наложенным платежом, иначе поле скрыто.41 visibleWhen: {42 field: "is_cod",43 equals: true44 },45 valueLabels: {46 "-1": "apiship.vat.noVat",47 "0": "apiship.vat.vat0",48 "5": "apiship.vat.vat5",49 "10": "apiship.vat.vat10",50 "20": "apiship.vat.vat20"51 /* ... */52 },53 },54 default_product_length: {55 type: "number",56 default: 10,57 positive: true,58 control: "number",59 label: "apiship.fields.defaultProductLength"60 },61 // ...width, height, weight62 sender_country_code: {63 type: "enum",64 values: ["RU", "KZ" /* ... */],65 control: "select",66 label: "apiship.fields.senderCountryCode"67 },68 // ...sender_address_string, sender_contact_name, sender_phone69 // Подключения к курьерским службам образуют список записей, а контрола для этого70 // в каталоге нет, поэтому у них остаётся свой Admin UI (см. шаг 4). Но они всё равно71 // часть схемы: шифруются и валидируются наравне с остальными полями.72 settings: {73 type: "json",74 default: {75 connections: []76 },77 control: "json",78 label: "apiship.fields.settings"79 },80 },8182 sections: [83 {84 id: "credentials",85 title: "apiship.sections.credentials",86 options: ["token", "is_test"]87 },88 {89 id: "payment_and_tax",90 title: "apiship.sections.paymentAndTax",91 column: "side",92 options: ["is_cod", "delivery_cost_vat"]93 },94 {95 id: "default_product_sizes",96 title: "apiship.sections.defaultProductSizes",97 column: "side",98 options: ["default_product_length" /* ...width, height, weight */]99 },100 {101 id: "sender",102 title: "apiship.sections.sender",103 options: ["sender_country_code" /* ...адрес, ФИО, телефон */]104 },105 ],106107 testConnection: async ({ options }) => {108 // Лёгкий read-only вызов к API ApiShip, подтверждающий валидность токена.109 // Возвращает { status, message }, исключений не бросает.110 },111})112113export type ApishipIntegrationOptions = z.infer<typeof descriptor.optionsSchema>114115export class ApishipIntegrationProvider extends AbstractIntegrationProvider {116 static identifier = ProviderKeys.APISHIP117118 get descriptor() {119 return descriptor120 }121}122123export default ApishipIntegrationProvider
Это распределяет настройки по тем же четырём секциям, что были на старой странице: доступы, оплата и налоги, размеры товара и отправитель. Единственное исключение составляют подключения к курьерским службам: они остаются одним параметром типа вместо собственной страницы.
у параметра зашифрует это поле перед сохранением. Держите значения по умолчанию в одной общей константе и ссылайтесь на неё из . Резолвер применит их во время выполнения, форма редактирования подставит в поля, а карточка покажет как текущее значение, так что все три останутся согласованными.
Параметры без не попадают в итоговую конфигурацию. Это то, что нужно, когда «пусто» является реальным состоянием поля. Адрес отправителя ApiShip, например, не нужен для расчёта цены, только для создания заказа, поэтому его проверяет провайдер в момент создания заказа, а не в дескрипторе. Если помечать такое поле , любая конфигурация, где оно не заполнено, станет неполной, а неполную интеграцию резолвер считает ненастроенной, то есть провайдер модуля фулфилмента перестанет работать и выдаст ошибку во время выполнения.
medusa-config.ts1// ...23const APISHIP_INTEGRATION_ID = "apiship-1"45module.exports = defineConfig({6 // ...7 plugins: [8 {9 resolve: "@gorgo/medusa-integration",10 options: {11 encryptionKey: process.env.INTEGRATION_ENCRYPTION_KEY,12 providers: [13 {14 resolve: "@gorgo/medusa-fulfillment-apiship/providers/integration-apiship",15 id: APISHIP_INTEGRATION_ID,16 options: {},17 },18 ],19 },20 },21 // ...22 ],23 modules: [24 {25 resolve: "@medusajs/medusa/fulfillment",26 options: {27 providers: [28 {29 resolve: "@gorgo/medusa-fulfillment-apiship/providers/fulfillment-apiship",30 id: "apiship",31 options: {32 id: APISHIP_INTEGRATION_ID33 },34 },35 ],36 },37 },38 // ...39 ],40})
Обе регистрации ссылаются на один и тот же : провайдер Модуля интеграций хранит настройки под этим id, а собственный провайдера модуля фулфилмента подсказывает ему, какие настройки читать.
У провайдера Модуля интеграций вы задаёте параметр , из него модуль собирает итоговый идентификатор инстанса вида . Такой же нужно передать провайдеру модуля фулфилмента, чтобы он знал, какие настройки читать. Если провайдер интеграций поддерживает несколько инстансов, у каждого будет свой , который можно передать разным провайдерам модуля фулфилмента.
Далее, добавьте секретный ключ шифрования в переменные окружения. Он нужен модулю интеграций, чтобы зашифровать секреты перед сохранением в базу данных:
.envINTEGRATION_ENCRYPTION_KEY=supersecret
Значение здесь должно совпадать с тем, что читает в выше.
Раньше провайдер модуля фулфилмента читал параметры из . Теперь он получает их из модуля интеграций через , передавая свой и инстанса (тот самый из ). Модуль возвращает значения параметров уже расшифрованными, проверенными и с применёнными значениями по умолчанию:
providers/fulfillment-apiship/core/apiship-base.ts1// ...2import { resolveIntegrationOptions } from "@gorgo/medusa-integration"3import { ProviderKeys } from "../../../types"4import { ApishipOptions } from "../../integration-apiship/services/apiship-integration"567class ApishipBase extends AbstractFulfillmentProviderService {8 protected instanceId_: string | null910 constructor({ logger }, options?: Record<string, unknown>) {11 super()12 this.logger_ = logger13 this.instanceId_ = (options?.id as string | undefined) ?? null14 }1516 private async getApishipOptions_(): Promise<ApishipOptions> {17 const options = await resolveIntegrationOptions<ApishipOptions>({18 identifier: ProviderKeys.APISHIP,19 instance_id: this.instanceId_,20 })21 return options22 }2324 // ...25}
, заданный из собственного регистрации в модуле фулфилмента, становится , который передаётся в . Поэтому каждый инстанс модуля фулфилмента получает настройки ровно того инстанса Модуля интеграций, с которым его связали в .
Раньше у ApiShip была своя страница настроек в Admin. Теперь эту страницу интеграции формирует Модуль интеграции по дескриптору: доступы, оплата и НДС, габариты товара и отправитель генерируются как секции.
Список подключений к курьерским службам встраивается в ту же страницу виджетом через зону расширения . Виджет получает ключ инстанса от Модуля интеграций через и передаёт его во все свои хуки и запросы:
admin/widgets/apiship-integration-main.tsx1// ...2import { defineWidgetConfig } from "@medusajs/admin-sdk"3import type { IntegrationSectionData } from "@gorgo/medusa-integration"4import { useApishipOptions } from "../hooks/api/apiship"56const ApishipIntegrationWidget = ({ data }: { data: IntegrationSectionData }) => {7 // Ключ инстанса от модуля: "int_apiship" или "int_apiship_<id>".8 const providerId = data.providerId910 // Читаем через собственный admin-роут, передавая ключ: виджет работает в браузере,11 // поэтому получать параметры с сервера он сам не может.12 const { apiship_options } = useApishipOptions(providerId)1314 return (15 <>16 // ...существующие компоненты, providerId передаётся дальше во все хуки и запросы17 </>18 )19}2021export const config = defineWidgetConfig({22 // зона расширения модуля: виджет встанет под сгенерированными секциями23 zone: "gorgo.integration.apiship.main.after",24})2526export default ApishipIntegrationWidget
Хук обращается к собственному admin-роуту, а не берёт список подключений из . Дело не в доступе к серверу. уже содержит несекретные параметры интеграции, включая . Дело в том, что подключения нужно редактировать по одной записи (добавлять, менять, удалять), а для этого нужен свой роут с обычным CRUD, а не разовый снимок.
работает только на сервере. Он запускает воркфлоу в контейнере приложения. Виджет вместо этого читает через ваш собственный admin-роут, и этот роут решает, что отдавать: не возвращайте секреты в ответ, а всё, что уже покрыто секцией дескриптора, пусть отдаёт Модуля интеграций.
Записывать значения параметров нужно так же. Соберите новое значение своего -параметра и передайте в воркфлоу Модуля интеграций. Он ограничит запись объявленными параметрами, объединит их с сохранёнными значениями, проверит их, зашифрует секреты и отправит событие , чтобы сбросить кэш:
workflows/update-apiship-options.ts1import { upsertIntegrationWorkflow } from "@gorgo/medusa-integration"23// ...сначала собираем новый список подключений из сохранённого, затем:4upsertIntegrationWorkflow.runAsStep({5 input: transform({ input, settings }, (d) => ({6 provider_id: d.input.provider_id ?? DEFAULT_APISHIP_PROVIDER_ID,7 // Без `section_id`: виджет передаёт идентификаторы параметров напрямую.8 values: { settings: d.settings },9 })),10})
Модуль передаёт в виджет готовый ключ инстанса через , так что передавать его в свои хуки и запросы можно напрямую. В итоге одна страница настроек для ApiShip совмещает сгенерированные секции и специфичный UI.
Миграция завершена. Теперь провайдер модуля фулфилмента получает настройки из Medusa Admin без правок кода и без повторного развёртывания, хранит секреты зашифрованными, по умолчанию поддерживает несколько инстансов и может проверять соединение с внешним API. Почти весь прежний собственный UI исчез, потому что модуль генерирует его из дескриптора, а то, что осталось, находится на одной странице со сгенерированными секциями.
Полный код провайдера модуля фулфилмента ApiShip после миграции доступен в репозитории .