auto-builder-sdk
Version:
SDK for building Auto Builder workflow plugins
950 lines (726 loc) • 28.2 kB
Markdown
# Building nodes using Auto-Builder SDK
Lightweight TypeScript toolkit for authoring custom **Auto-Builder** workflow nodes (plugins).
## Features
* 🔒 **Sandbox-first** execution – nodes run inside a VM2 sandbox by default.
* 🧩 Tiny surface: just extend `BaseNodeExecutor` and export it.
* 📊 Built-in telemetry & test/coverage enforcement.
* 🛠️ `npx auto-builder-sdk init <my-plugin>` scaffolds a ready-to-run plugin.
* 🗄️ **Database Service Integration** – access databases through dependency injection without importing drivers directly.
* 📄 **Standard Pagination Utilities** – comprehensive pagination support for consistent API and database interactions.
## Quick start
```bash
# create plugin folder & scaffold files
npx auto-builder-sdk init auto-builder-deskera-integration
cd my-awesome-plugin
# develop
npm run dev # watch mode (tsc)
# run unit tests (required ≥80 % coverage)
npm test
# publish to npm (prepublishOnly → tests+coverage)
npm publish --access public
```
## Installation
```bash
npm install auto-builder-sdk
```
## Anatomy of a plugin
```
my-awesome-plugin/
├─ src/
│ └─ index.ts # exports your node classes
├─ package.json # flagged with "auto-builder-plugin": true
└─ tsconfig.json
```
## Plugin manifest (`definePlugin`) & node metadata
Each SDK project exports **one default** plugin manifest created with
`definePlugin()`. The object is validated at **build-time** by **Zod** so you
can't accidentally publish an invalid plugin.
```ts
export default definePlugin({
name: 'acme-erp', // npm name or folder name
version: '1.2.3',
main: './dist/index.js', // compiled entry file
nodes: ['CreateOrder', 'Ping'],
engines: { 'auto-builder': '^1.0.0' },
sandbox: { timeoutMs: 20_000 }, // optional overrides
});
```
Inside a node you can expose extra metadata used by the builder UI:
```ts
static readonly definition = {
displayName: 'Create Order', // shown in node palette
icon: 'shopping-cart', // lucide icon name
group: ['action'],
version: 1,
description: 'Create a new order in Acme ERP',
category: 'action',
inputs: ['main'], outputs: ['main'],
properties: [ /* … */ ],
};
```
Only `properties` is mandatory — everything else falls back to reasonable
defaults if omitted.
### Minimal node executor
```ts
import { BaseNodeExecutor, definePlugin } from 'auto-builder-sdk';
export class HelloNode extends BaseNodeExecutor {
static type = 'hello.world'; // machine id (shown in UI)
readonly nodeType = HelloNode.type;
async execute(node, input, ctx) {
return [
{ json: { message: `Hello ${ctx.workflowId}!` }, binary: {} },
];
}
}
export default definePlugin({
name: 'my-awesome-plugin',
version: '0.0.1',
main: './dist/index.js',
nodes: ['HelloNode'],
engines: { 'auto-builder': '^1.0.0' },
});
```
### Dynamic parameter options (dropdown loaders)
Auto-Builder supports **dynamic dropdowns** in the property inspector.
Define them by adding `typeOptions.loadOptionsMethod` to a property and
implement a **static** (or instance) method on your executor class.
```ts
export class HelloNode extends BaseNodeExecutor {
static readonly type = 'hello.world';
readonly nodeType = HelloNode.type;
// 1️⃣ Loader method – can be async and may use credentials/params
static async listGreetings(): Promise<Array<{ name: string; value: string }>> {
return [
{ name: 'Hi', value: 'hi' },
{ name: 'Hello', value: 'hello' },
{ name: 'Howdy', value: 'howdy' },
];
}
// 2️⃣ Reference the method in the property definition
static readonly definition = {
inputs: ['main'], outputs: ['main'],
properties: [
{
displayName: 'Greeting',
name: 'greeting',
type: 'options',
default: 'hello',
typeOptions: {
loadOptionsMethod: 'listGreetings',
},
},
],
} as const;
}
```
The engine discovers the method automatically – **no server-side changes
required**.
## Debugging a plugin node
1. **Compile with source-maps** (`"sourceMap": true, "inlineSources": true` in
`tsconfig.json`).
2. Start the backend in debug/watch mode:
```bash
NODE_OPTIONS="--enable-source-maps --inspect=9229" \
PLUGINS_ENABLED=true \
PLUGIN_WATCH=true \
npm run dev
```
3. Attach VS Code (Run → "Node.js attach" → `localhost:9229`). Set break-points
in your **TypeScript** sources – thanks to source-maps they will hit inside
the sandbox.
4. Disable the sandbox temporarily (faster debugging):
* global: `PLUGIN_SAFE_MODE=false npm run dev`
* per plugin: add `{ "sandbox": { "enabled": false } }` in `definePlugin()`.
## Logging & error handling
```ts
import { log, NodeOperationError, NodeApiError } from 'auto-builder-sdk';
log.info('Fetching data', { url });
if (!apiKey) {
throw new NodeOperationError(node, 'Missing API key');
}
```
* When the plugin runs **inside Auto-Builder** the SDK logger proxies to the
main Winston logger so your messages appear in the service log files and
monitoring dashboards.
* In stand-alone tests the logger falls back to `console.*`.
* Throwing either of the SDK error classes lets the engine classify the failure
(operation vs API) but is **not mandatory** – any `Error` works.
## Importing engine types
All public interfaces are bundled with the SDK – no need to install the
`auto-builder` package:
```ts
import type { INode, IExecutionContext, INodeExecutionData } from 'auto-builder-sdk';
```
The file lives at `auto-builder-sdk/dist/auto-builder-sdk/src/ab-types.d.ts` and
is kept in sync with the backend on every release.
### Credential definitions (authentication)
> Added in SDK 0.1.x – no engine changes required.
Nodes can declare the kind of credential(s) they need via the **credential
registry**. A credential definition is just a JSON-ish object that describes the
fields users must enter and an optional `validate()` function that performs a
live API check.
```ts
import { registerCredential, type CredentialDefinition } from 'auto-builder-sdk';
const jiraApiToken: CredentialDefinition = {
name: 'jira', // identifier referenced by nodes
displayName: 'Jira Cloud',
properties: [
{ name: 'domain', displayName: 'Domain', type: 'string', required: true },
{ name: 'email', displayName: 'Email', type: 'string', required: true },
{ name: 'apiToken', displayName: 'API Token',type: 'password', required: true },
],
validate: async (data) => {
const { domain, email, apiToken } = data as Record<string,string>;
const auth = 'Basic ' + Buffer.from(`${email}:${apiToken}`).toString('base64');
const res = await fetch(`${domain}/rest/api/3/myself`, { headers:{Authorization:auth} });
if (!res.ok) throw new Error(`Auth failed (${res.status})`);
},
};
registerCredential(jiraApiToken);
```
Expose it on your node:
```ts
static readonly definition = {
// …
credentials: [ { name: 'jira', required: true } ],
};
```
Multiple schemes? Just register multiple definitions (e.g. `jiraOAuth2`,
`jiraBasic`) and list them all in `credentials:`; the builder UI will let users
pick the type they want.
When the node runs you retrieve whatever credential the user selected:
```ts
const creds = await this.getCredentials(node.credentials.jira);
console.log(creds.type); // "jira", "jiraOAuth2", …
```
No core-or UI-level changes are needed – adding a new credential definition is
as simple as shipping the file with your plugin.
### Unit test example (Vitest)
```ts
import { expect, it } from 'vitest';
import { HelloNode, makeStubContext, makeStubNode } from '../src';
it('returns greeting', async () => {
const nodeImpl = new HelloNode();
const ctx = makeStubContext({ workflowId: 'wf-123' });
const nodeDef = makeStubNode(HelloNode.type);
const res = await nodeImpl.execute(nodeDef, [], ctx);
expect(res[0].json.message).toBe('Hello wf-123!');
});
```
### Testing helpers
The SDK exposes two utilities to remove boiler-plate when writing tests:
```ts
import { makeStubContext, makeStubNode } from 'auto-builder-sdk';
const ctx = makeStubContext();
const nodeDef = makeStubNode('my.node');
```
Both helpers accept a partial override so you can customise only the
fields you care about.
### Coverage & CI script
Every scaffold includes a `vitest.config.ts` and the matching
`@vitest/coverage-v8` dev-dependency. Two npm-scripts are generated:
```json
"test": "vitest", // watch mode – fast dev loop
"verify": "vitest run --coverage" // used by prepublishOnly & CI
```
## Security & sandboxing
* The Auto-Builder engine executes each plugin in a **VM2** sandbox, limited by:
* Time-out (default 30 000 ms)
* Memory (default 64 MB)
* Per-plugin overrides via `sandbox` field:
```json
"sandbox": { "timeoutMs": 10000, "memoryMb": 32 }
```
* Global flag `PLUGIN_SAFE_MODE=false` (engine env) disables sandbox (dev only).
### Database Integration
The SDK provides two ways to access databases from your plugins:
#### 1. Shared Prisma Client
The SDK exposes `getDb()` which returns the **same** `PrismaClient` instance
the Auto-Builder backend already uses. That means plugin nodes can run SQL
without opening their own connection pools or adding the `@prisma/client`
dependency.
```ts
import { getDb, BaseNodeExecutor } from 'auto-builder-sdk';
export class ListUsersNode extends BaseNodeExecutor {
static readonly type = 'db.users.list';
readonly nodeType = ListUsersNode.type;
async execute() {
const db = getDb(); // shared PrismaClient
const rows = await db.user.findMany();
return [{ json: rows, binary: {} }];
}
}
```
#### 2. Database Service (Recommended)
**New in SDK 1.0.12**: The Auto-Builder engine provides a comprehensive database service that supports multiple database types through dependency injection. This is the recommended approach as it provides better security, connection pooling, and centralized management.
```ts
import { getDatabaseService, BaseNodeExecutor, registerCredential } from 'auto-builder-sdk';
// Register database credentials
registerCredential({
name: 'postgres', displayName: 'PostgreSQL',
properties: [
{ name: 'host', displayName: 'Host', type: 'string', required: true },
{ name: 'port', displayName: 'Port', type: 'number', required: true, default: 5432 },
{ name: 'database', displayName: 'Database', type: 'string', required: true },
{ name: 'username', displayName: 'Username', type: 'string', required: true },
{ name: 'password', displayName: 'Password', type: 'password', required: true },
],
});
export class PostgresQueryNode extends BaseNodeExecutor {
static readonly type = 'postgres.query';
readonly nodeType = PostgresQueryNode.type;
static readonly definition = {
credentials: [{ name: 'postgres', required: true }],
properties: [
{ name: 'sql', displayName: 'SQL Query', type: 'string', required: true }
],
} as const;
async execute(node) {
const { sql } = node.parameters as { sql: string };
const credentials = await this.getCredentials(node.credentials.postgres);
// Get the injected database service
const databaseService = getDatabaseService();
// Execute query through the service
const result = await databaseService.executeQuery(sql, {
type: 'postgres',
data: credentials.data
});
return [{
json: {
rows: result.rows,
rowCount: result.rowCount,
executionTime: result.executionTime
},
binary: {}
}];
}
}
```
**Supported Database Types:**
- PostgreSQL (`postgres`)
- MySQL (`mysql`)
- Oracle Database (`oracle`)
- Microsoft SQL Server (`mssql`)
- MongoDB (`mongodb`)
- Google BigQuery (`bigquery`)
**Benefits of Database Service:**
- 🔒 **Secure**: No direct driver imports in sandboxed plugins
- 🏊 **Connection Pooling**: Centralized connection management
- 📊 **Monitoring**: Built-in query logging and metrics
- 🛡️ **Validation**: Automatic credential and query validation
- 🔄 **Consistent**: Same interface across all database types
Below are minimal examples for different databases using the **legacy pattern** (direct driver imports). **Note**: This approach is deprecated and may not work in sandboxed environments. Use the Database Service approach above instead.
#### PostgreSQL / MySQL (using `pg` or `mysql2`) - Legacy Pattern
> **⚠️ Deprecated**: This example uses direct driver imports which may not work in sandboxed environments. Use the Database Service approach shown above instead.
```ts
import { BaseNodeExecutor, registerCredential } from 'auto-builder-sdk';
import { createPool } from 'mysql2/promise'; // or `pg` / `knex`
registerCredential({
name: 'mysql', displayName: 'MySQL',
properties: [
{ name: 'host', displayName: 'Host', type: 'string', required: true },
{ name: 'port', displayName: 'Port', type: 'number', required: true, default: 3306 },
{ name: 'database', displayName: 'Database', type: 'string', required: true },
{ name: 'username', displayName: 'Username', type: 'string', required: true },
{ name: 'password', displayName: 'Password', type: 'password',required: true },
],
});
export class MySqlQueryNode extends BaseNodeExecutor {
static readonly type = 'mysql.query'; readonly nodeType = MySqlQueryNode.type;
static readonly definition = {
credentials: [{ name: 'mysql', required: true }],
properties: [{ name: 'sql', displayName: 'SQL', type: 'string', required: true }],
} as const;
async execute(node) {
const { sql } = node.parameters as { sql: string };
const creds = await this.getCredentials(node.credentials.mysql);
const pool = createPool({
host: creds.data.host,
port: creds.data.port,
user: creds.data.username,
password: creds.data.password,
database: creds.data.database,
});
const [rows] = await pool.query(sql);
await pool.end();
return [{ json: rows, binary: {} }];
}
}
```
#### MongoDB (using `mongodb`) - Legacy Pattern
> **⚠️ Deprecated**: This example uses direct driver imports which may not work in sandboxed environments. Use the Database Service approach shown above instead.
```ts
import { MongoClient } from 'mongodb';
import { BaseNodeExecutor, registerCredential } from 'auto-builder-sdk';
registerCredential({
name: 'mongo', displayName: 'MongoDB',
properties: [
{ name: 'uri', displayName: 'Connection URI', type: 'string', required: true },
{ name: 'database', displayName: 'Database', type: 'string', required: true },
],
});
export class MongoFindNode extends BaseNodeExecutor {
static readonly type = 'mongo.find'; readonly nodeType = MongoFindNode.type;
static readonly definition = {
credentials: [{ name: 'mongo', required: true }],
properties: [
{ name: 'collection', displayName: 'Collection', type: 'string', required: true },
{ name: 'query', displayName: 'Query (JSON)', type: 'string', required: true },
],
} as const;
async execute(node) {
const { collection, query } = node.parameters as any;
const creds = await this.getCredentials(node.credentials.mongo);
const client = await MongoClient.connect(creds.data.uri);
const docs = await client.db(creds.data.database)
.collection(collection)
.find(JSON.parse(query)).toArray();
await client.close();
return [{ json: docs, binary: {} }];
}
}
```
#### Microsoft SQL Server (using `mssql`)
```ts
import sql from 'mssql';
import { BaseNodeExecutor, registerCredential } from 'auto-builder-sdk';
registerCredential({
name: 'mssql', displayName: 'MS SQL Server',
properties: [
{ name: 'server', displayName: 'Server', type: 'string', required: true },
{ name: 'user', displayName: 'User', type: 'string', required: true },
{ name: 'password',displayName: 'Password',type: 'password',required: true },
{ name: 'database',displayName: 'Database',type: 'string', required: true },
],
});
export class MsSqlQueryNode extends BaseNodeExecutor {
static readonly type = 'mssql.query'; readonly nodeType = MsSqlQueryNode.type;
static readonly definition = {
credentials: [{ name: 'mssql', required: true }],
properties: [{ name: 'sql', displayName: 'SQL', type: 'string', required: true }],
} as const;
async execute(node) {
const { sql: statement } = node.parameters as { sql: string };
const creds = await this.getCredentials(node.credentials.mssql);
await sql.connect({
server: creds.data.server,
user: creds.data.user,
password: creds.data.password,
database: creds.data.database,
options: { encrypt: true, trustServerCertificate: true },
});
const result = await sql.query(statement);
await sql.close();
return [{ json: result.recordset, binary: {} }];
}
}
```
#### Other engines / cloud warehouses
The pattern is identical for any future database. Two quick sketches:
• **ClickHouse** (columnar OLAP)
```ts
import { createClient } from '@clickhouse/client';
registerCredential({
name: 'clickhouse', displayName: 'ClickHouse',
properties: [
{ name: 'url', displayName: 'HTTP URL', type: 'string', required: true },
{ name: 'user', displayName: 'User', type: 'string' },
{ name: 'pass', displayName: 'Password', type: 'password' },
],
});
// inside execute()
const ch = createClient({
host: creds.data.url,
username: creds.data.user,
password: creds.data.pass,
});
const rows = await ch.query({ query: sql, format: 'JSONEachRow' });
```
• **BigQuery** (Google Cloud)
```ts
import { BigQuery } from '@google-cloud/bigquery';
registerCredential({
name: 'bigquery', displayName: 'BigQuery',
properties: [
{ name: 'projectId', displayName: 'Project ID', type: 'string', required: true },
{ name: 'jsonKey', displayName: 'Service Account JSON', type: 'string', required: true, typeOptions:{rows:8} },
],
});
// inside execute()
const client = new BigQuery({
projectId: creds.data.projectId,
credentials: JSON.parse(creds.data.jsonKey),
});
const [rows] = await client.query(sql);
```
Any engine that reuses the Postgres/MySQL wire-protocol (CockroachDB,
TimescaleDB, Aurora-PG/MySQL) can simply adopt the existing credential & node
without code changes.
> **Tip** For databases with native Prisma support (PostgreSQL, MySQL,
> SQLite, SQL Server, MongoDB) you can also generate a separate client in your
> plugin and skip the manual driver code. Use `getDb()` when you want to run
> queries against the *engine's* primary database.
## Deskera bearer-token helper (`ctx.getDeskeraToken()`)
> **New in SDK 0.2.x & Auto-Builder 1.1** – lazy, secure token retrieval
Each node receives an execution-context (`IExecutionContext`) instance. The
engine now injects an **async** helper `getDeskeraToken()` that returns a
user-/tenant-scoped **Bearer** token which you can pass to Deskera's REST
APIs:
```ts
interface IExecutionContext {
// … existing fields …
getDeskeraToken?(): Promise<string>; // optional for typing; always present at runtime
}
```
### Why use it?
* 🔐 **No secrets in plugins** – the helper is resolved inside the core engine.
* 🏷️ **Tenant isolation** – token is scoped to the *current* workflow/user.
* 🚀 **Caching** – the first call fetches the token from IAM, subsequent calls
within the same workflow execution are served from memory.
### Minimal example
```ts
import { BaseNodeExecutor } from 'auto-builder-sdk';
export class ListCustomersNode extends BaseNodeExecutor {
static readonly type = 'deskera.customers.list';
readonly nodeType = ListCustomersNode.type;
async execute(node, input, ctx) {
const token = await ctx.getDeskeraToken(); // ← one line, done!
const res = await fetch(
`${process.env.DESKERA_API_BASE_URL}/customers`,
{ headers: { Authorization: `Bearer ${token}` } },
);
if (!res.ok) throw new Error(`Deskera API ${res.status}`);
const customers = await res.json();
return [{ json: customers, binary: {} }];
}
}
```
### Unit-testing the helper
Because **BullMQ** serialises job payloads, functions are stripped when we bake
them into test contexts. The SDK helper `makeStubContext()` accepts an
override so you can stub the token call in tests:
```ts
import { expect, it } from 'vitest';
import { makeStubContext, makeStubNode } from 'auto-builder-sdk';
import { ListCustomersNode } from '../src';
it('retrieves customers', async () => {
const ctx = makeStubContext({
getDeskeraToken: async () => 'test-token',
});
const node = makeStubNode(ListCustomersNode.type);
const out = await new ListCustomersNode().execute(node, [], ctx);
expect(out[0].json).toBeTypeOf('object');
});
```
No further changes are required on the engine side – the helper works in both
inline and branch (parallel) execution modes.
## Sandboxing options per plugin
VM2 limits have sensible defaults (30 s / 64 MB). Override them globally via
env-vars or **per plugin** in the manifest:
```jsonc
{
"sandbox": {
"enabled": true, // false disables VM2 (DEV *only*)
"timeoutMs": 10000, // 10 seconds
"memoryMb": 32 // 32 MB
}
}
```
Developers can temporarily set `PLUGIN_SAFE_MODE=false` on the backend to turn
all sandboxes off while debugging.
## Resolving parameters – two options
Auto-Builder exposes the **same template engine** in two different ways so you can pick whatever fits your code best.
### 1. From inside a node executor (recommended for normal nodes)
When your class extends `BaseNodeExecutor`, just use the built-in protected helper:
```ts
import { BaseNodeExecutor } from 'auto-builder-sdk';
export class GreetNode extends BaseNodeExecutor {
readonly nodeType = 'demo.greet';
async execute(node, input, ctx) {
// inherited from BaseNodeExecutor
const opts = this.resolveParameters(node.parameters, ctx);
return [
{ json: { greeting: opts.message }, binary: {} },
];
}
}
```
### 2. Anywhere else (tests, helper libs, UI previews)
Need the same logic **without** subclassing? Import `ParameterResolver`:
```ts
import { ParameterResolver, type IExecutionContext } from 'auto-builder-sdk';
const ctx: IExecutionContext & { itemIndex?: number } = {
executionId: 'EX123',
workflowId: 'WF001',
workflow: {} as any,
node: {} as any,
inputData: [{ json: { name: 'Jane' }, binary: {} }],
runIndex: 0,
itemIndex: 0,
mode: 'manual',
timezone: 'UTC',
variables: {},
};
const raw = {
subject: 'Welcome {{ $json.name }}!',
sentAt: '{{ $now }}',
};
const resolved = ParameterResolver.resolve(raw, ctx);
// → { subject: 'Welcome Jane!', sentAt: '2025-06-28T14:00:00.000Z' }
```
Both methods share **exactly the same implementation** under the hood, so the resolved output is identical.
## Standard Pagination Utilities
**New in SDK 1.0.16**: The SDK provides comprehensive pagination utilities to enforce consistent pagination across all nodes and plugins.
### Basic Usage
```ts
import {
convertToLimitOffset,
validatePaginationParams,
createPaginationResponse,
PAGINATION_NODE_DEFINITION,
PAGINATION_SIZE_NODE_DEFINITION,
type StandardPaginationParams
} from 'auto-builder-sdk';
export class ListUsersNode extends BaseNodeExecutor {
static readonly type = 'api.users.list';
readonly nodeType = ListUsersNode.type;
static readonly definition = {
properties: [
// Add standard pagination parameters
PAGINATION_NODE_DEFINITION,
PAGINATION_SIZE_NODE_DEFINITION,
// ... other properties
]
} as const;
async execute(node) {
// Extract and validate pagination parameters
const paginationParams = validatePaginationParams({
page: node.parameters.page,
pageSize: node.parameters.pageSize
});
// Convert to API-specific format
const { limit, offset } = convertToLimitOffset(
paginationParams.page,
paginationParams.pageSize
);
// Make API call with pagination
const response = await fetch(`/api/users?limit=${limit}&offset=${offset}`);
const data = await response.json();
// Create standard pagination response
return [createPaginationResponse(
data.users,
paginationParams.page,
paginationParams.pageSize,
data.total
)];
}
}
```
### Configuration
The pagination system is fully configurable through environment variables:
```bash
# Default values
PAGINATION_DEFAULT_PAGE=1
PAGINATION_DEFAULT_PAGE_SIZE=100
# Validation limits
PAGINATION_MIN_PAGE=1
PAGINATION_MIN_PAGE_SIZE=1
PAGINATION_MAX_PAGE_SIZE=1000
# String processing limits
PAGINATION_MAX_STRING_LENGTH=15
```
### Advanced Configuration
```ts
import { PAGINATION_CONFIG, updatePaginationConfig, resetPaginationConfig } from 'auto-builder-sdk';
// Access current configuration
console.log(PAGINATION_CONFIG.DEFAULT_PAGE_SIZE); // 100
console.log(PAGINATION_CONFIG.MAX_PAGE_SIZE); // 1000
// Update configuration at runtime
updatePaginationConfig({
DEFAULT_PAGE_SIZE: 50,
MAX_PAGE_SIZE: 500
});
// Reset to environment variable defaults
resetPaginationConfig();
```
### API-Specific Pagination
```ts
import { buildApiPaginationParams, parseApiPaginationResponse } from 'auto-builder-sdk';
// For Jira API
const jiraParams = buildApiPaginationParams(
{ page: 1, pageSize: 50 },
'jira'
);
// → { startAt: 0, maxResults: 50 }
// For SharePoint API
const sharepointParams = buildApiPaginationParams(
{ page: 2, pageSize: 100 },
'sharepoint'
);
// → { top: 100, skip: 100 }
// Parse API responses
const standardResponse = parseApiPaginationResponse(
jiraApiResponse,
'jira'
);
```
### Legacy Parameter Conversion
```ts
import { convertLegacyPagination } from 'auto-builder-sdk';
// Convert old parameter names to standard format
const standardParams = convertLegacyPagination({
pageNo: 2,
limit: 25,
totalCount: 150
});
// → { page: 2, pageSize: 25, totalRecords: 150 }
```
### Database Query Pagination
```ts
import { convertToLimitOffset } from 'auto-builder-sdk';
const { limit, offset } = convertToLimitOffset(page, pageSize);
const sql = `SELECT * FROM users LIMIT ${limit} OFFSET ${offset}`;
```
### Benefits
- 🔄 **Consistent Interface**: All nodes use the same `page/pageSize` parameters
- 🛡️ **Validation**: Automatic parameter validation with sensible defaults
- 🔧 **API Flexibility**: Support for REST, GraphQL, OData, Jira, SharePoint formats
- 📊 **Metadata**: Automatic calculation of total pages, next/previous indicators
- 🔄 **Backward Compatibility**: Legacy parameter conversion support
## Telemetry hooks
You can subscribe to runtime metrics:
```ts
import { pluginTelemetry } from 'auto-builder/telemetry/plugin-telemetry';
pluginTelemetry.on('metric', (m) => console.log(m));
```
## Publishing & versioning workflow
1. Bump version (`npm version patch|minor|major`). Follow **semver**:
• **Patch** – docs/typos & additive helpers.
• **Minor** – new capabilities (dynamic loaders, logger).
• **Major** – breaking API changes.
2. `npm publish --access public` (the prepublish script runs tests & coverage).
3. In your Auto-Builder deployment update the dependency:
```bash
npm i auto-builder-sdk@latest # service repo
```
> Tip: Use Renovate or dependabot so services stay in sync automatically.
## Best practices
* Always write **unit tests** with ≥80 % coverage – enforced.
* Validate external inputs with `zod` inside `execute()`.
* Keep network/FS access minimal; prefer the SDK's helpers.
* Publish with **semver** and respect the peer range in `engines`.
* Document node properties with JSDoc – used by the marketplace doc generator.
## License
MIT