Просмотр исходного кода

docs: Add guide for implementing custom strategies in Vendure plugins (#3601)

David Höck 7 месяцев назад
Родитель
Сommit
6c77d0c494

+ 466 - 0
docs/docs/guides/developer-guide/custom-strategies-in-plugins/index.mdx

@@ -0,0 +1,466 @@
+---
+title: 'Custom Strategies in Plugins'
+---
+
+When building Vendure plugins, you often need to provide extensible, pluggable implementations for specific features. The **strategy pattern** is the perfect tool for this, allowing plugin users to customize behavior by providing their own implementations.
+
+This guide shows you how to implement custom strategies in your plugins, following Vendure's established patterns and best practices.
+
+## Overview
+
+A strategy in Vendure is a way to provide a pluggable implementation of a particular feature. Custom strategies in plugins allow users to:
+
+- Override default behavior with their own implementations
+- Inject dependencies and services through the `init()` lifecycle method
+- Clean up resources using the `destroy()` lifecycle method
+- Configure the strategy through the plugin's init options
+
+## Creating a Strategy Interface
+
+First, define the interface that your strategy must implement. All strategy interfaces should extend `InjectableStrategy` to support dependency injection and lifecycle methods.
+
+```ts title="src/strategies/my-custom-strategy.ts"
+import { InjectableStrategy, RequestContext } from '@vendure/core';
+
+export interface MyCustomStrategy extends InjectableStrategy {
+    /**
+     * Process some data and return a result
+     */
+    processData(ctx: RequestContext, data: any): Promise<string>;
+
+    /**
+     * Validate the input data
+     */
+    validateInput(data: any): boolean;
+}
+```
+
+## Implementing a Default Strategy
+
+Create a default implementation that users can extend or replace:
+
+```ts title="src/strategies/default-my-custom-strategy.ts"
+import { Injector, RequestContext, Logger } from '@vendure/core';
+import { MyCustomStrategy } from './my-custom-strategy';
+import { MyPluginService } from '../services/my-plugin.service';
+
+export class DefaultMyCustomStrategy implements MyCustomStrategy {
+    private myPluginService: MyPluginService;
+    private logger: Logger;
+
+    async init(injector: Injector): Promise<void> {
+        // Inject dependencies during the init phase
+        this.myPluginService = injector.get(MyPluginService);
+        this.logger = injector.get(Logger);
+
+        // Perform any setup logic
+        this.logger.info('DefaultMyCustomStrategy initialized');
+    }
+
+    async destroy(): Promise<void> {
+        // Clean up resources if needed
+        this.logger.info('DefaultMyCustomStrategy destroyed');
+    }
+
+    async processData(ctx: RequestContext, data: any): Promise<string> {
+        // Validate input first
+        if (!this.validateInput(data)) {
+            throw new Error('Invalid input data');
+        }
+
+        // Use injected service to process data
+        const result = await this.myPluginService.processData(ctx, data);
+        return result;
+    }
+
+    validateInput(data: any): boolean {
+        return data != null && typeof data === 'object';
+    }
+}
+```
+
+## Adding Strategy to Plugin Options
+
+Define your plugin's initialization options to include the strategy:
+
+```ts title="src/types.ts"
+import { MyCustomStrategy } from './strategies/my-custom-strategy';
+
+export interface MyPluginInitOptions {
+    /**
+     * Custom strategy for processing data
+     * @default DefaultMyCustomStrategy
+     */
+    processingStrategy?: MyCustomStrategy;
+
+    /**
+     * Other plugin options
+     */
+    someOtherOption?: string;
+}
+```
+
+## Configuring the Plugin
+
+In your plugin definition, provide the default strategy and allow users to override it:
+
+```ts title="src/my-plugin.ts"
+import { PluginCommonModule, VendurePlugin, Injector } from '@vendure/core';
+import { OnApplicationBootstrap, OnApplicationShutdown } from '@nestjs/common';
+import { ModuleRef } from '@nestjs/core';
+
+import { MY_PLUGIN_OPTIONS } from './constants';
+import { MyPluginInitOptions } from './types';
+import { DefaultMyCustomStrategy } from './strategies/default-my-custom-strategy';
+import { MyPluginService } from './services/my-plugin.service';
+
+@VendurePlugin({
+    imports: [PluginCommonModule],
+    providers: [
+        MyPluginService,
+        {
+            provide: MY_PLUGIN_OPTIONS,
+            useFactory: () => MyPlugin.options,
+        },
+    ],
+    configuration: config => {
+        // You can also configure core Vendure strategies here if needed
+        return config;
+    },
+    compatibility: '^3.0.0',
+})
+export class MyPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
+    static options: MyPluginInitOptions;
+
+    constructor(private moduleRef: ModuleRef) {}
+
+    static init(options: MyPluginInitOptions) {
+        this.options = {
+            // Provide default strategy if none specified
+            processingStrategy: new DefaultMyCustomStrategy(),
+            ...options,
+        };
+        return MyPlugin;
+    }
+
+    async onApplicationBootstrap() {
+        await this.initStrategy();
+    }
+
+    async onApplicationShutdown() {
+        await this.destroyStrategy();
+    }
+
+    private async initStrategy() {
+        const strategy = MyPlugin.options.processingStrategy;
+        if (strategy && typeof strategy.init === 'function') {
+            const injector = new Injector(this.moduleRef);
+            await strategy.init(injector);
+        }
+    }
+
+    private async destroyStrategy() {
+        const strategy = MyPlugin.options.processingStrategy;
+        if (strategy && typeof strategy.destroy === 'function') {
+            await strategy.destroy();
+        }
+    }
+}
+```
+
+## Using the Strategy in Services
+
+Access the strategy through dependency injection in your services:
+
+```ts title="src/services/my-plugin.service.ts"
+import { Injectable, Inject } from '@nestjs/common';
+import { RequestContext } from '@vendure/core';
+
+import { MY_PLUGIN_OPTIONS } from '../constants';
+import { MyPluginInitOptions } from '../types';
+
+@Injectable()
+export class MyPluginService {
+    constructor(@Inject(MY_PLUGIN_OPTIONS) private options: MyPluginInitOptions) {}
+
+    async processUserData(ctx: RequestContext, userData: any): Promise<string> {
+        // Delegate to the configured strategy
+        return this.options.processingStrategy.processData(ctx, userData);
+    }
+
+    validateUserInput(data: any): boolean {
+        return this.options.processingStrategy.validateInput(data);
+    }
+}
+```
+
+## User Implementation Example
+
+Plugin users can now provide their own strategy implementations:
+
+```ts title="src/my-custom-implementation.ts"
+import { Injector, RequestContext, Logger } from '@vendure/core';
+import { MyCustomStrategy } from '@my-org/my-plugin';
+import { ExternalApiService } from './external-api.service';
+
+export class CustomProcessingStrategy implements MyCustomStrategy {
+    private externalApi: ExternalApiService;
+    private logger: Logger;
+
+    async init(injector: Injector): Promise<void> {
+        this.externalApi = injector.get(ExternalApiService);
+        this.logger = injector.get(Logger);
+
+        // Initialize external API connection
+        await this.externalApi.connect();
+        this.logger.info('Custom processing strategy initialized');
+    }
+
+    async destroy(): Promise<void> {
+        // Clean up external connections
+        if (this.externalApi) {
+            await this.externalApi.disconnect();
+        }
+        this.logger.info('Custom processing strategy destroyed');
+    }
+
+    async processData(ctx: RequestContext, data: any): Promise<string> {
+        if (!this.validateInput(data)) {
+            throw new Error('Invalid data format');
+        }
+
+        // Use external API for processing
+        const result = await this.externalApi.processData(data);
+        return `Processed: ${result}`;
+    }
+
+    validateInput(data: any): boolean {
+        // Custom validation logic
+        return data && data.type === 'custom' && data.value;
+    }
+}
+```
+
+## Plugin Configuration by Users
+
+Users configure the plugin with their custom strategy:
+
+```ts title="vendure-config.ts"
+import { VendureConfig } from '@vendure/core';
+import { MyPlugin } from '@my-org/my-plugin';
+import { CustomProcessingStrategy } from './my-custom-implementation';
+
+export const config: VendureConfig = {
+    plugins: [
+        MyPlugin.init({
+            processingStrategy: new CustomProcessingStrategy(),
+            someOtherOption: 'custom-value',
+        }),
+    ],
+    // ... other config
+};
+```
+
+## Strategy with Options
+
+You can also create strategies that accept configuration options:
+
+```ts title="src/strategies/configurable-strategy.ts"
+import { Injector, RequestContext } from '@vendure/core';
+import { MyCustomStrategy } from './my-custom-strategy';
+
+export interface ConfigurableStrategyOptions {
+    timeout: number;
+    retries: number;
+    apiKey: string;
+}
+
+export class ConfigurableStrategy implements MyCustomStrategy {
+    constructor(private options: ConfigurableStrategyOptions) {}
+
+    async init(injector: Injector): Promise<void> {
+        // Use options during initialization
+        console.log(`Strategy configured with timeout: ${this.options.timeout}ms`);
+    }
+
+    async destroy(): Promise<void> {
+        // Cleanup logic
+    }
+
+    async processData(ctx: RequestContext, data: any): Promise<string> {
+        // Use configuration options
+        const timeout = this.options.timeout;
+        const retries = this.options.retries;
+
+        // Implementation using these options...
+        return 'processed with options';
+    }
+
+    validateInput(data: any): boolean {
+        return true;
+    }
+}
+```
+
+Usage:
+
+```ts title="vendure-config.ts"
+import { ConfigurableStrategy } from './strategies/configurable-strategy';
+
+// In plugin configuration
+MyPlugin.init({
+    processingStrategy: new ConfigurableStrategy({
+        timeout: 5000,
+        retries: 3,
+        apiKey: process.env.API_KEY,
+    }),
+});
+```
+
+## Multiple Strategies in One Plugin
+
+For complex plugins, you might need multiple strategies:
+
+```ts title="src/types.ts"
+export interface ComplexPluginOptions {
+    dataProcessingStrategy?: DataProcessingStrategy;
+    validationStrategy?: ValidationStrategy;
+    cacheStrategy?: CacheStrategy;
+}
+```
+
+```ts title="src/complex-plugin.ts"
+@VendurePlugin({
+    // ... plugin config
+})
+export class ComplexPlugin implements OnApplicationBootstrap, OnApplicationShutdown {
+    static options: ComplexPluginOptions;
+
+    static init(options: ComplexPluginOptions) {
+        this.options = {
+            dataProcessingStrategy: new DefaultDataProcessingStrategy(),
+            validationStrategy: new DefaultValidationStrategy(),
+            cacheStrategy: new DefaultCacheStrategy(),
+            ...options,
+        };
+        return ComplexPlugin;
+    }
+
+    async onApplicationBootstrap() {
+        await this.initAllStrategies();
+    }
+
+    async onApplicationShutdown() {
+        await this.destroyAllStrategies();
+    }
+
+    private async initAllStrategies() {
+        const injector = new Injector(this.moduleRef);
+        const strategies = [
+            ComplexPlugin.options.dataProcessingStrategy,
+            ComplexPlugin.options.validationStrategy,
+            ComplexPlugin.options.cacheStrategy,
+        ];
+
+        for (const strategy of strategies) {
+            if (strategy && typeof strategy.init === 'function') {
+                await strategy.init(injector);
+            }
+        }
+    }
+
+    private async destroyAllStrategies() {
+        const strategies = [
+            ComplexPlugin.options.dataProcessingStrategy,
+            ComplexPlugin.options.validationStrategy,
+            ComplexPlugin.options.cacheStrategy,
+        ];
+
+        for (const strategy of strategies) {
+            if (strategy && typeof strategy.destroy === 'function') {
+                await strategy.destroy();
+            }
+        }
+    }
+}
+```
+
+## Best Practices
+
+### 1. Always Extend InjectableStrategy
+
+```ts
+export interface MyStrategy extends InjectableStrategy {
+    // ... strategy methods
+}
+```
+
+### 2. Provide Sensible Defaults
+
+Always provide a default implementation so users can use your plugin out-of-the-box:
+
+```ts
+static init(options: MyPluginOptions) {
+    this.options = {
+        myStrategy: new DefaultMyStrategy(),
+        ...options,
+    };
+    return MyPlugin;
+}
+```
+
+### 3. Handle Lifecycle Properly
+
+Always implement proper init/destroy handling in your plugin:
+
+```ts
+async onApplicationBootstrap() {
+    await this.initStrategies();
+}
+
+async onApplicationShutdown() {
+    await this.destroyStrategies();
+}
+```
+
+### 4. Use TypeScript for Better DX
+
+Provide strong typing for better developer experience:
+
+```ts
+export interface MyStrategy extends InjectableStrategy {
+    processData<T>(ctx: RequestContext, data: T): Promise<ProcessedResult<T>>;
+}
+```
+
+### 5. Document Your Strategy Interface
+
+Provide comprehensive JSDoc comments:
+
+```ts
+export interface MyStrategy extends InjectableStrategy {
+    /**
+     * @description
+     * Processes the input data and returns a transformed result.
+     * This method is called for each data processing request.
+     *
+     * @param ctx - The current request context
+     * @param data - The input data to process
+     * @returns Promise resolving to the processed result
+     */
+    processData(ctx: RequestContext, data: any): Promise<string>;
+}
+```
+
+## Summary
+
+Custom strategies in plugins provide a powerful way to make your plugins extensible and configurable. By following the patterns outlined in this guide, you can:
+
+- Define clear strategy interfaces that extend `InjectableStrategy`
+- Provide default implementations that work out-of-the-box
+- Allow users to inject dependencies through the `init()` method
+- Properly manage strategy lifecycle with `init()` and `destroy()` methods
+- Enable users to provide their own implementations
+- Support configuration options for strategies
+
+This approach ensures your plugins are flexible, maintainable, and follow Vendure's established conventions.

+ 3 - 4
docs/sidebars.js

@@ -30,7 +30,7 @@ const icon = {
     <ellipse rx="11" ry="4.2" transform="rotate(60)"/>
     <ellipse rx="11" ry="4.2" transform="rotate(120)"/>
   </g>
-</svg>`
+</svg>`,
 };
 
 /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
@@ -100,6 +100,7 @@ const sidebars = {
                     value: 'Advanced Topics',
                     className: 'sidebar-section-header',
                 },
+                'guides/developer-guide/custom-strategies-in-plugins/index',
                 'guides/developer-guide/channel-aware/index',
                 'guides/developer-guide/translateable/index',
                 'guides/developer-guide/cache/index',
@@ -173,9 +174,7 @@ const sidebars = {
             customProps: {
                 icon: icon.reactLogo,
             },
-            items: [
-                'guides/extending-the-dashboard/getting-started/index',
-            ]
+            items: ['guides/extending-the-dashboard/getting-started/index'],
         },
         {
             type: 'category',