Plugin System
Extend DataFn with pluggable hooks.
DataFn provides a plugin system that lets you intercept and transform queries, mutations, and sync operations. Plugins run on the client, the server, or both, and are composed as an ordered chain.
DatafnPlugin Interface
interface DatafnPlugin {
name: string;
runsOn: ("client" | "server")[];
// Query hooks
beforeQuery?(ctx: DatafnHookContext, query: unknown): unknown;
afterQuery?(ctx: DatafnHookContext, query: unknown, result: unknown): unknown;
// Mutation hooks
beforeMutation?(ctx: DatafnHookContext, mutation: unknown | unknown[]): unknown;
afterMutation?(ctx: DatafnHookContext, mutation: unknown, result: unknown): void;
// Sync hooks
beforeSync?(ctx: DatafnHookContext, payload: unknown, ...args: unknown[]): unknown;
afterSync?(ctx: DatafnHookContext, payload: unknown, result: unknown, ...args: unknown[]): void;
}Hook Context
Every hook receives a DatafnHookContext:
interface DatafnHookContext {
env: "client" | "server";
schema: DatafnSchema;
}Hook Semantics
Before Hooks (Fail-Closed)
Before hooks run in order. Each hook receives the output of the previous hook. If any hook throws an error or returns a failure result, the entire operation is aborted and the error is returned to the caller.
// Before hooks transform the input and can reject operations
beforeQuery(ctx, query) {
// Transform the query
return { ...query, filters: { ...query.filters, tenantId: "t1" } };
}
beforeMutation(ctx, mutation) {
// Reject the mutation
throw new Error("Mutations are disabled during maintenance");
}After Hooks (Fail-Open)
After hooks run after the operation completes. Errors in after hooks are logged but do not affect the operation result. After hooks can observe results but generally should not transform them (except for afterQuery on the server, which supports result transformation).
afterMutation(ctx, mutation, result) {
// Log the mutation for auditing (error here won't affect the result)
console.log("Mutation applied:", mutation);
}Environment Filtering
Plugins declare where they run via the runsOn property. The hook runner filters plugins based on the current environment:
const auditPlugin: DatafnPlugin = {
name: "audit-log",
runsOn: ["server"], // Only runs on the server
afterMutation(ctx, mutation, result) {
recordAuditEntry(mutation);
},
};
const softDeletePlugin: DatafnPlugin = {
name: "soft-delete",
runsOn: ["client"], // Only runs on the client
beforeMutation(ctx, mutation) {
// Convert deletes to soft deletes
},
};Registering Plugins
Client
import { createDatafnClient, createSoftDeletePlugin } from "@datafn/client";
const client = createDatafnClient({
schema,
plugins: [
createSoftDeletePlugin({ resources: ["todos"] }),
],
});Server
import { createDatafnServer } from "@datafn/server";
const server = createDatafnServer({
schema,
plugins: [auditPlugin, validationPlugin],
});Hook Execution Order
Hooks execute in the order plugins are registered. For before hooks, the output of each hook becomes the input of the next:
Plugin A: beforeQuery(ctx, query) -> transformedQuery
Plugin B: beforeQuery(ctx, transformedQuery) -> finalQuerySync Hooks
Sync hooks intercept clone, pull, push, and seed operations:
beforeSync(ctx, payload, ...args) {
// args[0] is the sync phase: "clone", "pull", "push", "seed"
console.log("Starting sync phase:", args[0]);
return payload;
}
afterSync(ctx, payload, result, ...args) {
console.log("Sync complete:", args[0]);
}Built-In Plugins
- Soft Delete -- Convert hard deletes to soft deletes.
- Client Search -- Client-side full-text search.