Skip to content

Build a Storage Plugin

Implement IDbPlugin from @routier/core/plugins when Routier needs to target a backend that does not already have a storage plugin. Application users selecting a built-in backend do not need this SPI.

Requirements

A plugin must implement a small interface that Routier uses during reads/writes:

  • initialize/destroy lifecycle
  • add/update/remove batched entity operations
  • query execution with parameterized filters, ordering, skip/take
  • change tracking integration (apply computed/tracked fields after save)
  • identity/index awareness (keys, distinct, composite indexes)

Minimal skeleton

ts
import { BulkPersistResult } from "@routier/core/collections";
import type { IDbPlugin, DbPluginBulkPersistEvent, DbPluginQueryEvent, DbPluginEvent, ITranslatedValue } from "@routier/core/plugins";
import { PluginEventCallbackPartialResult, PluginEventCallbackResult, PluginEventResult } from "@routier/core/results";
import { JsonTranslator } from "@routier/core/plugins/translators";

export class MyPlugin implements IDbPlugin {

    // Use the constructor to take in any plugin options
    constructor() {

    }

    query<TShape>(event: DbPluginQueryEvent<any, TShape>, done: PluginEventCallbackResult<ITranslatedValue<TShape>>): void {
        // Execute the event.operation against your backend and invoke the callback
        // Use a translator to wrap results in ITranslatedValue (allows iteration for grouped queries
        // and determines if change tracking should be enabled)
        const translator = new JsonTranslator(event.operation);
        const results: unknown[] = []; // Your query results here

        // translate() automatically wraps results in ITranslatedValue
        const translatedValue = translator.translate(results);
        done(PluginEventResult.success(event.id, translatedValue));
    }

    bulkPersist(event: DbPluginBulkPersistEvent, done: PluginEventCallbackPartialResult<BulkPersistResult>): void {
        // Persist adds/updates/removes from event.operation to your backend

        // The operation is a key value pair of schema Id + changes
        for (const [schemaId, changes] of event.operation) {
            const {
                adds,
                updates,
                hasItems,
                removes,
                tags,
                total
            } = changes;
        }

        done(PluginEventResult.error(event.id, new Error("Not implemented")));
    }

    destroy(event: DbPluginEvent, done: PluginEventCallbackResult<never>): void {
        // Cleanup resources/connections
        done(PluginEventResult.success(event.id));
    }
}

Key behaviors

  • Respect schema metadata passed by collections (keys, indexes, nullable/optional/defaults).
  • Compute fields marked as computed() after persistence; persist when tracked().
  • For identity() columns, return generated values so entities can be updated in memory before saveChangesAsync() resolves.
  • For distinct()/index() ensure unique or indexed storage if supported by the backend.
  • Push what each query executed into event.executedQueries after it runs — { text, parameters? }, once per backend read. This is how your plugin supports .explain(). A plugin that does not push still works; explanations then mark its step as not reported.

Separation for single-collection datastores

If your datastore persists all entities into one physical table/collection (e.g. PouchDB), add a tracked computed property to each schema that records its collection name. This guarantees clear separation between entity types and prevents cross‑collection collisions when fields share names (like name). See the tracked + computed example in the schema modifiers reference: Tracked computed. With SQL/SQLite backends, this is not an issue since data is already isolated per table.

Testing your plugin

  • Start with the Memory plugin behavior as a reference.
  • Use the CRUD how‑to pages to validate operations and saved changes.
  • Wire into an example app and run live queries to ensure incremental updates behave as expected.

Examples to study

Next steps

Released under the MIT License.