Hướng dẫn cú pháp mẫu
Hướng dẫn cú pháp mẫu Enfyra có ba cách tương đương để truy cập thuộc tính context. Bạn có thể dùng cú pháp đầy đủ $ctx.$property , cú pháp template hoặc cú pháp bảng trực tiếp; cả ba hoạt động như nhau. Tổng quan Cả ba cú pháp đều được hỗ trợ đầy đủ và tương đương: // Cú pháp đầ
Hướng dẫn cú pháp mẫu
Enfyra có ba cách tương đương để truy cập thuộc tính context. Bạn có thể dùng cú pháp đầy đủ $ctx.$property, cú pháp template hoặc cú pháp bảng trực tiếp; cả ba hoạt động như nhau.
Tổng quan
Cả ba cú pháp đều được hỗ trợ đầy đủ và tương đương:
// Cú pháp đầy đủ (luôn hoạt động)
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.enfyra_user.find({...});
const slug = $ctx.$helpers.autoSlug('Hello World');
$ctx.$logs('Operation completed');
// Cú pháp template (lối tắt tiện dụng)
const data = await @CACHE.get('key');
const users = await @REPOS.enfyra_user.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;
// Cú pháp bảng trực tiếp (ngắn nhất khi truy cập database)
const data = await @CACHE.get('key');
const users = await #enfyra_user.find({...});
const slug = @HELPERS.autoSlug('Hello World');
@LOGS('Operation completed');
const userId = @USER.id;
const bodyData = @BODY.name;
Cú pháp template chỉ là syntactic sugar - Enfyra Server thay nó bằng cú pháp đầy đủ $ctx.$property trước khi code biên dịch được chuyển cho kernel executor. Bạn có thể dùng phong cách mình thích, kể cả trộn cả ba trong cùng một file.
Trong tuỳ chọn find(), truyền điều kiện qua filter. Endpoint danh sách REST cũng dùng tên query parameter là filter.
Template có sẵn
| Template | Thay thế thành | Mô tả |
|---|---|---|
@CACHE |
$ctx.$cache |
Thao tác cache và distributed lock |
@REPOS |
$ctx.$repos |
Truy cập database repository |
@HELPERS |
$ctx.$helpers |
Hàm tiện ích và helper |
@FETCH |
$ctx.$helpers.$fetch |
HTTP client đã tăng cường chống SSRF (viết tắt của @HELPERS.$fetch) |
@LOGS |
$ctx.$logs |
Hàm ghi log |
@BODY |
$ctx.$body |
Dữ liệu request body |
@ENV |
$ctx.$env |
Biến môi trường đã được làm sạch do host runtime cung cấp |
@DATA |
$ctx.$data |
Đối tượng dữ liệu response |
@STATUS |
$ctx.$statusCode |
HTTP status code (200 khi thành công, error code khi thất bại — có trong postHook) |
@ERROR |
$ctx.$error |
Error context trong postHook ({ message, name, statusCode, details, timestamp } — undefined khi thành công) |
@PARAMS |
$ctx.$params |
Tham số route |
@QUERY |
$ctx.$query |
Tham số query |
@USER |
$ctx.$user |
Thông tin người dùng hiện tại |
@REQ |
$ctx.$req |
Đối tượng request của Express |
@RES |
$ctx.$res |
Đối tượng response của Express (chỉ handler) |
@SHARE |
$ctx.$share |
Dữ liệu dùng chung giữa các hook |
@API |
$ctx.$api |
Thông tin request/response API |
@SOCKET |
$ctx.$socket |
Thao tác WebSocket (join, leave, reply, emitToUser, emitToRoom, emitToCurrentRoom, broadcastToRoom, emitToGateway, broadcast; disconnect chỉ trong connection handler) |
@TRIGGER |
$ctx.$trigger |
Kích hoạt flow theo id hoặc tên (@TRIGGER(flowIdOrName, payload?)) |
@FLOW |
$ctx.$flow |
Flow context hiện tại trong flow step (payload, output step trước, meta) |
@FLOW_PAYLOAD |
$ctx.$flow.$payload |
Payload gốc truyền vào flow |
@FLOW_LAST |
$ctx.$flow.$last |
Output của flow step trước |
@FLOW_META |
$ctx.$flow.$meta |
Metadata thực thi flow (id, name, runId, v.v.) |
@UPLOADED_FILE |
$ctx.$uploadedFile |
Thông tin file đã tải lên |
@PKGS |
$ctx.$pkgs |
npm package đã cài để dùng trong handler |
@THROW |
$ctx.$throw |
Hàm ném lỗi |
@THROW400 |
$ctx.$throw['400'] |
HTTP 400 Bad Request (lối tắt) |
@THROW401 |
$ctx.$throw['401'] |
HTTP 401 Unauthorized (lối tắt) |
@THROW403 |
$ctx.$throw['403'] |
HTTP 403 Forbidden (lối tắt) |
@THROW404 |
$ctx.$throw['404'] |
HTTP 404 Not Found (lối tắt) |
@THROW409 |
$ctx.$throw['409'] |
HTTP 409 Conflict (lối tắt) |
@THROW422 |
$ctx.$throw['422'] |
HTTP 422 Validation Error (lối tắt) |
@THROW429 |
$ctx.$throw['429'] |
HTTP 429 Rate Limit Exceeded (lối tắt) |
@THROW500 |
$ctx.$throw['500'] |
HTTP 500 Internal Error (lối tắt) |
@THROW503 |
$ctx.$throw['503'] |
HTTP 503 Service Unavailable (lối tắt) |
#table_name |
$ctx.$repos.table_name |
Truy cập bảng trực tiếp (ví dụ #enfyra_user, #product) |
%pkg_name |
$ctx.$pkgs.pkg_name |
Truy cập package viết tắt (ví dụ %axios, %lodash, %moment) |
Ví dụ sử dụng
Thao tác cache
@CACHE trỏ tới cùng managed user cache với $ctx.$cache. Chỉ dùng logical key; Enfyra tự áp namespace của app hiện tại. Không đưa NODE_NAME, user_cache: hay tiền tố Redis vào code template. Dữ liệu user cache bị giới hạn bởi REDIS_USER_CACHE_LIMIT_MB (mặc định 30 MB); Enfyra sẽ loại các key ít dùng nhất khi vượt giới hạn.
// Lấy dữ liệu từ cache
const cachedData = await @CACHE.get('user:123');
// Lưu dữ liệu vào cache kèm TTL
await @CACHE.set('user:123', userData, 3600000); // 1 hour
// Kiểm tra key có tồn tại
const exists = await @CACHE.exists('user:123');
// Xoá khỏi cache
await @CACHE.deleteKey('user:123');
// Distributed lock
const lockAcquired = await @CACHE.acquire('critical-operation', 'instance-1', 30000);
if (lockAcquired) {
try {
// Thao tác quan trọng ở đây
await performCriticalOperation();
} finally {
await @CACHE.release('critical-operation', 'instance-1');
}
}
Thao tác database
Dùng cú pháp @REPOS:
// Tìm bản ghi có filter và phân trang
const users = await @REPOS.enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name', // Only fetch required fields
limit: 10, // Max 10 records (default: 10)
sort: '-createdAt' // Sort by createdAt DESC
});
// Lấy TOÀN BỘ bản ghi (không giới hạn)
const allUsers = await @REPOS.enfyra_user.find({
filter: { isActive: true },
limit: 0 // 0 = fetch all
});
// Sắp xếp nhiều trường
const sorted = await @REPOS.enfyra_user.find({
sort: 'name,-createdAt' // Sort by name ASC, then createdAt DESC
});
// Quan hệ lồng nhau (lấy dữ liệu liên quan trong MỘT query)
const usersWithPosts = await @REPOS.enfyra_user.find({
fields: 'id,email,posts.title,posts.createdAt', // Nested field: posts.title
filter: { isActive: true }
});
// Lọc theo quan hệ lồng nhau
const usersInRole = await @REPOS.enfyra_user.find({
filter: {
role: {
name: { _eq: 'Admin' } // Filter by related role name
}
},
fields: 'id,email,role.name'
});
// Tạo bản ghi mới
const newUser = await @REPOS.enfyra_user.create({
data: {
email: '[email protected]',
name: 'John Doe',
isActive: true
}
});
// Cập nhật bản ghi theo ID
const updatedUser = await @REPOS.enfyra_user.update({
id: userId,
data: {
name: 'Jane Doe',
lastLogin: new Date()
}
});
// Xoá bản ghi theo ID
await @REPOS.enfyra_user.delete({ id: userId });
Dùng cú pháp #table_name (ngắn hơn):
// Tìm bản ghi có filter và phân trang
const users = await #enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name', // Only fetch required fields
limit: 10, // Max 10 records (default: 10)
sort: '-createdAt' // Sort by createdAt DESC
});
// Lấy TOÀN BỘ bản ghi (không giới hạn)
const allUsers = await #enfyra_user.find({
filter: { isActive: true },
limit: 0 // 0 = fetch all
});
// Sắp xếp nhiều trường
const sorted = await #enfyra_user.find({
sort: 'name,-createdAt' // Sort by name ASC, then createdAt DESC
});
// Quan hệ lồng nhau (lấy dữ liệu liên quan trong MỘT query)
const usersWithPosts = await #enfyra_user.find({
fields: 'id,email,posts.title,posts.createdAt', // Nested field: posts.title
filter: { isActive: true }
});
// Lọc theo quan hệ lồng nhau
const usersInRole = await #enfyra_user.find({
filter: {
role: {
name: { _eq: 'Admin' } // Filter by related role name
}
},
fields: 'id,email,role.name'
});
// Tạo bản ghi mới
const newUser = await #enfyra_user.create({ data: {
email: '[email protected]',
name: 'John Doe',
isActive: true
}});
// Cập nhật bản ghi theo ID
const updatedUser = await #enfyra_user.update({ id: userId, data: {
name: 'Jane Doe',
lastLogin: new Date()
}});
// Xoá bản ghi theo ID
await #enfyra_user.delete({ id: userId });
Hàm helper
// Tạo JWT token (gọi $jwt như một hàm: payload, expiresIn)
const token = await @HELPERS.$jwt({ userId: 123, role: 'admin' }, '1h');
// Hash mật khẩu (một đối số; salt round do hệ thống tự quản lý)
const hashedPassword = await @HELPERS.$bcrypt.hash('password123');
// Xác thực mật khẩu
const isValid = await @HELPERS.$bcrypt.compare('password123', hashedPassword);
// Tạo slug thân thiện với URL
const slug = @HELPERS.autoSlug('Hello World!'); // "hello-world"
Dùng package
Cú pháp truyền thống:
// Truy cập npm package đã cài
const axios = $ctx.$pkgs.axios;
const lodash = $ctx.$pkgs.lodash;
const moment = $ctx.$pkgs.moment;
// Dùng package như bình thường
const response = await axios.get('https://api.example.com/data');
// Chuyển đổi dữ liệu bằng lodash
const grouped = lodash.groupBy(response.data, 'category');
// Định dạng ngày bằng moment
const timestamp = moment().format('YYYY-MM-DD HH:mm:ss');
Cú pháp template (rút gọn):
// Access installed npm packages
const axios = @PKGS.axios;
const lodash = @PKGS.lodash;
const moment = @PKGS.moment;
// Dùng package như bình thường
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');
Cú pháp viết tắt (%):
// Truy cập package trực tiếp - ngắn nhất
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');
Cú pháp kết hợp:
// Bạn có thể kết hợp cả ba cách!
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');
Ghi log
// Ghi log cơ bản
@LOGS('User operation started');
// Ghi log kèm dữ liệu
@LOGS('User created:', { id: 123, email: '[email protected]' });
// Nhiều tham số
@LOGS('Cache operation', 'key:', 'user:123', 'result:', cachedData);
// Ghi log lỗi
@LOGS('Error occurred:', error.message, error.stack);
Tải file lên và streaming
// Truy cập file đã tải lên
const file = @UPLOADED_FILE;
@LOGS('File uploaded:', file.originalname, file.mimetype, file.size);
// Lưu file request đã tải lên vào storage và enfyra_file.
// Thao tác stream từ file tạm của server, không buffer toàn bộ file.
const savedFile = await @STORAGE.$upload({
file: @UPLOADED_FILE,
description: @BODY.description
});
// Stream response (cho file lớn hoặc xử lý ảnh)
const { Readable } = require('stream');
const sharp = @PKGS.sharp;
// Tải xuống và thay đổi kích thước ảnh
const response = await fetch(@QUERY.imageUrl);
const stream = Readable.fromWeb(response.body);
const transformer = sharp()
.resize(800, 600, { fit: 'inside' })
.jpeg({ quality: 85 });
// Stream tới client (tiết kiệm bộ nhớ)
@RES.stream(stream.pipe(transformer), {
mimetype: 'image/jpeg',
filename: 'resized-image.jpg'
});
** Xem File Handling** để biết đầy đủ về tải file lên, streaming và xử lý ảnh.
Xử lý lỗi
Lỗi HTTP status code (@THROW):
Cách 1: Dùng ngoặc vuông và dấu nháy
// Ném lỗi HTTP 400 Bad Request
@THROW['400']('Email is required');
// Ném lỗi HTTP 401 Unauthorized
@THROW['401']('Invalid credentials');
// Ném lỗi HTTP 403 Forbidden
@THROW['403']('Insufficient permissions');
// Ném lỗi HTTP 404 Not Found
@THROW['404']('User not found', 'user_id_123');
// Ném lỗi HTTP 409 Conflict (khi trùng lặp)
@THROW['409']('Email already exists', 'email', '[email protected]');
// Ném lỗi HTTP 422 Validation Error
@THROW['422']('Invalid data format');
// Ném lỗi HTTP 500 Internal Server Error
@THROW['500']('Database connection failed');
Cách 2: Lối tắt trực tiếp (không cần dấu nháy)
// Ném lỗi HTTP 400 Bad Request
@THROW400('Email is required');
// Ném lỗi HTTP 401 Unauthorized
@THROW401('Invalid credentials');
// Ném lỗi HTTP 403 Forbidden
@THROW403('Insufficient permissions');
// Ném lỗi HTTP 404 Not Found
@THROW404('User not found', 'user_id_123');
// Ném lỗi HTTP 409 Conflict (khi trùng lặp)
@THROW409('Email already exists');
// Ném lỗi HTTP 422 Validation Error
@THROW422('Invalid data format');
// Ném lỗi HTTP 429 Rate Limit Exceeded
@THROW429(100, 'per minute');
// Ném lỗi HTTP 500 Internal Server Error
@THROW500('Database connection failed');
// Ném lỗi HTTP 503 Service Unavailable
@THROW503('Service unavailable');
Cách dùng nâng cao
Chuỗi thao tác
// Cache kèm database fallback
let data = await @CACHE.get('products:featured');
if (!data) {
data = await #products.find({
filter: { featured: true },
fields: 'id,name,price,image'
});
await @CACHE.set('products:featured', data, 3600000);
}
@LOGS('Featured products loaded:', data.length);
Xử lý lỗi kèm logging
try {
const user = await #enfyra_user.find({ filter: { id: userId } });
if (!user.data.length) {
@THROW404('User not found');
}
const updatedUser = await #enfyra_user.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;
}
Logic nghiệp vụ phức tạp
// User registration with validation and caching
async function registerUser(userData) {
// Kiểm tra người dùng đã tồn tại
const existingUser = await #enfyra_user.find({
filter: { email: userData.email },
fields: 'id'
});
if (existingUser.data.length > 0) {
@THROW409('Email already exists');
}
// Hash mật khẩu
const hashedPassword = await @HELPERS.$bcrypt.hash(userData.password);
// Tạo người dùng
const newUser = await #enfyra_user.create({ data: {
...userData,
password: hashedPassword,
createdAt: new Date()
});
// Cache dữ liệu người dùng
await @CACHE.set(`user:${newUser.id}`, newUser, 1800000); // 30 minutes
// Tạo JWT token
const token = await @HELPERS.$jwt({ userId: newUser.id }, '24h');
@LOGS('User registered successfully:', newUser.id);
return { user: newUser, token };
}
Cách thay thế template hoạt động
Cú pháp template chỉ là tiện ích về cú pháp. Bên trong, Enfyra xử lý như sau:
- Gửi code: Code có
@CACHE,@REPOS, v.v. được gửi lên. - Xử lý template: Template tự động được thay bằng cú pháp đầy đủ
$ctx.$property. - Thực thi code: Code đã xử lý chạy bình thường trong handler executor.
- Trả kết quả: Việc thực thi tiếp tục bình thường với cú pháp đã thay thế.
Ví dụ chuyển đổi
// Code của bạn (cú pháp template):
const data = await @CACHE.get('key');
const users = await #enfyra_user.find({...});
// Nội dung thực sự chạy (sau khi thay thế):
const data = await $ctx.$cache.get('key');
const users = await $ctx.$repos.enfyra_user.find({...});
** Việc thay thế diễn ra trong suốt** - bạn có thể trộn cả ba cú pháp trong cùng một file nếu muốn.
Thực hành tốt
1. Chọn phong cách của bạn (cách nào cũng dùng được)
// Cách 1 - Dùng cú pháp đầy đủ xuyên suốt
const user = await $ctx.$repos.users.find({ filter: { id: userId } });
await $ctx.$cache.set(`user:${userId}`, user);
$ctx.$logs('User cached:', userId);
// Cách 2 - Dùng cú pháp template xuyên suốt
const user = await @REPOS.users.find({ filter: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);
// Cách 3 - Cú pháp bảng trực tiếp (ngắn nhất khi truy cập database)
const user = await #enfyra_user.find({ filter: { id: userId } });
await @CACHE.set(`user:${userId}`, user);
@LOGS('User cached:', userId);
// Cách 4 - Trộn cả ba (hoàn toàn ổn)
const user = await #enfyra_user.find({ filter: { id: userId } }); // Direct table
await $ctx.$cache.set(`user:${userId}`, user); // Full syntax
@LOGS('User cached:', userId); // Template syntax
// Cách 5 - Tổ hợp nào cũng hoạt động!
const user = await $ctx.$repos.enfyra_user.find({ filter: { id: userId } }); // Full
await @CACHE.set(`user:${userId}`, user); // Template
const products = await #product.find({ filter: { userId } }); // Direct
$ctx.$logs('All operations completed'); // Full
2. Tận dụng chọn trường
// Tốt - chỉ lấy trường cần thiết
const users = await #enfyra_user.find({
filter: { isActive: true },
fields: 'id,email,name' // Performance optimization
});
// Tốt - lấy source script có thể sửa mà không lấy compiled output sinh sẵn
const handlers = await #enfyra_route_handler.find({
fields: '-compiledCode'
});
// Tránh lấy mọi trường khi không cần thiết
const users = await #enfyra_user.find({
filter: { isActive: true }
// Mặc định lấy mọi trường
});
Khi bất kỳ tên trường nào bắt đầu bằng -, phạm vi fields đó dùng chế độ loại trừ. Ví dụ, fields: 'id,-compiledCode' vẫn trả về mọi trường có thể đọc trừ compiledCode.
3. Xử lý lỗi đúng cách
// Tốt - xử lý lỗi đầy đủ
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. Chiến lược cache
// Tốt - cache với TTL phù hợp
const cacheKey = `user:${userId}`;
let user = await @CACHE.get(cacheKey);
if (!user) {
user = await @REPOS.users.find({ filter: { id: userId } });
await @CACHE.set(cacheKey, user, 1800000); // 30 minutes
}
Tương thích và linh hoạt
- ** Hỗ trợ cả ba cú pháp**: Dùng
$ctx.$property,@TEMPLATEhoặc#table_nametheo lựa chọn của bạn. - ** Có thể kết hợp**: Bạn có thể dùng bất kỳ tổ hợp nào trong cùng một file.
- ** Dùng ở mọi context**: Hoạt động trong Bootstrap Script, Hook và Custom Handler.
- ** Hỗ trợ IDE**:
$ctx.$propertycó autocomplete tốt hơn. - ** Debugging**: Stack trace hiển thị cú pháp
$ctx.$propertysau xử lý. - ** Không ảnh hưởng hiệu năng**: Thay template chỉ là xử lý chuỗi.
- ** Linh hoạt tối đa**: Chọn cú pháp phù hợp nhất với phong cách của bạn.
Tài liệu liên quan
- Context Reference - Tài liệu đầy đủ về đối tượng
$ctx. - File Handling - Hướng dẫn tải file và response streaming.
- Custom Handlers - Dùng template trong handler.
- PreHooks - Dùng template trong prehook.
- postHooks - Dùng template trong postHook.