コンテンツにスキップ

Vendureプラグイン開発の基礎

このドキュメントでは、Vendureプラグインの実装手順とベストプラクティスを解説します。既存チームメンバーがプラグイン開発をスムーズに開始できるよう、Vendure公式ドキュメントをベースに、プロジェクト固有の情報を統合しています。

関連ドキュメント:

目次

プラグインとは

Vendureプラグインは、Vendureの機能を拡張するためのモジュールです。プラグインは以下のような機能を提供できます:

  • GraphQL APIの拡張: カスタムクエリ・ミューテーションの追加
  • データモデルの拡張: カスタムエンティティやカスタムフィールドの追加
  • ビジネスロジックの実装: サービスクラスによるビジネスロジック
  • イベントハンドリング: Vendureのイベントシステムとの統合
  • Vendure標準機能の拡張: ShippingCalculator、PromotionAction等のカスタム実装

プラグインの特徴

  • NestJSモジュールの拡張: Vendureプラグインは @VendurePlugin() デコレーターで装飾されたNestJSモジュール
  • 独立性: 各プラグインは独立したコンポーネントとして動作
  • 再利用性: パッケージとして公開・共有が可能
  • 設定可能性: 初期化時のオプションでカスタマイズ可能

プラグイン開発の推奨フロー

方法1: CLIによる自動生成(推奨)

Vendure CLIを使用すると、プラグインの雛形を自動生成できます。

# プラグインの自動生成
npx vendure add

# 対話式で以下を選択:
# - Plugin name: my-plugin
# - Package manager: pnpm
# - Plugin location: packages/plugins/my-plugin

生成されるファイル構成:

packages/plugins/my-plugin/
├── src/
│   ├── index.ts              # プラグイン定義
│   ├── api/
│   │   └── api-extensions.ts # GraphQL拡張
│   ├── entities/
│   │   └── my-entity.ts      # カスタムエンティティ
│   └── services/
│       └── my-service.ts     # ビジネスロジック
├── package.json
└── tsconfig.json

自動生成後のカスタマイズポイント:

  1. index.ts のプラグイン設定を要件に合わせて調整
  2. GraphQLスキーマの定義
  3. サービスクラスの実装
  4. エンティティの定義
  5. vendure-config.ts への登録

方法2: 手動作成

既存プラグインを参考に手動で作成することも可能です。

推奨参考例:

  • シンプルな例: /packages/plugins/src/inventory-management.ts
  • 完全な例: /packages/plugins/consent-system/src/index.ts

プラグインの基本構造

@VendurePluginデコレーター

プラグインは @VendurePlugin() デコレーターで装飾されたクラスとして定義します。

📚 公式ドキュメント: VendurePlugin API Reference

import { PluginCommonModule, VendurePlugin } from '@vendure/core';
import { MyService } from './services/my.service';
import { MyResolver } from './api/my.resolver';
import { MyEntity } from './entities/my.entity';
import gql from 'graphql-tag';

const myGraphqlSchema = gql`
  extend type Query {
    myCustomQuery: String!
  }
`;

@VendurePlugin({
  // NestJS標準オプション
  imports: [PluginCommonModule],
  providers: [MyService],
  exports: [MyService],

  // Vendure固有オプション
  entities: [MyEntity],
  compatibility: '^3.5.0',

  shopApiExtensions: {
    schema: myGraphqlSchema,
    resolvers: [MyResolver],
  },

  adminApiExtensions: {
    schema: myGraphqlSchema,
    resolvers: [MyResolver],
  },

  configuration: config => {
    // カスタムフィールドの追加等
    return config;
  },
})
export class MyPlugin {
  static init(options?: MyPluginOptions) {
    // プラグイン初期化ロジック
    return MyPlugin;
  }
}

メタデータオプション

NestJS標準オプション

オプション 説明
imports インポートする他のモジュール(通常はPluginCommonModuleを含む)
providers サービスやリゾルバーなどのプロバイダー
exports 他のモジュールで使用可能にするプロバイダー
controllers RESTコントローラー(Vendureでは通常使用しない)

Vendure固有オプション

オプション 説明
entities TypeORMエンティティの配列
compatibility Vendureバージョンの互換性(セマンティックバージョニング)
shopApiExtensions Shop API用のGraphQL拡張
adminApiExtensions Admin API用のGraphQL拡張
configuration VendureConfigを変更する関数

8ステップ実装ガイド

Step 1: プラグインファイルの作成

プラグインのエントリーポイント index.ts を作成します。

import { PluginCommonModule, VendurePlugin } from '@vendure/core';

@VendurePlugin({
  imports: [PluginCommonModule],
})
export class MyPlugin {}

Step 2: エンティティの定義

TypeORMエンティティを定義して、カスタムデータモデルを追加します。

📚 公式ドキュメント: Database Entities

import { Entity, Column, ManyToOne } from 'typeorm';
import { VendureEntity, DeepPartial, ID, Customer } from '@vendure/core';

@Entity()
export class MyCustomEntity extends VendureEntity {
  constructor(input?: DeepPartial<MyCustomEntity>) {
    super(input);
  }

  @ManyToOne(() => Customer)
  customer: Customer;

  @Column()
  customerId: ID;

  @Column()
  name: string;

  @Column('text')
  description: string;
}

重要:

  • カスタムエンティティは VendureEntity を継承
  • @Entity() デコレーターを使用
  • TypeORMの標準的なデコレーター(@Column, @ManyToOne等)を使用

プラグインにエンティティを登録:

@VendurePlugin({
  entities: [MyCustomEntity],
})
export class MyPlugin {}

Step 3: カスタムフィールドの追加

既存のVendureエンティティを拡張するには、カスタムフィールドを使用します。

📚 公式ドキュメント: Customizing Models with Custom Fields

@VendurePlugin({
  configuration: config => {
    config.customFields.Product = config.customFields.Product ?? [];

    config.customFields.Product.push({
      name: 'customField',
      type: 'string',
      label: [{ languageCode: 'ja', value: 'カスタムフィールド' }],
      description: [{ languageCode: 'ja', value: '説明' }],
      public: true, // Shop APIで公開するか
      nullable: true,
      defaultValue: 'default',
    });

    return config;
  },
})
export class MyPlugin {}

カスタムフィールドのタイプ:

  • string, localeString, int, float, boolean, datetime
  • text, localeText (長いテキスト)
  • relation (リレーション)

重要: カスタムフィールドを追加した後は、マイグレーションを生成して実行する必要があります。

Step 4: サービスの実装

ビジネスロジックをサービスクラスに実装します。

📚 公式ドキュメント: Vendure Services と RequestContext の詳細は 公式ドキュメントを参照

import { Injectable } from '@nestjs/common';
import { TransactionalConnection, RequestContext } from '@vendure/core';
import { MyCustomEntity } from '../entities/my-custom.entity';

@Injectable()
export class MyService {
  constructor(private connection: TransactionalConnection) {}

  async findAll(ctx: RequestContext): Promise<MyCustomEntity[]> {
    return this.connection.getRepository(ctx, MyCustomEntity).find();
  }

  async create(
    ctx: RequestContext,
    input: { name: string; description: string },
  ): Promise<MyCustomEntity> {
    const entity = new MyCustomEntity(input);
    return this.connection.getRepository(ctx, MyCustomEntity).save(entity);
  }
}

重要:

  • @Injectable() デコレーターを使用
  • TransactionalConnection を使用してデータベースにアクセス
  • すべてのメソッドで RequestContext を第一引数として受け取る

プラグインにサービスを登録:

@VendurePlugin({
  providers: [MyService],
})
export class MyPlugin {}

Step 5: GraphQLスキーマの拡張

GraphQLスキーマを定義してAPIを拡張します。

📚 公式ドキュメント: Extending the GraphQL API

import gql from 'graphql-tag';

const myGraphqlSchema = gql`
  type MyCustomEntity {
    id: ID!
    name: String!
    description: String!
  }

  extend type Query {
    myCustomEntities: [MyCustomEntity!]!
    myCustomEntity(id: ID!): MyCustomEntity
  }

  extend type Mutation {
    createMyCustomEntity(input: CreateMyCustomEntityInput!): MyCustomEntity!
  }

  input CreateMyCustomEntityInput {
    name: String!
    description: String!
  }
`;

プラグインに登録:

@VendurePlugin({
  shopApiExtensions: {
    schema: myGraphqlSchema,
    resolvers: [MyResolver],
  },
})
export class MyPlugin {}

Step 6: リゾルバーの実装

GraphQLリゾルバーを実装してAPIを提供します。

import { Resolver, Query, Mutation, Args } from '@nestjs/graphql';
import {
  Ctx,
  RequestContext,
  Allow,
  Permission,
  Transaction,
} from '@vendure/core';
import { MyService } from '../services/my.service';

@Resolver()
export class MyResolver {
  constructor(private myService: MyService) {}

  @Query()
  @Allow(Permission.Public) // 権限制御
  async myCustomEntities(@Ctx() ctx: RequestContext) {
    return this.myService.findAll(ctx);
  }

  @Mutation()
  @Transaction() // トランザクション管理
  @Allow(Permission.UpdateCatalog)
  async createMyCustomEntity(
    @Ctx() ctx: RequestContext,
    @Args('input') input: { name: string; description: string },
  ) {
    return this.myService.create(ctx, input);
  }
}

重要なデコレーター:

  • @Query(), @Mutation(): GraphQLのクエリ・ミューテーション
  • @Ctx(): RequestContextの取得
  • @Args(): GraphQLの引数取得
  • @Allow(): 権限制御
  • @Transaction(): トランザクション管理

Step 7: バージョン互換性の指定

プラグインがサポートするVendureのバージョンを指定します。

@VendurePlugin({
  compatibility: '^3.5.0', // Vendure 3.5.x と互換性あり
})
export class MyPlugin {}

本プロジェクトの標準:

  • Vendure: ^3.5.0
  • Node.js: >=22.11.0

Step 8: VendureConfigへの登録

vendure-config.ts でプラグインを登録します。

// apps/vendure-server/src/vendure-config.ts
import { MyPlugin } from '@ritsubi/plugins';

export const config: VendureConfig = {
  // ...
  plugins: [
    // ...既存のプラグイン
    MyPlugin.init({
      // プラグインオプション
      enableFeatureX: true,
    }),
  ],
};

ディレクトリ構造のベストプラクティス

標準的なプラグイン構成

packages/plugins/my-plugin/
├── src/
│   ├── index.ts                      # プラグイン定義(エントリーポイント)
│   ├── entities/                     # TypeORMエンティティ
│   │   ├── my-entity.entity.ts
│   │   └── related-entity.entity.ts
│   ├── services/                     # ビジネスロジック
│   │   ├── my.service.ts
│   │   └── helper.service.ts
│   ├── api/                          # GraphQL API
│   │   ├── my.resolver.ts
│   │   ├── schema.ts                 # GraphQLスキーマ定義
│   │   └── types.ts                  # TypeScript型定義
│   ├── strategies/                   # Vendure Strategy実装(オプション)
│   │   └── my-strategy.ts
│   └── jobs/                         # バックグラウンドジョブ(オプション)
│       └── my-job.ts
├── package.json
├── tsconfig.json
└── README.md

プロジェクト固有の構成

本プロジェクトでは、2つのプラグイン管理方法を併用しています:

パターン1: パッケージとして管理(推奨)

packages/plugins/my-plugin/
└── src/
    └── index.ts

例: consent-system, wishlist

パターン2: 共有ソースとして管理

packages/plugins/src/
└── my-plugin.ts

例: customer-visibility, sb-payment-link

マイグレーション管理

マイグレーションの生成

エンティティやカスタムフィールドを追加・変更した後、マイグレーションを生成します。

# Vendure CLIでマイグレーション生成
cd apps/vendure-server
pnpm run migrate:generate --name=add-my-custom-entity

生成されるファイル:

apps/vendure-server/src/migrations/
└── 1234567890123-add-my-custom-entity.ts

マイグレーションの実行

# 開発環境
pnpm run db:migrate:local

# 本番環境
pnpm run db:migrate

マイグレーションの確認

// 生成されたマイグレーションファイルの例
export class AddMyCustomEntity1234567890123 implements MigrationInterface {
  public async up(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`
      CREATE TABLE "my_custom_entity" (
        "id" SERIAL PRIMARY KEY,
        "createdAt" TIMESTAMP NOT NULL DEFAULT now(),
        "updatedAt" TIMESTAMP NOT NULL DEFAULT now(),
        "name" VARCHAR NOT NULL,
        "description" TEXT NOT NULL
      )
    `);
  }

  public async down(queryRunner: QueryRunner): Promise<void> {
    await queryRunner.query(`DROP TABLE "my_custom_entity"`);
  }
}

重要:

  • マイグレーションは自動生成されるため、手動での編集は最小限に
  • 本番環境では synchronize: false を設定し、必ずマイグレーションを使用
  • マイグレーションはバージョン管理にコミット

詳細は Vendure Migrations ガイド を参照してください。

ライフサイクルフック

NestJSライフサイクルフックの活用

プラグインはNestJSのライフサイクルフックを実装できます。

📚 公式ドキュメント: Plugin Lifecycle Methods

import { OnModuleInit, OnApplicationBootstrap } from '@nestjs/common';

@VendurePlugin({
  // ...
})
export class MyPlugin implements OnModuleInit, OnApplicationBootstrap {
  async onModuleInit() {
    // モジュール初期化時の処理
    console.log('MyPlugin: Module initialized');
  }

  async onApplicationBootstrap() {
    // アプリケーション起動時の処理
    console.log('MyPlugin: Application bootstrapped');
  }
}

利用可能なライフサイクルフック

フック 実行タイミング
onModuleInit モジュール初期化時
onApplicationBootstrap アプリケーション起動完了時
onModuleDestroy モジュール破棄時
beforeApplicationShutdown アプリケーションシャットダウン前
onApplicationShutdown アプリケーションシャットダウン時

サーバー/ワーカーコンテキストの区別

Vendureはサーバープロセスとワーカープロセスの両方で動作します。特定のコンテキストでのみ処理を実行したい場合は、ProcessContext を使用します。

import { ProcessContext, Inject } from '@vendure/core';

@VendurePlugin({
  // ...
})
export class MyPlugin implements OnApplicationBootstrap {
  constructor(@Inject(ProcessContext) private processContext: ProcessContext) {}

  async onApplicationBootstrap() {
    if (this.processContext === ProcessContext.Server) {
      // サーバープロセスでのみ実行
      console.log('Running in server process');
    } else if (this.processContext === ProcessContext.Worker) {
      // ワーカープロセスでのみ実行
      console.log('Running in worker process');
    }
  }
}

テストとデバッグ

GraphiQLでのAPI確認

GraphiQLを使用してプラグインのAPIをテストできます。

アクセス方法:

  • Shop API: http://localhost:4000/shop-api
  • Admin API: http://localhost:4000/admin-api

クエリ例:

query {
  myCustomEntities {
    id
    name
    description
  }
}

mutation {
  createMyCustomEntity(
    input: { name: "Test Entity", description: "Test Description" }
  ) {
    id
    name
  }
}

詳細は API ドキュメント・可視化ガイド を参照してください。

ユニットテスト

サービスとビジネスロジックのテスト例:

📚 公式ドキュメント: Testing

import { Test } from '@nestjs/testing';
import { MyService } from '../services/my.service';
import { TransactionalConnection } from '@vendure/core';

describe('MyService', () => {
  let service: MyService;

  beforeEach(async () => {
    const module = await Test.createTestingModule({
      providers: [
        MyService,
        {
          provide: TransactionalConnection,
          useValue: mockConnection,
        },
      ],
    }).compile();

    service = module.get<MyService>(MyService);
  });

  it('should be defined', () => {
    expect(service).toBeDefined();
  });

  it('should create entity', async () => {
    const result = await service.create(mockCtx, {
      name: 'Test',
      description: 'Test Description',
    });

    expect(result.name).toBe('Test');
  });
});

E2Eテスト

GraphQL APIのE2Eテスト例は 実装例 を参照してください。

プラグイン公開(参考)

npmパッケージとして公開

プラグインをnpmパッケージとして公開する場合の手順:

  1. package.json の設定
{
  "name": "@your-org/vendure-my-plugin",
  "version": "1.0.0",
  "main": "dist/index.js",
  "types": "dist/index.d.ts",
  "peerDependencies": {
    "@vendure/core": "^3.5.0"
  }
}
  1. ビルドと公開
pnpm build
pnpm publish

Vendure Hubへの登録

公開したプラグインは Vendure Hub に登録できます。

注意: 本プロジェクトのプラグインは社内利用のため、公開は不要です。

参考リンク

Vendure公式ドキュメント

プラグイン開発ガイド:

データモデル:

GraphQL API:

サービスとヘルパー:

テスト:

  • Testing - プラグインテストガイド

その他:

  • Vendure Hub - プラグインマーケットプレイス

プロジェクト内ドキュメント

実装例

  • シンプルな例: /packages/plugins/src/inventory-management.ts
  • 完全な例: /packages/plugins/consent-system/src/index.ts
  • 実装パターン集: implementation-examples/index.md

更新日: 2025-01-01 メンテナー: Ritsubi開発チーム