Docs

Template Syntax Guide

Template Syntax Guide Enfyra provides three equivalent ways to access context properties. You can use the full $ctx.$property syntax, template syntax, or direct table syntax - all work exactly the same way. Overview All three syntaxes are fully supported and equivalent: // Full s

Template Syntax Guide

Enfyra provides three equivalent ways to access context properties. You can use the full $ctx.$property syntax, template syntax, or direct table syntax - all work exactly the same way.

Overview

All three syntaxes are fully supported and equivalent:

//  Full syntax (always works)
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.user_definition.find({...});
const slug = $ctx.$helpers.autoSlug('Hello World');
$ctx.$logs('Operation completed');

//  Template syntax (convenience shortcut)
const data = await @CACHE.get('key');
const users = await @REPOS.user_definition.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;

//  Direct table syntax (shortest for database)
const data = await @CACHE.get('key');
const users = await #user_definition.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;

Template syntax is just syntactic sugar - ESV replaces it with the full $ctx.$property syntax before compiled code is passed to the kernel executor. Use whichever style you prefer - you can even mix all three in the same file!

In find() options, filter and where are the same object (REST list endpoints use the query parameter name filter only).

Available Templates

Template Replacement Description
@CACHE $ctx.$cache Cache operations and distributed locking
@REPOS $ctx.$repos Database repository access
@HELPERS $ctx.$helpers Utility functions and helpers
@FETCH $ctx.$helpers.$fetch SSRF-hardened HTTP client (shorthand for @HELPERS.$fetch)
@LOGS $ctx.$logs Logging functions
@BODY $ctx.$body Request body data
@ENV $ctx.$env Sanitized environment variables exposed by the host runtime
@DATA $ctx.$data Response data object
@STATUS $ctx.$statusCode HTTP status code (200 on success, error code on failure — available in postHooks)
@ERROR $ctx.$error Error context in postHooks ({ message, name, statusCode, details, timestamp }undefined on success)
@PARAMS $ctx.$params Route parameters
@QUERY $ctx.$query Query parameters
@USER $ctx.$user Current user information
@REQ $ctx.$req Express request object
@RES $ctx.$res Express response object (handlers only)
@SHARE $ctx.$share Shared data between hooks
@API $ctx.$api API request/response information
@SOCKET $ctx.$socket WebSocket operations (join, leave, reply, emitToUser, emitToRoom, emitToGateway, broadcast; disconnect only in connection handlers)
@TRIGGER $ctx.$trigger Trigger a flow by id or name (@TRIGGER(flowIdOrName, payload?))
@FLOW $ctx.$flow Current flow context inside flow steps (payload, last step output, meta)
@FLOW_PAYLOAD $ctx.$flow.$payload Original payload passed into the flow
@FLOW_LAST $ctx.$flow.$last Output of the previous flow step
@FLOW_META $ctx.$flow.$meta Flow execution metadata (id, name, runId, etc.)
@UPLOADED_FILE $ctx.$uploadedFile Uploaded file information
@PKGS $ctx.$pkgs Installed npm packages for use in handlers
@THROW $ctx.$throw Error throwing functions
@THROW400 $ctx.$throw['400'] HTTP 400 Bad Request (shortcut)
@THROW401 $ctx.$throw['401'] HTTP 401 Unauthorized (shortcut)
@THROW403 $ctx.$throw['403'] HTTP 403 Forbidden (shortcut)
@THROW404 $ctx.$throw['404'] HTTP 404 Not Found (shortcut)
@THROW409 $ctx.$throw['409'] HTTP 409 Conflict (shortcut)
@THROW422 $ctx.$throw['422'] HTTP 422 Validation Error (shortcut)
@THROW429 $ctx.$throw['429'] HTTP 429 Rate Limit Exceeded (shortcut)
@THROW500 $ctx.$throw['500'] HTTP 500 Internal Error (shortcut)
@THROW503 $ctx.$throw['503'] HTTP 503 Service Unavailable (shortcut)
#table_name $ctx.$repos.table_name Direct table access (e.g., #user_definition, #product_definition)
%pkg_name $ctx.$pkgs.pkg_name Shorthand package access (e.g., %axios, %lodash, %moment)

Usage Examples

Cache Operations

@CACHE maps to the same managed user cache as $ctx.$cache. Use logical keys only; Enfyra applies the current app namespace internally. Do not include NODE_NAME, user_cache:, or Redis prefixes in template code. User-cache data is limited by REDIS_USER_CACHE_LIMIT_MB (default 30 MB), and Enfyra evicts least-recently-used user-cache keys when the allocation is exceeded.

// Get data from cache
const cachedData = await @CACHE.get('user:123');

// Set data in cache with TTL
await @CACHE.set('user:123', userData, 3600000); // 1 hour

// Check if key exists
const exists = await @CACHE.exists('user:123');

// Delete from cache
await @CACHE.deleteKey('user:123');

// Distributed locking
const lockAcquired = await @CACHE.acquire('critical-operation', 'instance-1', 30000);
if (lockAcquired) {
  try {
    // Critical operation here
    await performCriticalOperation();
  } finally {
    await @CACHE.release('critical-operation', 'instance-1');
  }
}

Database Operations

Using @REPOS syntax:

// Find records with filtering and pagination
const users = await @REPOS.user_definition.find({
  where: { isActive: true },
  fields: 'id,email,name',   // Only fetch required fields
  limit: 10,                  // Max 10 records (default: 10)
  sort: '-createdAt'          // Sort by createdAt DESC
});

// Fetch ALL records (no limit)
const allUsers = await @REPOS.user_definition.find({
  where: { isActive: true },
  limit: 0  // 0 = fetch all
});

// Multi-field sorting
const sorted = await @REPOS.user_definition.find({
  sort: 'name,-createdAt'  // Sort by name ASC, then createdAt DESC
});

// Nested relations (get related data in ONE query)
const usersWithPosts = await @REPOS.user_definition.find({
  fields: 'id,email,posts.title,posts.createdAt',  // Nested field: posts.title
  where: { isActive: true }
});

// Filter by nested relation
const usersInRole = await @REPOS.user_definition.find({
  where: {
    role: {
      name: { _eq: 'Admin' }  // Filter by related role name
    }
  },
  fields: 'id,email,role.name'
});

// Create new record
const newUser = await @REPOS.user_definition.create({
  data: {
    email: '[email protected]',
    name: 'John Doe',
    isActive: true
  }
});

// Update record by ID
const updatedUser = await @REPOS.user_definition.update({
  id: userId,
  data: {
    name: 'Jane Doe',
    lastLogin: new Date()
  }
});

// Delete record by ID
await @REPOS.user_definition.delete({ id: userId });

Using #table_name syntax (shorter):

// Find records with filtering and pagination
const users = await #user_definition.find({
  where: { isActive: true },
  fields: 'id,email,name',   // Only fetch required fields
  limit: 10,                  // Max 10 records (default: 10)
  sort: '-createdAt'          // Sort by createdAt DESC
});

// Fetch ALL records (no limit)
const allUsers = await #user_definition.find({
  where: { isActive: true },
  limit: 0  // 0 = fetch all
});

// Multi-field sorting
const sorted = await #user_definition.find({
  sort: 'name,-createdAt'  // Sort by name ASC, then createdAt DESC
});

// Nested relations (get related data in ONE query)
const usersWithPosts = await #user_definition.find({
  fields: 'id,email,posts.title,posts.createdAt',  // Nested field: posts.title
  where: { isActive: true }
});

// Filter by nested relation
const usersInRole = await #user_definition.find({
  where: {
    role: {
      name: { _eq: 'Admin' }  // Filter by related role name
    }
  },
  fields: 'id,email,role.name'
});

// Create new record
const newUser = await #user_definition.create({ data: {
  email: '[email protected]',
  name: 'John Doe',
  isActive: true
}});

// Update record by ID
const updatedUser = await #user_definition.update({ id: userId, data: {
  name: 'Jane Doe',
  lastLogin: new Date()
}});

// Delete record by ID
await #user_definition.delete({ id: userId });

Helper Functions

// Generate JWT token (call $jwt as a function: payload, expiresIn)
const token = await @HELPERS.$jwt({ userId: 123, role: 'admin' }, '1h');

// Hash password (single argument; salt rounds are managed internally)
const hashedPassword = await @HELPERS.$bcrypt.hash('password123');

// Verify password
const isValid = await @HELPERS.$bcrypt.compare('password123', hashedPassword);

// Generate URL-friendly slug
const slug = @HELPERS.autoSlug('Hello World!'); // "hello-world"

Package Usage

Traditional Syntax:

// Access installed npm packages
const axios = $ctx.$pkgs.axios;
const lodash = $ctx.$pkgs.lodash;
const moment = $ctx.$pkgs.moment;

// Use package normally
const response = await axios.get('https://api.example.com/data');

// Transform data with lodash
const grouped = lodash.groupBy(response.data, 'category');

// Format dates with moment
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');

Template Syntax (Shortened):

// Access installed npm packages
const axios = @PKGS.axios;
const lodash = @PKGS.lodash;
const moment = @PKGS.moment;

// Use package normally
const response = await axios.get('https://api.example.com/data');

// Transform data with lodash
const grouped = lodash.groupBy(response.data, 'category');

// Format dates with moment
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');

Shorthand Syntax (%):

// Direct package access - shortest possible
const axios = %axios;
const lodash = %lodash;
const moment = %moment;

const response = await axios.get('https://api.example.com/data');
const summary = lodash.groupBy(response.data, 'category');
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');

Mixed Syntax:

// You can mix all three ways!
const axios = %axios;                         // Shorthand syntax
const lodash = @PKGS.lodash;                 // Template syntax  
const moment = $ctx.$pkgs.moment;             // Traditional syntax

const response = await axios.get('https://api.example.com/data');
const summary = lodash.groupBy(response.data, 'category');
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');

Logging

// Basic logging
@LOGS('User operation started');

// Log with data
@LOGS('User created:', { id: 123, email: '[email protected]' });

// Multiple parameters
@LOGS('Cache operation', 'key:', 'user:123', 'result:', cachedData);

// Error logging
@LOGS('Error occurred:', error.message, error.stack);

File Upload & Streaming

// Access uploaded file
const file = @UPLOADED_FILE;
@LOGS('File uploaded:', file.originalname, file.mimetype, file.size);

// Save uploaded request file to storage and file_definition.
// This streams from the server temp file and does not buffer the full file.
const savedFile = await @STORAGE.$upload({
  file: @UPLOADED_FILE,
  description: @BODY.description
});

// Stream response (for large files or image processing)
const { Readable } = require('stream');
const sharp = @PKGS.sharp;

// Download and resize image
const response = await fetch(@QUERY.imageUrl);
const stream = Readable.fromWeb(response.body);

const transformer = sharp()
  .resize(800, 600, { fit: 'inside' })
  .jpeg({ quality: 85 });

// Stream to client (memory efficient!)
@RES.stream(stream.pipe(transformer), {
  mimetype: 'image/jpeg',
  filename: 'resized-image.jpg'
});

** See File Handling** for complete guide on file uploads, streaming, and image processing.

Error Handling

HTTP Status Code Errors (@THROW):

Option 1: With brackets and quotes

// Throw HTTP 400 Bad Request
@THROW['400']('Email is required');

// Throw HTTP 401 Unauthorized
@THROW['401']('Invalid credentials');

// Throw HTTP 403 Forbidden
@THROW['403']('Insufficient permissions');

// Throw HTTP 404 Not Found
@THROW['404']('User not found', 'user_id_123');

// Throw HTTP 409 Conflict (for duplicates)
@THROW['409']('Email already exists', 'email', '[email protected]');

// Throw HTTP 422 Validation Error
@THROW['422']('Invalid data format');

// Throw HTTP 500 Internal Server Error
@THROW['500']('Database connection failed');

Option 2: Direct shortcuts (no quotes needed)

// Throw HTTP 400 Bad Request
@THROW400('Email is required');

// Throw HTTP 401 Unauthorized  
@THROW401('Invalid credentials');

// Throw HTTP 403 Forbidden
@THROW403('Insufficient permissions');

// Throw HTTP 404 Not Found
@THROW404('User not found', 'user_id_123');

// Throw HTTP 409 Conflict (for duplicates)
@THROW409('Email already exists');

// Throw HTTP 422 Validation Error
@THROW422('Invalid data format');

// Throw HTTP 429 Rate Limit Exceeded
@THROW429(100, 'per minute');

// Throw HTTP 500 Internal Server Error
@THROW500('Database connection failed');

// Throw HTTP 503 Service Unavailable
@THROW503('Service unavailable');

Advanced Usage

Chaining Operations

// Cache with database fallback
let data = await @CACHE.get('products:featured');
if (!data) {
  data = await #products.find({
    where: { featured: true },
    fields: 'id,name,price,image'
  });
  await @CACHE.set('products:featured', data, 3600000);
}
@LOGS('Featured products loaded:', data.length);

Error Handling with Logging

try {
  const user = await #user_definition.find({ where: { id: userId } });
  if (!user.data.length) {
    @THROW404('User not found');
  }

  const updatedUser = await #user_definition.update({
    id: userId,
    data: { lastLogin: new Date() }
  });

  @LOGS('User login updated:', updatedUser.id);
  return updatedUser;

} catch (error) {
  @LOGS('User update failed:', error.message);
  throw error;
}

Complex Business Logic

// User registration with validation and caching
async function registerUser(userData) {
  // Check if user exists
  const existingUser = await #user_definition.find({
    where: { email: userData.email },
    fields: 'id'
  });

  if (existingUser.data.length > 0) {
    @THROW409('Email already exists');
  }

  // Hash password
  const hashedPassword = await @HELPERS.$bcrypt.hash(userData.password);

  // Create user
  const newUser = await #user_definition.create({ data: {
    ...userData,
    password: hashedPassword,
    createdAt: new Date()
  });

  // Cache user data
  await @CACHE.set(`user:${newUser.id}`, newUser, 1800000); // 30 minutes

  // Generate JWT token
  const token = await @HELPERS.$jwt({ userId: newUser.id }, '24h');

  @LOGS('User registered successfully:', newUser.id);

  return { user: newUser, token };
}

How Template Replacement Works

Template syntax is purely a convenience feature. Here's what happens behind the scenes:

  1. Code Submission: Your code with @CACHE, @REPOS, etc. is submitted
  2. Template Processing: Templates are automatically replaced with full $ctx.$property syntax
  3. Code Execution: The processed code runs normally in the handler executor
  4. Result Return: Normal execution continues with the replaced syntax

Example Transformation

// Your code (template syntax):
const data = await @CACHE.get('key');
const users = await #user_definition.find({...});

// What actually runs (after replacement):
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.user_definition.find({...});

** The replacement is transparent** - you can mix all three syntaxes in the same file if you want!

Best Practices

1. Choose Your Style (All Work!)

//  Option 1 - Full syntax throughout
const user = await $ctx.$repos.users.find({ where: { id: userId } });
await $ctx.$cache.set(`user:${userId}`, user);
$ctx.$logs('User cached:', userId);

//  Option 2 - Template syntax throughout  
const user = await @REPOS.users.find({ where: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);

//  Option 3 - Direct table syntax (shortest for database)
const user = await #user_definition.find({ where: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);

//  Option 4 - Mix all three (totally fine!)
const user = await #user_definition.find({ where: { id: userId } });        // Direct table
await $ctx.$cache.set(`user:${userId}`, user);                    // Full syntax
@LOGS('User cached:', userId);                                     // Template syntax

//  Option 5 - Any combination works!
const user = await $ctx.$repos.user_definition.find({ where: { id: userId } }); // Full
await @CACHE.set(`user:${userId}`, user);                             // Template
const products = await #product_definition.find({ where: { userId } }); // Direct
$ctx.$logs('All operations completed');                               // Full

2. Leverage Field Selection

//  Good - only fetch needed fields
const users = await #user_definition.find({
  where: { isActive: true },
  fields: 'id,email,name' // Performance optimization
});

//  Avoid fetching all fields unnecessarily
const users = await #user_definition.find({
  where: { isActive: true }
  // Fetches all fields by default
});

3. Proper Error Handling

//  Good - comprehensive error handling
try {
  const result = await @REPOS.users.create({ data: userData });
  @LOGS('User created successfully');
  return result;
} catch (error) {
  @LOGS('User creation failed:', error.message);
  @THROW500('Failed to create user');
}

4. Cache Strategy

//  Good - cache with appropriate TTL
const cacheKey = `user:${userId}`;
let user = await @CACHE.get(cacheKey);

if (!user) {
  user = await @REPOS.users.find({ where: { id: userId } });
  await @CACHE.set(cacheKey, user, 1800000); // 30 minutes
}

Compatibility & Flexibility

  • ** All Three Syntaxes Supported**: Use $ctx.$property, @TEMPLATE, OR #table_name - your choice!
  • ** Mix and Match**: You can use any combination in the same file
  • ** All Contexts**: Works in Bootstrap Scripts, Hooks, and Custom Handlers
  • ** IDE Support**: $ctx.$property has better autocomplete support
  • ** Debugging**: Stack traces show the processed $ctx.$property syntax
  • ** No Performance Impact**: Template replacement is just string processing
  • ** Maximum Flexibility**: Choose the syntax that fits your style best

Related Documentation