@openfactu/plugin-sdk
v0.4.0
Published
SDK oficial para el desarrollo de plugins en la plataforma OpenFactu ERP.
Readme
@openfactu/plugin-sdk
SDK oficial para desarrollar plugins de OpenFactu.
Instalacion
npm install @openfactu/plugin-sdkInicio rapido
import type { PluginContext, HookContext } from '@openfactu/plugin-sdk';
import { tool } from 'ai';
import { z } from 'zod';
import { eq } from 'drizzle-orm';
const PLUGIN_ID = 'mi-plugin';
export const init = async ({ hooks, migration, documents, app, db, widgets, aiTools }: PluginContext) => {
// Añadir campo a una tabla
await migration.addCustomField({
pluginId: PLUGIN_ID,
tableName: 'BusinessPartner',
fieldName: 'loyalty_points',
type: 'INTEGER',
label: 'Puntos de fidelidad',
});
// Hook antes de crear factura
documents.onBeforeCreate('SalesInvoice', async (ctx: HookContext) => {
if (ctx.data.total > 10000) {
throw new Error('Limite excedido');
}
});
// Ruta API personalizada
app.get(`/api/plugins/${PLUGIN_ID}/status`, (req, res) => {
res.json({ status: 'active' });
});
// NUEVO: consultar la BD directamente, sin necesidad de un hook
const tenants = await db.public.select().from(db.schema.tenants);
if (tenants[0]) {
const tenantDb = await db.forTenant(tenants[0].id);
const items = await tenantDb.select().from(db.schema.items).limit(5);
console.log(`Items del tenant ${tenants[0].name}:`, items.length);
}
// NUEVO: registrar un widget de dashboard sin tocar manifest.json
widgets.registerDashboard({
id: 'mi-plugin-widget',
title: 'Mi Widget',
component: 'ui/MyWidget.tsx',
size: 'md',
order: 100,
});
// NUEVO: registrar una tool de chat de IA (Keiro) — "skill" del plugin
aiTools.register('mi_plugin_puntos_fidelidad', (ctx) =>
tool({
description: 'Consulta los puntos de fidelidad de un interlocutor.',
inputSchema: z.object({ partnerId: z.string() }),
execute: async ({ partnerId }) => {
const [row] = await ctx.tenantClient
.select()
.from(db.schema.businessPartners)
.where(eq(db.schema.businessPartners.id, partnerId));
return { points: row?.loyalty_points ?? 0 };
},
}),
);
};Tipos disponibles
| Tipo | Descripcion |
|------|-------------|
| PluginContext | Lo que recibe init(): app, migration, hooks, documents, factuApi, db, widgets, aiTools |
| HookContext | Lo que recibe un hook: tenantId, db, data, user |
| PluginInit | Tipo de la funcion init |
| PluginManifest | Estructura del manifest.json |
| PluginDashboardWidgetInput | Widget de dashboard registrado por código vía widgets.registerDashboard() |
| PluginDashboardWidget | Widget de dashboard declarado en manifest.json → ui.dashboardWidgets |
| PluginThemePreset | Preset de tema declarado en manifest.json → ui.themes |
| CoreTableName | Tablas del ERP que se pueden extender |
| DocumentType | Tipos de documento (salesInvoice, purchaseInvoice, etc.) |
| HookEvent | Eventos de hooks (salesInvoice.beforeCreate, etc.) |
| HookHandler | Tipo del handler de un hook |
context.db — queries a la base de datos sin necesidad de un hook
db.public— cliente Drizzle del schemapublic(Tenant, GlobalUser, PluginField, ...).db.forTenant(tenantId)— resuelvetenantId→ schema físico del tenant → cliente Drizzle. Es la única vía para acceder a datos de un tenant: siempre pasa por la tablaTenant, nunca acepta un nombre de schema arbitrario (evita fugas entre tenants).db.schema— el módulo de schema tipado del server, para construir queries Drizzle en vez de SQL crudo.
context.widgets — widgets de dashboard programáticos
widgets.registerDashboard(widget)— registra (o actualiza, porid) un widget de dashboard desdeinit(), sin necesidad de declararlo enmanifest.json. Usa el mismo mecanismo ya cableado (Dashboard →PluginComponentLoader), así que elcomponentapunta a un archivo del propio plugin igual que enmanifest.json.
context.aiTools — tools de chat de IA
aiTools.register(name, factory)— registra una tool para el asistente de IA interno (Keiro) sin tocar el core, igual que un hook o un widget.factoryrecibe el contexto por-request (AiChatToolContext:tenantClient,tenantId,tenantSchema,user,apiBase) y debe devolver el resultado detool({...})del paqueteai(conzodpara elinputSchema). Si elnameya lo usa otra tool (del core o de otro plugin), el registro se ignora — nunca sobreescribe una tool ya presente. Si tu tool crea o modifica algo, pásaleneedsApproval: trueatool({...})— el chat mostrará la misma tarjeta de confirmación que usan las acciones del core.
Componentes UI
Los plugins pueden tener componentes React que se cargan en el ERP. Usa @openfactu/ui para los componentes:
import React, { useState } from 'react';
import { Card, Button, Table } from '@openfactu/ui';
const Page = () => {
return (
<Card>
<h2>Mi Plugin</h2>
<Button onClick={() => alert('hola')}>Click</Button>
</Card>
);
};
export default Page;Manifest
{
"name": "Mi Plugin",
"version": "1.0.0",
"description": "Descripcion",
"logo": "Puzzle",
"ui": {
"routes": [
{
"path": "/plugin/mi-plugin",
"title": "Mi Plugin",
"type": "custom",
"config": { "component": "ui/Page.tsx" }
}
],
"menuItems": [
{ "label": "Mi Plugin", "path": "/plugin/mi-plugin", "icon": "Puzzle" }
]
}
}Desarrollo remoto
Sube tu plugin a un servidor OpenFactu sin necesidad de acceso SSH:
# Subir una vez
openfactu plugin push --server http://mi-servidor:3000 --client-id ofk_... --client-secret ofs_...
# Auto-sync al guardar
openfactu plugin watch --server http://mi-servidor:3000 --client-id ofk_... --client-secret ofs_...Las dev keys se generan desde la UI del ERP: Plugins > Desarrollo > Generar API Key.
Links
Licencia
MIT
