HotupdaterHot Updater
Database Plugins

Custom Database Plugin

Create your own database plugin for any database provider

Installation

npm install @hot-updater/plugin-core --save-dev

Overview

Build a custom database plugin to integrate any database provider. This guide shows how to use createDatabasePlugin to build and configure your own database plugin.

Creating a Database Plugin

Use createDatabasePlugin to build custom database plugins:

import { createDatabasePlugin } from "@hot-updater/plugin-core";

export const myDatabase = createDatabasePlugin<MyConfig>({
  name: "myDatabase",              // Plugin identifier
  factory: (config) => ({
    // Return these required methods:
    getBundleById: async (bundleId) => { /* ... */ },
    getBundles: async (options) => ({ data: [], pagination: {} }),
    getChannels: async () => [],
    commitBundle: async ({ changedSets }) => { /* ... */ },
    onUnmount: async () => { /* optional cleanup */ }
  })
});

Factory Function Return Type

Your factory function must return an object with these methods:

{
  // Fetch single bundle by ID
  getBundleById: (bundleId: string) => Promise<Bundle | null>;

  // Fetch paginated bundles with optional filtering
  getBundles: (options: {
    where?: { channel?: string; platform?: string };
    limit: number;
    offset: number;
  }) => Promise<{
    data: Bundle[];
    pagination: PaginationInfo;
  }>;

  // Get all available channels
  getChannels: () => Promise<string[]>;

  // Commit all pending changes (batch operation)
  commitBundle: (params: {
    changedSets: {
      operation: "insert" | "update" | "delete";
      data: Bundle;
    }[];
  }) => Promise<void>;

  // Optional: cleanup resources on unmount
  onUnmount?: () => Promise<void>;
}

Note: The factory does NOT need to implement updateBundle, appendBundle, or deleteBundle - these are auto-generated by createDatabasePlugin and tracked internally.

Implementation Example

Here's a complete custom database plugin using a REST API:

customDatabase.ts
import {
  type Bundle,
  type PaginationInfo,
  createDatabasePlugin,
} from "@hot-updater/plugin-core";

export interface CustomDatabaseConfig {
  baseUrl: string;
  apiKey: string;
}

export const customDatabase = createDatabasePlugin<CustomDatabaseConfig>({
  name: "customDatabase",
  factory: (config) => {
    const headers = {
      "Content-Type": "application/json",
      Authorization: `Bearer ${config.apiKey}`,
    };

    return {
      async getBundleById(bundleId) {
        const response = await fetch(
          `${config.baseUrl}/bundles/${bundleId}`,
          { headers }
        );

        if (!response.ok) {
          if (response.status === 404) return null;
          throw new Error(`Failed to fetch bundle: ${response.statusText}`);
        }

        return response.json();
      },

      async getBundles(options) {
        const params = new URLSearchParams({
          limit: String(options.limit),
          offset: String(options.offset),
        });

        if (options.where?.channel) {
          params.set("channel", options.where.channel);
        }
        if (options.where?.platform) {
          params.set("platform", options.where.platform);
        }

        const response = await fetch(
          `${config.baseUrl}/bundles?${params}`,
          { headers }
        );

        if (!response.ok) {
          throw new Error(`Failed to fetch bundles: ${response.statusText}`);
        }

        const result = await response.json();
        return {
          data: result.data,
          pagination: result.pagination,
        };
      },

      async getChannels() {
        const response = await fetch(`${config.baseUrl}/channels`, {
          headers,
        });

        if (!response.ok) {
          throw new Error(`Failed to fetch channels: ${response.statusText}`);
        }

        return response.json();
      },

      async commitBundle({ changedSets }) {
        // Process all changes in batch
        for (const change of changedSets) {
          if (change.operation === "insert" || change.operation === "update") {
            await fetch(`${config.baseUrl}/bundles`, {
              method: "POST",
              headers,
              body: JSON.stringify(change.data),
            });
          } else if (change.operation === "delete") {
            await fetch(`${config.baseUrl}/bundles/${change.data.id}`, {
              method: "DELETE",
              headers,
            });
          }
        }
      },

      async onUnmount() {
        // Optional: cleanup resources, close connections, etc.
        console.log("Database plugin unmounted");
      },
    };
  },
});

Helper Utilities

The @hot-updater/plugin-core package provides helper functions:

calculatePagination

Generates pagination metadata from total count and options:

import { calculatePagination } from "@hot-updater/plugin-core";

const pagination = calculatePagination(totalCount, {
  limit: 10,
  offset: 0,
});
// Result: { total, limit, offset, hasMore }

CLI Configuration

Use your custom plugin in hot-updater.config.ts:

import { defineConfig } from "@hot-updater/core";
import { customDatabase } from "./customDatabase";

export default defineConfig({
  database: customDatabase({
    baseUrl: process.env.DATABASE_BASE_URL!,
    apiKey: process.env.DATABASE_API_KEY!,
  }),
  // ... other config
});

Custom Server Usage

Use your plugin with createHotUpdater for self-hosted servers:

import { createHotUpdater } from "@hot-updater/core";
import { customDatabase } from "./customDatabase";

const hotUpdater = createHotUpdater({
  database: customDatabase({
    baseUrl: process.env.DATABASE_BASE_URL!,
    apiKey: process.env.DATABASE_API_KEY!,
  }),
  // ... other options
});

Best Practices

Security

  • Never hardcode credentials in plugin code
  • Use environment variables for sensitive data
  • Implement proper authentication headers
  • Validate input data before storing

Performance

  • Implement efficient filtering in getBundles
  • Use database indexes for common queries
  • Batch operations in commitBundle when possible
  • Cache channel lists if they don't change frequently

Error Handling

  • Return null from getBundleById when not found (don't throw)
  • Throw descriptive errors for other failures
  • Handle network errors gracefully
  • Validate bundle data structure