@adobe-commerce/aio-toolkit
v1.2.7
Published
A comprehensive TypeScript toolkit for Adobe App Builder applications providing standardized Adobe Commerce integrations, I/O Events orchestration, file storage utilities, authentication helpers, and robust backend development tools with 100% test coverag
Readme
@adobe-commerce/aio-toolkit
Overview
A comprehensive TypeScript toolkit for Adobe App Builder applications providing standardized Adobe Commerce integrations, I/O Events orchestration, file storage utilities, authentication helpers, and robust backend development tools with 100% test coverage and enterprise-grade reliability.
Installation
npm install @adobe-commerce/aio-toolkit \
@adobe/aio-sdk \
@adobe/aio-lib-db \
@adobe/aio-lib-ims \
@adobe/aio-lib-telemetry \
@opentelemetry/resources \
graphqlApp Builder projects already include
@adobe/aio-sdk,@adobe/aio-lib-ims, andgraphql— you only need to add the missing ones. See Dependency Resolution for details.
Usage
The toolkit is organized into five main modules:
🛠️ Framework Components
Core infrastructure for Adobe App Builder applications
RuntimeAction
HTTP request handling and business logic execution for Adobe I/O Runtime.
const {
RuntimeAction,
RuntimeActionResponse,
HttpMethod,
HttpStatus,
} = require('@adobe-commerce/aio-toolkit');
// Create a simple action
const myAction = RuntimeAction.execute(
'process-user', // Action name
[HttpMethod.POST], // Allowed HTTP methods
['userId'], // Required parameters
['authorization'], // Required headers
async (params, ctx) => {
// Your business logic here
const { userId } = params;
const { logger } = ctx;
logger.info(`Processing request for user: ${userId}`);
// Return success response
return RuntimeActionResponse.success({
message: 'Action completed successfully',
userId: userId,
});
}
);
// Export for Adobe I/O Runtime
exports.main = myAction;EventConsumerAction
Event-driven processing for Adobe I/O Events with automatic validation.
const {
EventConsumerAction,
RuntimeActionResponse,
HttpStatus
} = require('@adobe-commerce/aio-toolkit');
// Create a simple event consumer
const myEventConsumer = EventConsumerAction.execute(
'commerce-event-handler',
['eventType'], // Required parameters
['x-adobe-signature'], // Required headers
async (params, ctx) => {
const { eventType } = params;
const { logger } = ctx;
logger.info(`Processing event: ${eventType}`);
// Process the event
await processCommerceEvent(eventType, params);
// Return success response
return RuntimeActionResponse.success({
message: 'Event processed successfully',
eventType: eventType,
processedAt: new Date().toISOString()
});
}
);
// Export for Adobe I/O Runtime
exports.main = myEventConsumer;WebhookAction
Secure webhook request handler with built-in Adobe Commerce signature verification.
const {
WebhookAction,
WebhookActionResponse,
SignatureVerification
} = require('@adobe-commerce/aio-toolkit');
// Create a webhook action with signature verification enabled
const myWebhook = WebhookAction.execute(
'order-webhook',
['orderId'], // Required parameters
['x-adobe-commerce-webhook-id'], // Required headers
SignatureVerification.ENABLED, // Enable signature verification
async (params, ctx) => {
const { orderId } = params;
const { logger } = ctx;
logger.info(`Processing order webhook: ${orderId}`);
// Your webhook logic here
// Return structured webhook response
return [
WebhookActionResponse.add('result', {
orderId: orderId,
status: 'processed',
timestamp: new Date().toISOString()
}),
WebhookActionResponse.success()
];
}
);
// Export for Adobe I/O Runtime
exports.main = myWebhook;
// Disable signature verification for testing
const testWebhook = WebhookAction.execute(
'test-webhook',
['data'],
[],
SignatureVerification.DISABLED,
async (params, ctx) => {
return WebhookActionResponse.success();
}
);WebhookActionResponse Operations:
success(): Indicates successful webhook processingexception(message?, exceptionType?): Returns error response with an optional Magento exception type (e.g.'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException')add(path, value, instance?): Adds data to responsereplace(path, value, instance?): Replaces data in responseremove(path): Removes data from response
Exception response example:
// Return a typed Magento exception — Commerce will surface it as a proper error
return WebhookActionResponse.exception(
'Lease term not available for this cart',
'Magento\\Framework\\GraphQl\\Exception\\GraphQlInputException'
);
// Produces: { "op": "exception", "type": "Magento\\...", "message": "..." }PublishEvent
Event publishing component for Adobe I/O Events with CloudEvents support.
const { PublishEvent } = require('@adobe-commerce/aio-toolkit');
// Initialize the publisher
const publishEvent = new PublishEvent(
'your-ims-org-id@AdobeOrg',
'your-api-key',
'your-access-token',
logger // Optional custom logger
);
// Publish a simple event
const result = await publishEvent.execute(
'your-provider-id',
'commerce.order.created',
{
orderId: 'ORD-123456',
customerId: 'CUST-789',
amount: 199.99,
currency: 'USD'
},
'eventId'
);
console.log(`Event published: ${result.eventId}, Status: ${result.status}`);
// Publish an event with subject (eventId is optional; pass undefined to use auto-generated ID)
const orderResult = await publishEvent.execute(
'commerce-provider',
'com.adobe.commerce.order.shipped',
{
orderId: 'ORD-123456',
trackingNumber: 'TRK-789',
carrier: 'UPS',
estimatedDelivery: '2023-12-05T18:00:00Z'
},
undefined,
'orders/ORD-123456'
);
// Handle publishing results
if (orderResult.status === 'published') {
console.log(`Order shipped event published successfully: ${orderResult.eventId}`);
} else {
console.error(`Failed to publish event: ${orderResult.error}`);
}GraphQlAction
GraphQL server implementation with schema validation and introspection control.
const { GraphQlAction } = require('@adobe-commerce/aio-toolkit');
const schema = `
type Query {
hello: String
user(id: ID!): User
}
type User {
id: ID!
name: String!
email: String!
}
`;
const resolvers = async ctx => {
const { logger } = ctx;
return {
hello: () => 'Hello, World!',
user: ({ id }) => {
logger.debug(`Fetching user: ${id}`);
return {
id,
name: 'John Doe',
email: '[email protected]'
};
}
};
};
// Create and export GraphQL endpoint
exports.main = GraphQlAction.execute(schema, resolvers);OpenWhisk & OpenWhiskAction
OpenWhisk client for serverless action invocation and integration. OpenWhisk action wrapper with enhanced logging and error handling.
const { OpenWhisk, OpenwhiskAction, RuntimeActionResponse } = require('@adobe-commerce/aio-toolkit');
// Invoke another action
const invokeAction = async (params) => {
try {
// Initialize OpenWhisk client
const openwhisk = new Openwhisk(params.API_HOST, params.API_AUTH)
const result = await openwhisk.execute('hello-world', {
name: 'World'
});
console.log('Action result:', result);
return result;
} catch (error) {
console.error('Action invocation failed:', error);
throw error;
}
};
// Simple action with logging
const helloWorldAction = OpenwhiskAction.execute('hello-world', async (params, ctx) => {
const { logger } = ctx;
const { name = 'World' } = params;
logger.info(`Greeting request for: ${name}`);
const message = `Hello, ${name}!`;
logger.info(`Generated greeting: ${message}`);
return RuntimeActionResponse.success({
message,
timestamp: new Date().toISOString(),
action: 'hello-world'
});
});
// Export for Adobe I/O Runtime
exports.main = helloWorldAction;Telemetry
OpenTelemetry integration for enterprise-grade observability with distributed tracing, metrics collection, and structured logging.
Features:
- Automatic telemetry instrumentation for all framework action classes
- Request ID correlation across all log messages (
x-adobe-commerce-request-id) - Action type identification for better filtering (
action.type) - Access to current OpenTelemetry span via
ctx.telemetryfor custom instrumentation telemetry.instrument()method for wrapping functions with automatic span creation- Conditional span access based on
ENABLE_TELEMETRYflag - Multi-provider support (New Relic and Grafana LGTM implemented)
- Graceful fallback when telemetry is not configured
- Zero performance impact when disabled (opt-in via feature flags)
Configuration:
Configure telemetry in your app.config.yaml:
runtime-action:
function: actions/runtime-action/index.js
web: 'yes'
runtime: nodejs:22
inputs:
LOG_LEVEL: debug
ENVIRONMENT: $ENVIRONMENT
ENABLE_TELEMETRY: true
NEW_RELIC_TELEMETRY: true
NEW_RELIC_SERVICE_NAME: $NEW_RELIC_SERVICE_NAME
NEW_RELIC_LICENSE_KEY: $NEW_RELIC_LICENSE_KEY
annotations:
require-adobe-auth: true
final: trueSet environment variables in your .env file:
# Telemetry configuration
ENVIRONMENT=production
NEW_RELIC_SERVICE_NAME=my-app-builder-app
NEW_RELIC_LICENSE_KEY=your-license-key-here
NEW_RELIC_URL=https://otlp.nr-data.net:4318 # OptionalUsage Example:
Telemetry is automatically initialized when you use framework action classes:
/*
* <license header>
*/
const { RuntimeAction, RuntimeActionResponse, HttpMethod } = require("@adobe-commerce/aio-toolkit");
// Custom function with telemetry
const processData = (data, telemetry, logger) => {
const span = telemetry?.getCurrentSpan?.();
if (span) {
span.setAttribute('data.size', data.length);
}
logger.info('Processing data');
return { processed: true };
};
exports.main = RuntimeAction.execute(
'my-action',
[HttpMethod.POST],
['data'],
['Authorization'],
async (params, ctx) => {
const { logger, telemetry } = ctx;
// Logger automatically includes x-adobe-commerce-request-id and action.type
logger.info('Action started');
// Wrap function to create child span
const instrumented = telemetry?.instrument?.('data.process', processData) || processData;
const result = instrumented(params.data, telemetry, logger);
return RuntimeActionResponse.success(result);
}
);Key Features:
telemetry.getCurrentSpan(params)- Access current span for custom attributes and eventstelemetry.instrument(spanName, fn)- Wrap functions to create child spans automatically- Safe with optional chaining - gracefully degrades when telemetry is disabled
- Logger automatically includes request ID and action type for correlation
What Gets Logged Automatically:
All log messages automatically include:
x-adobe-commerce-request-id- Extracted from webhook headers for request correlationaction.type- Identifies the action type (runtime-action, webhook-action, event-consumer-action, openwhisk-action)- Action lifecycle events (start, parameters, completion, errors)
- Full stack traces for errors
Viewing in New Relic:
Once configured, you can query your logs in New Relic using these attributes:
-- Filter by request ID
SELECT * FROM Log WHERE `x-adobe-commerce-request-id` = 'request-123'
-- Filter by action type
SELECT * FROM Log WHERE `action.type` = 'webhook-action'
-- Find errors for a specific request
SELECT * FROM Log WHERE `x-adobe-commerce-request-id` = 'request-123' AND level = 'error'
-- Group by action type
SELECT count(*) FROM Log FACET `action.type` SINCE 1 hour agoGrafana LGTM Telemetry
The Grafana provider forwards traces, metrics, and logs to an OTLP/HTTP collector backed by the Grafana LGTM all-in-one stack (Loki · Grafana · Tempo · Mimir). It works in two modes:
| Mode | When | Collector URL |
|---|---|---|
| Dev | GRAFANA_DEV=true | http://localhost:4318 (Docker image) |
| Deployed | GRAFANA_DEV not set | GRAFANA_ENDPOINT (e.g. Cloudflare tunnel) |
Dev mode — local Docker stack
Start the LGTM stack with a single command:
docker run --rm -p 3000:3000 -p 4317:4317 -p 4318:4318 \
--name otel-lgtm \
grafana/otel-lgtm:latestThis exposes:
| Port | Service |
|---|---|
| 3000 | Grafana UI — open http://localhost:3000 (default credentials: admin / admin) |
| 4317 | OTLP gRPC endpoint |
| 4318 | OTLP HTTP endpoint (used by the toolkit) |
Configure app.config.yaml:
runtime-action:
function: actions/runtime-action/index.js
web: 'yes'
runtime: nodejs:22
inputs:
LOG_LEVEL: debug
ENABLE_TELEMETRY: true
GRAFANA_TELEMETRY: true
GRAFANA_DEV: true
GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
annotations:
require-adobe-auth: true
final: true.env:
GRAFANA_SERVICE_NAME=my-app-builder-appDeployed mode — Cloudflare tunnel
Adobe I/O Runtime actions run in the cloud and cannot reach localhost. Expose your local LGTM stack via a Cloudflare Tunnel:
npx cloudflared tunnel --url http://localhost:4318
# Outputs a URL like: https://abc123-def456.trycloudflare.comConfigure app.config.yaml:
runtime-action:
function: actions/runtime-action/index.js
web: 'yes'
runtime: nodejs:22
inputs:
LOG_LEVEL: debug
ENABLE_TELEMETRY: true
GRAFANA_TELEMETRY: true
GRAFANA_ENDPOINT: $GRAFANA_ENDPOINT
GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
annotations:
require-adobe-auth: true
final: true.env:
GRAFANA_ENDPOINT=https://abc123-def456.trycloudflare.com
GRAFANA_SERVICE_NAME=my-app-builder-appUsage example
Telemetry is automatically initialized when you use framework action classes:
/*
* <license header>
*/
const { RuntimeAction, RuntimeActionResponse, HttpMethod } = require('@adobe-commerce/aio-toolkit');
const name = 'runtime-action';
exports.main = RuntimeAction.execute(
name,
[HttpMethod.POST],
[],
['Authorization'],
async (params, ctx) => {
const { logger, telemetry } = ctx;
logger.info({
message: `${name}-log`,
params: JSON.stringify(params),
});
const sampleInstrumental = telemetry.instrument(
`runtime.action.${name}.sampleInstrumental`,
async () => {
const span = telemetry.getCurrentSpan();
if (span) {
span.setAttribute('test', 'ABC');
}
logger.info({
message: `${name}-sampleInstrumental`,
test: 'ABC',
});
return 'Hello World';
}
);
const result = await sampleInstrumental();
return RuntimeActionResponse.success(result);
}
);Grafana Cloud
Grafana Cloud provides a managed LGTM backend. It uses the same OTLP/HTTP protocol but requires HTTP Basic authentication with your stack's Instance ID and an API key.
Find your OTLP endpoint and instance ID in Grafana Cloud → Stack → OpenTelemetry.
Configure app.config.yaml:
runtime-action:
inputs:
ENABLE_TELEMETRY: true
GRAFANA_TELEMETRY: true
GRAFANA_CLOUD: true
GRAFANA_ENDPOINT: $GRAFANA_ENDPOINT
GRAFANA_SERVICE_NAME: $GRAFANA_SERVICE_NAME
GRAFANA_INSTANCE_ID: $GRAFANA_INSTANCE_ID
GRAFANA_API_KEY: $GRAFANA_API_KEY.env:
GRAFANA_ENDPOINT=https://otlp-gateway-prod-us-east-0.grafana.net/otlp
GRAFANA_SERVICE_NAME=my-app-builder-app
GRAFANA_INSTANCE_ID=123456
GRAFANA_API_KEY=glc_xxxEnvironment variable reference
| Variable | Mode | Required | Default | Description |
|---|---|---|---|---|
| ENABLE_TELEMETRY | all | ✅ | — | Must be true to enable any telemetry |
| GRAFANA_TELEMETRY | all | ✅ | — | Must be true to select the Grafana provider |
| GRAFANA_DEV | dev | — | false | Set true to use http://localhost:4318 |
| GRAFANA_CLOUD | cloud | — | false | Set true to enable Grafana Cloud Basic auth |
| GRAFANA_ENDPOINT | tunnel / cloud | ✅ | — | Collector base URL (not needed in dev mode) |
| GRAFANA_SERVICE_NAME | tunnel / cloud | ✅ | app-builder-app | Service name tag (not needed in dev mode) |
| GRAFANA_INSTANCE_ID | cloud | ✅ | — | Grafana Cloud stack instance ID |
| GRAFANA_API_KEY | cloud | ✅ | — | Grafana Cloud API key / token |
Multi-provider fallback chain:
The toolkit tries providers in this order:
- New Relic — when
NEW_RELIC_TELEMETRY=true - Grafana LGTM — when
GRAFANA_TELEMETRY=true - No telemetry — original action runs uninstrumented
Supported Action Classes:
Telemetry is automatically integrated with all framework action classes:
RuntimeAction- HTTP request handling with telemetryWebhookAction- Webhook processing with telemetryEventConsumerAction- Event-driven processing with telemetryOpenwhiskAction- OpenWhisk action handling with telemetry
All action classes provide structured logging with automatic request ID and action type tracking.
LogSanitizer
Utility class that deep-clones a value and replaces sensitive field values with "[REDACTED]" before writing to logs. Used internally by all action classes and available for direct use.
Sensitive key detection (case-insensitive):
| Match type | Keys / patterns |
|---|---|
| Exact | authorization, x-api-key, cookie, set-cookie |
| Suffix | *_api_key, *_secret, *_token, *_password, *_key, *-token, *-secret, *-key |
Constructor:
new LogSanitizer(
additionalSensitiveKeys?: ReadonlySet<string>, // extra keys to redact (lowercase)
maxDepth?: number // recursion limit, default 10
)LogSanitizer.parseSensitiveKeys(raw: unknown): ReadonlySet<string>
Static helper that parses the SENSITIVE_KEYS runtime param into a ReadonlySet<string> suitable for the constructor. Accepts three input forms:
| Input type | Example value | Result |
|---|---|---|
| Comma-separated string | "x-merchant-token,internal_id" | Set { "x-merchant-token", "internal_id" } |
| Array | ["x-merchant-token", "INTERNAL_ID"] | Set { "x-merchant-token", "internal_id" } |
| Single string | "x-merchant-token" | Set { "x-merchant-token" } |
| Anything else (incl. undefined) | — | Set {} (no-op) |
All keys are lowercased; whitespace around commas and in array elements is trimmed; non-string array elements are silently dropped.
Automatic SENSITIVE_KEYS param support:
All action classes (RuntimeAction, EventConsumerAction, OpenwhiskAction) and Telemetry.createLogger() automatically call parseSensitiveKeys(params.SENSITIVE_KEYS) when initializing LogSanitizer. To extend sanitization with project-specific keys, add them to app.config.yaml:
application:
actions:
my-action:
function: actions/my-action/index.js
inputs:
SENSITIVE_KEYS: "x-merchant-token,internal_order_id"Or pass an array when testing:
await myAction({ ...params, SENSITIVE_KEYS: ['x-merchant-token', 'internal_order_id'] });Usage:
const { LogSanitizer } = require('@adobe-commerce/aio-toolkit');
const sanitizer = new LogSanitizer();
// Sanitize HTTP headers before logging
logger.debug({
message: 'Headers received',
headers: sanitizer.execute(params.__ow_headers || {}),
});
// authorization, x-api-key, cookie → "[REDACTED]"
// Sanitize full action parameters (includes env-bound secrets)
logger.debug({
message: 'Parameters received',
parameters: sanitizer.execute(params),
});
// MY_API_KEY, NEW_RELIC_LICENSE_KEY, GRAFANA_API_KEY → "[REDACTED]"
// Extend with project-specific keys using parseSensitiveKeys
const customSanitizer = new LogSanitizer(
LogSanitizer.parseSensitiveKeys(params.SENSITIVE_KEYS)
);
customSanitizer.execute(data);
// Or construct a Set directly
const fixedSanitizer = new LogSanitizer(new Set(['x-merchant-token', 'my_internal_secret']));
fixedSanitizer.execute(data);Behaviour:
- Returns a deep copy — never mutates the input
- Handles nested objects and arrays recursively (configurable depth limit)
- Primitives (
string,number,boolean,null,undefined) are returned as-is
RuntimeApiGatewayService
Flexible Adobe I/O Runtime API Gateway client that accepts bearer tokens for authenticated requests.
const { RuntimeApiGatewayService, AdobeAuth } = require('@adobe-commerce/aio-toolkit');
// Step 1: Generate token using AdobeAuth (implement caching in your application)
const token = await AdobeAuth.getToken(
'your-client-id',
'your-client-secret',
'your-technical-account-id',
'[email protected]',
'your-ims-org-id@AdobeOrg',
['openid', 'AdobeID', 'adobeio_api']
);
// Step 2: Initialize the service with the token
const apiGatewayService = new RuntimeApiGatewayService(
'your-namespace-12345', // Runtime namespace
'your-ims-org-id@AdobeOrg', // IMS Org ID
token, // Bearer token
logger // Optional custom logger
);
// GET request
const data = await apiGatewayService.get('v1/web/my-package/my-action');
console.log('Action response:', data);
// POST request with payload
const result = await apiGatewayService.post('v1/web/my-package/process', {
orderId: 'ORD-123',
action: 'process'
});
// PUT request
const updateResult = await apiGatewayService.put('v1/web/my-package/update', {
id: '123',
status: 'completed'
});
// DELETE request
const deleteResult = await apiGatewayService.delete('v1/web/my-package/remove/123');Key Features:
- Built-in authentication headers
- Support for GET, POST, PUT, DELETE methods
- Flexible endpoint configuration
- Comprehensive error handling
- Accepts pre-generated bearer tokens (implement your own caching strategy)
- CustomLogger integration
💡 Best Practice: Implement token caching at the application level (Redis, Memcached, in-memory) and validate tokens with BearerToken.info() before use to avoid unnecessary token generation calls.
FileRepository
File-based storage with CRUD operations for Adobe I/O Runtime applications.
Key Methods:
save(payload, id?, overwrite?): Saves data with optional ID parameter and overwrite flag. Theidparameter takes precedence overpayload.id. IDs are automatically sanitized to alphanumeric + underscore characters. Setoverwrite: trueto replace entire file, orfalse(default) to merge with existing data.load(id): Loads data by ID with file system timestamps (createdAt,updatedAt)list(): Lists all stored records with file system timestampsmetadata(id?): Retrieves file metadata (size, timestamps) without reading content - faster thanlist()for large datasetsdelete(ids): Deletes records by ID array
New in v1.0.4:
- File System Timestamps:
createdAtandupdatedAtare now retrieved from actual file system properties instead of being stored in file content - Metadata Method: New
metadata()method for efficient file property retrieval without reading file content - Overwrite Flag: Control file update strategy with
save(payload, id, overwrite)- merge (default) or replace entire file
Best Practice: Create custom repository classes that extend FileRepository for specific entities.
1. Define Entity Repository
Create a custom repository class extending FileRepository:
const { FileRepository } = require("@adobe-commerce/aio-toolkit");
class EntityRepository extends FileRepository {
/**
* @constructor
*/
constructor() {
super("/toolkit-demo/entity");
}
}
module.exports = EntityRepository;2. List Action
Retrieve all entities from the repository:
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
exports.main = RuntimeAction.execute(
"entity-list",
[HttpMethod.POST],
[],
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
return RuntimeActionResponse.success(await entityRepository.list());
}
);3. Load Action
Load a specific entity by ID:
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
exports.main = RuntimeAction.execute(
"entity-load",
[HttpMethod.POST],
['id'],
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
return RuntimeActionResponse.success(await entityRepository.load(params.id));
}
);4. Save Action
Save entity data with flexible ID handling using the new optional ID parameter:
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
const requiredParams = [
"name",
"status"
];
exports.main = RuntimeAction.execute(
"entity-save",
[HttpMethod.POST],
requiredParams,
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
// Build payload with required fields
let payload = {};
for (const fieldName in requiredParams) {
payload[requiredParams[fieldName]] = params[requiredParams[fieldName]];
}
// Extract ID parameter for prioritized handling
const explicitId = params.id || params.customId || null;
// Save with optional ID parameter - it takes precedence over payload.id
const savedId = await entityRepository.save(payload, explicitId);
return RuntimeActionResponse.success({
id: savedId,
message: 'Entity saved successfully'
});
}
);5. Save with Overwrite Flag
Control file update strategy with the overwrite parameter:
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
exports.main = RuntimeAction.execute(
"entity-save-overwrite",
[HttpMethod.POST],
['name', 'status'],
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
const payload = {
name: params.name,
status: params.status
};
// Replace entire file instead of merging
const savedId = await entityRepository.save(payload, params.id, true);
return RuntimeActionResponse.success({
id: savedId,
message: 'Entity replaced successfully'
});
}
);6. Metadata Action
Retrieve file metadata without reading content (faster for large datasets):
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
exports.main = RuntimeAction.execute(
"entity-metadata",
[HttpMethod.POST],
[],
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
// Get metadata for all files
const allMetadata = await entityRepository.metadata();
// Or get metadata for specific file
const singleMetadata = params.id
? await entityRepository.metadata(params.id)
: null;
return RuntimeActionResponse.success({
all: allMetadata,
single: singleMetadata
});
}
);7. Delete Action
Delete entities by providing an array of IDs:
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require("@adobe-commerce/aio-toolkit");
const EntityRepository = require("@lib/EntityRepository");
exports.main = RuntimeAction.execute(
"entity-delete",
[HttpMethod.POST],
['ids'],
['Authorization'],
async (params) => {
const entityRepository = new EntityRepository();
return RuntimeActionResponse.success(await entityRepository.delete(params.ids));
}
);This approach provides:
- Separation of concerns: Each CRUD operation has its own action file
- Reusable repository: Custom repository can be shared across actions
- Proper validation: Required parameters and headers are enforced
- Consistent responses: All actions use RuntimeActionResponse for standardized output
- Flexible ID management: Support for explicit IDs, payload IDs, and auto-generation
- Automatic sanitization: IDs are cleaned to ensure file system compatibility
AbdbColumn & AbdbCollection
Typed schema definition and managed database connectivity via @adobe/aio-lib-db.
The recommended pattern is to create a dedicated collection class per entity by extending AbdbCollection, then use it directly inside runtime actions.
1. Provision a database
A database must be provisioned before any collection can connect. There are two ways to do this:
Declarative provisioning via app.config.yaml (runs on aio app deploy):
application:
runtimeManifest:
database:
auto-provision: true
region: emeaWhen the application is deployed with aio app deploy, a database is provisioned in the specified region unless one is already present. Declarative provisioning does not run during aio app run or aio app dev, so the database must be provisioned manually for local development (see below).
Manual provisioning via the AIO CLI:
aio app db provision [--region <area>]2. Configure authentication
Every @adobe/aio-lib-db call requires an IMS access token. The recommended approach is to use @adobe/aio-sdk, which handles token caching and refresh automatically:
const { generateAccessToken } = require('@adobe/aio-sdk').Core.AuthClient;
const libDb = require('@adobe/aio-lib-db');
async function main(params) {
const token = await generateAccessToken(params);
const db = await libDb.init({ token: token.access_token });
}Requirements:
- The AIO project workspace must include the App Builder Data Services API.
- The runtime action must have the
include-ims-credentialsannotation set totrueinapp.config.yaml:
actions:
action:
function: actions/generic/action.js
annotations:
include-ims-credentials: true
@adobe/aio-sdktransparently manages token caching and refresh — you do not need to implement this yourself.
3. Define a custom collection
Create a reusable collection class that declares the schema once:
// src/collections/UserCollection.js
'use strict';
const { AbdbCollection, AbdbColumnType } = require('@adobe-commerce/aio-toolkit');
/**
* ABDB collection for the `users` table.
*
* Columns:
* - `first_name` (STRING, required)
* - `last_name` (STRING, required)
* - `email` (STRING, required)
*/
class UserCollection extends AbdbCollection {
constructor() {
super('users', (c) => {
c.addColumn('first_name', AbdbColumnType.STRING, 'First name', true)
.addColumn('last_name', AbdbColumnType.STRING, 'Last name', true)
.addColumn('email', AbdbColumnType.STRING, 'Email address', true);
});
}
}
module.exports = UserCollection;4. Use it in a runtime action
const { HttpMethod, RuntimeAction, RuntimeActionResponse } = require('@adobe-commerce/aio-toolkit');
const { generateAccessToken } = require('@adobe/aio-sdk').Core.AuthClient;
const UserCollection = require('@lib/UserCollection');
exports.main = RuntimeAction.execute(
'users-create',
[HttpMethod.POST],
['first_name', 'last_name', 'email'],
['Authorization'],
async (params, ctx) => {
const { first_name, last_name, email } = params;
const { logger } = ctx;
const token = await generateAccessToken(params);
const accessToken = token.access_token;
const users = new UserCollection();
// Fail-fast validation — throws before touching the DB
users.validate({ first_name, last_name, email });
// Open DB connection, run the operation, close in finally
const result = await users.run(async (collection) => {
return await collection.insertOne({ first_name, last_name, email });
}, accessToken);
logger.info(`User inserted: ${result.insertedId}`);
return RuntimeActionResponse.success(result);
}
);5. API surface reference
addColumn call forms:
// Positional (short form) — description and isRequired are optional positional args
c.addColumn('email', AbdbColumnType.STRING, 'Email address', true);
// Options-object form — no undefined placeholder needed when setting only one option
c.addColumn('email', AbdbColumnType.STRING, { isRequired: true });
c.addColumn('note', AbdbColumnType.STRING, { description: 'An optional note' });Supported types (AbdbColumnType):
| Value | Accepts |
|---|---|
| STRING | Primitive string |
| NUMBER | Finite number, or numeric string (e.g. "3.14") |
| BOOLEAN | true/false, or "true"/"false" (case-insensitive) |
| DATETIME | Date, finite timestamp (ms), or ISO 8601 string |
Validation strategies:
// Fail-fast — throws on the first error (use before DB writes)
users.validate({ first_name: 'Jane', last_name: 'Doe', email: '[email protected]' });
// Collect all errors — ideal for API validation responses
const errors = users.validateAll({ first_name: '', email: 'not-an-email' });
// errors → ['"first_name" expects a string', '"last_name" is required']Column introspection:
users.hasColumn('email'); // true
users.getColumn('email'); // AbdbColumn instance (or undefined)
users.getColumns(); // Map<string, AbdbColumn> in insertion orderAbdbRepository
A generic CRUD repository that wraps an AbdbCollection and holds the IMS token and region for the lifetime of the instance. All write methods validate payloads against the collection schema before touching the database, and _created_at / _updated_at timestamps are stamped automatically.
Basic usage
const { AbdbRepository } = require('@adobe-commerce/aio-toolkit');
const { generateAccessToken } = require('@adobe/aio-sdk').Core.AuthClient;
const UserCollection = require('@lib/UserCollection');
exports.main = async (params) => {
const token = await generateAccessToken(params);
const accessToken = token.access_token;
const repo = new AbdbRepository(new UserCollection(), accessToken);
// Insert — returns raw insertOne result (result.insertedId contains the new _id)
const insertResult = await repo.save({ first_name: 'Jane', last_name: 'Doe', email: '[email protected]' });
const id = insertResult.insertedId;
// Read by _id (shorthand)
const doc = await repo.findById(id);
// Read one by any field
const byEmail = await repo.findOne({ email: '[email protected]' });
// Read all (optional filter + optional pagination/sort)
const all = await repo.find();
const active = await repo.find({ active: true });
const paged = await repo.find({ active: true }, { current_page: 2, page_size: 20, sort: { column: '_created_at', direction: 'desc' } });
// Partial update — only provided fields are validated; required fields already in the DB are not re-checked
await repo.save({ first_name: 'Janet' }, id);
// Delete by _id
await repo.deleteById(id);
};Automatic timestamps
The repository automatically registers two extra columns on the collection:
| Column | Set on | Value |
|---|---|---|
| _created_at | First insert only | ISO 8601 UTC string |
| _updated_at | Every insert and update | ISO 8601 UTC string |
These are added once per collection instance, so re-using the same collection across multiple repository instances is safe.
API reference
Single-document operations:
| Method | Description | Returns |
|---|---|---|
| save(payload) | Insert via insertOne (full validation, stamps both timestamps) | Promise<Record<string, any>> — raw insertOne result |
| save(payload, id) | Update via updateOne with upsert: true (partial validation, stamps _updated_at) | Promise<Record<string, any>> — raw updateOne result |
| insertOne(payload) | Single-document insert (full validation, stamps both timestamps) | Promise<Record<string, any>> — raw insertOne result (e.g. insertedId) |
| updateOne(payload, filter?, options?) | Single-document update via { $set: payload } (partial validation, stamps _updated_at) | Promise<Record<string, any>> — raw updateOne result (e.g. modifiedCount) |
| findOne(filter) | First document matching filter, e.g. { email: '[email protected]' } | Promise<T \| null> |
| findById(id) | Shorthand for findOne({ _id: new ObjectId(id) }) | Promise<T \| null> |
| find(filter?, options?) | All documents matching filter with optional pagination and sort (AbdbFindOptions) | Promise<T[]> |
| isIdExists(id) | Returns true if a document with the given _id exists (no-op for empty id) | Promise<boolean> |
| exists(filter?, options?) | Returns true if any document matching filter exists via countDocuments | Promise<boolean> |
| deleteOne(filter?) | Delete first matching document via deleteOne | Promise<Record<string, any>> — raw deleteOne result |
| deleteById(id) | Shorthand for deleteOne({ _id: new ObjectId(id) }) | Promise<Record<string, any>> |
| count(filter?, options?) | Count matching documents via countDocuments (default: all) | Promise<number> |
Bulk operations (single DB round-trip each):
| Method | Description | Returns |
|---|---|---|
| insert(payloads[]) | Bulk insert via insertMany — each payload stamped and validated | Promise<Record<string, any>> — raw insertMany result (e.g. insertedIds) |
| update(payload, filter?, options?) | Bulk update via updateMany with { $set: payload } — stamps _updated_at, partial validation | Promise<Record<string, any>> — raw updateMany result (e.g. modifiedCount) |
| delete(filter?) | Bulk delete via deleteMany (default: all documents) | Promise<Record<string, any>> — raw deleteMany result (e.g. deletedCount) |
Accessors:
| Method | Description |
|---|---|
| getName() | Returns the underlying collection name |
| getCollection() | Returns the underlying AbdbCollection instance |
Pagination and sorting
find accepts an optional second argument for server-side pagination and sorting:
// All active users, newest first
const all = await repo.find(
{ active: true },
{ sort: { column: '_created_at', direction: 'desc' } }
);
// Page 2, 20 results per page, sorted ascending by name
const page2 = await repo.find(
{},
{ current_page: 2, page_size: 20, sort: { column: 'name', direction: 'asc' } }
);
// Just limit — no pagination offset
const top10 = await repo.find({}, { page_size: 10 });AbdbFindOptions fields (all optional):
| Field | Type | Default | Description |
|---|---|---|---|
| sort.column | string | — | Field to sort by |
| sort.direction | 'asc' \| 'desc' | 'asc' | Sort direction |
| page_size | number | — | Max documents to return (enables .limit()) |
| current_page | number | — | 1-based page index; computes .skip((page-1) * page_size) |
Custom region
// Default region is 'amer'. Pass a third argument to override:
const repo = new AbdbRepository(new UserCollection(), accessToken, 'emea');Bulk operations
// Insert multiple documents in one DB call
const result = await repo.insert([
{ first_name: 'Alice', last_name: 'Smith', email: '[email protected]' },
{ first_name: 'Bob', last_name: 'Jones', email: '[email protected]' },
]);
console.log(result.insertedIds); // array of inserted _id values
// Update all matching documents in one DB call
const updateResult = await repo.update({ active: false }, { email: '[email protected]' });
console.log(updateResult.modifiedCount); // number of documents updated
// Delete all matching documents in one DB call
const deleteResult = await repo.delete({ active: false });
console.log(deleteResult.deletedCount); // number of documents deleted
// Delete everything in the collection
await repo.delete();🏪 Commerce Components
Adobe Commerce API integration and authentication
AdobeAuth
Adobe IMS authentication and token management.
const { AdobeAuth } = require('@adobe-commerce/aio-toolkit');
// Get authentication token
const token = await AdobeAuth.getToken(
'your-client-id',
'your-client-secret',
'your-technical-account-id',
'your-technical-account-email',
'your-ims-org-id',
['AdobeID', 'openid', 'adobeio_api'] // Scopes
);
console.log('Authentication token:', token);AdobeCommerceClient
HTTP client for Adobe Commerce API integration with multiple authentication methods.
OAuth 1.0a Authentication
const { AdobeCommerceClient, Oauth1aConnection } = require('@adobe-commerce/aio-toolkit');
const connection = new Oauth1aConnection(
'consumer-key',
'consumer-secret',
'access-token',
'access-token-secret',
logger // Optional custom logger
);
// Create client
const client = new AdobeCommerceClient('https://your-commerce-store.com/rest', connection);
// Make API calls
const products = await client.get('V1/products');
const newProduct = await client.post('V1/products', {}, productData);IMS (Identity Management System) Authentication
const { AdobeCommerceClient, ImsConnection, AdobeAuth, BearerToken } = require('@adobe-commerce/aio-toolkit');
// Step 1: Generate token using AdobeAuth (implement caching in your application)
// Check your application cache first
let token = await yourAppCache.get('commerce_ims_token');
if (!token || !BearerToken.info(token).isValid) {
// Generate new token
token = await AdobeAuth.getToken(
'client-id',
'client-secret',
'technical-account-id',
'technical-account-email',
'ims-org-id@AdobeOrg',
['AdobeID', 'openid', 'adobeio_api']
);
// Store in your application cache with appropriate TTL
const tokenInfo = BearerToken.info(token);
if (tokenInfo.isValid && tokenInfo.timeUntilExpiry) {
const ttl = Math.floor(tokenInfo.timeUntilExpiry / 1000) - 600; // 10-min buffer
await yourAppCache.set('commerce_ims_token', token, ttl);
}
}
// Step 2: Create connection with token
const connection = new ImsConnection(token, logger); // Optional logger
// Step 3: Create client with IMS authentication
const client = new AdobeCommerceClient('https://your-commerce-store.com', connection);
// Make API calls
const products = await client.get('V1/products');
const newProduct = await client.post('V1/products', {}, productData);💡 Best Practice: Implement token caching at the application level (Redis, Memcached, in-memory) to avoid unnecessary token generation calls.
⚠️ DEPRECATED: ImsToken Class
This class is deprecated due to built-in caching mechanism issues:
- Relies on Adobe I/O Runtime State API which has reliability issues
- State API may not be available in all runtime environments
- Hard-coded TTL buffers and minimum TTL are inflexible
- Limited control over caching strategy
Migration Required: Use
AdobeAuthdirectly and implement caching at the application level.
// ❌ OLD (Deprecated) - ImsToken with built-in caching
const { ImsToken } = require('@adobe-commerce/aio-toolkit');
const imsToken = new ImsToken(
'client-id',
'client-secret',
'technical-account-id',
'technical-account-email',
'ims-org-id@AdobeOrg',
['AdobeID', 'openid', 'adobeio_api'],
logger // optional
);
const token = await imsToken.execute();// ✅ NEW (Recommended) - AdobeAuth with application-level caching
const { AdobeAuth, BearerToken } = require('@adobe-commerce/aio-toolkit');
// Check your application cache first (Redis, Memcached, in-memory, etc.)
let token = await yourAppCache.get('ims_token');
// Validate cached token
if (!token || !BearerToken.info(token).isValid) {
// Generate new token using AdobeAuth
token = await AdobeAuth.getToken(
'client-id',
'client-secret',
'technical-account-id',
'technical-account-email',
'ims-org-id@AdobeOrg',
['AdobeID', 'openid', 'adobeio_api']
);
// Store in your application cache with appropriate TTL
const tokenInfo = BearerToken.info(token);
if (tokenInfo.isValid && tokenInfo.timeUntilExpiry) {
const ttl = Math.floor(tokenInfo.timeUntilExpiry / 1000) - 600; // 10-min buffer
await yourAppCache.set('ims_token', token, ttl);
}
}
// Use the token
console.log('Token:', token);Why Migrate?
- ✅ Full control over caching strategy (Redis, Memcached, in-memory, etc.)
- ✅ Better error handling and retry logic
- ✅ Works in any environment (not just Adobe I/O Runtime)
- ✅ Easier to test and mock
- ✅ Flexible TTL management based on your needs
Basic Authentication
const { AdobeCommerceClient, BasicAuthConnection } = require('@adobe-commerce/aio-toolkit');
const connection = new BasicAuthConnection(
'username',
'password',
logger // Optional custom logger
);
// Create client
const client = new AdobeCommerceClient('https://your-commerce-store.com/rest', connection);
// Make API calls
const products = await client.get('V1/products');ShippingCarrier
Fluent builder for creating custom shipping carriers for Adobe Commerce webhook extensibility.
const {
ShippingCarrier,
ShippingCarrierResponse
} = require('@adobe-commerce/aio-toolkit');
// Create a custom shipping carrier with methods
const carrier = new ShippingCarrier('fedex', (carrier) => {
carrier
.setTitle('FedEx Express')
.setStores(['default', 'store1'])
.setCountries(['US', 'CA', 'MX'])
.setSortOrder(10)
.setActive(true)
.setTrackingAvailable(true)
.setShippingLabelsAvailable(true)
.addMethod('standard', (method) => {
method
.setMethodTitle('Standard Shipping')
.setPrice(9.99)
.setCost(5.00)
.addAdditionalData('delivery_time', '3-5 business days')
.addAdditionalData('tracking_available', true);
})
.addMethod('express', (method) => {
method
.setMethodTitle('Express Shipping')
.setPrice(19.99)
.setCost(12.00)
.addAdditionalData('delivery_time', '1-2 business days');
})
.removeMethod('overnight'); // Remove a method
});
// Get carrier configuration
const carrierData = carrier.getData();
console.log(carrierData);
// Access added and removed methods
const addedMethods = carrier.getAddedMethods(); // ['standard', 'express']
const removedMethods = carrier.getRemovedMethods(); // ['overnight']
// Generate webhook response operations
const response = new ShippingCarrierResponse(carrier);
const operations = response.generate();
return operations; // Use in webhook action
// Update carrier data (code is immutable)
carrier.setData({
code: 'ups', // This will be ignored - code remains 'fedex'
title: 'Demo Postal Service',
stores: ['default'],
countries: ['US', 'CA'],
sort_order: 10,
active: true,
tracking_available: true,
shipping_labels_available: true
});
// Code property is immutable - create new instance if you need different code
const newCarrier = new ShippingCarrier('ups', (c) => {
c.addMethod('ground', (m) => {
m.setMethodTitle('UPS Ground').setPrice(12.99);
});
});Key Features:
- Builder pattern with method chaining
- Validation for carrier and method codes (alphanumeric and underscores only)
- Add and remove shipping methods dynamically
- Configure carrier properties (title, stores, countries, sort order, etc.)
- Immutable code property - prevents accidental carrier identity changes
- Public getter methods:
getAddedMethods()andgetRemovedMethods() - Generate webhook response operations
- Type-safe TypeScript interfaces
Validation Rules:
- Carrier and method codes must contain only alphanumeric characters and underscores
- No spaces, hyphens, dots, or special characters allowed
- Empty or whitespace-only codes throw errors
- Code property cannot be changed after initialization
OnboardCommerce
Complete Adobe Commerce I/O Events configuration orchestration with automated provider setup and event subscription management.
const {
OnboardCommerce,
AdobeCommerceClient,
ImsConnection
} = require('@adobe-commerce/aio-toolkit');
const Core = require('@adobe/aio-sdk').Core;
// Generate token using AdobeAuth (implement caching in your application)
const token = await AdobeAuth.getToken(
'client-id',
'client-secret',
'technical-account-id',
'technical-account-email',
'ims-org-id@AdobeOrg',
['AdobeID', 'openid', 'adobeio_api']
);
// Initialize Adobe Commerce client
const connection = new ImsConnection(token, logger);
const adobeCommerceClient = new AdobeCommerceClient(
'https://your-commerce-store.com',
connection
);
// Initialize logger
const logger = Core.Logger('onboard-client', {
level: 'debug'
});
// Initialize OnboardCommerce
const onboardCommerce = new OnboardCommerce(
adobeCommerceClient,
process.env.COMMERCE_ADOBE_IO_EVENTS_MERCHANT_ID || '',
process.env.COMMERCE_ADOBE_IO_EVENTS_ENVIRONMENT_ID || '',
logger,
false // isPaaS: set to true for PaaS instances (defaults to false)
);
// Define commerce provider
const commerceProvider = {
raw: {
id: 'commerce-provider-id',
label: 'Commerce Events Provider',
description: 'Provider for Adobe Commerce events',
docsUrl: 'https://developer.adobe.com/commerce/events'
}
};
// Define workspace configuration
const workspaceConfig = {
project: {
id: process.env.ADOBE_PROJECT_ID,
name: 'My Commerce Project',
title: 'Commerce Integration'
},
workspace: {
id: process.env.ADOBE_WORKSPACE_ID,
name: 'Production'
}
};
// Define commerce events configuration
const commerceEventsConfig = [
{
event: {
name: 'com.adobe.commerce.observer.catalog_product_save_after',
label: 'Product Saved',
description: 'Triggered when a product is saved'
}
},
{
event: {
name: 'com.adobe.commerce.observer.sales_order_save_after',
label: 'Order Saved',
description: 'Triggered when an order is saved'
}
}
];
// Execute configuration
const result = await onboardCommerce.process(
commerceProvider.raw,
workspaceConfig,
commerceEventsConfig
);
if (result.success) {
console.log('✅ Commerce events configured successfully');
console.log(`Provider: ${result.provider?.label}`);
} else {
console.error('❌ Configuration failed:', result.error);
}Key Features:
- Automated provider configuration with validation
- Event subscription management with duplicate detection
- Intelligent event metadata validation against supported Commerce events
- PaaS support for Adobe Commerce Cloud instances with native event handling
- Structured logging with prefixes:
[START],[CREATE],[SKIP],[ERROR],[IMPORTANT] - Comprehensive summary reporting with event subscription statistics
- Integration with Adobe Commerce API for event discovery
- Automatic post-subscription CLI instructions for PaaS instances
- 100% test coverage
PaaS Support:
For Adobe Commerce PaaS (Platform as a Service) instances, enable PaaS mode to support native events:
// Initialize OnboardCommerce for PaaS instances
const onboardCommercePaaS = new OnboardCommerce(
adobeCommerceClient,
merchantId,
environmentId,
logger,
true // Enable PaaS mode
);
// Define native PaaS events (observer.*, plugin.*)
const paasEventsConfig = [
{
event: {
name: 'observer.catalog_product_save_after',
label: 'Product Saved',
description: 'Native observer event'
}
},
{
event: {
name: 'plugin.magento.catalog.model.product.save',
label: 'Product Save Plugin',
description: 'Native plugin event'
}
}
];
// Process PaaS events
const result = await onboardCommercePaaS.process(
commerceProvider.raw,
workspaceConfig,
paasEventsConfig
);
// PaaS mode automatically:
// - Skips supported events validation for better performance
// - Handles native events without the 'parent' field
// - Displays post-subscription Magento CLI commandsAfter successful event subscription on PaaS, the system displays:
[IMPORTANT] ⚠️ Post-Subscription Steps for PaaS:
[IMPORTANT] 1. Run: bin/magento events:generate:module to generate module after successful event subscription
[IMPORTANT] 2. Run: bin/magento setup:upgrade && bin/magento setup:di:compile && bin/magento cache:flush to install the generated module🎨 Experience Components
Adobe Commerce Admin UI extension and user experience tools
AdminUiSdk
Create and manage Adobe Commerce Admin UI extensions with menu items, sections, and page configurations.
const { AdminUiSdk } = require('@adobe-commerce/aio-toolkit');
const sdk = new AdminUiSdk('dataMappingTool');
// Add menu section with external parent
sdk.addMenuSection(
'dataMappingTool::checkout_integration',
'Checkout Integration',
100,
'Magento_Backend::system'
);
// Add menu item
sdk.addMenuItem(
'dataMappingTool::application',
'Application',
1,
'dataMappingTool::checkout_integration'
);
// Set page title and get registration
sdk.addPage('Data Mapping Tool Dashboard');
const registration = sdk.getRegistration();🔗 Integration Components
External API integration and utility functions
RestClient
HTTP client for external API integration with support for various payload types.
Basic Usage
const { RestClient } = require('@adobe-commerce/aio-toolkit');
const client = new RestClient();
// GET request
const response = await client.get('https://api.example.com/data', {
'Authorization': 'Bearer token'
});JSON Payloads (default)
// POST with JSON (automatic Content-Type: application/json)
const jsonData = { name: 'Product', price: 99.99 };
const response = await client.post('https://api.example.com/products', {
'Authorization': 'Bearer token'
}, jsonData);Form-Encoded Requests
// URLSearchParams for form-encoded data (automatic Content-Type: application/x-www-form-urlencoded)
const formData = new URLSearchParams({
grant_type: 'client_credentials',
client_id: 'your-client-id',
client_secret: 'your-client-secret'
});
const tokenResponse = await client.post('https://auth.example.com/token', {
Accept: 'application/json'
}, formData);File Upload
// FormData for file uploads (Content-Type boundary handled automatically)
const uploadData = new FormData();
uploadData.append('file', fileBuffer, 'document.pdf');
uploadData.append('description', 'Important document');
const uploadResponse = await client.post('https://api.example.com/upload', {
'Authorization': 'Bearer token'
}, uploadData);Text/XML Payloads
// String payloads with custom content type
const xmlData = '<?xml version="1.0"?><order><id>123</id></order>';
const xmlResponse = await client.post('https://api.example.com/orders', {
'Authorization': 'Bearer token',
'Content-Type': 'application/xml'
}, xmlData);BearerToken
Bearer token extraction and JWT analysis utility. Supports both standard HTTP headers and OpenWhisk format for maximum portability.
Extract from Standard HTTP Headers
const { BearerToken } = require('@adobe-commerce/aio-toolkit');
const headers = { authorization: 'Bearer abc123token' };
const tokenInfo = BearerToken.extract(headers);
console.log(tokenInfo);
// Output: {
// token: 'abc123token',
// tokenLength: 11,
// isValid: true,
// expiry: '2024-01-02T12:00:00.000Z',
// timeUntilExpiry: 86400000
// }Extract from OpenWhisk Params (Backward Compatibility)
const params = { __ow_headers: { authorization: 'Bearer abc123token' } };
const owTokenInfo = BearerToken.extract(params);
console.log(owTokenInfo); // Same output format as aboveDirect Token Analysis
const directInfo = BearerToken.info('jwt-token-string');
if (directInfo.isValid) {
console.log(`Token expires at: ${directInfo.expiry}`);
console.log(`Time until expiry: ${directInfo.timeUntilExpiry}ms`);
} else {
console.log('Token is invalid or expired');
}Methods:
extract(headersOrParams)- Extracts Bearer token from headers or OpenWhisk paramsinfo(token)- Analyzes token string and returns validation/expiry details
RabbitMQClient
Pull-based AMQP consumer and publisher for RabbitMQ. Manages the full connection lifecycle per call (connect → channel → close) and exposes a clean, typed interface without exposing raw AMQP primitives to the caller.
Installation
npm install amqplibBasic Usage
const { RabbitMQClient } = require('@adobe-commerce/aio-toolkit');
const client = new RabbitMQClient({
host: 'rabbitmq.example.com',
port: '5672',
username: 'user',
password: 'secret',
vhost: '/',
secure: false, // set true to use amqps://
});Consuming messages
const stats = await client.consume(
'orders-queue',
{ batchSize: 100, maxParallel: 10 },
async (queueName, content) => {
const order = JSON.parse(content);
await processOrder(order);
}
);
console.log(`consumed=${stats.consumed} acked=${stats.acked} nacked=${stats.nacked}`);
if (stats.errors.length) {
console.error('Failed messages:', stats.errors);
}Consuming with exchange binding
const stats = await client.consume(
'orders-queue',
{
batchSize: 50,
maxParallel: 5,
exchange: 'orders-exchange', // asserts exchange + binds queue before consuming
nackRequeue: false, // dead-letter failed messages instead of requeueing
},
async (queueName, content) => {
await processOrder(JSON.parse(content));
}
);Publishing messages
const payloads = orders.map(o => JSON.stringify(o));
const stats = await client.publish('orders-queue', payloads);
console.log(`published=${stats.published} failed=${stats.failed}`);
if (stats.errors.length) {
console.error('Failed payloads:', stats.errors);
}API Reference
| Method | Description | Returns |
|---|---|---|
| consume(queueName, options, handler) | Checks queue depth and pull-consumes up to batchSize messages via channel.get in parallel windows of maxParallel | Promise<ConsumeStats> |
| publish(queueName, payloads) | Asserts queue and enqueues each payload as a persistent message | Promise<PublishStats> |
RabbitMQConsumeOptions
| Field | Type | Required | Description |
|---|---|---|---|
| batchSize | number | ✅ | Maximum number of messages to process in this call |
| maxParallel | number | ✅ | Max messages processed concurrently per window (client-side) |
| nackRequeue | boolean | — | Requeue on nack (default: true) |
| exchange | string | — | If set, asserts a direct exchange and binds the queue before consuming |
ConsumeStats
| Field | Type | Description |
|---|---|---|
| consumed | number | Total messages received from the broker |
| acked | number | Messages successfully processed and acked |
| nacked | number | Messages that failed handler and were nacked |
| errors | Array<{ content: string; error: unknown }> | Per-failure details |
PublishStats
| Field | Type | Description |
|---|---|---|
| published | number | Messages successfully enqueued |
| failed | number | Messages that could not be enqueued |
| errors | Array<{ payload: string; error: unknown }> | Per-failure details (thrown errors only; write-buffer-full is handled via backpressure) |
AmazonSQSClient
Batched publisher and long-polling consumer for Amazon SQS standard and FIFO queues.
Installation
npm install @aws-sdk/client-sqsBasic Usage
const { AmazonSQSClient } = require('@adobe-commerce/aio-toolkit');
const client = new AmazonSQSClient({
region: 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/my-queue',
});Publishing messages
const payloads = orders.map(o => JSON.stringify(o));
const stats = await client.publish(payloads);
console.log(`published=${stats.published} failed=${stats.failed}`);
if (stats.errors.length) {
console.error('Failed payloads:', stats.errors);
}Publishing to a FIFO queue
const fifoClient = new AmazonSQSClient({
region: 'us-east-1',
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
queueUrl: 'https://sqs.us-east-1.amazonaws.com/123456789/my-queue.fifo',
messageGroupId: 'order-processing', // required for FIFO
messageDeduplicationId: 'unique-batch-id', // optional — omit to use content-based dedup
});
const stats = await fifoClient.publish(payloads);Consuming messages
const stats = await client.consume(
{ batchSize: 10, waitTimeSeconds: 20 }, // long polling
async (message) => {
const order = JSON.parse(message.body);
await processOrder(order);
}
);
console.log(`consumed=${stats.consumed} acked=${stats.acked} failed=${stats.failed}`);API Reference
| Method | Description | Returns |
|---|---|---|
| publish(payloads) | Sends each payload as an SQS message; FIFO queues include group/dedup IDs | Promise<PublishStats> |
| consume(options, handler) | Long-polls the queue, invokes handler for each message, auto-deletes on success | Promise<ConsumeStats> |
InfiniteLoopBreaker
Detect and prevent infinite loops in event-driven applications.
const { InfiniteLoopBreaker } = require('@adobe-commerce/aio-toolkit');
// Basic infinite loop detection
const isLoop = await InfiniteLoopBreaker.isInfiniteLoop({
keyFn: 'order-processing-key',
fingerprintFn: orderData,
eventTypes: ['order.created', 'order.updated'],
event: 'order.created'
});
if (isLoop) {
console.log('Infinite loop detected, skipping processing');
return;
}
// Process the event
await processOrderEvent(orderData);
// Store fingerprint to prevent future loops
await InfiniteLoopBreaker.storeFingerPrint(
'order-processing-key',
orderData,
300 // 5 minutes TTL
);OnboardIOEvents (formerly OnboardEvents)
Complete onboarding orchestration for Adobe I/O Events (providers, metadata, and registrations).
Note:
OnboardEventsis deprecated and will be removed in v2.0.0. Please useOnboardIOEventsinstead.
const { OnboardIOEvents } = require('@adobe-commerce/aio-toolkit');
// ✅ Recommended - Use OnboardIOEvents
const onboardEvents = new OnboardIOEvents(
'My E-commerce Store',
process.env.ADOBE_CONSUMER_ID!,
process.env.ADOBE_PROJECT_ID!,
process.env.ADOBE_WORKSPACE_ID!,
process.env.ADOBE_API_KEY!,
process.env.ADOBE_ACCESS_TOKEN!
);
const basicOnboardingExample = async () => {
const input = {
providers: [
{
key: 'ecommerce-provider',
label: 'E-commerce Events Provider',
description: 'Provider for e-commerce platform events',
docsUrl: 'https://docs.mystore.com/events',
registrations: [
{
key: 'order-events',
label: 'Order Events Registration',
description: 'Registration for order-related events',
events: [
{
eventCode: 'com.mystore.order.placed',
runtimeAction: 'mystore/order-placed-handler',
deliveryType: 'webhook',
sampleEventTemplate: {
orderId: 'ord_123456',
customerId: 'cust_789',
totalAmount: 99.99,
currency: 'USD',
status: 'placed',
timestamp: '2023-01-01T12:00:00Z'
}
},
{
eventCode: 'com.mystore.order.shipped',
runtimeAction: 'mystore/order-shipped-handler',
deliveryType: 'journal',
sampleEventTemplate: {
orderId: 'ord_123456',
trackingNumber: 'TN123456789',
carrier: 'UPS',
shippedAt: '2023-01-02T10:00:00Z'
}
}
]
}
]
}
]
};
try {
const result = await onboar