Advanced Features
Context Reference - Advanced Features Advanced context features including file uploads, API information, shared context, and package access. Transactions Use await @TRANSACTION.run(async () => { ... }) when repository mutations must either all commit or all roll back. It is equiv
Context Reference - Advanced Features
Advanced context features including file uploads, API information, shared context, and package access.
Transactions
Use await @TRANSACTION.run(async () => { ... }) when repository mutations must either all commit or all roll back. It is equivalent to $ctx.$transaction.run(...) and works across the configured database backend.
await @TRANSACTION.run(async () => {
await @REPOS.orders.create({ data: @BODY.order });
await @REPOS.inventory.update({
id: @BODY.inventoryId,
data: { reserved: true }
});
});
Only repository operations participate in the transaction. Do not assume @FETCH, @STORAGE, @CACHE, @SOCKET, or @TRIGGER can be rolled back; defer those effects until after a successful transaction when that distinction matters.
File Uploads
Access information about uploaded files.
Named multipart methods expose $ctx.$uploadFile by configured field name; count-one fields hold a file and multi-file fields hold arrays. Optional fields are absent, and reading a missing key throws. Check Object.keys before optional access. See File Handling for configuration, temporary-file lifetime and storage side effects.
$ctx.$uploadedFile
Information about the uploaded file (if any).
if ($ctx.$uploadedFile) {
const filename = $ctx.$uploadedFile.originalname;
const mimetype = $ctx.$uploadedFile.mimetype;
const size = $ctx.$uploadedFile.size;
const fieldname = $ctx.$uploadedFile.fieldname;
}
$ctx.$uploadedFile describes a multipart request file. It does not expose the file body as a buffer. Pass it directly to $ctx.$storage.$upload({ file: $ctx.$uploadedFile }) or $ctx.$storage.$update(id, { file: $ctx.$uploadedFile }) so Enfyra streams from the temp file path.
Processing Uploaded Files
if ($ctx.$uploadedFile) {
$ctx.$logs(`File uploaded: ${$ctx.$uploadedFile.originalname}`);
$ctx.$logs(`File size: ${$ctx.$uploadedFile.size} bytes`);
$ctx.$logs(`MIME type: ${$ctx.$uploadedFile.mimetype}`);
// Save request file using the streaming helper.
const fileResult = await $ctx.$storage.$upload({
file: $ctx.$uploadedFile,
description: $ctx.$body.description
});
}
Streaming an HTTP response
Use an installed Server package with a single-consumer readable body. Resolve an allow-listed server-owned upstream URL and private credentials; do not use a caller-controlled URL for a privileged proxy.
const upstream = await @PKGS.undici.request(configuredUpstreamUrl, {
method: 'GET',
headers: configuredPrivateHeaders,
});
const ready = await $ctx.$streams.preflight(upstream.body, {
timeoutMs: 5000,
maxFirstChunkBytes: 65536,
});
return await @RES.stream(ready.stream, {
mimetype: 'text/event-stream',
});
preflight observes the first non-empty raw chunk before response commit and returns { stream, firstChunk, cancel }. Relay stream, not the original body, so the first chunk appears once. Direct @RES.stream(upstream.body) commits before reading; use it when that is the intended contract. Reasoning-only SSE or other raw bytes count as activity, not semantic generation success.
The composed $ctx.$streams helpers share the task budget:
readBytes/readTextconsume without opening a response, withtimeoutMs,idleTimeoutMsandmaxBytes(8 MiB by default).readTextdecodes split UTF-8 incrementally.guardreturns a readable with total/idle/byte bounds.tapobserves raw chunks with serialized callback backpressure.cancelcloses the owned stream; preflight also returns a cancel function.- Each readable has one consumer. Do not preflight/collect then reuse the original body or issue concurrent pulls. Preflight's first-byte deadline does not become the replay deadline; bound the remaining relay under the enclosing method budget.
ERR_PACKAGE_STREAM_TIMEOUTcloses the target stream;ERR_PACKAGE_STREAM_EMPTYmeans EOF before a first byte. Task cancellation still closes task-owned resources. Verify deployed Server/Kernel support for these APIs.
@RES.stream is terminal: await it and return no second payload. transform(text, kind) receives arbitrary UTF-8-decoded chunk/end fragments; return string/null/undefined to replace/suppress/preserve output. Buffer complete SSE records across callbacks rather than assuming network chunk boundaries. A transform error fails the stream; observer is best-effort inspection after commit, not readiness or required settlement. @HELPERS.$fetch buffers a body and is not a streaming transport. Keep provider headers and secrets private.
See postHooks for the separate best-effort terminal audit path; upstream errors, client disconnects and Server stream deadlines are different causes.
API Information
Access detailed information about the current API request and response.
Request Information
const method = $ctx.$api.request.method; // HTTP method
const url = $ctx.$api.request.url; // Request URL
const timestamp = $ctx.$api.request.timestamp; // Request timestamp
const correlationId = $ctx.$api.request.correlationId; // Unique request ID
const executionId = $ctx.$api.request.executionId;
const userAgent = $ctx.$api.request.userAgent; // User agent
const ip = $ctx.$api.request.ip; // Client IP
Response Information
const statusCode = $ctx.$api.response.statusCode; // HTTP status code
const responseTime = $ctx.$api.response.responseTime; // Response time in ms
const timestamp = $ctx.$api.response.timestamp; // Response timestamp
Error Information (postHook Only)
if ($ctx.$api.error) {
const message = $ctx.$api.error.message; // Error message
const stack = $ctx.$api.error.stack; // Stack trace
const name = $ctx.$api.error.name; // Error class name
const statusCode = $ctx.$api.error.statusCode; // HTTP error status
const timestamp = $ctx.$api.error.timestamp; // Error timestamp
const details = $ctx.$api.error.details; // Additional details
}
Shared Context
Store data that persists across hooks and handlers.
$ctx.$share
Shared data container for passing data between hooks.
// In preHook - store data
$ctx.$share.validationPassed = true;
$ctx.$share.processStartTime = Date.now();
$ctx.$share.userId = $ctx.$user.id;
// In postHook - access shared data
if ($ctx.$share.validationPassed) {
const processingTime = Date.now() - $ctx.$share.processStartTime;
$ctx.$data.processingTime = processingTime;
}
Accessing Logs
// All logs are stored in $ctx.$share.$logs
const allLogs = $ctx.$share.$logs; // Array of all logged messages
Environment Access
Read non-secret process environment values from the sanitized environment snapshot.
$ctx.$env
$ctx.$env exposes a sanitized copy of process.env for dynamic scripts. It is useful for non-secret runtime switches such as node names, deployment labels, public URLs, or feature flags.
const nodeName = $ctx.$env.NODE_NAME || 'default';
const appMode = $ctx.$env.ENFYRA_MODE || 'all';
Sensitive infrastructure keys are not exposed. Current exact deny-list keys are:
DB_URIDB_REPLICA_URISREDIS_URISECRET_KEYADMIN_PASSWORD
Do not use $ctx.$env as an application secret store. Store application secrets in unpublished table fields marked isEncrypted=true, then read and write plaintext through normal repository or REST operations.
Encrypted table fields are tied to the server SECRET_KEY. In self-hosted deployments, keep SECRET_KEY stable, backed up, and identical across every server instance for the same Enfyra app. Changing or losing it prevents existing encrypted field values from decrypting.
Package Access
Access NPM packages installed in the system.
$ctx.$pkgs
Access installed packages by name.
// Use installed packages
const axios = $ctx.$pkgs.axios;
const lodash = $ctx.$pkgs.lodash;
const moment = $ctx.$pkgs.moment;
// Example: Using axios
if ($ctx.$pkgs.axios) {
const response = await $ctx.$pkgs.axios.get('https://api.example.com/data');
}
Note: Packages must be installed through Package Management in the UI to be accessible.
Common Patterns
Pattern 1: Validate and Transform Request Data
// Validate required fields
if (!$ctx.$body.email) {
@THROW400('Email is required');
return;
}
// Transform data
$ctx.$body.email = $ctx.$body.email.toLowerCase().trim();
Pattern 2: Check User Permissions
if (!$ctx.$user) {
@THROW401('Authentication required');
return;
}
if ($ctx.$user.role !== 'admin') {
@THROW403('Admin access required');
return;
}
Pattern 3: Use Shared Context Between Hooks
// In preHook
$ctx.$share.userId = $ctx.$user.id;
$ctx.$share.validationData = { passed: true };
// In postHook
const userId = $ctx.$share.userId;
if ($ctx.$share.validationData.passed) {
// Use validation result
}
Pattern 4: Cache Frequently Accessed Data
// Check cache first
const cacheKey = `user:${$ctx.$params.id}`;
let user = await $ctx.$cache.get(cacheKey);
if (!user) {
// Cache miss - fetch from database
const result = await $ctx.$repos.enfyra_user.find({
filter: { id: { _eq: $ctx.$params.id } }
});
if (result.data.length > 0) {
user = result.data[0];
// Cache for 5 minutes
await $ctx.$cache.set(cacheKey, user, 300000);
}
}
Pattern 5: Handle Errors in postHook
// In postHook
if ($ctx.$api.error) {
// Log error
$ctx.$logs(`Error: ${$ctx.$api.error.message}`);
// Log to audit system
await $ctx.$repos.audit_logs.create({
data: {
action: 'error_occurred',
userId: $ctx.$user?.id,
errorMessage: $ctx.$api.error.message,
statusCode: $ctx.$api.error.statusCode,
timestamp: new Date()
}
});
} else {
// Success - add metadata
$ctx.$data.processedAt = new Date();
$ctx.$data.processedBy = $ctx.$user?.id;
}
Next Steps
- See File Handling for complete file upload guide
- Check Cache Operations for detailed cache patterns
- Learn about Error Handling for error handling patterns