├── CODEOWNERS ├── .gitignore ├── app ├── hooks-handlers │ ├── README.md │ ├── hooks.md │ └── package-management.md ├── relation-picker.md ├── filter-system.md ├── README.md ├── menu-management.md ├── permission-builder.md ├── storage-management.md ├── page-header.md ├── permission-components.md └── routing-management.md ├── server ├── context-reference │ ├── README.md │ ├── repositories.md │ ├── logging-errors.md │ ├── request-data.md │ ├── helpers-cache.md │ └── advanced.md ├── hooks-handlers │ ├── README.md │ ├── afterhooks.md │ ├── prehooks.md │ ├── patterns.md │ └── custom-handlers.md ├── repository-methods │ ├── README.md │ ├── patterns.md │ ├── find.md │ └── create-update-delete.md ├── cluster-architecture.md ├── README.md ├── cache-operations.md ├── error-handling.md ├── query-filtering.md └── file-handling.md ├── getting-started ├── getting-started.md └── data-management.md ├── architecture-overview.md └── docker └── README.md /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @dothinh115 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Backup folders 2 | server-old-backup/ 3 | -------------------------------------------------------------------------------- /app/hooks-handlers/README.md: -------------------------------------------------------------------------------- 1 | # Hooks & Handlers (App UI) 2 | 3 | This section covers everything you configure from the admin app that affects **server-side hooks and handlers**: 4 | 5 | - **[Hooks](./hooks.md)** – Hooks System UI (preHooks and afterHooks) 6 | - **[Custom Handlers](./custom-handlers.md)** – Custom Handlers UI for replacing default CRUD 7 | - **[Package Management](./package-management.md)** – Installing NPM packages for use in hooks and handlers 8 | 9 | From the app perspective, you use visual tools to configure behavior; the **actual execution** happens on the Enfyra Server. 10 | 11 | For the full server-side behavior and APIs, see: 12 | 13 | - **[Server Hooks & Handlers](../../server/hooks-handlers/README.md)** – Server hooks & handlers overview 14 | - **[API Lifecycle](../../server/api-lifecycle.md)** – Complete request lifecycle and context sharing 15 | - **[Context Reference](../../server/context-reference/README.md)** – `$ctx` object reference 16 | 17 | -------------------------------------------------------------------------------- /server/context-reference/README.md: -------------------------------------------------------------------------------- 1 | # Context Object ($ctx) Reference 2 | 3 | The `$ctx` (context) object is available in all hooks and handlers. It provides access to request data, database repositories, helper functions, cache operations, and more. 4 | 5 | ## Quick Navigation 6 | 7 | - [Request Data](./request-data.md) - `$ctx.$body`, `$ctx.$params`, `$ctx.$query`, `$ctx.$user` 8 | - [Repositories](./repositories.md) - `$ctx.$repos` for database operations 9 | - [Helpers & Cache](./helpers-cache.md) - `$ctx.$helpers` and `$ctx.$cache` 10 | - [Logging & Error Handling](./logging-errors.md) - `$ctx.$logs()` and `$ctx.$throw` 11 | - [Advanced Features](./advanced.md) - File uploads, API info, shared context, packages, and patterns 12 | 13 | ## Documentation 14 | 15 | - **[Request Data](./request-data.md)** - Access HTTP request information and parameters 16 | - **[Repositories](./repositories.md)** - Database operations through repositories 17 | - **[Helpers & Cache](./helpers-cache.md)** - Utility functions and Redis cache operations 18 | - **[Logging & Error Handling](./logging-errors.md)** - Adding logs and throwing errors 19 | - **[Advanced Features](./advanced.md)** - File uploads, API information, shared context, and more 20 | 21 | ## Next Steps 22 | 23 | - See [Repository Methods](../repository-methods/) for database operations 24 | - Check [API Lifecycle](../api-lifecycle.md) to understand when context is available 25 | - Learn about [Hooks and Handlers](../hooks-handlers/) for using context in custom code 26 | -------------------------------------------------------------------------------- /server/context-reference/repositories.md: -------------------------------------------------------------------------------- 1 | # Context Reference - Repositories 2 | 3 | Access database tables through repositories. See [Repository Methods](../repository-methods/) for complete details. 4 | 5 | ## Accessing Repositories 6 | 7 | ```javascript 8 | // Access repository by table name 9 | const productsRepo = $ctx.$repos.products; 10 | const usersRepo = $ctx.$repos.user_definition; 11 | 12 | // Access main table repository 13 | const mainRepo = $ctx.$repos.main; 14 | 15 | // Check if repository exists 16 | if ($ctx.$repos.products) { 17 | const result = await $ctx.$repos.products.find({}); 18 | } 19 | ``` 20 | 21 | ## Repository Methods 22 | 23 | Each repository provides these methods: 24 | 25 | ```javascript 26 | // Find records 27 | const result = await $ctx.$repos.products.find({ 28 | where: { category: { _eq: 'electronics' } }, 29 | fields: 'id,name,price', 30 | limit: 10, 31 | sort: '-createdAt' 32 | }); 33 | 34 | // Create record 35 | const createResult = await $ctx.$repos.products.create({ 36 | data: { 37 | name: 'New Product', 38 | price: 99.99 39 | } 40 | }); 41 | 42 | // Update record 43 | const updateResult = await $ctx.$repos.products.update({ 44 | id: 123, 45 | data: { price: 89.99 } 46 | }); 47 | 48 | // Delete record 49 | const deleteResult = await $ctx.$repos.products.delete({ 50 | id: 123 51 | }); 52 | ``` 53 | 54 | **Important:** All repository methods return `{ data: [...], meta: {...} }` format. 55 | 56 | See [Repository Methods Guide](../repository-methods/) for complete documentation. 57 | 58 | ## Next Steps 59 | 60 | - See [Request Data](./request-data.md) for accessing request information 61 | - Check [Helpers & Cache](./helpers-cache.md) for utility functions 62 | - Learn about [Repository Methods](../repository-methods/) for complete method reference 63 | 64 | -------------------------------------------------------------------------------- /server/hooks-handlers/README.md: -------------------------------------------------------------------------------- 1 | # Hooks and Handlers 2 | 3 | Hooks and handlers allow you to customize API behavior at different points in the request lifecycle. Hooks run before and after handlers, while handlers contain the main business logic. 4 | 5 | ## Quick Navigation 6 | 7 | - [Overview](#overview) - What are hooks and handlers 8 | - [preHooks](./prehooks.md) - Execute before handler 9 | - [afterHooks](./afterhooks.md) - Execute after handler 10 | - [Custom Handlers](./custom-handlers.md) - Replace default CRUD 11 | - [Common Patterns](./patterns.md) - Real-world examples and best practices 12 | 13 | ## Overview 14 | 15 | ### Hooks 16 | 17 | Hooks are code snippets that run at specific points in the request lifecycle: 18 | - **preHooks**: Execute before the handler 19 | - **afterHooks**: Execute after the handler 20 | 21 | ### Handlers 22 | 23 | Handlers contain the main business logic: 24 | - **Custom Handler**: Your custom code 25 | - **Default CRUD**: Automatic CRUD operation based on HTTP method 26 | 27 | ### Execution Flow 28 | 29 | ``` 30 | preHook #1 preHook #2 Handler afterHook #1 afterHook #2 31 | ``` 32 | 33 | All hooks and handlers have access to the same `$ctx` object, so changes in one phase are visible to all subsequent phases. 34 | 35 | ## Documentation 36 | 37 | - **[preHooks](./prehooks.md)** - Validation, data transformation, and permission checks 38 | - **[afterHooks](./afterhooks.md)** - Response transformation, audit logging, and side effects 39 | - **[Custom Handlers](./custom-handlers.md)** - Custom business logic and hook types 40 | - **[Common Patterns](./patterns.md)** - Best practices and real-world examples 41 | 42 | ## Next Steps 43 | 44 | - Learn about [Repository Methods](../repository-methods/) for database operations 45 | - See [Context Reference](../context-reference/) for all available properties 46 | - Check [API Lifecycle](../api-lifecycle.md) to understand execution order 47 | -------------------------------------------------------------------------------- /server/repository-methods/README.md: -------------------------------------------------------------------------------- 1 | # Repository Methods 2 | 3 | Repositories are the main way to interact with your database tables in Enfyra. Every table you create automatically gets a repository that you can access through `$ctx.$repos.tableName`. 4 | 5 | ## Quick Reference 6 | 7 | **All repository methods return data in this format:** 8 | ```javascript 9 | { 10 | data: [...], // Array of records 11 | meta: { // Metadata (when requested) 12 | totalCount: 100, 13 | filterCount: 25 14 | } 15 | } 16 | ``` 17 | 18 | **Available Methods:** 19 | - [`find()`](./find.md) - Query records with filtering, sorting, and pagination 20 | - [`create()`](./create-update-delete.md#create) - Create new records 21 | - [`update()`](./create-update-delete.md#update) - Update existing records by ID 22 | - [`delete()`](./create-update-delete.md#delete) - Delete records by ID 23 | 24 | ## Accessing Repositories 25 | 26 | Repositories are available through the context object (`$ctx`) in hooks and handlers: 27 | 28 | ```javascript 29 | // Access a repository by table name 30 | const productsRepo = $ctx.$repos.products; 31 | const usersRepo = $ctx.$repos.user_definition; 32 | 33 | // Access the main table repository (if configured in route) 34 | const mainRepo = $ctx.$repos.main; 35 | ``` 36 | 37 | **Important:** 38 | - Tables must be configured in the route's "Target Tables" field to be accessible as repositories 39 | - The main table is always available as `$ctx.$repos.main` 40 | - All repository methods are async and require `await` 41 | 42 | ## Documentation 43 | 44 | - **[find() Method](./find.md)** - Complete guide to querying records 45 | - **[create(), update(), delete() Methods](./create-update-delete.md)** - Create, update, and delete operations 46 | - **[Common Patterns](./patterns.md)** - Best practices and common patterns 47 | 48 | ## Next Steps 49 | 50 | - Learn about the [Context Object ($ctx)](../context-reference/) to understand all available properties 51 | - See [Query Filtering](../query-filtering.md) for advanced filtering patterns 52 | - Check [API Lifecycle](../api-lifecycle.md) to understand how repositories fit into the request flow 53 | -------------------------------------------------------------------------------- /server/hooks-handlers/afterhooks.md: -------------------------------------------------------------------------------- 1 | # Hooks and Handlers - afterHooks 2 | 3 | afterHooks execute after the handler. Use them for response transformation, audit logging, and side effects. 4 | 5 | ## When to Use afterHooks 6 | 7 | - Transform response data 8 | - Add computed fields to response 9 | - Log audit trails 10 | - Trigger side effects (emails, notifications) 11 | - Handle errors that occurred in handler 12 | - Add metadata to response 13 | 14 | ## Basic afterHook Example 15 | 16 | ```javascript 17 | // Transform response 18 | if ($ctx.$data && Array.isArray($ctx.$data.data)) { 19 | $ctx.$data.data = $ctx.$data.data.map(item => ({ 20 | ...item, 21 | fullName: `${item.firstName} ${item.lastName}` 22 | })); 23 | } 24 | ``` 25 | 26 | ## Response Enhancement 27 | 28 | ```javascript 29 | // Add metadata 30 | if ($ctx.$data) { 31 | $ctx.$data.meta = { 32 | processedAt: new Date(), 33 | processedBy: $ctx.$user?.id, 34 | version: '1.0' 35 | }; 36 | } 37 | 38 | // Add computed fields to each item 39 | if ($ctx.$data && Array.isArray($ctx.$data.data)) { 40 | $ctx.$data.data = $ctx.$data.data.map(item => ({ 41 | ...item, 42 | isActive: item.status === 'active', 43 | displayName: item.nickname || item.name 44 | })); 45 | } 46 | ``` 47 | 48 | ## Audit Logging 49 | 50 | ```javascript 51 | // Log successful operations 52 | if (!$ctx.$api.error && $ctx.$data) { 53 | await $ctx.$repos.audit_logs.create({ 54 | data: { 55 | action: `${$ctx.$req.method} ${$ctx.$req.url}`, 56 | userId: $ctx.$user?.id, 57 | resourceId: $ctx.$params.id, 58 | statusCode: $ctx.$statusCode, 59 | timestamp: new Date() 60 | } 61 | }); 62 | } 63 | ``` 64 | 65 | ## Error Handling 66 | 67 | ```javascript 68 | // Handle errors in afterHook 69 | if ($ctx.$api.error) { 70 | // Error occurred 71 | $ctx.$logs(`Error: ${$ctx.$api.error.message}`); 72 | 73 | // Log to audit system 74 | await $ctx.$repos.error_logs.create({ 75 | data: { 76 | errorMessage: $ctx.$api.error.message, 77 | statusCode: $ctx.$api.error.statusCode, 78 | userId: $ctx.$user?.id, 79 | url: $ctx.$req.url, 80 | timestamp: new Date() 81 | } 82 | }); 83 | 84 | // Optionally modify error response 85 | // (though usually you'd want to handle this in error handlers) 86 | } else { 87 | // Success case 88 | $ctx.$logs('Operation completed successfully'); 89 | } 90 | ``` 91 | 92 | ## Next Steps 93 | 94 | - See [preHooks](./prehooks.md) for pre-handler operations 95 | - Learn about [Custom Handlers](./custom-handlers.md) for custom business logic 96 | - Check [Common Patterns](./patterns.md) for best practices 97 | 98 | -------------------------------------------------------------------------------- /server/hooks-handlers/prehooks.md: -------------------------------------------------------------------------------- 1 | # Hooks and Handlers - preHooks 2 | 3 | preHooks execute before the handler. Use them for validation, data transformation, and permission checks. 4 | 5 | ## When to Use preHooks 6 | 7 | - Validate request data 8 | - Transform or normalize input data 9 | - Check user permissions 10 | - Modify request body or query parameters 11 | - Store data in shared context for later use 12 | 13 | ## Basic preHook Example 14 | 15 | ```javascript 16 | // Validate required fields 17 | if (!$ctx.$body.email) { 18 | $ctx.$throw['400']('Email is required'); 19 | return; 20 | } 21 | 22 | if (!$ctx.$body.password) { 23 | $ctx.$throw['400']('Password is required'); 24 | return; 25 | } 26 | 27 | // Normalize email 28 | $ctx.$body.email = $ctx.$body.email.toLowerCase().trim(); 29 | 30 | // Store validation result 31 | $ctx.$share.validationPassed = true; 32 | ``` 33 | 34 | ## Data Transformation 35 | 36 | ```javascript 37 | // Normalize data 38 | $ctx.$body.email = $ctx.$body.email.toLowerCase(); 39 | $ctx.$body.name = $ctx.$body.name.trim(); 40 | 41 | // Generate slug 42 | if ($ctx.$body.title) { 43 | $ctx.$body.slug = await $ctx.$helpers.autoSlug($ctx.$body.title); 44 | } 45 | 46 | // Add computed fields 47 | $ctx.$body.createdBy = $ctx.$user.id; 48 | $ctx.$body.createdAt = new Date(); 49 | ``` 50 | 51 | ## Permission Checking 52 | 53 | ```javascript 54 | // Check authentication 55 | if (!$ctx.$user) { 56 | $ctx.$throw['401']('Authentication required'); 57 | return; 58 | } 59 | 60 | // Check role 61 | if ($ctx.$user.role !== 'admin') { 62 | $ctx.$throw['403']('Admin access required'); 63 | return; 64 | } 65 | 66 | // Check resource ownership 67 | const resource = await $ctx.$repos.resources.find({ 68 | where: { id: { _eq: $ctx.$params.id } } 69 | }); 70 | 71 | if (resource.data.length === 0) { 72 | $ctx.$throw['404']('Resource not found'); 73 | return; 74 | } 75 | 76 | if (resource.data[0].userId !== $ctx.$user.id) { 77 | $ctx.$throw['403']('Access denied'); 78 | return; 79 | } 80 | ``` 81 | 82 | ## Modifying Query Parameters 83 | 84 | ```javascript 85 | // Add user-specific filter 86 | if ($ctx.$user.role !== 'admin') { 87 | // Non-admins only see their own records 88 | $ctx.$query.filter = { 89 | ...($ctx.$query.filter || {}), 90 | userId: { _eq: $ctx.$user.id } 91 | }; 92 | } 93 | ``` 94 | 95 | ## Next Steps 96 | 97 | - See [afterHooks](./afterhooks.md) for post-handler operations 98 | - Learn about [Custom Handlers](./custom-handlers.md) for custom business logic 99 | - Check [Common Patterns](./patterns.md) for best practices 100 | 101 | -------------------------------------------------------------------------------- /app/relation-picker.md: -------------------------------------------------------------------------------- 1 | # Relation Picker System 2 | 3 | The Relation Picker lets you select related records when working with forms. When you see a **pencil icon** next to a form field, that's a relation field that connects to records in another table. Instead of typing IDs, the Relation Picker provides a user-friendly way to browse and select related data. 4 | 5 | ## How to Use Relation Fields 6 | 7 | **Select Related Records:** 8 | 1. **Click the field** with the pencil icon 9 | 2. **Choose records** from the list that appears 10 | 3. **Click "Apply"** to confirm your selection 11 | 12 | **For Single Selection** (like choosing one customer): 13 | - Click a record to select it 14 | - Clicking another record replaces your selection 15 | 16 | **For Multiple Selection** (like choosing multiple categories): 17 | - Click records to add/remove them from your selection 18 | - Selected records show a checkmark 19 | 20 | ## Additional Actions 21 | 22 | **Create New Records:** 23 | - Click the **"+ Create"** button to add new records to the related table 24 | - Fill out the form that appears 25 | - New records are automatically selected when created 26 | 27 | **Filter Records:** 28 | - Click the **Filter** button to search through available records 29 | - Use the same filtering system as described in the [Filter System](./filter-system.md) guide 30 | 31 | **View Record Details:** 32 | - Click the **eye icon** next to any record to see its full details 33 | - This doesn't affect your current selection 34 | 35 | ## Common Examples 36 | 37 | **Blog Post Categories:** 38 | - **"Categories" field** shows pencil icon 39 | - **Click field** see list of all categories 40 | - **Click multiple categories** they get selected with checkmarks 41 | - **Click "Apply"** field shows "3 categories selected" 42 | 43 | **Order Customer:** 44 | - **"Customer" field** shows pencil icon 45 | - **Click field** see list of all customers 46 | - **Click one customer** previous selection is replaced 47 | - **Click "Apply"** field shows the selected customer's name 48 | 49 | **Product Supplier:** 50 | - **"Supplier" field** shows pencil icon 51 | - **Click field** see many suppliers 52 | - **Click Filter button** search by location or name 53 | - **Select supplier** field shows the chosen supplier 54 | 55 | ## Tips 56 | 57 | **Finding Records:** 58 | - Use the Filter button if you have many records to choose from 59 | - Look for meaningful names (the system shows name, title, or other identifying fields) 60 | - Use the eye icon to preview a record's full details before selecting 61 | 62 | **Working Efficiently:** 63 | - Create new related records directly from the picker without leaving your form 64 | - For multiple selections, you can select several records before clicking Apply 65 | - Selected records persist if you close and reopen the picker (until you Apply or Cancel) -------------------------------------------------------------------------------- /server/context-reference/logging-errors.md: -------------------------------------------------------------------------------- 1 | # Context Reference - Logging & Error Handling 2 | 3 | Add logs to API responses and throw HTTP errors with proper status codes. 4 | 5 | ## Logging 6 | 7 | Add logs to API responses. Logs are automatically included in the response. 8 | 9 | ### Basic Logging 10 | 11 | ```javascript 12 | $ctx.$logs('Operation started'); 13 | $ctx.$logs('User created successfully'); 14 | $ctx.$logs(`Processing order: ${orderId}`); 15 | ``` 16 | 17 | ### Logging Variables 18 | 19 | ```javascript 20 | const userId = $ctx.$user.id; 21 | const orderId = 123; 22 | 23 | $ctx.$logs(`User ID: ${userId}`); 24 | $ctx.$logs(`Order ID: ${orderId}`); 25 | $ctx.$logs(`Processing data:`, dataObject); 26 | ``` 27 | 28 | ### Logging in Different Phases 29 | 30 | ```javascript 31 | // In preHook 32 | $ctx.$logs('Validation started'); 33 | 34 | // In handler 35 | $ctx.$logs('Creating user...'); 36 | 37 | // In afterHook 38 | $ctx.$logs('User created successfully'); 39 | ``` 40 | 41 | **Note:** Logs appear automatically in API responses. You don't need to manually include them in the return value. 42 | 43 | ## Error Handling 44 | 45 | Throw HTTP errors with proper status codes. 46 | 47 | ### Throw HTTP Status Errors 48 | 49 | ```javascript 50 | // Bad Request 51 | $ctx.$throw['400']('Invalid input data'); 52 | 53 | // Unauthorized 54 | $ctx.$throw['401']('Authentication required'); 55 | 56 | // Forbidden 57 | $ctx.$throw['403']('Insufficient permissions'); 58 | 59 | // Not Found 60 | $ctx.$throw['404']('Resource not found'); 61 | 62 | // Conflict 63 | $ctx.$throw['409']('Email already exists'); 64 | 65 | // Unprocessable Entity 66 | $ctx.$throw['422']('Validation failed'); 67 | 68 | // Internal Server Error 69 | $ctx.$throw['500']('Internal server error'); 70 | ``` 71 | 72 | ### Error Handling in AfterHook 73 | 74 | In afterHook, you can check for errors that occurred during the request: 75 | 76 | ```javascript 77 | // Check if error occurred 78 | if ($ctx.$api.error) { 79 | // Error case 80 | $ctx.$logs(`Error occurred: ${$ctx.$api.error.message}`); 81 | $ctx.$logs(`Error status: ${$ctx.$api.error.statusCode}`); 82 | 83 | // $ctx.$data will be null when error occurs 84 | // $ctx.$statusCode will be the error status code 85 | } else { 86 | // Success case 87 | $ctx.$logs('Operation completed successfully'); 88 | // $ctx.$data contains the response data 89 | // $ctx.$statusCode contains success status (200, 201, etc.) 90 | } 91 | ``` 92 | 93 | **Note:** `$ctx.$api.error` is only available in afterHook, not in preHook. 94 | 95 | ## Next Steps 96 | 97 | - See [Error Handling](../error-handling.md) for complete error handling guide 98 | - Check [Advanced Features](./advanced.md) for API information and error details 99 | - Learn about [Hooks and Handlers](../hooks-handlers/) for using logging in hooks 100 | 101 | -------------------------------------------------------------------------------- /server/context-reference/request-data.md: -------------------------------------------------------------------------------- 1 | # Context Reference - Request Data 2 | 3 | Access HTTP request information and parameters. 4 | 5 | ## $ctx.$body 6 | 7 | Request body data (POST, PATCH, PUT requests). 8 | 9 | ```javascript 10 | // Access request body 11 | const email = $ctx.$body.email; 12 | const name = $ctx.$body.name; 13 | 14 | // Modify request body (in preHook) 15 | $ctx.$body.email = $ctx.$body.email.toLowerCase(); 16 | ``` 17 | 18 | ## $ctx.$params 19 | 20 | URL path parameters from route definitions. 21 | 22 | ```javascript 23 | // Route: /users/:id 24 | const userId = $ctx.$params.id; 25 | 26 | // Route: /orders/:orderId/products/:productId 27 | const orderId = $ctx.$params.orderId; 28 | const productId = $ctx.$params.productId; 29 | ``` 30 | 31 | ## $ctx.$query 32 | 33 | Query string parameters from URL. 34 | 35 | ```javascript 36 | // URL: /products?page=1&limit=20&sort=-price 37 | const page = $ctx.$query.page; // 1 38 | const limit = $ctx.$query.limit; // 20 39 | const sort = $ctx.$query.sort; // "-price" 40 | 41 | // Access filter from query 42 | const filter = $ctx.$query.filter; // Filter object from ?filter={...} 43 | ``` 44 | 45 | ## $ctx.$user 46 | 47 | Current authenticated user information. 48 | 49 | ```javascript 50 | const userId = $ctx.$user.id; 51 | const userEmail = $ctx.$user.email; 52 | const userRole = $ctx.$user.role; 53 | 54 | // Check if user is authenticated 55 | if (!$ctx.$user) { 56 | $ctx.$throw['401']('Unauthorized'); 57 | return; 58 | } 59 | ``` 60 | 61 | ## $ctx.$req 62 | 63 | Express request object with additional details. 64 | 65 | ```javascript 66 | const method = $ctx.$req.method; // 'GET', 'POST', 'PATCH', 'DELETE' 67 | const url = $ctx.$req.url; // Full request URL 68 | const ip = $ctx.$req.ip; // Client IP address 69 | const headers = $ctx.$req.headers; // Request headers 70 | const userAgent = $ctx.$req.headers['user-agent']; 71 | ``` 72 | 73 | ## $ctx.$data 74 | 75 | Response data (available in afterHook and handlers). 76 | 77 | ```javascript 78 | // In afterHook - modify response data 79 | if ($ctx.$data && Array.isArray($ctx.$data.data)) { 80 | $ctx.$data.data = $ctx.$data.data.map(item => ({ 81 | ...item, 82 | fullName: `${item.firstName} ${item.lastName}` 83 | })); 84 | } 85 | ``` 86 | 87 | ## $ctx.$statusCode 88 | 89 | HTTP status code. Can be modified in hooks. 90 | 91 | ```javascript 92 | // Change status code 93 | $ctx.$statusCode = 201; // Created 94 | 95 | // Check current status 96 | if ($ctx.$statusCode === 200) { 97 | // Success response 98 | } 99 | ``` 100 | 101 | ## Next Steps 102 | 103 | - See [Repositories](./repositories.md) for database operations 104 | - Check [Helpers & Cache](./helpers-cache.md) for utility functions 105 | - Learn about [Logging & Error Handling](./logging-errors.md) 106 | 107 | -------------------------------------------------------------------------------- /getting-started/getting-started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | After completing the installation of both Enfyra backend and app, follow these steps to get started. 4 | 5 | > **New to Enfyra?** Start with the [Installation Guide](./installation.md) to set up your backend and frontend. 6 | 7 | ## First Login 8 | 9 | 1. Navigate to your Enfyra app (default: `http://localhost:3000`) 10 | 2. You'll be redirected to the login page 11 | 3. Use the admin account created during backend setup: 12 | - The credentials you provided when setting up the backend 13 | - If you used default setup, check your backend console for the admin credentials 14 | 15 | ## After Login 16 | 17 | Once logged in, you'll see the main Enfyra interface with several key components: 18 | 19 | ### Interface Layout 20 | 21 | **Left Side:** 22 | - **Sidebar** - Contains full navigation menu with menus and dropdowns (visible on desktop by default, toggle on mobile) 23 | - **Collapse Toggle** - Button to show/hide the sidebar (collapsed shows icon-only view) 24 | 25 | **Main Area:** 26 | - **Header** - Top section with page title and action buttons 27 | - **Sub-Header** - Secondary navigation and breadcrumbs ("Home > settings > routings") 28 | - **Content Area** - Main page content with gradient background and subtle patterns 29 | 30 | **Header Actions:** 31 | - Located in top-right corner of each page 32 | - Context-specific buttons (Filter, Create, Save, Delete, etc.) 33 | - Green "Create" buttons for adding new items 34 | 35 | ### Sidebar Navigation 36 | 37 | - **Dashboard** (Grid icon) - Overview and quick stats of your system 38 | - **Data** (List icon) - Browse, create, edit and delete records in your tables 39 | - **Collections** (Database icon) - Create and manage database tables/schemas 40 | - **Settings** (Gear icon) - System configuration, users, roles, and permissions 41 | - **Storage** (Folder icon) - Upload and manage media files, folders, and storage configurations 42 | 43 | **Sidebar Behavior:** 44 | - Click menu items to navigate or expand/collapse dropdown menus 45 | - Desktop: Sidebar visible by default, can be collapsed to icon-only view 46 | - Mobile/Tablet: Collapsed by default, overlay when opened 47 | - Toggle button to show/hide full sidebar menu 48 | 49 | ## Next Steps: Create Your First Table 50 | 51 | Now that you're familiar with the interface, it's time to create your first table and start building your application. 52 | 53 | ** [Table Creation Guide](./table-creation.md)** - Complete step-by-step guide to creating tables with all field types, relations, and constraints. 54 | 55 | This comprehensive guide covers: 56 | - **Table creation workflow** - Step-by-step process 57 | - **All field types** - From basic text to rich content and relations 58 | - **Advanced features** - Constraints, indexes, and relationships 59 | - **What happens after creation** - Automatic API generation and integration 60 | 61 | After creating your table, you'll have: 62 | - **4 automatic CRUD endpoints** on your backend server 63 | - **Frontend integration** in the Data section 64 | - **Full API access** for external applications 65 | 66 | **Then continue with:** 67 | - **[Data Management](./data-management.md)** - Learn to add, edit, and manage records in your tables 68 | -------------------------------------------------------------------------------- /app/filter-system.md: -------------------------------------------------------------------------------- 1 | # Filter System 2 | 3 | The Filter System helps you search and filter data in your tables. Instead of scrolling through many records, create search conditions to find exactly what you need. Look for the **Filter** button - it shows "Filter" normally, and "Filters (N)" when active. 4 | 5 | ## How to Filter Data 6 | 7 | **Start Filtering:** 8 | 1. Click the **Filter** button in your table toolbar 9 | 2. Click **"+ Add Filter"** to create your first condition 10 | 11 | **Build a Condition:** 12 | 1. **Choose a field** from the first dropdown (your table columns or related table fields) 13 | 2. **Pick how to compare** from the second dropdown (equals, contains, greater than, etc.) 14 | 3. **Enter your search value** in the input box (if needed) 15 | 16 | **Apply Your Filter:** 17 | - Click **"Apply Filters"** to search your data 18 | - Your table updates to show only matching records 19 | - The Filter button now shows "Filters (X)" to indicate it's active 20 | 21 | ## Multiple Conditions and Logic 22 | 23 | **Add More Conditions:** 24 | - Click **"+ Add Filter"** again for additional search criteria 25 | - Choose **"AND"** if all conditions must be true 26 | - Choose **"OR"** if any condition can be true 27 | 28 | **Group Complex Logic:** 29 | - Click **"+ Add Group"** to create nested conditions 30 | - Each group has its own AND/OR logic 31 | - Use this for complex searches like: *(name contains "John" OR email contains "john") AND status = "active"* 32 | 33 | **Remove Conditions:** 34 | - Click the **X button** on any condition to remove it 35 | - Click **"Clear All"** to remove everything and start over 36 | 37 | ## Using Saved Filters 38 | 39 | **Auto-Save:** 40 | - When you apply filters, they're automatically saved for later use 41 | - Each filter gets a smart name like "status = active" or "name contains John" 42 | 43 | **Reuse Filters:** 44 | - Click any saved filter to instantly apply it 45 | - Popular filters (most used) appear at the top 46 | - Rename filters by clicking the edit button 47 | - Delete unwanted filters with the trash button 48 | 49 | **Search Saved Filters:** 50 | - If you have many saved filters, use the search box to find specific ones 51 | 52 | ## Common Filter Examples 53 | 54 | **Text Searches:** 55 | - Find customers: `name contains "Smith"` 56 | - Find emails: `email ends with "@company.com"` 57 | - Find empty descriptions: `description is empty` 58 | 59 | **Number Ranges:** 60 | - Find expensive products: `price > 100` 61 | - Find age range: `age between 25 and 65` 62 | - Find recent orders: `total >= 50` 63 | 64 | **Date Filtering:** 65 | - Recent records: `created_date > "2024-01-01"` 66 | - Date range: `order_date between "2024-01-01" and "2024-12-31"` 67 | 68 | **Multiple Conditions:** 69 | - Active customers in New York: `status = "active" AND city = "New York"` 70 | - Premium or VIP customers: `plan = "premium" OR plan = "VIP"` 71 | 72 | **Relation Filtering:** 73 | - Orders from specific customers: `customer name contains "John"` 74 | - Products in certain categories: `category name = "Electronics"` 75 | - **Note**: These relation fields come from table relationships you create - see [Getting Started](../getting-started/getting-started.md) for setting up relations, and [Relation Picker System](./relation-picker.md) for selecting related records 76 | 77 | ## Tips for Better Filtering 78 | 79 | **Keep It Simple:** 80 | - Start with one condition, add more as needed 81 | - Use clear, descriptive names when renaming saved filters 82 | - Delete old filters you no longer use 83 | 84 | **Performance:** 85 | - Simple filters (like status = "active") are fastest 86 | - Complex nested relations may take longer to load 87 | - Use specific conditions rather than very broad searches 88 | 89 | **Organization:** 90 | - Save commonly used filters for quick access 91 | - Use groups for complex "and/or" logic 92 | - Name your custom filters clearly (e.g., "Active NY Customers" instead of "Filter 1") -------------------------------------------------------------------------------- /server/hooks-handlers/patterns.md: -------------------------------------------------------------------------------- 1 | # Hooks and Handlers - Common Patterns 2 | 3 | Common patterns and best practices for working with hooks and handlers. 4 | 5 | ## Common Patterns 6 | 7 | ### Pattern 1: Validation and Transformation 8 | 9 | ```javascript 10 | // preHook 11 | if (!$ctx.$body.email) { 12 | $ctx.$throw['400']('Email is required'); 13 | return; 14 | } 15 | 16 | $ctx.$body.email = $ctx.$body.email.toLowerCase().trim(); 17 | $ctx.$share.validationPassed = true; 18 | 19 | // Handler uses normalized data automatically 20 | ``` 21 | 22 | ### Pattern 2: Permission-Based Filtering 23 | 24 | ```javascript 25 | // preHook 26 | if ($ctx.$user.role !== 'admin') { 27 | // Non-admins only see their own records 28 | $ctx.$query.filter = { 29 | ...($ctx.$query.filter || {}), 30 | userId: { _eq: $ctx.$user.id } 31 | }; 32 | } 33 | 34 | // Handler/Default CRUD uses filtered query 35 | ``` 36 | 37 | ### Pattern 3: Response Enhancement 38 | 39 | ```javascript 40 | // afterHook 41 | if ($ctx.$data && Array.isArray($ctx.$data.data)) { 42 | $ctx.$data.data = $ctx.$data.data.map(item => ({ 43 | ...item, 44 | displayName: `${item.firstName} ${item.lastName}`, 45 | formattedPrice: `$${item.price.toFixed(2)}` 46 | })); 47 | } 48 | ``` 49 | 50 | ### Pattern 4: Audit Trail 51 | 52 | ```javascript 53 | // afterHook 54 | if (!$ctx.$api.error) { 55 | await $ctx.$repos.audit_logs.create({ 56 | data: { 57 | action: `${$ctx.$req.method} ${$ctx.$req.url}`, 58 | userId: $ctx.$user?.id, 59 | statusCode: $ctx.$statusCode, 60 | timestamp: new Date() 61 | } 62 | }); 63 | } 64 | ``` 65 | 66 | ### Pattern 5: Shared Context 67 | 68 | ```javascript 69 | // preHook 70 | $ctx.$share.processStartTime = Date.now(); 71 | $ctx.$share.userId = $ctx.$user.id; 72 | 73 | // afterHook 74 | if ($ctx.$share.processStartTime) { 75 | const processingTime = Date.now() - $ctx.$share.processStartTime; 76 | $ctx.$data.processingTime = processingTime; 77 | } 78 | ``` 79 | 80 | ### Pattern 6: Error Recovery 81 | 82 | ```javascript 83 | // afterHook 84 | if ($ctx.$api.error) { 85 | // Log error 86 | $ctx.$logs(`Error occurred: ${$ctx.$api.error.message}`); 87 | 88 | // Create error log 89 | await $ctx.$repos.error_logs.create({ 90 | data: { 91 | errorMessage: $ctx.$api.error.message, 92 | statusCode: $ctx.$api.error.statusCode, 93 | userId: $ctx.$user?.id, 94 | url: $ctx.$req.url 95 | } 96 | }); 97 | 98 | // Optionally send notification 99 | // await sendErrorNotification($ctx.$api.error); 100 | } 101 | ``` 102 | 103 | ## Best Practices 104 | 105 | ### Hook Organization 106 | 107 | 1. **Global hooks** for cross-cutting concerns (auth, logging) 108 | 2. **Route hooks** for route-specific logic 109 | 3. **Keep hooks focused** - one responsibility per hook 110 | 4. **Use descriptive names** in hook definitions 111 | 112 | ### Code Quality 113 | 114 | 1. **Validate early** in preHooks 115 | 2. **Transform data** in preHooks before handler 116 | 3. **Enhance responses** in afterHooks after handler 117 | 4. **Use shared context** to pass data between hooks 118 | 5. **Log important steps** for debugging 119 | 120 | ### Error Handling 121 | 122 | 1. **Throw errors early** in preHooks for fast failure 123 | 2. **Handle errors gracefully** in afterHooks 124 | 3. **Provide meaningful error messages** 125 | 4. **Use appropriate HTTP status codes** 126 | 127 | ### Performance 128 | 129 | 1. **Minimize database calls** - batch operations when possible 130 | 2. **Cache expensive operations** in shared context 131 | 3. **Use early returns** to avoid unnecessary processing 132 | 4. **Consider execution order** for optimal performance 133 | 134 | ## Next Steps 135 | 136 | - See [preHooks](./prehooks.md) for pre-handler operations 137 | - Learn about [afterHooks](./afterhooks.md) for post-handler operations 138 | - Check [Custom Handlers](./custom-handlers.md) for custom business logic 139 | - Learn about [Repository Methods](../repository-methods/) for database operations 140 | - See [Context Reference](../context-reference/) for all available properties 141 | - Check [API Lifecycle](../api-lifecycle.md) to understand execution order 142 | 143 | -------------------------------------------------------------------------------- /architecture-overview.md: -------------------------------------------------------------------------------- 1 | # Architecture Overview 2 | 3 | This diagram shows how Enfyra's two-component system works. 4 | 5 | > **New to Enfyra?** Start with the [Installation Guide](./getting-started/installation.md) to set up your backend and frontend. 6 | 7 | ## System Architecture Diagram 8 | 9 | ``` 10 | ┌─────────────────────┐ HTTP Requests ┌─────────────────────┐ 11 | │ │ ────────────────► │ │ 12 | │ Frontend App │ │ Backend Server │ 13 | │ (Port 3000) │ ◄──────────────── │ (Port 1105) │ 14 | │ │ JSON Response │ │ 15 | └─────────────────────┘ └─────────────────────┘ 16 | │ │ 17 | │ │ 18 | │ ▼ 19 | │ ┌─────────────────────┐ 20 | │ │ │ 21 | │ │ Database │ 22 | │ │ (MySQL/PostgreSQL/ │ 23 | │ │ MariaDB/MongoDB) │ 24 | │ └─────────────────────┘ 25 | │ │ 26 | │ │ 27 | ▼ ▼ 28 | ┌─────────────────────┐ ┌─────────────────────┐ 29 | │ │ │ │ 30 | │ Admin Interface │ │ REST & GraphQL │ 31 | │ Forms, Tables, │ │ API Generation │ 32 | │ Data Management │ │ Custom Handlers │ 33 | │ │ │ Hooks, Permissions │ 34 | └─────────────────────┘ └─────────────────────┘ 35 | ``` 36 | 37 | ## Component Responsibilities 38 | 39 | ### Backend Server (Port 1105) 40 | - **Database Management**: Direct connection to MySQL/PostgreSQL/MariaDB/MongoDB 41 | - **API Generation**: Automatically creates REST & GraphQL APIs from database schema 42 | - **Business Logic**: Custom handlers, hooks, and validation 43 | - **Security**: Authentication, authorization, and permissions 44 | - **Schema Management**: Table creation, relationships, and migrations 45 | 46 | ### Frontend App (Port 3000) 47 | - **User Interface**: Admin panel, forms, tables, dashboards 48 | - **API Consumer**: Makes HTTP requests to backend endpoints 49 | - **State Management**: Handles UI state, caching, and user sessions 50 | - **Extensions**: Custom Vue components and pages 51 | - **No Database Access**: Never directly connects to database 52 | 53 | ## Data Flow 54 | 55 | 1. **User Action**: User interacts with frontend admin interface 56 | 2. **HTTP Request**: Frontend makes API call to backend server 57 | 3. **Processing**: Backend processes request through hooks/handlers 58 | 4. **Database Operation**: Backend performs database operation 59 | 5. **Response**: Backend returns JSON response to frontend 60 | 6. **UI Update**: Frontend updates interface with new data 61 | 62 | ## Key Points 63 | 64 | - **All APIs originate from backend**: Frontend is purely a client 65 | - **Single Database Connection**: Only backend connects to database 66 | - **Stateless Frontend**: No server-side logic in frontend 67 | - **API-First Design**: Backend serves APIs, frontend consumes them 68 | - **Independent Deployment**: Backend and frontend can be deployed separately 69 | 70 | ## Development Workflow 71 | 72 | ``` 73 | 1. Create/Modify Tables (Backend Admin Interface) 74 | 2. Backend generates API endpoints automatically 75 | 3. Frontend consumes APIs via HTTP requests 76 | 4. Zero additional configuration needed 77 | ``` 78 | 79 | ## Production Deployment 80 | 81 | ``` 82 | Backend Server (1105) API Endpoints Frontend App (3000) User Browser 83 | ↓ 84 | Database Server 85 | ↓ 86 | Redis (Synchronization) 87 | ``` 88 | 89 | ## Related Documentation 90 | 91 | - **Server Documentation**: `./server/README.md` – Server architecture, APIs, repositories, and context object 92 | - **App Documentation**: `./app/README.md` – Frontend/admin app behavior, extensions, forms, and permissions 93 | -------------------------------------------------------------------------------- /server/repository-methods/patterns.md: -------------------------------------------------------------------------------- 1 | # Repository Methods - Common Patterns 2 | 3 | Common patterns and best practices for working with repository methods. 4 | 5 | ## Common Patterns 6 | 7 | ### Pattern 1: Create and Return Full Record 8 | 9 | ```javascript 10 | const result = await $ctx.$repos.products.create({ 11 | data: { 12 | name: 'New Product', 13 | price: 99.99 14 | } 15 | }); 16 | 17 | // result.data[0] contains the full record with ID, timestamps, etc. 18 | const newProduct = result.data[0]; 19 | ``` 20 | 21 | ### Pattern 2: Update Based on Current Data 22 | 23 | ```javascript 24 | // Get current record 25 | const current = await $ctx.$repos.products.find({ 26 | where: { id: { _eq: 123 } } 27 | }); 28 | 29 | if (current.data.length === 0) { 30 | $ctx.$throw['404']('Product not found'); 31 | return; 32 | } 33 | 34 | const product = current.data[0]; 35 | 36 | // Update based on current data 37 | const result = await $ctx.$repos.products.update({ 38 | id: 123, 39 | data: { 40 | stock: (product.stock || 0) + 10 // Increment stock 41 | } 42 | }); 43 | ``` 44 | 45 | ### Pattern 3: Conditional Update or Create 46 | 47 | ```javascript 48 | const existing = await $ctx.$repos.products.find({ 49 | where: { name: { _eq: 'Product Name' } } 50 | }); 51 | 52 | if (existing.data.length > 0) { 53 | // Update existing 54 | const result = await $ctx.$repos.products.update({ 55 | id: existing.data[0].id, 56 | data: { 57 | price: 89.99 58 | } 59 | }); 60 | } else { 61 | // Create new 62 | const result = await $ctx.$repos.products.create({ 63 | data: { 64 | name: 'Product Name', 65 | price: 89.99 66 | } 67 | }); 68 | } 69 | ``` 70 | 71 | ### Pattern 4: Delete with Validation 72 | 73 | ```javascript 74 | // Check if record can be deleted 75 | const product = await $ctx.$repos.products.find({ 76 | where: { id: { _eq: 123 } } 77 | }); 78 | 79 | if (product.data.length === 0) { 80 | $ctx.$throw['404']('Product not found'); 81 | return; 82 | } 83 | 84 | // Check business rules 85 | if (product.data[0].status === 'active') { 86 | $ctx.$throw['400']('Cannot delete active product'); 87 | return; 88 | } 89 | 90 | // Delete 91 | const result = await $ctx.$repos.products.delete({ 92 | id: 123 93 | }); 94 | ``` 95 | 96 | --- 97 | 98 | ## Return Value Format 99 | 100 | All repository methods return data in a consistent format: 101 | 102 | ### Success Response 103 | 104 | ```javascript 105 | { 106 | data: [ 107 | { id: 1, name: 'Product 1', price: 99.99 }, 108 | { id: 2, name: 'Product 2', price: 149.99 } 109 | ], 110 | meta: { // Only when meta parameter is provided 111 | totalCount: 100, 112 | filterCount: 2 113 | } 114 | } 115 | ``` 116 | 117 | ### Error Response 118 | 119 | When an error occurs, the system throws an exception that should be caught: 120 | 121 | ```javascript 122 | try { 123 | const result = await $ctx.$repos.products.find({ ... }); 124 | } catch (error) { 125 | // error.message contains the error description 126 | // Use $ctx.$throw to throw proper HTTP errors 127 | } 128 | ``` 129 | 130 | --- 131 | 132 | ## Best Practices 133 | 134 | 1. **Always check data arrays**: Repository methods return `{data: []}`, always check `data.length` before accessing `data[0]` 135 | 2. **Use fields parameter**: When you only need specific fields, use the `fields` parameter to reduce data transfer 136 | 3. **Validate before operations**: Check if records exist before updating or deleting 137 | 4. **Handle errors**: Always wrap repository calls in try-catch blocks for proper error handling 138 | 5. **Use limit for queries**: Don't fetch all records unless necessary - use appropriate limits 139 | 6. **Leverage metadata**: Use `meta` parameter to get counts without fetching all data 140 | 7. **Batch operations**: When possible, use filters to get multiple records in one query instead of multiple individual queries 141 | 142 | ## Next Steps 143 | 144 | - Learn about the [find() method](./find.md) for querying records 145 | - See [create(), update(), delete() methods](./create-update-delete.md) 146 | - Check [Context Reference](../context-reference/) to understand all available properties 147 | - See [Query Filtering](../query-filtering.md) for advanced filtering patterns 148 | - Check [API Lifecycle](../api-lifecycle.md) to understand how repositories fit into the request flow 149 | 150 | -------------------------------------------------------------------------------- /server/context-reference/helpers-cache.md: -------------------------------------------------------------------------------- 1 | # Context Reference - Helpers & Cache 2 | 3 | Utility functions for common tasks and distributed caching operations. 4 | 5 | ## Helpers 6 | 7 | Utility functions for common tasks. All helper functions require `await`. 8 | 9 | ### JWT Token Generation 10 | 11 | ```javascript 12 | const token = await $ctx.$helpers.$jwt(payload, expiration); 13 | 14 | // Example 15 | const token = await $ctx.$helpers.$jwt( 16 | { userId: 123, email: 'user@example.com' }, 17 | '7d' // Expires in 7 days 18 | ); 19 | ``` 20 | 21 | **Expiration formats:** 22 | - `'15m'` - 15 minutes 23 | - `'1h'` - 1 hour 24 | - `'1d'` - 1 day 25 | - `'7d'` - 7 days 26 | - `'30d'` - 30 days 27 | 28 | ### Password Hashing 29 | 30 | ```javascript 31 | // Hash password 32 | const hash = await $ctx.$helpers.$bcrypt.hash(plainPassword); 33 | 34 | // Verify password 35 | const isValid = await $ctx.$helpers.$bcrypt.compare(plainPassword, hashedPassword); 36 | 37 | // Example 38 | const hashedPassword = await $ctx.$helpers.$bcrypt.hash('myPassword123'); 39 | const isValid = await $ctx.$helpers.$bcrypt.compare('myPassword123', hashedPassword); 40 | ``` 41 | 42 | ### Auto Slug Generation 43 | 44 | Generate URL-friendly slugs from text. 45 | 46 | ```javascript 47 | const slug = await $ctx.$helpers.autoSlug(text); 48 | 49 | // Example 50 | const slug = await $ctx.$helpers.autoSlug('My Product Name'); 51 | // Result: 'my-product-name' 52 | ``` 53 | 54 | ### File Upload Helper 55 | 56 | Upload files to storage. 57 | 58 | ```javascript 59 | const fileResult = await $ctx.$helpers.$uploadFile({ 60 | originalname: 'image.jpg', 61 | filename: 'custom-filename.jpg', 62 | mimetype: 'image/jpeg', 63 | buffer: fileBuffer, 64 | size: 1024000, 65 | folder: 123, // Optional: folder ID 66 | storageConfig: 1, // Optional: storage config ID 67 | title: 'My Image', // Optional 68 | description: 'Image description' // Optional 69 | }); 70 | ``` 71 | 72 | ### File Update Helper 73 | 74 | Update existing files. 75 | 76 | ```javascript 77 | await $ctx.$helpers.$updateFile(fileId, { 78 | buffer: newFileBuffer, 79 | originalname: 'new-name.jpg', 80 | mimetype: 'image/jpeg', 81 | folder: 456, 82 | title: 'Updated Title' 83 | }); 84 | ``` 85 | 86 | ### File Delete Helper 87 | 88 | Delete files. 89 | 90 | ```javascript 91 | await $ctx.$helpers.$deleteFile(fileId); 92 | ``` 93 | 94 | ## Cache 95 | 96 | Distributed caching and locking operations. All cache functions require `await`. 97 | 98 | ### Get Cached Value 99 | 100 | ```javascript 101 | const cachedValue = await $ctx.$cache.get(key); 102 | 103 | // Example 104 | const user = await $ctx.$cache.get('user:123'); 105 | ``` 106 | 107 | ### Set Cached Value 108 | 109 | ```javascript 110 | // Set with TTL (time-to-live in milliseconds) 111 | await $ctx.$cache.set(key, value, ttlMs); 112 | 113 | // Example - cache for 5 minutes 114 | await $ctx.$cache.set('user:123', userData, 300000); 115 | 116 | // Set without expiration 117 | await $ctx.$cache.setNoExpire(key, value); 118 | ``` 119 | 120 | ### Distributed Locking 121 | 122 | Acquire and release locks for critical operations. 123 | 124 | ```javascript 125 | // Acquire lock 126 | const lockAcquired = await $ctx.$cache.acquire(key, value, ttlMs); 127 | 128 | // Release lock 129 | const released = await $ctx.$cache.release(key, value); 130 | 131 | // Example 132 | const lockKey = `user-lock:${userId}`; 133 | const lockValue = $ctx.$user.id; 134 | const acquired = await $ctx.$cache.acquire(lockKey, lockValue, 10000); // 10 seconds 135 | 136 | if (acquired) { 137 | try { 138 | // Critical operation here 139 | } finally { 140 | await $ctx.$cache.release(lockKey, lockValue); 141 | } 142 | } 143 | ``` 144 | 145 | ### Check Cache Exists 146 | 147 | ```javascript 148 | const exists = await $ctx.$cache.exists(key, value); 149 | 150 | // Example 151 | const lockExists = await $ctx.$cache.exists('user-lock:123', 'user-456'); 152 | ``` 153 | 154 | ### Delete Cache Key 155 | 156 | ```javascript 157 | await $ctx.$cache.deleteKey(key); 158 | 159 | // Example 160 | await $ctx.$cache.deleteKey('user:123'); 161 | ``` 162 | 163 | ## Next Steps 164 | 165 | - See [Logging & Error Handling](./logging-errors.md) for logging and error throwing 166 | - Check [Advanced Features](./advanced.md) for file uploads and more 167 | - Learn about [Cache Operations](../cache-operations.md) for detailed cache patterns 168 | 169 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | # Enfyra App (Frontend) Documentation 2 | 3 | This section covers everything that runs in the Enfyra **admin app (port 3000)** – the UI, extensions, forms, permissions, and how the app talks to the backend server. 4 | 5 | The app is a **pure API client**: it never connects directly to your database. All data comes from the **Enfyra Server (port 1105)** via HTTP APIs. 6 | 7 | > **New to Enfyra?** Start with the [Installation Guide](../getting-started/installation.md) to set up your backend and frontend. 8 | 9 | ## Quick Navigation 10 | 11 | - **API Integration** 12 | - **[API Integration](./api-integration.md)** – How the app calls backend APIs using the official Enfyra SDKs (`@enfyra/sdk-nuxt`, `@enfyra/sdk-next`), `useApi`, and `useEnfyraApi` 13 | 14 | - **Extensions & Widgets** 15 | - **[Extension System](./extension-system.md)** – Create custom pages and widgets with Vue components 16 | - **[Header Actions](./header-actions.md)** – Inject custom buttons into the app header and sub-header 17 | 18 | - **Forms & Data Entry** 19 | - **[Form System](./form-system.md)** – Auto-generated forms, validation, relations, and change tracking 20 | - **[Relation Picker](./relation-picker.md)** – Selecting related records inside forms 21 | - **[Filter System](./filter-system.md)** – Advanced filtering for tables and pickers 22 | - **[Enfyra Configuration](../getting-started/enfyra-config.md)** – Application configuration including Rich Text Editor 23 | 24 | - **Permissions & Visibility** 25 | - **[Permission Builder](./permission-builder.md)** – Visual permission rule builder 26 | - **[Permission Components](./permission-components.md)** – `PermissionGate` component and `usePermissions` composable 27 | 28 | - **Navigation & Layout** 29 | - **[Menu Management](./menu-management.md)** – Sidebar and menu configuration 30 | - **[Page Header](./page-header.md)** – Page headers with stats and gradients 31 | 32 | - **Hooks, Handlers & Packages (UI side)** 33 | - **[Hooks](hooks-handlers/hooks.md)** – Managing hooks from the app UI 34 | - **[Custom Handlers](hooks-handlers/custom-handlers.md)** – Managing custom handlers from the app UI 35 | - **[Package Management](hooks-handlers/package-management.md)** – Installing NPM packages for hooks and handlers 36 | 37 | - **Storage & Files** 38 | - **[Storage Management](./storage-management.md)** – File uploads, folders, and storage configurations 39 | 40 | ## Frontend Learning Path 41 | 42 | If you are **building on top of the Enfyra App UI**, this is a suggested order: 43 | 44 | 1. **Understand the architecture** 45 | - **[Architecture Overview](../architecture-overview.md)** – How frontend and backend work together 46 | - **[Server Overview](../server/README.md)** – Server-side concepts and APIs 47 | 48 | 2. **Work with data from the app** 49 | - **[Form System](./form-system.md)** – How forms are generated from tables 50 | - **[Filter System](./filter-system.md)** – Filtering data in tables and relation pickers 51 | - **[API Integration](./api-integration.md)** – Calling APIs from pages, extensions, and widgets 52 | 53 | 3. **Build custom UI and workflows** 54 | - **[Extension System](./extension-system.md)** – Custom pages and widgets 55 | - **[Header Actions](./header-actions.md)** – Custom header actions 56 | - **[Menu Management](./menu-management.md)** – Connecting extensions to menus 57 | 58 | 4. **Secure and control the UI** 59 | - **[Permission Builder](./permission-builder.md)** – Define permission rules 60 | - **[Permission Components](./permission-components.md)** – Hide/show UI based on permissions 61 | - **[Permission Builder](./permission-builder.md)** – Backend permission evaluation (see Permission Builder for details) 62 | 63 | 5. **Go deeper into backend customization (from the app UI)** 64 | - **[Hooks](hooks-handlers/hooks.md)** – Configure hooks that run on the server 65 | - **[Custom Handlers](hooks-handlers/custom-handlers.md)** – Override default CRUD behavior 66 | - **[Package Management](hooks-handlers/package-management.md)** – Install NPM packages for hooks/handlers 67 | - **[Server Hooks & Handlers](../server/hooks-handlers/README.md)** – Full server-side behavior 68 | 69 | ## How This Relates to the Server Docs 70 | 71 | - The **Server docs** (`../server/README.md`) describe **what APIs exist and how they behave**. 72 | - The **App docs** (this folder) describe **how the admin app uses those APIs** through forms, tables, extensions, and permissions. 73 | 74 | When in doubt: 75 | 76 | - If you are asking **“Which endpoint / field / filter can I use?”** – go to the **server docs**. 77 | - If you are asking **“How do I show this in the UI / extension?”** – stay in the **app docs**. 78 | 79 | -------------------------------------------------------------------------------- /server/hooks-handlers/custom-handlers.md: -------------------------------------------------------------------------------- 1 | # Hooks and Handlers - Custom Handlers & Hook Types 2 | 3 | Custom handlers replace the default CRUD operation with your own business logic. 4 | 5 | ## Custom Handlers 6 | 7 | ### When to Use Custom Handlers 8 | 9 | - Complex business logic that doesn't fit CRUD 10 | - Multi-step operations 11 | - External API integrations 12 | - Custom validation beyond preHooks 13 | - Special response formats 14 | 15 | ### Basic Custom Handler 16 | 17 | ```javascript 18 | // Create with additional logic 19 | const result = await $ctx.$repos.products.create({ 20 | data: { 21 | name: $ctx.$body.name, 22 | price: $ctx.$body.price, 23 | createdBy: $ctx.$user.id 24 | } 25 | }); 26 | 27 | // Perform additional operations 28 | const product = result.data[0]; 29 | 30 | // Create related records 31 | await $ctx.$repos.product_images.create({ 32 | data: { 33 | productId: product.id, 34 | imageUrl: $ctx.$body.imageUrl 35 | } 36 | }); 37 | 38 | // Return custom response 39 | return { 40 | success: true, 41 | product: product, 42 | message: 'Product created successfully' 43 | }; 44 | ``` 45 | 46 | ### Multi-Step Operations 47 | 48 | ```javascript 49 | // Create order with items 50 | const orderResult = await $ctx.$repos.orders.create({ 51 | data: { 52 | customerId: $ctx.$body.customerId, 53 | total: 0, 54 | status: 'pending' 55 | } 56 | }); 57 | 58 | const order = orderResult.data[0]; 59 | let total = 0; 60 | 61 | // Create order items 62 | for (const item of $ctx.$body.items) { 63 | const productResult = await $ctx.$repos.products.find({ 64 | where: { id: { _eq: item.productId } } 65 | }); 66 | 67 | if (productResult.data.length === 0) { 68 | $ctx.$throw['404'](`Product ${item.productId} not found`); 69 | return; 70 | } 71 | 72 | const product = productResult.data[0]; 73 | const itemTotal = product.price * item.quantity; 74 | total += itemTotal; 75 | 76 | await $ctx.$repos.order_items.create({ 77 | data: { 78 | orderId: order.id, 79 | productId: item.productId, 80 | quantity: item.quantity, 81 | price: product.price, 82 | total: itemTotal 83 | } 84 | }); 85 | } 86 | 87 | // Update order total 88 | await $ctx.$repos.orders.update({ 89 | id: order.id, 90 | data: { total: total } 91 | }); 92 | 93 | // Return complete order 94 | const finalOrder = await $ctx.$repos.orders.find({ 95 | where: { id: { _eq: order.id } } 96 | }); 97 | 98 | return finalOrder; 99 | ``` 100 | 101 | ### Default CRUD Behavior 102 | 103 | If you don't provide a custom handler, the system automatically performs CRUD operations based on HTTP method: 104 | 105 | - **GET**: Query records using repository `find()` 106 | - **POST**: Create record using repository `create()` 107 | - **PATCH/PUT**: Update record using repository `update()` 108 | - **DELETE**: Delete record using repository `delete()` 109 | 110 | The default CRUD uses data from: 111 | - `$ctx.$query` for GET requests (filter, fields, limit, sort) 112 | - `$ctx.$body` for POST/PATCH requests (data to create/update) 113 | - `$ctx.$params.id` for PATCH/DELETE requests (record ID) 114 | 115 | --- 116 | 117 | ## Hook Types 118 | 119 | ### Global Hooks 120 | 121 | Global hooks run on all routes. 122 | 123 | **Configuration:** 124 | - Route: `null` (no specific route) 125 | - Methods: `[]` (all methods) or specific methods like `['POST', 'PUT']` 126 | 127 | **Example:** 128 | ```javascript 129 | // Global preHook - runs on all routes, all methods 130 | // Configuration: route = null, methods = [] 131 | 132 | // Global preHook - runs on all routes, POST only 133 | // Configuration: route = null, methods = ['POST'] 134 | ``` 135 | 136 | ### Route-Specific Hooks 137 | 138 | Route-specific hooks run only on a specific route. 139 | 140 | **Configuration:** 141 | - Route: Specific route (e.g., route with path `/users`) 142 | - Methods: `[]` (all methods) or specific methods 143 | 144 | **Example:** 145 | ```javascript 146 | // Route preHook - runs on /users route, all methods 147 | // Configuration: route = /users route, methods = [] 148 | 149 | // Route preHook - runs on /users route, POST only 150 | // Configuration: route = /users route, methods = ['POST'] 151 | ``` 152 | 153 | ### Execution Order 154 | 155 | Hooks execute in this order: 156 | 157 | 1. Global hooks (all methods) 158 | 2. Global hooks (specific method) 159 | 3. Route hooks (all methods) 160 | 4. Route hooks (specific method) 161 | 162 | **Example for `POST /users`:** 163 | ``` 164 | Global preHook (all) Global preHook (POST) Route preHook (all) Route preHook (POST) Handler afterHooks in reverse order 165 | ``` 166 | 167 | ## Next Steps 168 | 169 | - See [preHooks](./prehooks.md) for pre-handler operations 170 | - Learn about [afterHooks](./afterhooks.md) for post-handler operations 171 | - Check [Common Patterns](./patterns.md) for best practices 172 | 173 | -------------------------------------------------------------------------------- /getting-started/data-management.md: -------------------------------------------------------------------------------- 1 | # Data Management 2 | 3 | Data Management is where you create, view, edit, and delete records in your tables. After creating tables, you can access your data through **Data** in the sidebar, then select your table. 4 | 5 | > **Prerequisites**: 6 | > - Complete [Installation](./installation.md) and [Getting Started Guide](./getting-started.md) 7 | > - Create at least one table using the [Table Creation Guide](./table-creation.md) 8 | 9 | ## How Data Operations Work 10 | 11 | **Important**: All data operations happen through backend APIs: 12 | - When you create/edit/delete records, the frontend sends HTTP requests to the backend server 13 | - Backend processes the request and updates the database 14 | - Frontend receives the response and updates the UI 15 | 16 | **No API exists on the frontend** - it's purely a client interface consuming backend-produced APIs. 17 | 18 | ## Navigating to Your Table Data 19 | 20 | **How to Access:** 21 | 1. Click **Data** in the sidebar 22 | 2. Select your table from the submenu 23 | 3. You'll see the data management page for that table 24 | 25 | ## Data Table View 26 | 27 | **What You'll See:** 28 | - **Table header** with your table name 29 | - **Action buttons** in the top-right corner 30 | - **Data table** showing all records with pagination 31 | - **Empty state** if no records exist yet 32 | 33 | ## Main Actions 34 | 35 | ### Filter Button 36 | - Shows **"Filter"** when no filters are active 37 | - Changes to **"Filters (N)"** when filters are applied 38 | - Click to open the filter drawer - see [Filter System](../app/filter-system.md) 39 | - Active filters show as a badge above the table with a "Clear" button 40 | 41 | ### Create Button 42 | - Blue **"Create"** button with plus icon 43 | - Click to go to the record creation form 44 | - Only appears if you have create permissions 45 | 46 | ### Select Items Mode 47 | - Click **"Select Items"** to enable selection mode 48 | - Button changes to **"Cancel Selection"** when active 49 | - Select multiple records by clicking checkboxes 50 | - **"Delete Selected (N)"** button appears when records are selected 51 | - Only appears if you have delete permissions 52 | 53 | ### Column Selector 54 | - Dropdown to show/hide table columns 55 | - Select which fields to display in the table 56 | - Your preferences are saved locally 57 | 58 | ## Viewing Records 59 | 60 | **Table Display:** 61 | - Each row represents one record 62 | - **Click any row** to view and edit that record's details 63 | - Fields display based on their type: 64 | - **Dates/Timestamps** show formatted date/time 65 | - **Booleans** show as "Yes/No" badges 66 | - **Long text** truncates at 50 characters 67 | - **Empty values** show as "-" 68 | 69 | **Actions Column:** 70 | - Three-dot menu on the right of each row 71 | - **Delete** option (if you have permission) 72 | - More actions may appear based on your permissions 73 | 74 | **Pagination:** 75 | - Shows at the bottom when you have multiple pages 76 | - Navigate with Previous/Next buttons or page numbers 77 | - URL updates with page number for bookmarking 78 | 79 | ## Creating New Records 80 | 81 | **Click "Create" Button:** 82 | 1. Opens the creation form page 83 | 2. Form shows all fields based on your table schema 84 | 3. Required fields are marked with asterisks 85 | 4. Relation fields show with pencil icons - see [Relation Picker](../app/relation-picker.md) 86 | 87 | **Form Behavior:** 88 | - Fields appear based on column configuration 89 | - Default values are pre-filled 90 | - Field descriptions show as help text below labels 91 | - Validation runs when you save 92 | 93 | **Save Process:** 94 | 1. Click **"Save"** button in the top-right 95 | 2. Validation checks all required fields 96 | 3. If invalid, error messages appear under fields 97 | 4. If valid, record is created and you're redirected to the edit page 98 | 5. Success notification appears 99 | 100 | ## Editing Records 101 | 102 | **Click Any Row in the Table:** 103 | 1. Opens the edit form for that record 104 | 2. All current values are loaded 105 | 3. Form looks similar to create form 106 | 107 | **Edit Form Features:** 108 | - **Save button** - Only enabled when you make changes 109 | - **Delete button** - Red button to delete the record 110 | - Field validation works the same as creation 111 | - Changes are tracked automatically 112 | 113 | **Making Changes:** 114 | 1. Modify any fields you need 115 | 2. Save button becomes enabled when changes are detected 116 | 3. Click **"Save"** to update the record 117 | 4. Success notification confirms the update 118 | 119 | **Deleting a Record:** 120 | 1. Click the red **"Delete"** button 121 | 2. Confirmation dialog appears 122 | 3. Confirm to delete the record 123 | 4. You're redirected back to the table view 124 | 125 | ## Bulk Operations 126 | 127 | **Selecting Multiple Records:** 128 | 1. Click **"Select Items"** to enable selection 129 | 2. Checkboxes appear on each row 130 | 3. Click checkboxes to select records 131 | 4. Selected count shows in the button 132 | 133 | **Bulk Delete:** 134 | 1. Select records you want to delete 135 | 2. Click **"Delete Selected (N)"** button 136 | 3. Confirm the bulk deletion 137 | 4. All selected records are deleted 138 | 5. Table refreshes automatically 139 | 140 | ## Working with Related Data 141 | 142 | **Relation Fields in Forms:** 143 | - Show with a pencil icon 144 | - Click to open the relation picker 145 | - Select related records from other tables 146 | - See [Relation Picker System](../app/relation-picker.md) for details 147 | 148 | **Viewing Relations:** 149 | - Related data may show as IDs or names in the table 150 | - Click the row to see full relation details in edit form 151 | - Use filters to search by related data 152 | 153 | ## Tips for Data Management 154 | 155 | **Performance:** 156 | - Use filters to find records quickly 157 | - Hide unnecessary columns for cleaner view 158 | - Pagination loads data in chunks for speed 159 | 160 | **Organization:** 161 | - Save commonly used filters 162 | - Customize visible columns per table 163 | - Use bulk operations for multiple changes 164 | 165 | **Navigation:** 166 | - URLs update with filters and pages - bookmark specific views 167 | - Browser back/forward works with navigation 168 | - Table state persists when you return -------------------------------------------------------------------------------- /app/hooks-handlers/hooks.md: -------------------------------------------------------------------------------- 1 | # Hooks System 2 | 3 | Hooks are a powerful feature that allows you to inject custom code at specific points in the API request lifecycle. Instead of creating full [Custom Handlers](./custom-handlers.md), you can use hooks to modify requests and responses with minimal code. 4 | 5 | **For complete lifecycle understanding, see [API Lifecycle](../../server/api-lifecycle.md)** 6 | 7 | ## What are Hooks? 8 | 9 | Hooks execute JavaScript code at two key moments: 10 | - **PreHook**: Runs **before** the main operation (before database operations) 11 | - **AfterHook**: Runs **after** the main operation (after database operations) 12 | 13 | **Key Advantage**: Hooks share the same context throughout the entire request lifecycle, making them perfect for lightweight modifications without replacing the entire handler. 14 | 15 | ## When to Use Hooks vs Handlers 16 | 17 | ### Use Hooks When: 18 | - **Modify request data**: Transform input before database operations 19 | - **Add validation**: Check data before processing 20 | - **Modify query filters**: Change filtering programmatically 21 | - **Transform responses**: Format output data after operations 22 | - **Add logging**: Track operations without changing core logic 23 | - **Auto-generate fields**: Create slugs, hash passwords, set timestamps 24 | 25 | ### Use Custom Handlers When: 26 | - **Complex business logic**: Multi-step operations across multiple tables 27 | - **Replace entire operation**: Completely different behavior than CRUD 28 | - **External API calls**: Third-party integrations requiring complex workflows 29 | 30 | ## The $ctx Context Object 31 | 32 | Hooks have access to a shared context object (`$ctx`) that persists throughout the entire API request lifecycle. This context contains request data, database repositories, helper functions, and user information. 33 | 34 | **For complete $ctx documentation and lifecycle details, see [API Lifecycle](../../server/api-lifecycle.md#context-sharing-ctx)** 35 | 36 | ### Key Context Properties Available in Hooks 37 | 38 | ```javascript 39 | $ctx = { 40 | // Request data 41 | $body: {}, // Request body (can be modified) 42 | $params: {}, // URL parameters (/users/:id) 43 | $query: {}, // Query parameters (?limit=10) 44 | $user: {}, // Current authenticated user 45 | 46 | // Database access 47 | $repos: { // Auto-generated repositories 48 | main: repository, // Main table repository 49 | users: repository, // Named repositories for target tables 50 | }, 51 | 52 | // NPM packages (installed via Package Management) 53 | $pkgs: { // Access to installed NPM packages 54 | axios: axios, // HTTP client library 55 | lodash: lodash, // Utility functions 56 | moment: moment, // Date manipulation 57 | }, 58 | 59 | // Utilities 60 | $helpers: { // Helper functions 61 | $jwt: function, // Create JWT tokens 62 | $bcrypt: object, // Hash/compare passwords 63 | autoSlug: function // Generate URL slugs 64 | }, 65 | 66 | // Logging & sharing 67 | $logs: function, // Add logs to response 68 | $share: { // Share data between hooks 69 | $logs: [] 70 | } 71 | }; 72 | ``` 73 | 74 | **Important:** The `$ctx` object is the **same reference** across all hooks and handlers in a request. Changes made in preHooks affect afterHooks and handlers. 75 | 76 | ## Creating Hooks 77 | 78 | ### Step 1: Access Hooks Management 79 | 1. Navigate to **Settings Hooks** in the sidebar 80 | 2. Click **"Create New Hook"** button 81 | 82 | ### Step 2: Configure Hook 83 | You'll see the hook creation form with these fields: 84 | 85 | - **Name**: Descriptive name for the hook 86 | - **PreHook**: JavaScript code that runs before the operation 87 | - **AfterHook**: JavaScript code that runs after the operation 88 | - **Priority**: Execution order (0 = highest priority) 89 | - **IsEnabled**: Toggle to activate/deactivate the hook 90 | - **Description**: Documentation for the hook's purpose 91 | - **Route**: Click the relation picker to select which route this applies to 92 | - **Methods**: Click the relation picker to select HTTP methods (GET, POST, PATCH, DELETE) 93 | 94 | ### Step 3: Link to Route and Methods 95 | - **Route Selection**: Use the relation picker to search and select the target route 96 | - **Method Selection**: Use the relation picker to choose which HTTP methods trigger this hook 97 | - **Multiple Methods**: A single hook can apply to multiple HTTP methods on the same route 98 | 99 | ## Writing Hook Code 100 | 101 | ### Using NPM Packages in Hooks 102 | 103 | Hooks have full access to NPM packages installed via [Package Management](./package-management.md): 104 | 105 | ```javascript 106 | // PreHook example - Using lodash to validate data 107 | const _ = $ctx.$pkgs.lodash; 108 | 109 | if (!_.isObject($ctx.$body) || _.isEmpty($ctx.$body)) { 110 | throw new Error('Request body must be a non-empty object'); 111 | } 112 | 113 | // AfterHook example - Using moment for timestamps 114 | const moment = $ctx.$pkgs.moment; 115 | 116 | $ctx.$response.data.forEach(record => { 117 | record.formatted_date = moment(record.created_at).format('YYYY-MM-DD HH:mm:ss'); 118 | }); 119 | ``` 120 | 121 | **Complete Package Guide**: See [Package Management](./package-management.md) for installing and using NPM packages. 122 | 123 | ### Advanced Hook Development 124 | 125 | For detailed information about writing JavaScript code within hooks, including the context object (`$ctx`), available functions, and comprehensive examples, see: 126 | 127 | **[Hooks and Handlers Guide](../../server/hooks-handlers/)** - Complete guide to preHooks and afterHooks 128 | 129 | This covers: 130 | - Hook context and available variables 131 | - PreHook and AfterHook code examples 132 | - Database access and utility functions 133 | - Execution flow and priority system 134 | - Best practices and debugging techniques 135 | 136 | ## Practical Examples 137 | 138 | **[User Registration Example](../../examples/user-registration-example.md)** - See hooks in action with welcome email AfterHook using nodemailer package. 139 | 140 | Hooks provide the perfect balance between simplicity and power, allowing you to customize API behavior without the complexity of full custom handlers. 141 | 142 | -------------------------------------------------------------------------------- /server/context-reference/advanced.md: -------------------------------------------------------------------------------- 1 | # Context Reference - Advanced Features 2 | 3 | Advanced context features including file uploads, API information, shared context, and package access. 4 | 5 | ## File Uploads 6 | 7 | Access information about uploaded files. 8 | 9 | ### $ctx.$uploadedFile 10 | 11 | Information about the uploaded file (if any). 12 | 13 | ```javascript 14 | if ($ctx.$uploadedFile) { 15 | const filename = $ctx.$uploadedFile.originalname; 16 | const mimetype = $ctx.$uploadedFile.mimetype; 17 | const size = $ctx.$uploadedFile.size; 18 | const buffer = $ctx.$uploadedFile.buffer; 19 | const fieldname = $ctx.$uploadedFile.fieldname; 20 | } 21 | ``` 22 | 23 | ### Processing Uploaded Files 24 | 25 | ```javascript 26 | if ($ctx.$uploadedFile) { 27 | $ctx.$logs(`File uploaded: ${$ctx.$uploadedFile.originalname}`); 28 | $ctx.$logs(`File size: ${$ctx.$uploadedFile.size} bytes`); 29 | $ctx.$logs(`MIME type: ${$ctx.$uploadedFile.mimetype}`); 30 | 31 | // Save file using helper 32 | const fileResult = await $ctx.$helpers.$uploadFile({ 33 | originalname: $ctx.$uploadedFile.originalname, 34 | mimetype: $ctx.$uploadedFile.mimetype, 35 | buffer: $ctx.$uploadedFile.buffer, 36 | size: $ctx.$uploadedFile.size 37 | }); 38 | } 39 | ``` 40 | 41 | ## API Information 42 | 43 | Access detailed information about the current API request and response. 44 | 45 | ### Request Information 46 | 47 | ```javascript 48 | const method = $ctx.$api.request.method; // HTTP method 49 | const url = $ctx.$api.request.url; // Request URL 50 | const timestamp = $ctx.$api.request.timestamp; // Request timestamp 51 | const correlationId = $ctx.$api.request.correlationId; // Unique request ID 52 | const userAgent = $ctx.$api.request.userAgent; // User agent 53 | const ip = $ctx.$api.request.ip; // Client IP 54 | ``` 55 | 56 | ### Response Information 57 | 58 | ```javascript 59 | const statusCode = $ctx.$api.response.statusCode; // HTTP status code 60 | const responseTime = $ctx.$api.response.responseTime; // Response time in ms 61 | const timestamp = $ctx.$api.response.timestamp; // Response timestamp 62 | ``` 63 | 64 | ### Error Information (AfterHook Only) 65 | 66 | ```javascript 67 | if ($ctx.$api.error) { 68 | const message = $ctx.$api.error.message; // Error message 69 | const stack = $ctx.$api.error.stack; // Stack trace 70 | const name = $ctx.$api.error.name; // Error class name 71 | const statusCode = $ctx.$api.error.statusCode; // HTTP error status 72 | const timestamp = $ctx.$api.error.timestamp; // Error timestamp 73 | const details = $ctx.$api.error.details; // Additional details 74 | } 75 | ``` 76 | 77 | ## Shared Context 78 | 79 | Store data that persists across hooks and handlers. 80 | 81 | ### $ctx.$share 82 | 83 | Shared data container for passing data between hooks. 84 | 85 | ```javascript 86 | // In preHook - store data 87 | $ctx.$share.validationPassed = true; 88 | $ctx.$share.processStartTime = Date.now(); 89 | $ctx.$share.userId = $ctx.$user.id; 90 | 91 | // In afterHook - access shared data 92 | if ($ctx.$share.validationPassed) { 93 | const processingTime = Date.now() - $ctx.$share.processStartTime; 94 | $ctx.$data.processingTime = processingTime; 95 | } 96 | ``` 97 | 98 | ### Accessing Logs 99 | 100 | ```javascript 101 | // All logs are stored in $ctx.$share.$logs 102 | const allLogs = $ctx.$share.$logs; // Array of all logged messages 103 | ``` 104 | 105 | ## Package Access 106 | 107 | Access NPM packages installed in the system. 108 | 109 | ### $ctx.$pkgs 110 | 111 | Access installed packages by name. 112 | 113 | ```javascript 114 | // Use installed packages 115 | const axios = $ctx.$pkgs.axios; 116 | const lodash = $ctx.$pkgs.lodash; 117 | const moment = $ctx.$pkgs.moment; 118 | 119 | // Example: Using axios 120 | if ($ctx.$pkgs.axios) { 121 | const response = await $ctx.$pkgs.axios.get('https://api.example.com/data'); 122 | } 123 | ``` 124 | 125 | **Note:** Packages must be installed through Package Management in the UI to be accessible. 126 | 127 | ## Common Patterns 128 | 129 | ### Pattern 1: Validate and Transform Request Data 130 | 131 | ```javascript 132 | // Validate required fields 133 | if (!$ctx.$body.email) { 134 | $ctx.$throw['400']('Email is required'); 135 | return; 136 | } 137 | 138 | // Transform data 139 | $ctx.$body.email = $ctx.$body.email.toLowerCase().trim(); 140 | ``` 141 | 142 | ### Pattern 2: Check User Permissions 143 | 144 | ```javascript 145 | if (!$ctx.$user) { 146 | $ctx.$throw['401']('Authentication required'); 147 | return; 148 | } 149 | 150 | if ($ctx.$user.role !== 'admin') { 151 | $ctx.$throw['403']('Admin access required'); 152 | return; 153 | } 154 | ``` 155 | 156 | ### Pattern 3: Use Shared Context Between Hooks 157 | 158 | ```javascript 159 | // In preHook 160 | $ctx.$share.userId = $ctx.$user.id; 161 | $ctx.$share.validationData = { passed: true }; 162 | 163 | // In afterHook 164 | const userId = $ctx.$share.userId; 165 | if ($ctx.$share.validationData.passed) { 166 | // Use validation result 167 | } 168 | ``` 169 | 170 | ### Pattern 4: Cache Frequently Accessed Data 171 | 172 | ```javascript 173 | // Check cache first 174 | const cacheKey = `user:${$ctx.$params.id}`; 175 | let user = await $ctx.$cache.get(cacheKey); 176 | 177 | if (!user) { 178 | // Cache miss - fetch from database 179 | const result = await $ctx.$repos.user_definition.find({ 180 | where: { id: { _eq: $ctx.$params.id } } 181 | }); 182 | 183 | if (result.data.length > 0) { 184 | user = result.data[0]; 185 | // Cache for 5 minutes 186 | await $ctx.$cache.set(cacheKey, user, 300000); 187 | } 188 | } 189 | ``` 190 | 191 | ### Pattern 5: Handle Errors in AfterHook 192 | 193 | ```javascript 194 | // In afterHook 195 | if ($ctx.$api.error) { 196 | // Log error 197 | $ctx.$logs(`Error: ${$ctx.$api.error.message}`); 198 | 199 | // Log to audit system 200 | await $ctx.$repos.audit_logs.create({ 201 | data: { 202 | action: 'error_occurred', 203 | userId: $ctx.$user?.id, 204 | errorMessage: $ctx.$api.error.message, 205 | statusCode: $ctx.$api.error.statusCode, 206 | timestamp: new Date() 207 | } 208 | }); 209 | } else { 210 | // Success - add metadata 211 | $ctx.$data.processedAt = new Date(); 212 | $ctx.$data.processedBy = $ctx.$user?.id; 213 | } 214 | ``` 215 | 216 | ## Next Steps 217 | 218 | - See [File Handling](../file-handling.md) for complete file upload guide 219 | - Check [Cache Operations](../cache-operations.md) for detailed cache patterns 220 | - Learn about [Error Handling](../error-handling.md) for error handling patterns 221 | 222 | -------------------------------------------------------------------------------- /server/cluster-architecture.md: -------------------------------------------------------------------------------- 1 | # Cluster Architecture 2 | 3 | Enfyra is designed to run in a cluster environment where multiple instances can work together seamlessly. The system uses Redis for coordination and maintains no state in memory, making it easy to scale horizontally. 4 | 5 | ## Core Principles 6 | 7 | ### Stateless Design 8 | Every Enfyra instance is completely stateless. All data lives in the database or Redis cache. This means any instance can handle any request without needing to know about previous requests. 9 | 10 | ### Shared Resources 11 | All instances in a cluster share the same database and Redis instance. This ensures they all have access to the same data and can coordinate their actions. 12 | 13 | ### Instance Identity 14 | Each instance gets a unique ID when it starts up. This ID is used to identify which instance is performing actions and to prevent instances from reacting to their own events. 15 | 16 | ## How Instances Coordinate 17 | 18 | ### Cache Synchronization 19 | When one instance needs to reload cached data (like table schemas, routes, or packages), it follows this pattern: 20 | 21 | 1. The instance tries to acquire a lock in Redis 22 | 2. If another instance already has the lock, it waits for that instance to finish and broadcast the results 23 | 3. If it gets the lock, it loads fresh data from the database 24 | 4. It broadcasts the new cache data to all other instances via Redis Pub/Sub 25 | 5. Other instances receive the broadcast and update their local cache 26 | 6. The lock is released 27 | 28 | This ensures that only one instance does the expensive database query, while all instances benefit from the results. 29 | 30 | ### What Gets Synchronized 31 | 32 | Several types of cache are synchronized across instances: 33 | 34 | - **Metadata Cache**: Table definitions, columns, and relationships 35 | - **Route Cache**: API route definitions and handlers 36 | - **Package Cache**: List of installed npm packages 37 | - **Storage Config Cache**: File storage configurations 38 | - **AI Config Cache**: AI agent configurations 39 | 40 | ### Bootstrap Scripts 41 | When the application starts, bootstrap scripts need to run to set up initial data. To prevent all instances from running these scripts at the same time: 42 | 43 | 1. Each instance tries to acquire a bootstrap execution lock 44 | 2. Only one instance gets the lock and runs the scripts 45 | 3. Other instances skip bootstrap execution 46 | 4. The lock is released when scripts complete 47 | 48 | ### Session Cleanup 49 | Periodic cleanup of expired sessions also uses distributed locking so only one instance performs cleanup at a time. 50 | 51 | ## Process Flow Example 52 | 53 | Here's what happens when a table schema changes: 54 | 55 | 1. Instance A receives a request to update a table 56 | 2. Instance A acquires a metadata reload lock 57 | 3. Instance A updates the database schema 58 | 4. Instance A loads fresh metadata from the database 59 | 5. Instance A publishes the new metadata to Redis Pub/Sub channel 60 | 6. Instances B, C, and D receive the broadcast message 61 | 7. Each instance checks if the message came from itself (using instance ID) 62 | 8. If not, they update their local cache with the new metadata 63 | 9. Instance A releases the lock 64 | 65 | The same pattern applies to route updates, package installations, and other cache changes. 66 | 67 | ## Redis Channels and Keys 68 | 69 | The system uses specific Redis channels for different types of synchronization: 70 | 71 | - `enfyra:metadata-cache-sync` - Table metadata updates 72 | - `enfyra:route-cache-sync` - Route definition updates 73 | - `enfyra:package-cache-sync` - Package list updates 74 | - `enfyra:storage-config-cache-sync` - Storage configuration updates 75 | - `enfyra:ai-config-cache-sync` - AI configuration updates 76 | - `enfyra:bootstrap-script-reload` - Bootstrap script reload notifications 77 | 78 | Locks are stored as Redis keys with specific prefixes and include instance IDs to ensure only the locking instance can release them. 79 | 80 | ## Node Names and Channels 81 | 82 | Each instance can have a node name configured via the `NODE_NAME` environment variable. Redis channels are decorated with the node name, allowing instances on the same physical node to have isolated channels while still participating in the cluster. 83 | 84 | If no node name is set, channels use their base names and all instances communicate on the same channels. 85 | 86 | ## Fault Tolerance 87 | 88 | ### Redis Connection Loss 89 | If Redis becomes unavailable: 90 | - Instances continue serving requests using their existing cache 91 | - Cache reloads will fail until Redis is restored 92 | - Database operations continue normally 93 | - Once Redis is back, synchronization resumes automatically 94 | 95 | ### Lock Timeouts 96 | All locks have time-to-live (TTL) values: 97 | - Bootstrap locks: 30 seconds 98 | - Cache reload locks: 30 seconds 99 | - Session cleanup locks: 1 hour 100 | 101 | If an instance crashes while holding a lock, the lock automatically expires, allowing other instances to proceed. 102 | 103 | ### Message Handling 104 | If an instance receives a synchronization message: 105 | - It checks the instance ID to avoid processing its own messages 106 | - It validates the message format before processing 107 | - Errors in message processing are logged but don't crash the instance 108 | 109 | ## Setup Requirements 110 | 111 | To run Enfyra in a cluster: 112 | 113 | 1. **Shared Redis**: All instances must connect to the same Redis instance 114 | ``` 115 | REDIS_URI=redis://your-shared-redis:6379 116 | ``` 117 | 118 | 2. **Shared Database**: All instances must connect to the same database 119 | ``` 120 | DB_URI=mysql://user:password@your-database-host:3306/enfyra 121 | # Or for PostgreSQL: 122 | # DB_URI=postgresql://user:password@your-database-host:5432/enfyra 123 | ``` 124 | 125 | > **Note**: If your password contains special characters (`@`, `:`, `/`, `%`, `#`, `?`, `&`), URL-encode them in the URI. For example, password `p@ssw0rd` should be written as `p%40ssw0rd` in the URI. 126 | 127 | **Optional - Read Replicas**: Add `DB_REPLICA_URIS` (comma-separated) to distribute read queries across replicas. Connection pool is automatically distributed between master and replicas. Read queries use round-robin routing. Set `DB_READ_FROM_MASTER=true` to include master in the round-robin pool. 128 | 129 | 3. **Node Names** (optional): Set unique node names for instances on different physical machines 130 | ``` 131 | # Instance on Node 1 132 | NODE_NAME=production-node-1 133 | 134 | # Instance on Node 2 135 | NODE_NAME=production-node-2 136 | ``` 137 | 138 | 4. **Same Configuration**: All instances should use the same configuration values for consistency 139 | 140 | ## Benefits 141 | 142 | Running Enfyra in a cluster provides: 143 | 144 | - **Horizontal Scaling**: Add more instances to handle increased load 145 | - **High Availability**: If one instance fails, others continue serving requests 146 | - **Zero Downtime Deployments**: Deploy new versions by rolling out instances one at a time 147 | - **Load Distribution**: Requests can be distributed across multiple instances 148 | 149 | The cluster coordination happens automatically - you just need to ensure all instances share the same Redis and database. 150 | -------------------------------------------------------------------------------- /server/repository-methods/find.md: -------------------------------------------------------------------------------- 1 | # Repository Methods - find() 2 | 3 | Query records from a table with filtering, sorting, pagination, and field selection. 4 | 5 | ## Basic Usage 6 | 7 | ```javascript 8 | // Find all records (up to default limit of 10) 9 | const result = await $ctx.$repos.products.find({}); 10 | 11 | // Access the data 12 | const products = result.data; // Array of product records 13 | ``` 14 | 15 | ## Parameters 16 | 17 | ```javascript 18 | await $ctx.$repos.tableName.find({ 19 | where: { ... }, // Filter conditions (optional) 20 | fields: '...', // Fields to return (optional) 21 | limit: 10, // Max records to return (optional, default: 10) 22 | sort: '...', // Sort order (optional, default: 'id') 23 | meta: 'totalCount' // Request metadata (optional) 24 | }) 25 | ``` 26 | 27 | ## Finding Records 28 | 29 | ### Get all records (no limit) 30 | ```javascript 31 | const result = await $ctx.$repos.products.find({ 32 | limit: 0 // 0 = no limit, fetch all records 33 | }); 34 | const allProducts = result.data; 35 | ``` 36 | 37 | ### Get limited records 38 | ```javascript 39 | const result = await $ctx.$repos.products.find({ 40 | limit: 20 // Return max 20 records 41 | }); 42 | ``` 43 | 44 | ### Filter records 45 | ```javascript 46 | const result = await $ctx.$repos.products.find({ 47 | where: { 48 | category: { _eq: 'electronics' }, 49 | price: { _gte: 100 } 50 | } 51 | }); 52 | ``` 53 | 54 | ### Filter with multiple conditions 55 | ```javascript 56 | const result = await $ctx.$repos.products.find({ 57 | where: { 58 | _and: [ 59 | { category: { _eq: 'electronics' } }, 60 | { price: { _between: [100, 500] } }, 61 | { isActive: { _eq: true } } 62 | ] 63 | } 64 | }); 65 | ``` 66 | 67 | ## Selecting Fields 68 | 69 | ### Return specific fields 70 | ```javascript 71 | const result = await $ctx.$repos.products.find({ 72 | fields: 'id,name,price' // Comma-separated field names 73 | }); 74 | ``` 75 | 76 | ### Return all fields (default) 77 | ```javascript 78 | const result = await $ctx.$repos.products.find({ 79 | // No fields parameter = return all fields 80 | }); 81 | ``` 82 | 83 | ### Include related table fields 84 | ```javascript 85 | const result = await $ctx.$repos.products.find({ 86 | fields: 'id,name,category.name,category.description' 87 | }); 88 | // Returns: [{id: 1, name: "Phone", category: {name: "Electronics", description: "..."}}] 89 | ``` 90 | 91 | ### Include all fields from a relation 92 | ```javascript 93 | const result = await $ctx.$repos.products.find({ 94 | fields: 'id,name,category.*' // category.* = all category fields 95 | }); 96 | ``` 97 | 98 | ## Sorting 99 | 100 | ### Sort ascending 101 | ```javascript 102 | const result = await $ctx.$repos.products.find({ 103 | sort: 'name' // Sort by name ascending 104 | }); 105 | ``` 106 | 107 | ### Sort descending 108 | ```javascript 109 | const result = await $ctx.$repos.products.find({ 110 | sort: '-price' // Prefix with "-" for descending 111 | }); 112 | ``` 113 | 114 | ### Multi-field sorting 115 | ```javascript 116 | const result = await $ctx.$repos.products.find({ 117 | sort: 'category,-price' // Sort by category ASC, then price DESC 118 | }); 119 | ``` 120 | 121 | ## Filter Operators 122 | 123 | The `where` parameter supports MongoDB-like operators: 124 | 125 | ### Comparison Operators 126 | ```javascript 127 | where: { 128 | price: { _eq: 100 }, // Equal to 129 | price: { _neq: 100 }, // Not equal to 130 | price: { _gt: 100 }, // Greater than 131 | price: { _gte: 100 }, // Greater than or equal 132 | price: { _lt: 500 }, // Less than 133 | price: { _lte: 500 }, // Less than or equal 134 | price: { _between: [100, 500] } // Between (inclusive) 135 | } 136 | ``` 137 | 138 | ### Array Operators 139 | ```javascript 140 | where: { 141 | category: { _in: ['electronics', 'gadgets'] }, // In array 142 | category: { _not_in: ['discontinued', 'old'] } // Not in array 143 | } 144 | ``` 145 | 146 | ### Text Search Operators 147 | ```javascript 148 | where: { 149 | name: { _contains: 'phone' }, // Contains text (case-insensitive) 150 | name: { _starts_with: 'Apple' }, // Starts with 151 | name: { _ends_with: 'Pro' } // Ends with 152 | } 153 | ``` 154 | 155 | ### Null Checks 156 | ```javascript 157 | where: { 158 | description: { _is_null: true }, // Field is null 159 | description: { _is_not_null: true } // Field is not null 160 | } 161 | ``` 162 | 163 | ### Logical Operators 164 | ```javascript 165 | where: { 166 | _and: [ // All conditions must match 167 | { category: { _eq: 'electronics' } }, 168 | { price: { _gte: 100 } } 169 | ], 170 | _or: [ // At least one condition must match 171 | { status: { _eq: 'active' } }, 172 | { status: { _eq: 'pending' } } 173 | ], 174 | _not: { // Negate condition 175 | category: { _eq: 'discontinued' } 176 | } 177 | } 178 | ``` 179 | 180 | ### Complex Combinations 181 | ```javascript 182 | where: { 183 | _and: [ 184 | { category: { _in: ['electronics', 'gadgets'] } }, 185 | { 186 | _or: [ 187 | { price: { _lt: 100 } }, 188 | { isOnSale: { _eq: true } } 189 | ] 190 | }, 191 | { description: { _is_not_null: true } } 192 | ] 193 | } 194 | ``` 195 | 196 | ## Filtering by Relations 197 | 198 | Filter records based on related table data: 199 | 200 | ```javascript 201 | // Find products where category name is 'Electronics' 202 | const result = await $ctx.$repos.products.find({ 203 | where: { 204 | category: { 205 | name: { _eq: 'Electronics' } 206 | } 207 | } 208 | }); 209 | ``` 210 | 211 | ## Metadata 212 | 213 | Request metadata about the query: 214 | 215 | ```javascript 216 | const result = await $ctx.$repos.products.find({ 217 | where: { category: { _eq: 'electronics' } }, 218 | meta: 'totalCount' // Get total count of all records (before filter) 219 | }); 220 | 221 | console.log(result.meta.totalCount); // Total records in table 222 | console.log(result.data.length); // Records matching filter 223 | ``` 224 | 225 | Available metadata options: 226 | - `'totalCount'` - Total number of records in table (ignores filter) 227 | - `'filterCount'` - Number of records matching the filter 228 | - `['totalCount', 'filterCount']` - Both counts 229 | 230 | ## Complete Example 231 | 232 | ```javascript 233 | const result = await $ctx.$repos.products.find({ 234 | where: { 235 | _and: [ 236 | { category: { _in: ['electronics', 'gadgets'] } }, 237 | { price: { _between: [100, 500] } }, 238 | { isActive: { _eq: true } }, 239 | { description: { _is_not_null: true } } 240 | ] 241 | }, 242 | fields: 'id,name,price,category.name', 243 | sort: '-price', 244 | limit: 20, 245 | meta: ['totalCount', 'filterCount'] 246 | }); 247 | 248 | const products = result.data; 249 | const totalCount = result.meta.totalCount; 250 | const filteredCount = result.meta.filterCount; 251 | ``` 252 | 253 | ## Next Steps 254 | 255 | - See [create(), update(), delete() methods](./create-update-delete.md) 256 | - Learn about [Common Patterns](./patterns.md) 257 | - Check [Query Filtering](../query-filtering.md) for more filtering examples 258 | 259 | -------------------------------------------------------------------------------- /server/README.md: -------------------------------------------------------------------------------- 1 | # Enfyra Server Documentation 2 | 3 | This documentation covers the Enfyra server architecture, APIs, and development guides. It's organized to help you find information quickly, whether you're learning the basics or looking up specific details. 4 | 5 | > **New to Enfyra?** Start with the [Installation Guide](../getting-started/installation.md) to set up your backend and frontend. 6 | 7 | ## Quick Navigation 8 | 9 | ### Getting Started 10 | - **[Repository Methods](repository-methods/find.md)** - Complete guide to database operations (find, create, update, delete) 11 | - **[Context Object ($ctx)](./context-reference/)** - All available properties and methods in the context 12 | - **[API Lifecycle](./api-lifecycle.md)** - How requests flow through the system 13 | 14 | ### Core Concepts 15 | - **[Hooks and Handlers](hooks-handlers/prehooks.md)** - Creating preHooks, afterHooks, and custom handlers 16 | - **[Query Filtering](./query-filtering.md)** - MongoDB-like filtering operators and examples 17 | - **[Error Handling](./error-handling.md)** - Throwing errors and handling exceptions 18 | 19 | ### Advanced Topics 20 | - **[Cache Operations](./cache-operations.md)** - Distributed caching and locking 21 | - **[File Handling](./file-handling.md)** - File uploads and management 22 | - **[Cluster Architecture](./cluster-architecture.md)** - Multi-instance coordination 23 | 24 | ## Finding What You Need 25 | 26 | ### "I want to query data from a table" 27 | See [Repository Methods - find()](./repository-methods/find.md) 28 | 29 | ### "I need to create a new record" 30 | See [Repository Methods - create()](repository-methods/find.md) 31 | 32 | ### "I want to update a record" 33 | See [Repository Methods - update()](repository-methods/find.md) 34 | 35 | ### "I need to delete a record" 36 | See [Repository Methods - delete()](repository-methods/find.md) 37 | 38 | ### "What properties are available in $ctx?" 39 | See [Context Reference](context-reference/request-data.md) 40 | 41 | ### "How do I access request body and params?" 42 | See [Context Reference - Request Data](./context-reference/request-data.md) 43 | 44 | ### "How do I use repositories in my code?" 45 | See [Context Reference - Repositories](./context-reference/repositories.md) 46 | 47 | ### "What helper functions are available?" 48 | See [Context Reference - Helpers & Cache](./context-reference/helpers-cache.md) 49 | 50 | ### "How does the request lifecycle work?" 51 | See [API Lifecycle](./api-lifecycle.md) 52 | 53 | ### "How do hooks execute in order?" 54 | See [API Lifecycle - Execution Order](./api-lifecycle.md#execution-order) 55 | 56 | ### "I want to validate data before saving" 57 | See [Hooks and Handlers - preHooks](./hooks-handlers/prehooks.md) 58 | 59 | ### "I need to modify response data" 60 | See [Hooks and Handlers - afterHooks](./hooks-handlers/afterhooks.md) 61 | 62 | ### "I want to write custom business logic" 63 | See [Hooks and Handlers - Custom Handlers](./hooks-handlers/custom-handlers.md) 64 | 65 | ### "How do I filter data with complex conditions?" 66 | See [Query Filtering](./query-filtering.md) 67 | 68 | ### "How do I throw errors properly?" 69 | See [Error Handling](./error-handling.md) 70 | 71 | ### "I need to use Redis cache" 72 | See [Cache Operations](./cache-operations.md) 73 | 74 | ### "I want to upload files" 75 | See [File Handling](./file-handling.md) 76 | 77 | ### "How do repositories work?" 78 | See [Repository Methods](repository-methods/find.md) 79 | 80 | ### "What methods does repository have?" 81 | See [Repository Methods](repository-methods/find.md) - Complete list of find, create, update, delete methods 82 | 83 | ## Documentation Structure 84 | 85 | All documentation is organized step-by-step, with clear examples and explanations. Each document focuses on a specific topic and includes: 86 | 87 | 1. **Overview** - What the topic is about 88 | 2. **Step-by-step guides** - How to use it 89 | 3. **Examples** - Real code examples 90 | 4. **Reference** - Complete API reference 91 | 5. **Common patterns** - Best practices and tips 92 | 93 | ## Repository Methods Overview 94 | 95 | The repository is the main way to interact with your database tables. Each table you create automatically gets a repository accessible through `$ctx.$repos.tableName`. 96 | 97 | **Available Methods:** 98 | - `find()` - Query records with filtering, sorting, and pagination 99 | - `create()` - Create new records 100 | - `update()` - Update existing records by ID 101 | - `delete()` - Delete records by ID 102 | 103 | All methods return data in a consistent format: `{ data: [...], meta: {...} }` 104 | 105 | See [Repository Methods Guide](repository-methods/find.md) for complete details. 106 | 107 | ## Context Object Overview 108 | 109 | The `$ctx` (context) object is available in all hooks and handlers. It provides access to: 110 | 111 | - **Request Data**: `$ctx.$body`, `$ctx.$params`, `$ctx.$query`, `$ctx.$user` 112 | - **Repositories**: `$ctx.$repos.tableName` for database operations 113 | - **Helpers**: `$ctx.$helpers` for JWT, bcrypt, file operations 114 | - **Cache**: `$ctx.$cache` for Redis operations 115 | - **Logging**: `$ctx.$logs()` for adding logs to responses 116 | - **Error Handling**: `$ctx.$throw['400']()` for throwing errors 117 | 118 | See [Context Reference](context-reference/request-data.md) for complete details. 119 | 120 | ## API Lifecycle Overview 121 | 122 | Every API request follows this flow: 123 | 124 | 1. **Route Detection** - System matches request to route definition 125 | 2. **Context Setup** - Creates `$ctx` with repositories and helpers 126 | 3. **preHooks Execution** - Runs all matching preHooks sequentially 127 | 4. **Handler Execution** - Custom handler or default CRUD operation 128 | 5. **afterHooks Execution** - Runs all matching afterHooks sequentially 129 | 6. **Response** - Returns processed data 130 | 131 | The same `$ctx` object flows through all phases, so modifications in preHooks are visible to handlers and afterHooks. 132 | 133 | See [API Lifecycle](./api-lifecycle.md) for complete details. 134 | 135 | ## Learning Path 136 | 137 | **New to Enfyra Server?** Follow this step-by-step path: 138 | 139 | 1. **[Repository Methods](repository-methods/find.md)** - Start here! Learn how to query, create, update, and delete records 140 | 2. **[Context Reference](context-reference/request-data.md)** - Understand all available properties and methods in `$ctx` 141 | 3. **[API Lifecycle](./api-lifecycle.md)** - Learn how requests flow through the system 142 | 4. **[Hooks and Handlers](hooks-handlers/prehooks.md)** - Customize API behavior with hooks and handlers 143 | 5. **[Query Filtering](./query-filtering.md)** - Master filtering and querying data 144 | 6. **[Error Handling](./error-handling.md)** - Handle errors properly 145 | 146 | **Advanced topics:** 147 | - [Cache Operations](./cache-operations.md) - Distributed caching and locking 148 | - [File Handling](./file-handling.md) - File uploads and management 149 | - [Cluster Architecture](./cluster-architecture.md) - Multi-instance coordination 150 | 151 | ## Next Steps 152 | 153 | 1. Start with [Repository Methods](repository-methods/find.md) to learn database operations 154 | 2. Read [Context Reference](context-reference/request-data.md) to understand available properties 155 | 3. Check [API Lifecycle](./api-lifecycle.md) to see how everything fits together 156 | 4. Explore [Hooks and Handlers](hooks-handlers/prehooks.md) for customization 157 | 158 | For specific questions, use the Quick Navigation section above to jump directly to relevant documentation. 159 | 160 | -------------------------------------------------------------------------------- /app/menu-management.md: -------------------------------------------------------------------------------- 1 | # Menu Management 2 | 3 | Menu Management lets you create custom navigation menus for your application. You can create top-level menus, dropdown menus, and individual menu items with custom permissions and ordering. 4 | 5 | ## Accessing Menu Management 6 | 7 | 1. Navigate to **Settings Menu** in the sidebar 8 | 2. You'll see all existing menus displayed as cards 9 | 3. System menus (like Dashboard, Settings) are protected and cannot be deleted 10 | 11 | ## Menu Types 12 | 13 | ### Menu 14 | - **Purpose**: Top-level navigation items or individual menu links 15 | - **Displays**: As clickable menu items in the sidebar 16 | - **Can be**: 17 | - A simple menu with a direct route (navigates to a page) 18 | - A menu group with children items (displays as expandable section) 19 | - **Can contain**: Child menu items (optional) 20 | 21 | ### Dropdown Menu 22 | - **Purpose**: Collapsible menu sections that group related menu items 23 | - **Displays**: As expandable sections with arrow icons 24 | - **Can contain**: Individual menu items 25 | - **Behavior**: Click to expand/collapse and reveal child items 26 | 27 | ## Creating a New Menu 28 | 29 | ### Step 1: Start Creation 30 | 1. Click the **"Create"** button at the top right 31 | 2. You'll see the menu creation form 32 | 33 | ### Step 2: Basic Information 34 | - **Label**: The display name for your menu item 35 | - **Description**: Optional description of what this menu does 36 | - **Icon**: Choose a Lucide icon (defaults to "menu") 37 | - **Path**: The URL route this menu navigates to (e.g., `/my-custom-page`) 38 | 39 | ### Step 3: Choose Menu Type 40 | Select the type from the dropdown: 41 | - **Menu**: Creates a top-level menu item (can have a route or contain child items) 42 | - **Dropdown Menu**: Creates a collapsible menu section for grouping related items 43 | 44 | ### Step 4: Set Hierarchy (Optional) 45 | 46 | **For Menu:** 47 | - **Parent**: Use the relation picker to select a parent menu (Dropdown Menu or Menu with children) 48 | - If no parent is selected, the menu appears at the top level 49 | - **Path**: Set the route path if this menu should navigate to a page 50 | 51 | **For Dropdown Menu:** 52 | - **Parent**: Use the relation picker to select a parent menu (optional) 53 | - If no parent is selected, the dropdown appears at the top level 54 | - Dropdown menus typically don't have routes - they're containers for other items 55 | 56 | **Note**: You can nest menus under other menus or dropdown menus to create hierarchical structures. 57 | 58 | ### Step 5: Configure Settings 59 | - **Order**: Number to control display order (lower numbers appear first) 60 | - **Is Enabled**: Toggle to enable/disable the menu item 61 | - **Permission**: Click "Configure Permission" to set access rules (see below) 62 | 63 | ### Step 6: Save 64 | Click **"Create"** to save your new menu item 65 | 66 | ## Managing Existing Menus 67 | 68 | ### Quick Actions 69 | - **Enable/Disable Toggle**: Click the switch on each menu card to enable or disable 70 | - **Edit Menu**: Click on any menu card to open the edit form 71 | - **Delete Menu**: Use the delete button (not available for system menus) 72 | 73 | ### Bulk Operations 74 | - **Filter Menus**: Use the filter dropdown to show only specific menu types 75 | - **Select Multiple**: Use checkboxes to select multiple menus for bulk operations 76 | 77 | ## Permission System 78 | 79 | ### How Menu Permissions Work 80 | 81 | The menu system uses `PermissionGate` and `usePermissions` internally to automatically show/hide menu items based on user permissions. When you configure permissions for a menu, the system: 82 | 83 | 1. **Evaluates permissions** when the menu loads 84 | 2. **Shows menu** only if user has required permissions 85 | 3. **Hides parent menus** if user can't access any child items 86 | 4. **Updates automatically** when permissions change 87 | 88 | **See [Permission Components](./permission-components.md) for technical details** 89 | 90 | ### Setting Menu Permissions 91 | 92 | 1. Click **"Configure Permission"** in the menu form 93 | 2. Use the [Permission Builder](./permission-builder.md) to create rules: 94 | - **Allow All**: Makes menu visible to everyone 95 | - **Route + Actions**: Specify which route permissions are required 96 | - **AND/OR Logic**: Combine multiple permission conditions 97 | 98 | ### Common Permission Examples 99 | 100 | **Admin Only:** 101 | - Select route: `/admin` 102 | - Enable action: Read 103 | 104 | **Multiple Route Access (OR):** 105 | - Add OR group 106 | - Add permission 1: Route `/users`, Action: Read 107 | - Add permission 2: Route `/settings`, Action: Update 108 | 109 | **Complex Permissions (AND/OR):** 110 | - Add AND group 111 | - Add permission: Route `/products`, Action: Read 112 | - Add OR sub-group: 113 | - Permission 1: Route `/products`, Action: Create 114 | - Permission 2: Route `/products`, Action: Update 115 | 116 | ## Menu Hierarchy Examples 117 | 118 | ### Simple Top-Level Menu 119 | 1. Create **Menu**: "Reports" with path `/reports` 120 | 2. This appears as a clickable menu item that navigates to the reports page 121 | 122 | ### Menu with Children 123 | 1. Create **Menu**: "Management" (no path, or set a default path) 124 | 2. Create **Menu** items with parent set to "Management": 125 | - "User List" (`/management/users`) 126 | - "User Roles" (`/management/roles`) 127 | 3. The Management menu becomes expandable and shows its children when clicked 128 | 129 | ### Dropdown Menu Structure 130 | 1. Create **Dropdown Menu**: "User Management" (no path needed) 131 | 2. Create **Menu** items with parent set to "User Management": 132 | - "User List" (`/management/users`) 133 | - "User Roles" (`/management/roles`) 134 | 3. The dropdown menu appears as a collapsible section with arrow icon 135 | 136 | ### Complex Nested Structure 137 | 1. Create **Menu**: "Settings" (top level) 138 | 2. Create **Dropdown Menu**: "User Management" with parent "Settings" 139 | 3. Create **Menu** items with parent "User Management": 140 | - "User List" (`/settings/users`) 141 | - "User Roles" (`/settings/roles`) 142 | 4. This creates: Settings User Management (User List, User Roles) 143 | 144 | ## Important Notes 145 | 146 | ### System Menus 147 | - **Protected**: System menus (Dashboard, Data, Collections, Settings, Storage) cannot be deleted 148 | - **Limited Editing**: You can modify labels and icons but core functionality is protected 149 | - **Recognition**: System menus show a "System" badge 150 | 151 | ### Path Requirements 152 | - **Must start with /**: All paths must begin with a forward slash 153 | - **Unique paths**: Each menu should have a unique path to avoid conflicts 154 | - **Route matching**: Ensure your path matches actual routes in your application 155 | 156 | ### Order and Organization 157 | - **Numerical ordering**: Lower numbers appear first (0, 1, 2, etc.) 158 | - **Visual hierarchy**: Menus can contain Dropdown Menus or other Menu items 159 | - **Logical grouping**: Group related functionality under the same parent menu or dropdown 160 | - **Position**: Use the position field to place menus at "top" or "bottom" of the sidebar 161 | 162 | ## Troubleshooting 163 | 164 | ### Menu Not Appearing 165 | - Check that **Is Enabled** is turned on 166 | - Verify the user has required permissions 167 | - Ensure the menu type and hierarchy are set correctly 168 | 169 | ### Permission Issues 170 | - Confirm permission rules are configured correctly 171 | - Check that the user's role includes the required route permissions 172 | - Test with "Allow All" to isolate permission vs. other issues 173 | 174 | ### Hierarchy Problems 175 | - **Menu**: Can have a parent (Menu or Dropdown Menu) or be top-level 176 | - **Dropdown Menu**: Can have a parent (Menu or Dropdown Menu) or be top-level 177 | - **Circular references**: Ensure menus don't reference themselves as parents 178 | - **Path conflicts**: Each menu with a route should have a unique path -------------------------------------------------------------------------------- /app/permission-builder.md: -------------------------------------------------------------------------------- 1 | # Permission Builder 2 | 3 | The Permission Builder is a visual interface for creating complex access control rules. Instead of writing permission code, you can build permission conditions using a drag-and-drop style interface with logical operators. 4 | 5 | ## Accessing Permission Builder 6 | 7 | Permission fields appear in various forms throughout Enfyra: 8 | - **Menu Management**: Set menu visibility permissions 9 | - **Route Management**: Configure route access permissions 10 | - **User Management**: Set user-specific permissions 11 | - **Custom Forms**: Any form field with permission type 12 | 13 | **How to Identify Permission Fields:** 14 | - Look for fields with shield icons () 15 | - Click the field to open the Permission Builder drawer 16 | 17 | ## Building Permissions 18 | 19 | ### Step 1: Choose Permission Type 20 | 21 | When you open the Permission Builder, you have two options: 22 | 23 | **Allow All Access:** 24 | - Toggle **"Allow All"** to grant unrestricted access 25 | - This bypasses all permission checks 26 | - Use sparingly for admin-level access only 27 | 28 | **Build Custom Permissions:** 29 | - Leave "Allow All" off to create specific permission rules 30 | - Build detailed conditions using groups and rules 31 | 32 | ### Step 2: Create Permission Groups 33 | 34 | **Add a Group:** 35 | 1. Click **"+ Add Group"** to create a permission container 36 | 2. Choose the group logic: 37 | - **AND**: All conditions in the group must be true 38 | - **OR**: Any condition in the group can be true 39 | 40 | **Group Examples:** 41 | - **AND Group**: User must have both "read posts" AND "edit posts" 42 | - **OR Group**: User needs either "admin role" OR "editor role" 43 | 44 | ### Step 3: Add Permission Rules 45 | 46 | **Within each group:** 47 | 1. Click **"+ Add Permission"** to create a rule 48 | 2. **Select Route**: Click the route picker to choose which API endpoint 49 | 3. **Choose Actions**: Toggle the actions this rule allows: 50 | - **Create** (POST): Can create new records 51 | - **Read** (GET): Can view/list records 52 | - **Update** (PATCH): Can edit existing records 53 | - **Delete** (DELETE): Can remove records 54 | 55 | **Route Picker:** 56 | - Search for routes by name or path 57 | - Routes are displayed as `/table-name` or custom paths 58 | - Select the specific endpoint you want to control 59 | 60 | **Action Toggles:** 61 | - Click action badges to enable/disable permissions 62 | - Enabled actions show in color, disabled are grayed out 63 | - Must select at least one action for the rule to be valid 64 | 65 | ### Step 4: Build Complex Logic 66 | 67 | **Nested Groups:** 68 | - Add groups within groups for complex conditions 69 | - Example: `(Admin OR Editor) AND (Read Posts OR Write Posts)` 70 | 71 | **Multiple Rules:** 72 | - Add multiple permission rules to the same group 73 | - Each rule can target different routes 74 | - Combine different routes with AND/OR logic 75 | 76 | ## Permission Builder Examples 77 | 78 | ### Simple Permission 79 | **Goal**: Allow reading user profiles 80 | 81 | 1. **Add Group** (AND logic - default) 82 | 2. **Add Permission** in the group: 83 | - **Route**: `/users` 84 | - **Actions**: Read 85 | 86 | ### Complex Permission 87 | **Goal**: Admin or Editor can manage posts, OR any user can read posts 88 | 89 | 1. **Main Group** (OR logic) 90 | 91 | 2. **First Sub-Group** (AND logic): 92 | - **Permission 1**: Route `/roles`, Actions: Read 93 | - **Permission 2**: Route `/posts`, Actions: Create , Update , Delete 94 | 95 | 3. **Second Permission** (in main OR group): 96 | - **Route**: `/posts` 97 | - **Actions**: Read 98 | 99 | ### Real-World Menu Example 100 | **Goal**: Show "User Management" menu only to users who can manage users OR view user reports 101 | 102 | 1. **Main Group** (OR logic) 103 | 2. **Permission 1**: Route `/users`, Actions: Create , Update , Delete 104 | 3. **Permission 2**: Route `/reports/users`, Actions: Read 105 | 106 | ## Visual Interface Guide 107 | 108 | ### Permission Field Display 109 | - **Shield with Check** : Permissions configured 110 | - **Plain Shield** : Allow All enabled 111 | - **Shield with X** : No permissions configured 112 | 113 | ### Group Headers 114 | - **AND/OR Toggle**: Switch between logical operators 115 | - **Group Actions**: Add permissions, add sub-groups, delete group 116 | 117 | ### Permission Rules 118 | - **Route Badge**: Shows selected route path 119 | - **Action Badges**: Colored badges for enabled actions 120 | - **Edit Button**: Modify rule settings 121 | - **Delete Button**: Remove rule 122 | 123 | ### Validation Indicators 124 | - **Red Border**: Required fields missing 125 | - **Error Messages**: Specific validation issues 126 | - **Save Button State**: Disabled until form is valid 127 | 128 | ## Integration with Other Systems 129 | 130 | ### Menu System 131 | Permission Builder controls menu visibility: 132 | - Menus only show for users with required permissions 133 | - See [Menu Management](./menu-management.md) for menu-specific usage 134 | 135 | ### Route System 136 | Controls API endpoint access: 137 | - Works with Published Methods and Role Permissions 138 | - See [Permission Components](./permission-components.md) for UI integration details 139 | 140 | ### Form System 141 | Integrated into Enfyra's form rendering: 142 | - Automatic permission field detection 143 | - Consistent UI across all permission fields 144 | - See [Form System](#form-system-integration) below 145 | 146 | ## Form System Integration 147 | 148 | ### Automatic Field Detection 149 | Enfyra automatically detects permission fields in forms and renders the Permission Builder interface. 150 | 151 | ### Field Types 152 | - **Permission Field**: Renders Permission Builder on click 153 | - **Inline Editor**: Shows permission summary with edit capability 154 | - **JSON Editor**: Advanced users can edit permission JSON directly 155 | 156 | ### Form Validation 157 | - Permission fields validate automatically 158 | - Required routes and actions are checked 159 | - Form submission blocked until permissions are valid 160 | 161 | ### Data Binding 162 | - Permission changes save automatically when you close the builder 163 | - No need to click additional save buttons 164 | - Real-time validation during editing 165 | 166 | ## Best Practices 167 | 168 | ### Permission Design 169 | - **Start Simple**: Begin with basic permissions, add complexity as needed 170 | - **Logical Grouping**: Group related permissions together with appropriate AND/OR logic 171 | - **Clear Purpose**: Each permission rule should have a clear business purpose 172 | 173 | ### User Experience 174 | - **Test Thoroughly**: Verify permissions work correctly with test users 175 | - **Document Complex Rules**: Add descriptions for complex permission structures 176 | - **Regular Review**: Periodically audit permissions to ensure they're still needed 177 | 178 | ### Performance Considerations 179 | - **Avoid Over-Complexity**: Too many nested groups can impact performance 180 | - **Route Specificity**: Use specific routes rather than overly broad permissions 181 | - **Cache-Friendly**: Simple permission structures cache better 182 | 183 | ## Troubleshooting 184 | 185 | ### Permission Not Working 186 | - **Check Route Path**: Ensure the route path exactly matches the API endpoint 187 | - **Verify Actions**: Confirm the correct actions (CRUD) are selected 188 | - **Test Group Logic**: Make sure AND/OR operators create the intended logic 189 | 190 | ### Form Validation Errors 191 | - **Required Route**: Each permission rule needs a route selected 192 | - **Required Actions**: At least one action must be enabled per rule 193 | - **Empty Groups**: Remove groups that contain no permission rules 194 | 195 | ### Performance Issues 196 | - **Simplify Structure**: Reduce nested groups if experiencing slowness 197 | - **Specific Routes**: Use targeted routes instead of broad permissions 198 | - **Cache Reset**: Complex permission changes may require cache refresh -------------------------------------------------------------------------------- /server/repository-methods/create-update-delete.md: -------------------------------------------------------------------------------- 1 | # Repository Methods - create(), update(), delete() 2 | 3 | ## create() 4 | 5 | Create a new record in the table. Automatically returns the full created record including ID and timestamps. 6 | 7 | ### Basic Usage 8 | 9 | ```javascript 10 | const result = await $ctx.$repos.products.create({ 11 | data: { 12 | name: 'New Product', 13 | price: 99.99, 14 | category: 'electronics' 15 | } 16 | }); 17 | 18 | const newProduct = result.data[0]; // Full product record with ID, timestamps, etc. 19 | ``` 20 | 21 | ### Parameters 22 | 23 | ```javascript 24 | await $ctx.$repos.tableName.create({ 25 | data: { ... }, // Data to insert (required) 26 | fields: '...' // Fields to return (optional) 27 | }) 28 | ``` 29 | 30 | ### Creating Records 31 | 32 | #### Simple create 33 | ```javascript 34 | const result = await $ctx.$repos.products.create({ 35 | data: { 36 | name: 'iPhone 15', 37 | price: 999, 38 | category: 'electronics' 39 | } 40 | }); 41 | ``` 42 | 43 | #### Create with all fields 44 | ```javascript 45 | const result = await $ctx.$repos.products.create({ 46 | data: { 47 | name: 'iPhone 15', 48 | price: 999, 49 | category: 'electronics', 50 | description: 'Latest iPhone model', 51 | isActive: true, 52 | stock: 50 53 | } 54 | }); 55 | ``` 56 | 57 | #### Return specific fields after create 58 | ```javascript 59 | const result = await $ctx.$repos.products.create({ 60 | data: { 61 | name: 'iPhone 15', 62 | price: 999 63 | }, 64 | fields: 'id,name,price' // Only return these fields 65 | }); 66 | ``` 67 | 68 | ### Important Notes 69 | 70 | 1. **ID is auto-generated**: Never include `id` in the data object - it will be automatically generated by the database 71 | 2. **Timestamps are auto-generated**: `createdAt` and `updatedAt` are automatically added by the system 72 | 3. **Full record is returned**: The method automatically calls `find()` after insertion to return the complete record 73 | 4. **Validation happens automatically**: System validates table schema and system protection rules 74 | 75 | ### Create with Relations 76 | 77 | ```javascript 78 | // Create order with related items 79 | const orderResult = await $ctx.$repos.orders.create({ 80 | data: { 81 | customerId: 123, 82 | total: 299.99, 83 | status: 'pending' 84 | } 85 | }); 86 | 87 | const order = orderResult.data[0]; 88 | 89 | // Create order items 90 | const itemResult = await $ctx.$repos.order_items.create({ 91 | data: { 92 | orderId: order.id, 93 | productId: 456, 94 | quantity: 2, 95 | price: 149.99 96 | } 97 | }); 98 | ``` 99 | 100 | ### Error Handling 101 | 102 | ```javascript 103 | try { 104 | const result = await $ctx.$repos.products.create({ 105 | data: { 106 | name: 'Product Name', 107 | price: 99.99 108 | } 109 | }); 110 | } catch (error) { 111 | // Handle validation errors, constraint violations, etc. 112 | $ctx.$logs(`Failed to create product: ${error.message}`); 113 | } 114 | ``` 115 | 116 | --- 117 | 118 | ## update() 119 | 120 | Update an existing record by ID. Automatically returns the full updated record. 121 | 122 | ### Basic Usage 123 | 124 | ```javascript 125 | const result = await $ctx.$repos.products.update({ 126 | id: 123, 127 | data: { 128 | price: 89.99, 129 | isOnSale: true 130 | } 131 | }); 132 | 133 | const updatedProduct = result.data[0]; // Full updated product record 134 | ``` 135 | 136 | ### Parameters 137 | 138 | ```javascript 139 | await $ctx.$repos.tableName.update({ 140 | id: 123, // Record ID to update (required) 141 | data: { ... }, // Fields to update (required) 142 | fields: '...' // Fields to return (optional) 143 | }) 144 | ``` 145 | 146 | ### Updating Records 147 | 148 | #### Update single field 149 | ```javascript 150 | const result = await $ctx.$repos.products.update({ 151 | id: 123, 152 | data: { 153 | price: 89.99 154 | } 155 | }); 156 | ``` 157 | 158 | #### Update multiple fields 159 | ```javascript 160 | const result = await $ctx.$repos.products.update({ 161 | id: 123, 162 | data: { 163 | price: 89.99, 164 | isOnSale: true, 165 | description: 'Updated description' 166 | } 167 | }); 168 | ``` 169 | 170 | #### Return specific fields after update 171 | ```javascript 172 | const result = await $ctx.$repos.products.update({ 173 | id: 123, 174 | data: { 175 | price: 89.99 176 | }, 177 | fields: 'id,name,price' // Only return these fields 178 | }); 179 | ``` 180 | 181 | ### Important Notes 182 | 183 | 1. **ID must exist**: The record with the given ID must exist, otherwise an error is thrown 184 | 2. **Partial updates**: You only need to include fields you want to update 185 | 3. **Full record is returned**: The method automatically calls `find()` after update to return the complete record 186 | 4. **Timestamps are auto-updated**: `updatedAt` is automatically updated by the system 187 | 5. **Validation happens automatically**: System validates table schema and system protection rules 188 | 189 | ### Check if Record Exists First 190 | 191 | ```javascript 192 | // Find the record first 193 | const findResult = await $ctx.$repos.products.find({ 194 | where: { id: { _eq: 123 } } 195 | }); 196 | 197 | if (findResult.data.length === 0) { 198 | $ctx.$throw['404']('Product not found'); 199 | return; 200 | } 201 | 202 | // Update the record 203 | const result = await $ctx.$repos.products.update({ 204 | id: 123, 205 | data: { 206 | price: 89.99 207 | } 208 | }); 209 | ``` 210 | 211 | ### Error Handling 212 | 213 | ```javascript 214 | try { 215 | const result = await $ctx.$repos.products.update({ 216 | id: 123, 217 | data: { 218 | price: 89.99 219 | } 220 | }); 221 | } catch (error) { 222 | // Handle errors: record not found, validation errors, etc. 223 | $ctx.$logs(`Failed to update product: ${error.message}`); 224 | } 225 | ``` 226 | 227 | --- 228 | 229 | ## delete() 230 | 231 | Delete a record by ID. Returns a success message. 232 | 233 | ### Basic Usage 234 | 235 | ```javascript 236 | const result = await $ctx.$repos.products.delete({ 237 | id: 123 238 | }); 239 | 240 | // result: { message: 'Delete successfully!', statusCode: 200 } 241 | ``` 242 | 243 | ### Parameters 244 | 245 | ```javascript 246 | await $ctx.$repos.tableName.delete({ 247 | id: 123 // Record ID to delete (required) 248 | }) 249 | ``` 250 | 251 | ### Deleting Records 252 | 253 | #### Simple delete 254 | ```javascript 255 | const result = await $ctx.$repos.products.delete({ 256 | id: 123 257 | }); 258 | ``` 259 | 260 | #### Check if record exists before deleting 261 | ```javascript 262 | // Find the record first 263 | const findResult = await $ctx.$repos.products.find({ 264 | where: { id: { _eq: 123 } } 265 | }); 266 | 267 | if (findResult.data.length === 0) { 268 | $ctx.$throw['404']('Product not found'); 269 | return; 270 | } 271 | 272 | // Delete the record 273 | const result = await $ctx.$repos.products.delete({ 274 | id: 123 275 | }); 276 | ``` 277 | 278 | ### Important Notes 279 | 280 | 1. **ID must exist**: The record with the given ID must exist, otherwise an error is thrown 281 | 2. **Cascade behavior**: If the table has relations with `onDelete: 'cascade'`, related records will be deleted automatically 282 | 3. **Returns success message**: The method returns `{ message: 'Delete successfully!', statusCode: 200 }` 283 | 4. **Cannot be undone**: Deletion is permanent - make sure to validate before deleting 284 | 285 | ### Error Handling 286 | 287 | ```javascript 288 | try { 289 | const result = await $ctx.$repos.products.delete({ 290 | id: 123 291 | }); 292 | } catch (error) { 293 | // Handle errors: record not found, constraint violations, etc. 294 | $ctx.$logs(`Failed to delete product: ${error.message}`); 295 | } 296 | ``` 297 | 298 | ## Next Steps 299 | 300 | - See [find() method](./find.md) for querying records 301 | - Learn about [Common Patterns](./patterns.md) for best practices 302 | - Check [Error Handling](../error-handling.md) for proper error handling 303 | 304 | -------------------------------------------------------------------------------- /app/storage-management.md: -------------------------------------------------------------------------------- 1 | # Storage Management 2 | 3 | Storage Management allows you to organize and manage files and folders in your application. You can upload files, create folders, and configure storage locations. 4 | 5 | ## Accessing Storage Management 6 | 7 | 1. Navigate to **Storage Management** in the sidebar 8 | 2. You'll see the file management interface with folders and files 9 | 10 | ## Routes 11 | 12 | - **Storage Management**: `/storage/management` - Main file management page 13 | - **File Details**: `/storage/management/file/:id` - View and edit file information 14 | - **Folder Details**: `/storage/management/folder/:id` - View and manage folder contents 15 | - **Storage Configuration**: `/storage/config` - Manage storage configurations 16 | 17 | ## Uploading Files 18 | 19 | ### From Storage Management Page 20 | 21 | 1. Click the **"Upload Files"** button in the header (top right) 22 | 2. The upload modal will open 23 | 3. **Optional**: Select a storage location from the dropdown (if you have multiple storage configurations) 24 | 4. Choose files by either: 25 | - Clicking **"Choose File"** button to browse your computer 26 | - Dragging and dropping files into the upload area 27 | 5. Selected files will appear in a list below the upload area 28 | 6. Click **"Upload"** button to start uploading 29 | 7. Files will be uploaded to the root level (not in any folder) 30 | 31 | ### From Folder Details Page 32 | 33 | 1. Navigate into a folder by clicking on it 34 | 2. Click the **"Upload Files"** button in the header 35 | 3. Follow the same steps as above 36 | 4. Files will be uploaded into the current folder 37 | 38 | ### Upload Features 39 | 40 | - **Multiple Files**: You can upload multiple files at once 41 | - **File Size Limit**: Maximum file size is 50MB per file 42 | - **Storage Selection**: You can optionally choose which storage configuration to use for the upload 43 | - **File Types**: All file types are accepted by default 44 | 45 | ## Creating Folders 46 | 47 | ### From Storage Management Page 48 | 49 | 1. Click the **"New Folder"** button in the header (next to Upload Files) 50 | 2. A modal will open asking for folder name 51 | 3. Enter the folder name 52 | 4. Click **"Create"** to create the folder at root level 53 | 54 | ### From Folder Details Page 55 | 56 | 1. Navigate into a folder 57 | 2. Click the **"New Folder"** button 58 | 3. Enter the folder name 59 | 4. The new folder will be created inside the current folder 60 | 61 | ## Viewing Files and Folders 62 | 63 | ### Grid View (Default) 64 | 65 | - Files and folders are displayed as cards 66 | - Each card shows: 67 | - Icon (folder icon for folders, file type icon for files) 68 | - Name 69 | - Additional metadata (for files) 70 | 71 | ### List View 72 | 73 | - Click the **"List View"** button in the subheader to switch to list view 74 | - Files and folders are displayed in a table format 75 | - Shows more detailed information 76 | 77 | ### Switching Views 78 | 79 | - Use the view toggle button in the subheader (left side) 80 | - Toggle between Grid View and List View 81 | 82 | ## Managing Files 83 | 84 | ### Viewing File Details 85 | 86 | 1. Click on any file card or name 87 | 2. You'll be taken to the file details page 88 | 3. You can see: 89 | - File preview (for images) 90 | - File icon (for other file types) 91 | - File information form 92 | - File permissions section 93 | 94 | ### Editing File Information 95 | 96 | 1. Navigate to file details page 97 | 2. Edit any field in the form 98 | 3. Click **"Save"** button in the header to save changes 99 | 4. Click **"Reset"** to discard changes 100 | 101 | ### Replacing a File 102 | 103 | 1. Navigate to file details page 104 | 2. Click **"Replace File"** button in the subheader 105 | 3. Upload modal will open 106 | 4. Select a new file to replace the current one 107 | 5. Click **"Upload"** to replace 108 | 6. **Warning**: The old file will be permanently lost 109 | 110 | ### Deleting Files 111 | 112 | **From File Details Page:** 113 | 1. Navigate to file details 114 | 2. Click the **"Delete"** button in the header (red button) 115 | 3. Confirm the deletion 116 | 117 | **From File Manager:** 118 | 1. Right-click on a file 119 | 2. Select **"Delete"** from the context menu 120 | 3. Confirm the deletion 121 | 122 | **Multiple Files:** 123 | 1. Enter selection mode (click "Select Items" in subheader) 124 | 2. Select multiple files by clicking on them 125 | 3. Use the delete action from the context menu or toolbar 126 | 127 | ## Managing Folders 128 | 129 | ### Viewing Folder Contents 130 | 131 | 1. Click on any folder 132 | 2. You'll be taken to the folder details page 133 | 3. You can see: 134 | - All subfolders 135 | - All files in the folder 136 | - Folder information 137 | 138 | ### Editing Folder Information 139 | 140 | 1. Navigate to folder details page 141 | 2. Edit folder fields in the form 142 | 3. Click **"Save"** to save changes 143 | 144 | ### Deleting Folders 145 | 146 | **From Folder Details:** 147 | 1. Right-click on a folder 148 | 2. Select **"Delete"** from the context menu 149 | 3. Confirm the deletion 150 | 151 | **Warning**: Deleting a folder will also delete all files and subfolders inside it. 152 | 153 | ## Storage Configuration 154 | 155 | ### Viewing Storage Configurations 156 | 157 | 1. Navigate to **Storage Config** in the sidebar 158 | 2. You'll see a list of all storage configurations 159 | 3. Each configuration shows: 160 | - Name 161 | - Storage type (Amazon S3, Google Cloud Storage, Cloudflare R2, Local Storage) 162 | - Status (Active/Inactive) 163 | - Description 164 | - Badge color indicating storage type: 165 | - **Blue (Primary)**: Amazon S3 166 | - **Cyan (Info)**: Google Cloud Storage 167 | - **Orange (Warning)**: Cloudflare R2 168 | - **Gray (Neutral)**: Local Storage 169 | 170 | ### Creating Storage Configuration 171 | 172 | 1. Go to Storage Config page 173 | 2. Click **"Create Storage"** button in the header 174 | 3. The form will initially show only these fields: 175 | - **Name**: A descriptive name for this storage configuration 176 | - **Bucket**: Storage bucket/container name 177 | - **Description**: Optional description 178 | - **Type**: Select the storage type (Amazon S3, Google Cloud Storage, or Cloudflare R2) 179 | - **Note**: Local Storage cannot be created through the UI 180 | 181 | 4. After selecting a storage type, additional fields will appear based on the type: 182 | 183 | **For Amazon S3:** 184 | - **Access Key ID**: Your AWS access key ID 185 | - **Secret Access Key**: Your AWS secret access key 186 | - **Bucket**: S3 bucket name 187 | - **Region**: AWS region (e.g., us-east-1, eu-west-1) 188 | 189 | **For Google Cloud Storage:** 190 | - **Credentials**: JSON credentials for GCS service account 191 | - **Bucket**: GCS bucket name 192 | 193 | **For Cloudflare R2:** 194 | - **Access Key ID**: Your R2 access key ID 195 | - **Secret Access Key**: Your R2 secret access key 196 | - **Account ID**: Your Cloudflare account ID 197 | - **Bucket**: R2 bucket name 198 | 199 | 5. Fill in all required fields for your selected storage type 200 | 6. Click **"Save"** to create the configuration 201 | 202 | ### Editing Storage Configuration 203 | 204 | 1. Click on any storage configuration card 205 | 2. The form will show all relevant fields based on the storage type 206 | 3. **Note**: The **Type** field is disabled and cannot be changed after creation 207 | 4. Edit the fields you want to modify 208 | 5. Click **"Save"** to save changes 209 | 6. Click **"Reset"** to discard changes 210 | 211 | ### Enabling/Disabling Storage 212 | 213 | 1. On the Storage Config list page 214 | 2. Toggle the switch on any configuration card 215 | 3. Active configurations can be used for file uploads 216 | 4. Inactive configurations cannot be selected during upload 217 | 218 | ### Deleting Storage Configuration 219 | 220 | 1. On the Storage Config list page 221 | 2. Click the **"Delete"** button on a configuration card 222 | 3. Confirm the deletion 223 | 224 | ## File Actions 225 | 226 | ### Context Menu Actions 227 | 228 | Right-click on any file to see available actions: 229 | - **View**: Open file in new tab 230 | - **Download**: Download the file 231 | - **Copy URL**: Copy file URL to clipboard 232 | - **Details**: Navigate to file details page 233 | - **Delete**: Delete the file (if you have permission) 234 | 235 | ### Selection Mode 236 | 237 | 1. Click **"Select Items"** button in the subheader 238 | 2. Click on files/folders to select them 239 | 3. Selected items will be highlighted 240 | 4. Use bulk actions (delete, move, etc.) 241 | 5. Click **"Cancel Selection"** to exit selection mode 242 | 243 | ## Navigation 244 | 245 | ### Breadcrumbs 246 | 247 | - Use breadcrumbs at the top to navigate back to parent folders 248 | - Click on any folder name in the breadcrumb to go to that folder 249 | 250 | ### Back Navigation 251 | 252 | - Use browser back button 253 | - Or click on parent folder in breadcrumbs 254 | 255 | ## Pagination 256 | 257 | When you have many files or folders: 258 | - Pagination controls appear at the bottom 259 | - Separate pagination for folders and files 260 | - Use page numbers to navigate through pages 261 | 262 | ## Permissions 263 | 264 | - **Upload Files**: Requires create permission on `/file_definition` 265 | - **Create Folders**: Requires create permission on `/folder_definition` 266 | - **Edit Files**: Requires update permission on `/file_definition` 267 | - **Delete Files**: Requires delete permission on `/file_definition` 268 | - **Manage Storage Config**: Requires permissions on `/storage_config_definition` 269 | 270 | Actions will only appear if you have the required permissions. 271 | 272 | -------------------------------------------------------------------------------- /app/page-header.md: -------------------------------------------------------------------------------- 1 | # Page Header System 2 | 3 | The Page Header System allows you to register custom page headers that appear at the top of each page, providing context and navigation information to users. 4 | 5 | ## Table of Contents 6 | 7 | - [Overview](#overview) 8 | - [Basic Usage](#basic-usage) 9 | - [Header Variants](#header-variants) 10 | - [Gradient Options](#gradient-options) 11 | - [Stats Display](#stats-display) 12 | - [Advanced Features](#advanced-features) 13 | - [Real-World Examples](#real-world-examples) 14 | - [Best Practices](#best-practices) 15 | - [API Reference](#api-reference) 16 | 17 | ## Overview 18 | 19 | Page headers provide: 20 | - **Title and Description**: Clear page context for users 21 | - **Stats Display**: Show key metrics at a glance 22 | - **Visual Variants**: Different layouts for different use cases 23 | - **Gradient Themes**: Purple, blue, cyan backgrounds 24 | - **Auto-Cleanup**: Headers automatically cleared on route change 25 | 26 | ## Basic Usage 27 | 28 | ### Simple Page Header 29 | 30 | ```vue 31 | 41 | ``` 42 | 43 | ### Page Header with Description 44 | 45 | ```vue 46 | 56 | ``` 57 | 58 | ## Header Variants 59 | 60 | ### Default Variant 61 | Standard layout with title, description, and optional stats. 62 | 63 | ```vue 64 | registerPageHeader({ 65 | title: "Collections", 66 | description: "Manage your data collections", 67 | variant: "default", 68 | gradient: "purple" 69 | }); 70 | ``` 71 | 72 | ### Minimal Variant 73 | Compact layout focusing on title only. 74 | 75 | ```vue 76 | registerPageHeader({ 77 | title: "Dashboard", 78 | variant: "minimal", 79 | gradient: "cyan" 80 | }); 81 | ``` 82 | 83 | ### Stats Focus Variant 84 | Emphasizes statistics display. 85 | 86 | ```vue 87 | registerPageHeader({ 88 | title: "Analytics", 89 | description: "View your analytics data", 90 | variant: "stats-focus", 91 | gradient: "blue", 92 | stats: [ 93 | { label: "Total Users", value: 1250 }, 94 | { label: "Active Sessions", value: 42 } 95 | ] 96 | }); 97 | ``` 98 | 99 | ## Gradient Options 100 | 101 | Available gradients: 102 | - **purple**: Purple gradient background 103 | - **blue**: Blue gradient background 104 | - **cyan**: Cyan gradient background 105 | - **none**: No gradient (solid background) 106 | 107 | ```vue 108 | // Purple gradient 109 | registerPageHeader({ 110 | title: "Settings", 111 | gradient: "purple" 112 | }); 113 | 114 | // No gradient 115 | registerPageHeader({ 116 | title: "Simple Page", 117 | gradient: "none" 118 | }); 119 | ``` 120 | 121 | ## Stats Display 122 | 123 | ### Basic Stats 124 | 125 | ```vue 126 | const { registerPageHeader } = usePageHeaderRegistry(); 127 | 128 | registerPageHeader({ 129 | title: "User Manager", 130 | stats: [ 131 | { label: "Total Users", value: 1250 }, 132 | { label: "Active", value: 892 }, 133 | { label: "Pending", value: 45 } 134 | ] 135 | }); 136 | ``` 137 | 138 | ### Reactive Stats 139 | 140 | ```vue 141 | 162 | ``` 163 | 164 | ## Advanced Features 165 | 166 | ### Conditional Page Headers 167 | 168 | ```vue 169 | 181 | ``` 182 | 183 | ### Dynamic Titles 184 | 185 | ```vue 186 | 196 | ``` 197 | 198 | ### Checking Header Status 199 | 200 | ```vue 201 | 209 | ``` 210 | 211 | ## Real-World Examples 212 | 213 | ### 1. Data Table Page 214 | 215 | ```vue 216 | 221 | 222 | 240 | ``` 241 | 242 | ### 2. Settings Page 243 | 244 | ```vue 245 | 251 | 252 | 262 | ``` 263 | 264 | ### 3. Analytics Dashboard 265 | 266 | ```vue 267 | 272 | 273 | 300 | ``` 301 | 302 | ## Best Practices 303 | 304 | ### 1. Use Appropriate Variants 305 | 306 | ```javascript 307 | // Dashboard/landing pages 308 | { variant: "minimal" } 309 | 310 | // Data listing pages 311 | { variant: "default", stats: [...] } 312 | 313 | // Analytics pages 314 | { variant: "stats-focus", stats: [...] } 315 | ``` 316 | 317 | ### 2. Match Gradient to Content 318 | 319 | ```javascript 320 | // Data/collections 321 | { gradient: "blue" } 322 | 323 | // Settings/configuration 324 | { gradient: "purple" } 325 | 326 | // Dashboard/overview 327 | { gradient: "cyan" } 328 | ``` 329 | 330 | ### 3. Keep Descriptions Concise 331 | 332 | ```javascript 333 | // Good 334 | { description: "Manage user accounts and permissions" } 335 | 336 | // Too long 337 | { description: "This page allows you to manage all user accounts in the system including creating new users, editing existing users, and configuring their permissions..." } 338 | ``` 339 | 340 | ### 4. Use Reactive Stats 341 | 342 | ```javascript 343 | // Good - reactive 344 | const stats = computed(() => [ 345 | { label: "Total", value: count.value } 346 | ]); 347 | 348 | // Avoid - static 349 | const stats = [ 350 | { label: "Total", value: 100 } 351 | ]; 352 | ``` 353 | 354 | ### 5. Clear Headers When Not Needed 355 | 356 | ```javascript 357 | const { clearPageHeader } = usePageHeaderRegistry(); 358 | 359 | // Clear when navigating away 360 | onUnmounted(() => { 361 | clearPageHeader(); 362 | }); 363 | ``` 364 | 365 | ## API Reference 366 | 367 | ### PageHeaderConfig Interface 368 | 369 | ```typescript 370 | interface PageHeaderConfig { 371 | title: string; // Page title (required) 372 | description?: string; // Page description 373 | stats?: PageHeaderStat[]; // Statistics to display 374 | variant?: "default" | "minimal" | "stats-focus"; // Layout variant 375 | gradient?: "purple" | "blue" | "cyan" | "none"; // Background gradient 376 | } 377 | ``` 378 | 379 | ### PageHeaderStat Interface 380 | 381 | ```typescript 382 | interface PageHeaderStat { 383 | label: string; // Stat label 384 | value: string | number; // Stat value 385 | } 386 | ``` 387 | 388 | ### usePageHeaderRegistry() 389 | 390 | ```typescript 391 | const { 392 | // Read-only current header config 393 | pageHeader: Readonly>, 394 | 395 | // Check if header is registered 396 | hasPageHeader: ComputedRef, 397 | 398 | // Register page header 399 | registerPageHeader: (config: PageHeaderConfig) => void, 400 | 401 | // Clear page header 402 | clearPageHeader: () => void 403 | } = usePageHeaderRegistry(); 404 | ``` 405 | 406 | ## Summary 407 | 408 | The Page Header System provides: 409 | 410 | **Consistent page headers** across the application 411 | **Multiple layout variants** for different page types 412 | **Gradient themes** for visual variety 413 | **Stats display** for key metrics 414 | **Auto-cleanup** on route changes 415 | **Reactive support** for dynamic content 416 | 417 | **Related Documentation:** 418 | - **[Header Actions](./header-actions.md)** - Adding actions to headers 419 | - **[Form System](./form-system.md)** - Dynamic form generation 420 | -------------------------------------------------------------------------------- /docker/README.md: -------------------------------------------------------------------------------- 1 | # Enfyra Docker 2 | 3 | All-in-one Docker image for Enfyra platform, including Server + App + Embedded Redis + Embedded Database. 4 | 5 | > **New to Enfyra?** See the [Installation Guide](../getting-started/installation.md) for complete setup instructions, including Docker and manual installation options. 6 | 7 | ## 🚀 Quick Start 8 | 9 | ### Simplest way (single command) 10 | 11 | ```bash 12 | docker run -d \ 13 | -p 3000:3000 \ 14 | -e DB_TYPE=postgres \ 15 | dothinh115/enfyra:latest 16 | ``` 17 | 18 | → Runs with embedded PostgreSQL and Redis, no additional configuration needed! 19 | 20 | ### With MySQL 21 | 22 | ```bash 23 | docker run -d \ 24 | -p 3000:3000 \ 25 | -e DB_TYPE=mysql \ 26 | dothinh115/enfyra:latest 27 | ``` 28 | 29 | ### With MongoDB (requires MONGO_URI) 30 | 31 | ```bash 32 | docker run -d \ 33 | -p 3000:3000 \ 34 | -e DB_TYPE=mongodb \ 35 | -e MONGO_URI=mongodb://user:pass@host:27017/dbname \ 36 | dothinh115/enfyra:latest 37 | ``` 38 | 39 | ## 🧹 Clean Up Old Images 40 | 41 | To remove old Docker images and free up disk space: 42 | 43 | ```bash 44 | cd docker 45 | 46 | # List all Enfyra images 47 | ./cleanup-images.sh --list 48 | 49 | # Keep only 5 latest images (default) 50 | ./cleanup-images.sh --keep 5 51 | 52 | # Keep only 3 latest images 53 | ./cleanup-images.sh --keep 3 54 | 55 | # Remove only dangling images 56 | ./cleanup-images.sh --dangling 57 | 58 | # Remove ALL Enfyra images (⚠️ careful!) 59 | ./cleanup-images.sh --all 60 | ``` 61 | 62 | ## 📦 Build and Push Image 63 | 64 | ### Build and push with automatic version (from server/package.json) 65 | 66 | ```bash 67 | cd docker 68 | ./build-and-push.sh 69 | ``` 70 | 71 | ### Build and push with specific version 72 | 73 | ```bash 74 | cd docker 75 | ./build-and-push.sh 1.2.5 76 | ``` 77 | 78 | ### Use environment variable to override namespace 79 | 80 | ```bash 81 | DOCKER_NAMESPACE=enfyra ./build-and-push.sh 82 | ``` 83 | 84 | ## ⚙️ Configuration 85 | 86 | ### Environment Variables 87 | 88 | #### Required 89 | - `DB_TYPE`: Database type (`postgres`, `mysql`, `mongodb`) - **default: `postgres`** 90 | 91 | #### Database Configuration 92 | 93 | **Primary (Prisma-style URI - Recommended):** 94 | - `DB_URI`: Database connection URI (if not set → uses embedded DB) 95 | - PostgreSQL: `postgresql://user:password@host:port/database` 96 | - MySQL: `mysql://user:password@host:port/database` 97 | - Example: `postgresql://enfyra:secret@my-postgres:5432/enfyra` 98 | - **Note**: If password contains special characters (`@`, `:`, `/`, etc.), URL-encode them: 99 | - `@` → `%40`, `:` → `%3A`, `/` → `%2F`, `%` → `%25`, `#` → `%23`, `?` → `%3F`, `&` → `%26` 100 | 101 | **Replica Configuration (Optional):** 102 | - `DB_REPLICA_URIS`: Comma-separated replica URIs (e.g., `postgresql://user:pass@replica1:5432/db,postgresql://user:pass@replica2:5432/db`) 103 | - `DB_READ_FROM_MASTER`: Include master in read pool (`true`/`false`, default: `false`) 104 | 105 | **Legacy (Backward Compatible):** 106 | - `DB_HOST`: Database host (if `DB_URI` not set) 107 | - `DB_PORT`: Database port 108 | - `DB_USERNAME`: Database username 109 | - `DB_PASSWORD`: Database password 110 | - `DB_NAME`: Database name 111 | 112 | **MongoDB:** 113 | - `MONGO_URI`: MongoDB connection string (only when `DB_TYPE=mongodb`) 114 | 115 | #### Redis Configuration 116 | - `REDIS_URI`: Redis connection string (if not set → uses embedded Redis) 117 | 118 | #### Mode Configuration 119 | - `ENFYRA_MODE`: Run mode (`all`, `server`, `app`) - **default: `all`** 120 | - `all`: Run both server + app (default) 121 | - `server`: Run backend server only 122 | - `app`: Run frontend app only 123 | 124 | #### Database Connection Pool (Optional) 125 | - `DB_POOL_MIN_SIZE`: Minimum connection pool size - **default: `2`** 126 | - `DB_POOL_MAX_SIZE`: Maximum connection pool size - **default: `100`** 127 | - `DB_POOL_MASTER_RATIO`: Master pool ratio (0.0-1.0) - **default: `0.6`** (60% master, 40% replicas) 128 | - `DB_ACQUIRE_TIMEOUT`: Connection acquisition timeout (ms) - **default: `60000`** 129 | - `DB_IDLE_TIMEOUT`: Idle connection timeout (ms) - **default: `30000`** 130 | 131 | #### Server Configuration 132 | - `PORT`: Server port - **default: `1105`** 133 | - `ENFYRA_SERVER_WORKERS`: Number of worker processes for cluster - **default: `1`** 134 | - `SECRET_KEY`: JWT secret key - **default: `enfyra_secret_key_change_in_production`** 135 | - `BACKEND_URL`: Backend URL (for Swagger/docs) - **default: `http://localhost:1105`** 136 | - `NODE_NAME`: Node instance name (for logs/cluster) - **default: `enfyra_docker`** 137 | - `DEFAULT_HANDLER_TIMEOUT`: Handler execution timeout (ms) - **default: `20000`** 138 | - `DEFAULT_PREHOOK_TIMEOUT`: Prehook timeout (ms) - **default: `20000`** 139 | - `DEFAULT_AFTERHOOK_TIMEOUT`: Afterhook timeout (ms) - **default: `20000`** 140 | 141 | #### Auth Configuration (Optional) 142 | - `SALT_ROUNDS`: bcrypt salt rounds - **default: `10`** 143 | - `ACCESS_TOKEN_EXP`: Access token expiration - **default: `15m`** 144 | - `REFRESH_TOKEN_NO_REMEMBER_EXP`: Refresh token expiration (no remember) - **default: `1d`** 145 | - `REFRESH_TOKEN_REMEMBER_EXP`: Refresh token expiration (remember) - **default: `7d`** 146 | 147 | #### Redis Configuration 148 | - `REDIS_URI`: Redis connection string (if not set → uses embedded Redis) 149 | - Format: `redis://user:pass@host:port/db` or `redis://host:port/db` 150 | - `DEFAULT_TTL`: Default TTL for cache entries (seconds) - **default: `5`** 151 | 152 | #### App Configuration 153 | - `ENFYRA_APP_PORT`: App port - **default: `3000`** 154 | - `API_URL`: Backend API URL (automatically set if `ENFYRA_MODE=all`) 155 | 156 | #### Handler Executor (Optional) 157 | - `HANDLER_EXECUTOR_MAX_MEMORY`: Max memory per child process (MB) - **default: `512`** 158 | - `HANDLER_EXECUTOR_POOL_MIN`: Minimum pool size - **default: `2`** 159 | - `HANDLER_EXECUTOR_POOL_MAX`: Maximum pool size - **default: `4`** 160 | 161 | #### Package Manager (Optional) 162 | - `PACKAGE_MANAGER`: Package manager (`yarn`, `npm`, `pnpm`) - **default: `yarn`** 163 | 164 | #### Environment 165 | - `NODE_ENV`: Environment (`development`, `production`, `test`) - **default: `production`** 166 | 167 | ### Usage Examples 168 | 169 | #### 1. All-in-one with embedded services (simplest) 170 | 171 | ```bash 172 | docker run -d \ 173 | -p 3000:3000 \ 174 | -e DB_TYPE=postgres \ 175 | -v enfyra-data:/app/data \ 176 | dothinh115/enfyra:latest 177 | ``` 178 | 179 | #### 2. Use external database and Redis (with DB_URI - Recommended) 180 | 181 | ```bash 182 | docker run -d \ 183 | -p 3000:3000 \ 184 | -e DB_TYPE=postgres \ 185 | -e DB_URI=postgresql://enfyra:secret@my-postgres:5432/enfyra \ 186 | -e REDIS_URI=redis://my-redis:6379/0 \ 187 | dothinh115/enfyra:latest 188 | ``` 189 | 190 | **Or with legacy vars (backward compatible):** 191 | 192 | ```bash 193 | docker run -d \ 194 | -p 3000:3000 \ 195 | -e DB_TYPE=postgres \ 196 | -e DB_HOST=my-postgres \ 197 | -e DB_PORT=5432 \ 198 | -e DB_USERNAME=enfyra \ 199 | -e DB_PASSWORD=secret \ 200 | -e DB_NAME=enfyra \ 201 | -e REDIS_URI=redis://my-redis:6379/0 \ 202 | dothinh115/enfyra:latest 203 | ``` 204 | 205 | #### 3. Run server only (for cluster) 206 | 207 | ```bash 208 | docker run -d \ 209 | -p 1105:1105 \ 210 | -e ENFYRA_MODE=server \ 211 | -e DB_TYPE=postgres \ 212 | -e DB_URI=postgresql://enfyra:secret@my-postgres:5432/enfyra \ 213 | -e REDIS_URI=redis://my-redis:6379/0 \ 214 | -e ENFYRA_SERVER_WORKERS=4 \ 215 | dothinh115/enfyra:latest 216 | ``` 217 | 218 | **With replica databases:** 219 | 220 | ```bash 221 | docker run -d \ 222 | -p 1105:1105 \ 223 | -e ENFYRA_MODE=server \ 224 | -e DB_TYPE=postgres \ 225 | -e DB_URI=postgresql://enfyra:secret@master-postgres:5432/enfyra \ 226 | -e DB_REPLICA_URIS=postgresql://enfyra:secret@replica1:5432/enfyra,postgresql://enfyra:secret@replica2:5432/enfyra \ 227 | -e DB_READ_FROM_MASTER=false \ 228 | -e REDIS_URI=redis://my-redis:6379/0 \ 229 | -e ENFYRA_SERVER_WORKERS=4 \ 230 | dothinh115/enfyra:latest 231 | ``` 232 | 233 | #### 4. Run app only (frontend only) 234 | 235 | ```bash 236 | docker run -d \ 237 | -p 3000:3000 \ 238 | -e ENFYRA_MODE=app \ 239 | -e API_URL=https://api.my-domain.com/ \ 240 | dothinh115/enfyra:latest 241 | ``` 242 | 243 | ## 🏗️ Architecture 244 | 245 | This image contains: 246 | - **Server**: NestJS backend (port 1105) 247 | - **App**: Nuxt frontend (port 3000) 248 | - **Redis**: Embedded Redis server (port 6379, internal - expose with `-p 6379:6379` if needed) 249 | - **PostgreSQL**: Embedded database (port 5432, internal - expose with `-p 5432:5432` if needed) 250 | - **MySQL**: Embedded database (port 3306, internal - expose with `-p 3306:3306` if needed) 251 | 252 | All managed by **supervisor** in a single container. 253 | 254 | ### Default Credentials 255 | 256 | When using embedded database, default credentials: 257 | - **PostgreSQL/MySQL**: 258 | - Username: `enfyra` 259 | - Password: `enfyra_password_123` 260 | - Database: `enfyra` 261 | - **Admin User** (auto-created): 262 | - Email: `enfyra@admin.com` 263 | - Password: `1234` 264 | 265 | ## 📝 Notes 266 | 267 | - **Data persistence**: Use volume to persist embedded DB/Redis data: 268 | ```bash 269 | -v enfyra-data:/app/data 270 | ``` 271 | 272 | - **Production**: Recommended to use external database and Redis for production for better HA and backup. 273 | 274 | - **Cluster**: Set `ENFYRA_SERVER_WORKERS > 1` to run backend cluster in container, or scale multiple containers with `ENFYRA_MODE=server`. 275 | 276 | - **Environment Files**: `.env` files are automatically generated for both server and app based on `env_example` files. All env vars have reasonable defaults, but you can override by setting env vars when `docker run`. 277 | 278 | - **Embedded Ports**: Embedded services use default ports. Expose them when running if you need external access: 279 | ```bash 280 | -p 5432:5432 # PostgreSQL 281 | -p 3306:3306 # MySQL 282 | -p 6379:6379 # Redis 283 | ``` 284 | 285 | **Note**: If you already have these services running on your host, either: 286 | - Use external services (set `DB_URI` or `DB_HOST`, `REDIS_URI`) 287 | - Use different host ports: `-p 5433:5432` (maps host 5433 to container 5432) 288 | 289 | ## 🔧 Build Requirements 290 | 291 | - Docker installed and running 292 | - Logged in to Docker Hub: `docker login` 293 | - Access to `server/` and `app/` directories 294 | - Stable internet connection (for pulling base images) 295 | 296 | ### Troubleshooting Build Issues 297 | 298 | **Network timeout when pulling base images:** 299 | ```bash 300 | # Pre-pull base image to avoid timeout 301 | docker pull node:20-alpine 302 | 303 | # Then retry build 304 | ./build-and-push.sh 305 | ``` 306 | 307 | **If build still fails:** 308 | - Check your internet connection 309 | - Try again later (Docker Hub might be slow) 310 | - The build script has automatic retry (3 attempts) 311 | 312 | 313 | -------------------------------------------------------------------------------- /server/cache-operations.md: -------------------------------------------------------------------------------- 1 | # Cache Operations 2 | 3 | Enfyra provides distributed caching and locking operations through Redis. Use cache operations for performance optimization, rate limiting, and coordinating between instances. 4 | 5 | ## Quick Navigation 6 | 7 | - [Getting Started](#getting-started) - Basic cache operations 8 | - [Cache Storage](#cache-storage) - Getting and setting cached values 9 | - [Distributed Locking](#distributed-locking) - Coordinating operations across instances 10 | - [Common Patterns](#common-patterns) - Real-world usage examples 11 | 12 | ## Getting Started 13 | 14 | All cache operations are available through `$ctx.$cache` and require `await`. 15 | 16 | ```javascript 17 | // All cache functions require await 18 | const value = await $ctx.$cache.get('key'); 19 | await $ctx.$cache.set('key', value, 60000); 20 | ``` 21 | 22 | ## Cache Storage 23 | 24 | ### Get Cached Value 25 | 26 | Retrieve a value from cache. 27 | 28 | ```javascript 29 | const cachedValue = await $ctx.$cache.get(key); 30 | 31 | // Example 32 | const user = await $ctx.$cache.get('user:123'); 33 | if (user) { 34 | // Use cached value 35 | } else { 36 | // Cache miss - fetch from database 37 | } 38 | ``` 39 | 40 | ### Set Cached Value with TTL 41 | 42 | Store a value in cache with expiration time. 43 | 44 | ```javascript 45 | await $ctx.$cache.set(key, value, ttlMs); 46 | 47 | // Example - cache for 5 minutes (300000 milliseconds) 48 | await $ctx.$cache.set('user:123', userData, 300000); 49 | 50 | // Example - cache for 1 hour (3600000 milliseconds) 51 | await $ctx.$cache.set('product:456', productData, 3600000); 52 | ``` 53 | 54 | **TTL (Time-To-Live) is in milliseconds:** 55 | - 1000 = 1 second 56 | - 60000 = 1 minute 57 | - 300000 = 5 minutes 58 | - 3600000 = 1 hour 59 | - 86400000 = 1 day 60 | 61 | ### Set Cached Value Without Expiration 62 | 63 | Store a value in cache that never expires. 64 | 65 | ```javascript 66 | await $ctx.$cache.setNoExpire(key, value); 67 | 68 | // Example 69 | await $ctx.$cache.setNoExpire('config:app', configData); 70 | ``` 71 | 72 | ### Delete Cache Key 73 | 74 | Remove a value from cache. 75 | 76 | ```javascript 77 | await $ctx.$cache.deleteKey(key); 78 | 79 | // Example 80 | await $ctx.$cache.deleteKey('user:123'); 81 | ``` 82 | 83 | ### Check if Key Exists 84 | 85 | Check if a cache key exists with a specific value. 86 | 87 | ```javascript 88 | const exists = await $ctx.$cache.exists(key, value); 89 | 90 | // Example 91 | const lockExists = await $ctx.$cache.exists('user-lock:123', 'user-456'); 92 | ``` 93 | 94 | ## Distributed Locking 95 | 96 | Distributed locking prevents concurrent operations across multiple instances. 97 | 98 | ### Acquire Lock 99 | 100 | Try to acquire a lock. Returns `true` if successful, `false` if lock is already held. 101 | 102 | ```javascript 103 | const lockAcquired = await $ctx.$cache.acquire(key, value, ttlMs); 104 | 105 | // Example 106 | const lockKey = `user-lock:${userId}`; 107 | const lockValue = $ctx.$user.id; 108 | const acquired = await $ctx.$cache.acquire(lockKey, lockValue, 10000); // 10 seconds 109 | 110 | if (acquired) { 111 | // Lock acquired - proceed with critical operation 112 | } else { 113 | // Lock already held by another instance 114 | } 115 | ``` 116 | 117 | ### Release Lock 118 | 119 | Release a lock that you acquired. 120 | 121 | ```javascript 122 | const released = await $ctx.$cache.release(key, value); 123 | 124 | // Example 125 | const lockKey = `user-lock:${userId}`; 126 | const lockValue = $ctx.$user.id; 127 | const released = await $ctx.$cache.release(lockKey, lockValue); 128 | ``` 129 | 130 | **Important:** Only the instance that acquired the lock can release it (verified by value). 131 | 132 | ### Lock Pattern with Try-Finally 133 | 134 | Always release locks in a `finally` block to ensure cleanup even if errors occur. 135 | 136 | ```javascript 137 | const lockKey = `record-lock:${recordId}`; 138 | const lockValue = $ctx.$user.id; 139 | const lockAcquired = await $ctx.$cache.acquire(lockKey, lockValue, 10000); 140 | 141 | if (!lockAcquired) { 142 | $ctx.$throw['409']('Record is currently being modified'); 143 | return; 144 | } 145 | 146 | try { 147 | // Critical operation here 148 | $ctx.$logs(`Acquired lock for record: ${recordId}`); 149 | 150 | // Perform the operation 151 | await $ctx.$repos.records.update({ 152 | id: recordId, 153 | data: updateData 154 | }); 155 | 156 | } finally { 157 | // Always release the lock 158 | await $ctx.$cache.release(lockKey, lockValue); 159 | $ctx.$logs(`Released lock for record: ${recordId}`); 160 | } 161 | ``` 162 | 163 | ## Common Patterns 164 | 165 | ### Pattern 1: Cache-First Data Retrieval 166 | 167 | Check cache first, fallback to database if cache miss. 168 | 169 | ```javascript 170 | const cacheKey = `user-profile:${$ctx.$params.id}`; 171 | let userProfile = await $ctx.$cache.get(cacheKey); 172 | 173 | if (!userProfile) { 174 | // Cache miss - fetch from database 175 | const result = await $ctx.$repos.user_definition.find({ 176 | where: { id: { _eq: $ctx.$params.id } } 177 | }); 178 | 179 | if (result.data.length > 0) { 180 | userProfile = result.data[0]; 181 | // Cache for 5 minutes 182 | await $ctx.$cache.set(cacheKey, userProfile, 300000); 183 | $ctx.$logs(`User profile cached: ${$ctx.$params.id}`); 184 | } 185 | } else { 186 | $ctx.$logs(`User profile served from cache: ${$ctx.$params.id}`); 187 | } 188 | 189 | // Use userProfile... 190 | ``` 191 | 192 | ### Pattern 2: Invalidate Cache on Update 193 | 194 | Clear cache when data is updated. 195 | 196 | ```javascript 197 | // Update record 198 | const result = await $ctx.$repos.products.update({ 199 | id: productId, 200 | data: updateData 201 | }); 202 | 203 | // Invalidate cache 204 | await $ctx.$cache.deleteKey(`product:${productId}`); 205 | $ctx.$logs(`Cache invalidated for product: ${productId}`); 206 | ``` 207 | 208 | ### Pattern 3: Rate Limiting 209 | 210 | Use cache to implement rate limiting. 211 | 212 | ```javascript 213 | // Rate limit: max 10 requests per minute per user 214 | const rateLimitKey = `rate-limit:${$ctx.$user.id}:${$ctx.$req.url}`; 215 | const currentCount = await $ctx.$cache.get(rateLimitKey) || 0; 216 | 217 | if (currentCount >= 10) { 218 | $ctx.$throw['429']('Rate limit exceeded. Please try again later.'); 219 | return; 220 | } 221 | 222 | // Increment counter with 60 second TTL 223 | await $ctx.$cache.set(rateLimitKey, currentCount + 1, 60000); 224 | $ctx.$logs(`Rate limit check passed for user: ${$ctx.$user.id}`); 225 | ``` 226 | 227 | ### Pattern 4: Prevent Concurrent Modifications 228 | 229 | Use distributed locking to prevent concurrent modifications. 230 | 231 | ```javascript 232 | const lockKey = `record-lock:${$ctx.$params.id}`; 233 | const lockValue = $ctx.$user.id; 234 | 235 | const lockAcquired = await $ctx.$cache.acquire(lockKey, lockValue, 10000); 236 | if (!lockAcquired) { 237 | $ctx.$throw['409']('Record is currently being modified by another user'); 238 | return; 239 | } 240 | 241 | try { 242 | // Get current record 243 | const current = await $ctx.$repos.products.find({ 244 | where: { id: { _eq: $ctx.$params.id } } 245 | }); 246 | 247 | if (current.data.length === 0) { 248 | $ctx.$throw['404']('Product not found'); 249 | return; 250 | } 251 | 252 | // Perform update 253 | const result = await $ctx.$repos.products.update({ 254 | id: $ctx.$params.id, 255 | data: updateData 256 | }); 257 | 258 | // Invalidate cache 259 | await $ctx.$cache.deleteKey(`product:${$ctx.$params.id}`); 260 | 261 | } finally { 262 | await $ctx.$cache.release(lockKey, lockValue); 263 | } 264 | ``` 265 | 266 | ### Pattern 5: Cache Configuration Data 267 | 268 | Cache configuration data that doesn't change frequently. 269 | 270 | ```javascript 271 | const configKey = 'app:configuration'; 272 | let config = await $ctx.$cache.get(configKey); 273 | 274 | if (!config) { 275 | // Load from database 276 | const result = await $ctx.$repos.configurations.find({ 277 | where: { isActive: { _eq: true } } 278 | }); 279 | 280 | if (result.data.length > 0) { 281 | config = result.data[0]; 282 | // Cache for 1 hour 283 | await $ctx.$cache.set(configKey, config, 3600000); 284 | } 285 | } 286 | 287 | // Use config... 288 | ``` 289 | 290 | ### Pattern 6: Session Management 291 | 292 | Store session data in cache with expiration. 293 | 294 | ```javascript 295 | // Create session 296 | const sessionId = generateSessionId(); 297 | const sessionData = { 298 | userId: user.id, 299 | email: user.email, 300 | createdAt: new Date() 301 | }; 302 | 303 | // Cache session for 7 days 304 | await $ctx.$cache.set(`session:${sessionId}`, sessionData, 7 * 24 * 60 * 60 * 1000); 305 | 306 | // Later: Retrieve session 307 | const session = await $ctx.$cache.get(`session:${sessionId}`); 308 | if (!session) { 309 | $ctx.$throw['401']('Session expired'); 310 | return; 311 | } 312 | ``` 313 | 314 | ### Pattern 7: Cache Warming 315 | 316 | Pre-populate cache with frequently accessed data. 317 | 318 | ```javascript 319 | // In a background process or bootstrap script 320 | const popularProducts = await $ctx.$repos.products.find({ 321 | where: { isPopular: { _eq: true } }, 322 | limit: 100 323 | }); 324 | 325 | for (const product of popularProducts.data) { 326 | await $ctx.$cache.set( 327 | `product:${product.id}`, 328 | product, 329 | 3600000 // 1 hour 330 | ); 331 | } 332 | ``` 333 | 334 | ## Best Practices 335 | 336 | 1. **Always use await** - All cache functions are async and require await 337 | 2. **Set appropriate TTL** - Choose TTL based on how often data changes 338 | 3. **Use consistent key patterns** - Use patterns like `user:123`, `session:456`, `lock:789` 339 | 4. **Always release locks** - Use try-finally blocks to ensure locks are released 340 | 5. **Invalidate cache on updates** - Delete cache keys when data is updated 341 | 6. **Handle cache misses** - Always have a fallback to database when cache misses 342 | 7. **Use locking for critical operations** - Prevent concurrent modifications with distributed locks 343 | 344 | ## Key Naming Conventions 345 | 346 | Use consistent naming patterns for cache keys: 347 | 348 | ```javascript 349 | // Resource by ID 350 | `user:${userId}` 351 | `product:${productId}` 352 | `order:${orderId}` 353 | 354 | // Locks 355 | `lock:user:${userId}` 356 | `lock:record:${recordId}` 357 | 358 | // Rate limiting 359 | `rate-limit:${userId}:${endpoint}` 360 | 361 | // Sessions 362 | `session:${sessionId}` 363 | 364 | // Configuration 365 | `config:${configName}` 366 | 367 | // Aggregated data 368 | `stats:daily:${date}` 369 | ``` 370 | 371 | ## Next Steps 372 | 373 | - Learn about [Context Reference](./context-reference/) for cache access 374 | - See [Cluster Architecture](./cluster-architecture.md) to understand distributed coordination 375 | - Check [Error Handling](./error-handling.md) for handling cache failures 376 | 377 | -------------------------------------------------------------------------------- /app/permission-components.md: -------------------------------------------------------------------------------- 1 | # Permission Components 2 | 3 | Enfyra provides two main tools for controlling UI visibility based on user permissions: the `PermissionGate` component for declarative rendering and the `usePermissions` composable for programmatic checks. 4 | 5 | ## PermissionGate Component 6 | 7 | The `PermissionGate` component wraps UI elements and automatically shows/hides them based on user permissions. 8 | 9 | ### Basic Usage 10 | 11 | ```vue 12 | 13 |
This content only shows if user can read users
14 |
15 | ``` 16 | 17 | ### Multiple Actions 18 | 19 | Check if user has ANY of the listed actions: 20 | 21 | ```vue 22 | 23 | Edit User 24 | 25 | ``` 26 | 27 | ### Complex Conditions 28 | 29 | #### AND Logic 30 | User must have ALL permissions: 31 | 32 | ```vue 33 | 39 |
User can read both users AND roles
40 |
41 | ``` 42 | 43 | #### OR Logic 44 | User needs ANY of these permissions: 45 | 46 | ```vue 47 | 53 | Modify User 54 | 55 | ``` 56 | 57 | #### Nested Conditions 58 | Combine AND/OR for complex logic: 59 | 60 | ```vue 61 | 72 |
Admin OR (can read AND update users)
73 |
74 | ``` 75 | 76 | ## usePermissions Composable 77 | 78 | The `usePermissions` composable provides programmatic permission checking in your Vue components. 79 | 80 | ### Setup 81 | 82 | ```vue 83 | 86 | ``` 87 | 88 | ### Check Specific Permission 89 | 90 | ```vue 91 | 113 | ``` 114 | 115 | ### Check Complex Conditions 116 | 117 | ```vue 118 | 136 | ``` 137 | 138 | ### HTTP Method Mapping 139 | 140 | The system maps actions to HTTP methods: 141 | 142 | | Action | HTTP Method | Usage | 143 | |--------|-------------|-------| 144 | | `read` | GET | View/list data | 145 | | `create` | POST | Create new records | 146 | | `update` | PATCH | Modify existing records | 147 | | `delete` | DELETE | Remove records | 148 | 149 | ## Integration with Menu System 150 | 151 | The menu system uses both `PermissionGate` and `usePermissions` internally to control menu visibility. 152 | 153 | ### How Menus Use Permissions 154 | 155 | When you set permissions on a menu item: 156 | 157 | ```javascript 158 | // Menu configuration 159 | { 160 | label: 'User Management', 161 | route: '/settings/users', 162 | permission: { 163 | or: [ 164 | { route: '/users', actions: ['read'] }, 165 | { route: '/users', actions: ['create'] } 166 | ] 167 | } 168 | } 169 | ``` 170 | 171 | The menu system: 172 | 1. Uses `checkPermissionCondition` to evaluate the permission 173 | 2. Only renders the menu item if permission check passes 174 | 3. Automatically hides parent menus if no child items are accessible 175 | 176 | ### Menu Components 177 | 178 | **Menu System** uses permissions: 179 | ```vue 180 | // Internally filters menu items based on permissions 181 | const visibleItems = menuGroups.filter(item => { 182 | if (!item.permission) return true; 183 | return checkPermissionCondition(item.permission); 184 | }); 185 | ``` 186 | 187 | **Menu Items** wrapped in PermissionGate: 188 | ```vue 189 | 190 | 191 | 192 | ``` 193 | 194 | **Result**: Menus automatically adapt to user permissions without manual configuration. 195 | 196 | ## Common Patterns 197 | 198 | ### Conditional Buttons 199 | 200 | ```vue 201 | 216 | ``` 217 | 218 | ### Table Actions 219 | 220 | ```vue 221 | 234 | ``` 235 | 236 | ### Form Submission 237 | 238 | ```vue 239 | 263 | ``` 264 | 265 | ### Header Actions 266 | 267 | ```vue 268 | 280 | ``` 281 | 282 | ## Special Cases 283 | 284 | ### Root Admin 285 | 286 | Root admins bypass all permission checks: 287 | 288 | ```vue 289 | 297 | ``` 298 | 299 | ### Allow All 300 | 301 | Grant unrestricted access (use sparingly): 302 | 303 | ```vue 304 | 305 |
Always visible content
306 |
307 | ``` 308 | 309 | ### Direct User Permissions 310 | 311 | Users can have permissions that bypass their role: 312 | 313 | ```javascript 314 | // User's direct permissions override role permissions 315 | // Checked automatically by usePermissions 316 | ``` 317 | 318 | ## Best Practices 319 | 320 | ### Use PermissionGate for UI 321 | - Wrap buttons, menu items, sections 322 | - Keeps templates clean and declarative 323 | - Automatically handles permission changes 324 | 325 | ### Use usePermissions for Logic 326 | - Business logic and validation 327 | - Computed properties for complex checks 328 | - API calls and data processing 329 | 330 | ### Cache Permission Checks 331 | ```vue 332 | 339 | ``` 340 | 341 | ### Match API Routes 342 | Always use actual API endpoint paths: 343 | ```javascript 344 | // Good - matches API endpoint 345 | { route: '/user_definition', actions: ['read'] } 346 | 347 | // Bad - doesn't match actual route 348 | { route: '/users', actions: ['read'] } 349 | ``` 350 | 351 | ## Debugging 352 | 353 | ### Check Current Permissions 354 | 355 | ```vue 356 | 369 | ``` 370 | 371 | ### Test Permission Conditions 372 | 373 | ```vue 374 | 385 | ``` 386 | 387 | ## Related Documentation 388 | 389 | - **[Permission Builder](./permission-builder.md)** - Backend permission architecture 390 | - **[Permission Builder](./permission-builder.md)** - Visual permission configuration 391 | - **[Menu Management](./menu-management.md)** - How menus use permissions 392 | - **[Form System](./form-system.md)** - Permission integration in forms -------------------------------------------------------------------------------- /server/error-handling.md: -------------------------------------------------------------------------------- 1 | # Error Handling 2 | 3 | Enfyra provides built-in error handling mechanisms for throwing HTTP errors and handling exceptions in hooks and handlers. 4 | 5 | ## Quick Navigation 6 | 7 | - [Throwing Errors](#throwing-errors) - How to throw HTTP errors 8 | - [HTTP Status Codes](#http-status-codes) - Available status codes 9 | - [Error Handling in afterHook](#error-handling-in-afterhook) - Handling errors that occurred 10 | - [Common Patterns](#common-patterns) - Real-world error handling examples 11 | 12 | ## Throwing Errors 13 | 14 | Use `$ctx.$throw` to throw HTTP errors with appropriate status codes. 15 | 16 | ### Basic Syntax 17 | 18 | ```javascript 19 | $ctx.$throw['STATUS_CODE']('Error message'); 20 | ``` 21 | 22 | ### Common HTTP Status Codes 23 | 24 | ```javascript 25 | // Bad Request (400) 26 | $ctx.$throw['400']('Invalid input data'); 27 | 28 | // Unauthorized (401) 29 | $ctx.$throw['401']('Authentication required'); 30 | 31 | // Forbidden (403) 32 | $ctx.$throw['403']('Insufficient permissions'); 33 | 34 | // Not Found (404) 35 | $ctx.$throw['404']('Resource not found'); 36 | 37 | // Conflict (409) 38 | $ctx.$throw['409']('Email already exists'); 39 | 40 | // Unprocessable Entity (422) 41 | $ctx.$throw['422']('Validation failed'); 42 | 43 | // Internal Server Error (500) 44 | $ctx.$throw['500']('Internal server error'); 45 | ``` 46 | 47 | ## HTTP Status Codes 48 | 49 | ### 400 Bad Request 50 | 51 | Use for invalid input data or malformed requests. 52 | 53 | ```javascript 54 | if (!$ctx.$body.email) { 55 | $ctx.$throw['400']('Email is required'); 56 | return; 57 | } 58 | 59 | if (!isValidEmail($ctx.$body.email)) { 60 | $ctx.$throw['400']('Invalid email format'); 61 | return; 62 | } 63 | ``` 64 | 65 | ### 401 Unauthorized 66 | 67 | Use when authentication is required but not provided. 68 | 69 | ```javascript 70 | if (!$ctx.$user) { 71 | $ctx.$throw['401']('Authentication required'); 72 | return; 73 | } 74 | 75 | if (!isValidToken($ctx.$req.headers.authorization)) { 76 | $ctx.$throw['401']('Invalid or expired token'); 77 | return; 78 | } 79 | ``` 80 | 81 | ### 403 Forbidden 82 | 83 | Use when user is authenticated but doesn't have permission. 84 | 85 | ```javascript 86 | if ($ctx.$user.role !== 'admin') { 87 | $ctx.$throw['403']('Admin access required'); 88 | return; 89 | } 90 | 91 | // Check resource ownership 92 | const resource = await $ctx.$repos.resources.find({ 93 | where: { id: { _eq: $ctx.$params.id } } 94 | }); 95 | 96 | if (resource.data[0].userId !== $ctx.$user.id) { 97 | $ctx.$throw['403']('Access denied'); 98 | return; 99 | } 100 | ``` 101 | 102 | ### 404 Not Found 103 | 104 | Use when requested resource doesn't exist. 105 | 106 | ```javascript 107 | const product = await $ctx.$repos.products.find({ 108 | where: { id: { _eq: $ctx.$params.id } } 109 | }); 110 | 111 | if (product.data.length === 0) { 112 | $ctx.$throw['404']('Product not found'); 113 | return; 114 | } 115 | ``` 116 | 117 | ### 409 Conflict 118 | 119 | Use when there's a conflict with current state (e.g., duplicate entry). 120 | 121 | ```javascript 122 | // Check if email already exists 123 | const existing = await $ctx.$repos.user_definition.find({ 124 | where: { email: { _eq: $ctx.$body.email } } 125 | }); 126 | 127 | if (existing.data.length > 0) { 128 | $ctx.$throw['409']('Email already exists'); 129 | return; 130 | } 131 | ``` 132 | 133 | ### 422 Unprocessable Entity 134 | 135 | Use for validation errors when data format is correct but business rules fail. 136 | 137 | ```javascript 138 | if ($ctx.$body.password && $ctx.$body.password.length < 6) { 139 | $ctx.$throw['422']('Password must be at least 6 characters'); 140 | return; 141 | } 142 | 143 | if ($ctx.$body.age && ($ctx.$body.age < 0 || $ctx.$body.age > 120)) { 144 | $ctx.$throw['422']('Age must be between 0 and 120'); 145 | return; 146 | } 147 | ``` 148 | 149 | ### 500 Internal Server Error 150 | 151 | Use for unexpected server errors. 152 | 153 | ```javascript 154 | try { 155 | // Some operation 156 | } catch (error) { 157 | $ctx.$logs(`Unexpected error: ${error.message}`); 158 | $ctx.$throw['500']('Internal server error'); 159 | return; 160 | } 161 | ``` 162 | 163 | ## Error Handling in afterHook 164 | 165 | In afterHook, you can check if an error occurred during the request and handle it appropriately. 166 | 167 | ### Checking for Errors 168 | 169 | ```javascript 170 | // In afterHook 171 | if ($ctx.$api.error) { 172 | // Error occurred 173 | // $ctx.$api.error contains error details 174 | // $ctx.$data will be null 175 | // $ctx.$statusCode will be the error status code 176 | } else { 177 | // Success 178 | // $ctx.$data contains response data 179 | // $ctx.$statusCode contains success status (200, 201, etc.) 180 | } 181 | ``` 182 | 183 | **Important:** `$ctx.$api.error` is only available in afterHook, not in preHook. 184 | 185 | ### Error Object Properties 186 | 187 | ```javascript 188 | if ($ctx.$api.error) { 189 | const message = $ctx.$api.error.message; // Error message 190 | const stack = $ctx.$api.error.stack; // Stack trace 191 | const name = $ctx.$api.error.name; // Error class name 192 | const statusCode = $ctx.$api.error.statusCode; // HTTP error status 193 | const timestamp = $ctx.$api.error.timestamp; // Error timestamp 194 | const details = $ctx.$api.error.details; // Additional details 195 | } 196 | ``` 197 | 198 | ### Logging Errors 199 | 200 | ```javascript 201 | // In afterHook 202 | if ($ctx.$api.error) { 203 | $ctx.$logs(`Error occurred: ${$ctx.$api.error.message}`); 204 | $ctx.$logs(`Error status: ${$ctx.$api.error.statusCode}`); 205 | $ctx.$logs(`Error stack: ${$ctx.$api.error.stack}`); 206 | } 207 | ``` 208 | 209 | ### Creating Error Logs 210 | 211 | ```javascript 212 | // In afterHook 213 | if ($ctx.$api.error) { 214 | // Log to audit system 215 | await $ctx.$repos.error_logs.create({ 216 | data: { 217 | errorMessage: $ctx.$api.error.message, 218 | statusCode: $ctx.$api.error.statusCode, 219 | userId: $ctx.$user?.id, 220 | url: $ctx.$req.url, 221 | method: $ctx.$req.method, 222 | timestamp: new Date() 223 | } 224 | }); 225 | } 226 | ``` 227 | 228 | ## Common Patterns 229 | 230 | ### Pattern 1: Validate Required Fields 231 | 232 | ```javascript 233 | // In preHook 234 | const requiredFields = ['email', 'password', 'name']; 235 | 236 | for (const field of requiredFields) { 237 | if (!$ctx.$body[field]) { 238 | $ctx.$throw['400'](`${field} is required`); 239 | return; 240 | } 241 | } 242 | ``` 243 | 244 | ### Pattern 2: Check Resource Existence 245 | 246 | ```javascript 247 | // In preHook or handler 248 | const resource = await $ctx.$repos.products.find({ 249 | where: { id: { _eq: $ctx.$params.id } } 250 | }); 251 | 252 | if (resource.data.length === 0) { 253 | $ctx.$throw['404']('Product not found'); 254 | return; 255 | } 256 | 257 | // Use resource.data[0] safely 258 | const product = resource.data[0]; 259 | ``` 260 | 261 | ### Pattern 3: Validate Business Rules 262 | 263 | ```javascript 264 | // In preHook 265 | if ($ctx.$body.email) { 266 | // Check email format 267 | const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; 268 | if (!emailRegex.test($ctx.$body.email)) { 269 | $ctx.$throw['422']('Invalid email format'); 270 | return; 271 | } 272 | 273 | // Check if email already exists 274 | const existing = await $ctx.$repos.user_definition.find({ 275 | where: { email: { _eq: $ctx.$body.email } } 276 | }); 277 | 278 | if (existing.data.length > 0) { 279 | $ctx.$throw['409']('Email already exists'); 280 | return; 281 | } 282 | } 283 | ``` 284 | 285 | ### Pattern 4: Permission Checking 286 | 287 | ```javascript 288 | // In preHook 289 | if (!$ctx.$user) { 290 | $ctx.$throw['401']('Authentication required'); 291 | return; 292 | } 293 | 294 | if ($ctx.$user.role !== 'admin') { 295 | $ctx.$throw['403']('Admin access required'); 296 | return; 297 | } 298 | 299 | // Check resource ownership 300 | const resource = await $ctx.$repos.resources.find({ 301 | where: { id: { _eq: $ctx.$params.id } } 302 | }); 303 | 304 | if (resource.data.length === 0) { 305 | $ctx.$throw['404']('Resource not found'); 306 | return; 307 | } 308 | 309 | if (resource.data[0].userId !== $ctx.$user.id && $ctx.$user.role !== 'admin') { 310 | $ctx.$throw['403']('Access denied'); 311 | return; 312 | } 313 | ``` 314 | 315 | ### Pattern 5: Error Recovery in afterHook 316 | 317 | ```javascript 318 | // In afterHook 319 | if ($ctx.$api.error) { 320 | // Log error 321 | $ctx.$logs(`Error: ${$ctx.$api.error.message}`); 322 | 323 | // Log to audit system 324 | await $ctx.$repos.error_logs.create({ 325 | data: { 326 | errorMessage: $ctx.$api.error.message, 327 | statusCode: $ctx.$api.error.statusCode, 328 | userId: $ctx.$user?.id, 329 | url: $ctx.$req.url, 330 | timestamp: new Date() 331 | } 332 | }); 333 | 334 | // Optionally send notification 335 | // await sendErrorNotification($ctx.$api.error); 336 | } else { 337 | // Success - log audit trail 338 | await $ctx.$repos.audit_logs.create({ 339 | data: { 340 | action: `${$ctx.$req.method} ${$ctx.$req.url}`, 341 | userId: $ctx.$user?.id, 342 | statusCode: $ctx.$statusCode, 343 | timestamp: new Date() 344 | } 345 | }); 346 | } 347 | ``` 348 | 349 | ### Pattern 6: Try-Catch for External Operations 350 | 351 | ```javascript 352 | // In handler 353 | try { 354 | // External API call or complex operation 355 | const result = await externalApiCall($ctx.$body); 356 | return result; 357 | } catch (error) { 358 | $ctx.$logs(`External API error: ${error.message}`); 359 | $ctx.$throw['500']('Failed to process request'); 360 | return; 361 | } 362 | ``` 363 | 364 | ### Pattern 7: Validate Data Types 365 | 366 | ```javascript 367 | // In preHook 368 | if ($ctx.$body.price !== undefined) { 369 | if (typeof $ctx.$body.price !== 'number') { 370 | $ctx.$throw['400']('Price must be a number'); 371 | return; 372 | } 373 | 374 | if ($ctx.$body.price < 0) { 375 | $ctx.$throw['422']('Price cannot be negative'); 376 | return; 377 | } 378 | } 379 | ``` 380 | 381 | ## Best Practices 382 | 383 | 1. **Throw errors early** - Validate and throw errors as soon as possible (in preHooks) 384 | 2. **Use appropriate status codes** - Choose the right HTTP status code for each error type 385 | 3. **Provide clear error messages** - Help users understand what went wrong 386 | 4. **Log errors** - Use `$ctx.$logs()` to log errors for debugging 387 | 5. **Handle errors gracefully** - Use afterHook to handle errors that occurred in handlers 388 | 6. **Don't expose sensitive information** - Don't include internal details in error messages 389 | 7. **Return early** - Use `return` after throwing errors to stop execution 390 | 391 | ## Next Steps 392 | 393 | - Learn about [Context Reference](./context-reference/) for error-related properties 394 | - See [API Lifecycle](./api-lifecycle.md) to understand when errors occur 395 | - Check [Hooks and Handlers](./hooks-handlers/) for error handling in different phases 396 | 397 | -------------------------------------------------------------------------------- /app/hooks-handlers/package-management.md: -------------------------------------------------------------------------------- 1 | # Package Management 2 | 3 | Package Management lets you install NPM packages directly from the Enfyra interface and use them in your custom handlers and hooks. Instead of managing dependencies manually, you can search, install, and configure packages through the UI - they're automatically available in your custom code as `$ctx.$pkgs.packagename`. 4 | 5 | **Related Documentation**: [Custom Handlers](./custom-handlers.md) | [Hooks and Handlers](../../server/hooks-handlers/) | [User Registration Example](../../examples/user-registration-example.md) 6 | 7 | ## Why Use Package Management? 8 | 9 | - **Extend Functionality**: Add powerful libraries like axios, lodash, moment.js to your handlers 10 | - **No Configuration**: Packages are instantly available in your custom code without setup 11 | - **Visual Interface**: Search and install packages without touching package.json 12 | - **Isolated Environment**: Packages run safely in your custom handler execution context 13 | 14 | ## Installing a Package 15 | 16 | ### Step 1: Access Package Installation 17 | 1. Navigate to **Settings Packages** in the sidebar 18 | 2. Click **Install Package** button in the header 19 | 20 | ### Step 2: Choose Package Type 21 | You'll see two package type options: 22 | - **Backend Package**: For use in custom handlers and hooks 23 | - **App Package**: Coming soon in a future update 24 | 25 | Select **Backend Package** to proceed. 26 | 27 | ### Step 3: Search NPM Packages 28 | Start typing in the search field to find packages: 29 | - **Real-time search** with auto-suggestions as you type 30 | - **Rich package information** including descriptions and version numbers 31 | - **Version badges** showing the latest available version 32 | - **Package icons** for visual identification 33 | 34 | **Popular package examples:** 35 | ``` 36 | axios # HTTP requests and API calls 37 | lodash # Utility functions for data manipulation 38 | moment # Date and time handling 39 | uuid # Generate unique identifiers 40 | joi # Data validation schemas 41 | ``` 42 | 43 | ### Step 4: Select and Configure 44 | 1. **Click on a package** from the dropdown results 45 | 2. **Form auto-fills** with package name, version, and description 46 | 3. **Customize details** if needed (description, flags, etc.) 47 | 4. **Click Install** to add the package to your project 48 | 49 | **Important**: The package name field is automatically populated and cannot be edited - this ensures correct package resolution. 50 | 51 | ## Using Installed Packages 52 | 53 | Once installed, packages are automatically available in your custom handlers and hooks through the `$ctx.$pkgs` object. No imports needed - just access them directly. 54 | 55 | ### Basic Usage Pattern 56 | ```javascript 57 | // In any custom handler or hook 58 | export default async function handler({ $ctx }) { 59 | // Access installed packages via $ctx.$pkgs 60 | const packageName = $ctx.$pkgs.packagename; 61 | 62 | // Use the package normally 63 | return packageName.someMethod(); 64 | } 65 | ``` 66 | 67 | ### Real-World Examples 68 | 69 | **HTTP Requests with Axios** 70 | ```javascript 71 | // Custom handler for external API integration 72 | export default async function handler({ $ctx }) { 73 | const axios = $ctx.$pkgs.axios; 74 | 75 | try { 76 | const response = await axios.get('https://jsonplaceholder.typicode.com/users'); 77 | return { 78 | success: true, 79 | users: response.data 80 | }; 81 | } catch (error) { 82 | return { 83 | success: false, 84 | error: error.message 85 | }; 86 | } 87 | } 88 | ``` 89 | 90 | **Data Processing with Lodash** 91 | ```javascript 92 | // Handler for complex data transformation 93 | export default async function handler({ $ctx }) { 94 | const _ = $ctx.$pkgs.lodash; 95 | 96 | // Simulate processing user data 97 | const users = $ctx.$body.users || []; 98 | 99 | // Group users by department and get counts 100 | const grouped = _.groupBy(users, 'department'); 101 | const summary = _.mapValues(grouped, group => ({ 102 | count: group.length, 103 | active: _.filter(group, 'isActive').length 104 | })); 105 | 106 | return { 107 | departmentSummary: summary, 108 | totalUsers: users.length 109 | }; 110 | } 111 | ``` 112 | 113 | **Date Operations with Moment** 114 | ```javascript 115 | // Handler for date-based operations 116 | export default async function handler({ $ctx }) { 117 | const moment = $ctx.$pkgs.moment; 118 | 119 | const startDate = moment($ctx.$query.startDate); 120 | const endDate = moment($ctx.$query.endDate); 121 | 122 | return { 123 | period: { 124 | start: startDate.format('YYYY-MM-DD'), 125 | end: endDate.format('YYYY-MM-DD'), 126 | days: endDate.diff(startDate, 'days') 127 | }, 128 | timestamps: { 129 | created: moment().unix(), 130 | formatted: moment().format('YYYY-MM-DD HH:mm:ss') 131 | } 132 | }; 133 | } 134 | ``` 135 | 136 | ## Managing Installed Packages 137 | 138 | ### Viewing All Packages 139 | 1. Navigate to **Settings Packages** in the sidebar 140 | 2. Click **Backend** to view all installed backend packages 141 | 142 | **Package card information:** 143 | - **Package name** with npm icon 144 | - **Current version** badge 145 | - **Description** and installation date 146 | - **Quick actions** (view details, uninstall) 147 | 148 | ### Updating Package Information 149 | 1. **Click on any package card** to open the detail view 150 | 2. **Modify information** like description or flags 151 | 3. **Click Save** to update the package metadata 152 | 153 | > **Note**: Package name is locked after installation to prevent breaking existing code references. 154 | 155 | ### Updating Package Version 156 | To update a package to a specific version: 157 | 158 | 1. **Navigate to the package detail page** 159 | 2. **Edit the version field** in the form to specify the desired version 160 | 3. **Click Save** to install the specified version 161 | 4. **Verify the update** - the version badge will show the new version number 162 | 163 | **Example version update flow:** 164 | ``` 165 | Current: lodash v4.17.20 166 | ↓ Edit version field to "4.17.21" 167 | ↓ Click Save 168 | Updated: lodash v4.17.21 169 | ``` 170 | 171 | **Version Update Notes:** 172 | - **Manual specification**: You must enter the exact version number you want 173 | - **Version validation**: Ensure the version exists on NPM before updating 174 | - **Backward compatibility**: Test your handlers after version updates, especially major version changes 175 | - **Check NPM**: Visit [npmjs.com/package/packagename](https://npmjs.com) to see available versions 176 | 177 | ### Removing Packages 178 | 1. **Open package details** by clicking on the package card 179 | 2. **Click Uninstall** button in the header (red trash icon) 180 | 3. **Confirm removal** when prompted 181 | 182 | **Warning**: Uninstalling removes the package from all handlers and hooks immediately. Make sure no active code depends on it first. 183 | 184 | ## Common Package Use Cases 185 | 186 | ### Essential Packages for Most Projects 187 | 188 | **HTTP & API Integration** 189 | ```javascript 190 | // axios - HTTP requests 191 | $ctx.$pkgs.axios.get('https://api.example.com/data') 192 | 193 | // node-fetch - Alternative HTTP client 194 | $ctx.$pkgs.fetch('https://api.example.com') 195 | ``` 196 | 197 | **Data Manipulation** 198 | ```javascript 199 | // lodash - Comprehensive utilities 200 | $ctx.$pkgs.lodash.groupBy(data, 'category') 201 | 202 | // ramda - Functional programming 203 | $ctx.$pkgs.ramda.filter(R.propEq('active', true)) 204 | ``` 205 | 206 | **Date & Time Processing** 207 | ```javascript 208 | // moment - Full-featured date library 209 | $ctx.$pkgs.moment().format('YYYY-MM-DD') 210 | 211 | // dayjs - Lightweight alternative 212 | $ctx.$pkgs.dayjs().add(1, 'month') 213 | ``` 214 | 215 | **Validation & Security** 216 | ```javascript 217 | // joi - Schema validation 218 | $ctx.$pkgs.joi.object({ email: joi.string().email() }) 219 | 220 | // bcrypt - Password hashing 221 | $ctx.$pkgs.bcrypt.hash(password, 10) 222 | ``` 223 | 224 | **Utilities** 225 | ```javascript 226 | // uuid - Generate unique IDs 227 | $ctx.$pkgs.uuid.v4() 228 | 229 | // slugify - URL-friendly strings 230 | $ctx.$pkgs.slugify('Hello World', { lower: true }) 231 | ``` 232 | 233 | ## Troubleshooting 234 | 235 | ### Package Not Found 236 | ```javascript 237 | // Wrong - package not installed 238 | const axios = $ctx.$pkgs.axios; // undefined 239 | 240 | // Correct - install package first via UI 241 | // Then access as shown above 242 | ``` 243 | 244 | **Solutions:** 245 | 1. Verify package is installed in **Settings Packages Backend** 246 | 2. Check package name spelling (case-sensitive) 247 | 3. Ensure you're using `$ctx.$pkgs.exactname` syntax 248 | 249 | ### Package Search Issues 250 | - **Type at least 2 characters** before search activates 251 | - **Try different keywords** if no results appear 252 | - **Check internet connection** for external NPM queries 253 | 254 | ### Installation Problems 255 | - **Verify package exists** on [npmjs.com](https://npmjs.com) 256 | - **Try clearing browser cache** and refreshing 257 | - **Check package name spelling** in search 258 | 259 | ### Version Update Issues 260 | - **Check package compatibility** before updating major versions 261 | - **Test handlers/hooks** after version updates to ensure compatibility 262 | - **Revert if needed** by uninstalling and reinstalling the previous version 263 | 264 | ## Package Recommendations 265 | 266 | **Starter Pack** - Essential packages for most projects: 267 | - `axios` - HTTP requests and API integration 268 | - `lodash` - Data manipulation and utilities 269 | - `moment` - Date/time operations 270 | - `uuid` - Generate unique identifiers 271 | - `joi` - Input validation 272 | 273 | **Data Processing Pack**: 274 | - `csv-parser` - Parse CSV files 275 | - `fast-xml-parser` - XML parsing 276 | - `jsonwebtoken` - JWT token handling 277 | - `bcrypt` - Password encryption 278 | 279 | **Integration Pack**: 280 | - `nodemailer` - Send emails 281 | - `twilio` - SMS and communication 282 | - `stripe` - Payment processing 283 | - `aws-sdk` - Amazon Web Services 284 | 285 | **Start with the Starter Pack packages** - they cover 80% of common backend needs and work well together. 286 | 287 | ## Best Practices 288 | 289 | ### Package Management 290 | - **Check for updates**: Regularly check NPM for newer versions of your installed packages 291 | - **Manual version control**: Specify exact versions when updating to maintain consistency 292 | - **Test after updates**: Always test your handlers and hooks after version updates 293 | - **Clean unused packages**: Remove packages that are no longer used to keep the system lean 294 | 295 | ### Version Strategy 296 | - **Controlled updates**: Manually specify versions rather than auto-updating to avoid breaking changes 297 | - **Research versions**: Check package release notes and changelogs before updating 298 | - **Test in staging**: For production systems, test version updates in a staging environment first 299 | - **Pin important versions**: Keep stable versions for critical functionality 300 | - **Document dependencies**: Keep track of which handlers/hooks use which packages and versions 301 | 302 | -------------------------------------------------------------------------------- /server/query-filtering.md: -------------------------------------------------------------------------------- 1 | # Query Filtering 2 | 3 | Enfyra provides MongoDB-like filtering operators for querying data. Use these operators in the `where` parameter of repository `find()` calls or in URL query strings. 4 | 5 | ## Quick Navigation 6 | 7 | - [Comparison Operators](#comparison-operators) - `_eq`, `_gt`, `_lt`, etc. 8 | - [Array Operators](#array-operators) - `_in`, `_not_in` 9 | - [Text Search](#text-search) - `_contains`, `_starts_with`, `_ends_with` 10 | - [Null Checks](#null-checks) - `_is_null`, `_is_not_null` 11 | - [Logical Operators](#logical-operators) - `_and`, `_or`, `_not` 12 | - [Range Operators](#range-operators) - `_between` 13 | - [Complex Examples](#complex-examples) - Real-world patterns 14 | 15 | ## Comparison Operators 16 | 17 | ### _eq (Equal) 18 | 19 | Match exact value. 20 | 21 | ```javascript 22 | // Find products with price = 100 23 | const result = await $ctx.$repos.products.find({ 24 | where: { price: { _eq: 100 } } 25 | }); 26 | 27 | // URL: /products?filter={"price":{"_eq":100}} 28 | ``` 29 | 30 | ### _neq (Not Equal) 31 | 32 | Exclude matching value. 33 | 34 | ```javascript 35 | // Find products where price != 0 36 | const result = await $ctx.$repos.products.find({ 37 | where: { price: { _neq: 0 } } 38 | }); 39 | ``` 40 | 41 | ### _gt (Greater Than) 42 | 43 | Match values greater than. 44 | 45 | ```javascript 46 | // Find products with price > 100 47 | const result = await $ctx.$repos.products.find({ 48 | where: { price: { _gt: 100 } } 49 | }); 50 | ``` 51 | 52 | ### _gte (Greater Than or Equal) 53 | 54 | Match values greater than or equal to. 55 | 56 | ```javascript 57 | // Find products with price >= 100 58 | const result = await $ctx.$repos.products.find({ 59 | where: { price: { _gte: 100 } } 60 | }); 61 | ``` 62 | 63 | ### _lt (Less Than) 64 | 65 | Match values less than. 66 | 67 | ```javascript 68 | // Find products with price < 500 69 | const result = await $ctx.$repos.products.find({ 70 | where: { price: { _lt: 500 } } 71 | }); 72 | ``` 73 | 74 | ### _lte (Less Than or Equal) 75 | 76 | Match values less than or equal to. 77 | 78 | ```javascript 79 | // Find products with price <= 500 80 | const result = await $ctx.$repos.products.find({ 81 | where: { price: { _lte: 500 } } 82 | }); 83 | ``` 84 | 85 | ## Array Operators 86 | 87 | ### _in (In Array) 88 | 89 | Match any value in the array. 90 | 91 | ```javascript 92 | // Find products in specific categories 93 | const result = await $ctx.$repos.products.find({ 94 | where: { 95 | category: { _in: ['electronics', 'gadgets', 'phones'] } 96 | } 97 | }); 98 | 99 | // URL: /products?filter={"category":{"_in":["electronics","gadgets"]}} 100 | ``` 101 | 102 | ### _not_in (Not In Array) 103 | 104 | Exclude values in the array. 105 | 106 | ```javascript 107 | // Find products not in these categories 108 | const result = await $ctx.$repos.products.find({ 109 | where: { 110 | category: { _not_in: ['discontinued', 'old'] } 111 | } 112 | }); 113 | ``` 114 | 115 | ## Text Search 116 | 117 | Text search operators are case-insensitive. 118 | 119 | ### _contains (Contains Text) 120 | 121 | Match fields containing the text. 122 | 123 | ```javascript 124 | // Find products with name containing "phone" 125 | const result = await $ctx.$repos.products.find({ 126 | where: { 127 | name: { _contains: 'phone' } 128 | } 129 | }); 130 | 131 | // Matches: "iPhone", "Phone Case", "Smartphone" 132 | ``` 133 | 134 | ### _starts_with (Starts With Text) 135 | 136 | Match fields starting with the text. 137 | 138 | ```javascript 139 | // Find products starting with "Apple" 140 | const result = await $ctx.$repos.products.find({ 141 | where: { 142 | name: { _starts_with: 'Apple' } 143 | } 144 | }); 145 | 146 | // Matches: "Apple iPhone", "Apple Watch" 147 | // Doesn't match: "iPhone", "My Apple Product" 148 | ``` 149 | 150 | ### _ends_with (Ends With Text) 151 | 152 | Match fields ending with the text. 153 | 154 | ```javascript 155 | // Find products ending with "Pro" 156 | const result = await $ctx.$repos.products.find({ 157 | where: { 158 | name: { _ends_with: 'Pro' } 159 | } 160 | }); 161 | 162 | // Matches: "iPhone Pro", "MacBook Pro" 163 | // Doesn't match: "Pro iPhone", "Professional" 164 | ``` 165 | 166 | ## Null Checks 167 | 168 | ### _is_null (Field Is Null) 169 | 170 | Match fields that are null. 171 | 172 | ```javascript 173 | // Find products without description 174 | const result = await $ctx.$repos.products.find({ 175 | where: { 176 | description: { _is_null: true } 177 | } 178 | }); 179 | ``` 180 | 181 | ### _is_not_null (Field Is Not Null) 182 | 183 | Match fields that are not null. 184 | 185 | ```javascript 186 | // Find products with description 187 | const result = await $ctx.$repos.products.find({ 188 | where: { 189 | description: { _is_not_null: true } 190 | } 191 | }); 192 | ``` 193 | 194 | ## Range Operators 195 | 196 | ### _between (Between Values) 197 | 198 | Match values between two numbers (inclusive). 199 | 200 | ```javascript 201 | // Find products with price between 100 and 500 202 | const result = await $ctx.$repos.products.find({ 203 | where: { 204 | price: { _between: [100, 500] } 205 | } 206 | }); 207 | 208 | // Equivalent to: price >= 100 AND price <= 500 209 | ``` 210 | 211 | ## Logical Operators 212 | 213 | ### _and (All Conditions Must Match) 214 | 215 | Combine multiple conditions with AND logic. 216 | 217 | ```javascript 218 | // Find active products in electronics category with price >= 100 219 | const result = await $ctx.$repos.products.find({ 220 | where: { 221 | _and: [ 222 | { category: { _eq: 'electronics' } }, 223 | { price: { _gte: 100 } }, 224 | { isActive: { _eq: true } } 225 | ] 226 | } 227 | }); 228 | ``` 229 | 230 | ### _or (At Least One Condition Must Match) 231 | 232 | Combine multiple conditions with OR logic. 233 | 234 | ```javascript 235 | // Find products that are active OR on sale 236 | const result = await $ctx.$repos.products.find({ 237 | where: { 238 | _or: [ 239 | { isActive: { _eq: true } }, 240 | { isOnSale: { _eq: true } } 241 | ] 242 | } 243 | }); 244 | ``` 245 | 246 | ### _not (Negate Condition) 247 | 248 | Negate a condition. 249 | 250 | ```javascript 251 | // Find products that are not discontinued 252 | const result = await $ctx.$repos.products.find({ 253 | where: { 254 | _not: { 255 | status: { _eq: 'discontinued' } 256 | } 257 | } 258 | }); 259 | ``` 260 | 261 | ### Complex Logical Combinations 262 | 263 | Combine logical operators for complex queries. 264 | 265 | ```javascript 266 | // Find active products in electronics OR gadgets, with price between 100-500 267 | const result = await $ctx.$repos.products.find({ 268 | where: { 269 | _and: [ 270 | { 271 | _or: [ 272 | { category: { _eq: 'electronics' } }, 273 | { category: { _eq: 'gadgets' } } 274 | ] 275 | }, 276 | { price: { _between: [100, 500] } }, 277 | { isActive: { _eq: true } } 278 | ] 279 | } 280 | }); 281 | ``` 282 | 283 | ## Complex Examples 284 | 285 | ### Example 1: Multiple Conditions 286 | 287 | ```javascript 288 | // Find active products in electronics or gadgets category 289 | // with price between 100-500 and name containing "phone" 290 | const result = await $ctx.$repos.products.find({ 291 | where: { 292 | _and: [ 293 | { 294 | _or: [ 295 | { category: { _eq: 'electronics' } }, 296 | { category: { _eq: 'gadgets' } } 297 | ] 298 | }, 299 | { price: { _between: [100, 500] } }, 300 | { name: { _contains: 'phone' } }, 301 | { isActive: { _eq: true } } 302 | ] 303 | } 304 | }); 305 | ``` 306 | 307 | ### Example 2: Exclude Specific Values 308 | 309 | ```javascript 310 | // Find products not in discontinued or old categories 311 | // with description and price >= 50 312 | const result = await $ctx.$repos.products.find({ 313 | where: { 314 | _and: [ 315 | { 316 | category: { _not_in: ['discontinued', 'old'] } 317 | }, 318 | { description: { _is_not_null: true } }, 319 | { price: { _gte: 50 } } 320 | ] 321 | } 322 | }); 323 | ``` 324 | 325 | ### Example 3: Text Search with Other Conditions 326 | 327 | ```javascript 328 | // Find products with name starting with "Apple" 329 | // price >= 200, and not discontinued 330 | const result = await $ctx.$repos.products.find({ 331 | where: { 332 | _and: [ 333 | { name: { _starts_with: 'Apple' } }, 334 | { price: { _gte: 200 } }, 335 | { 336 | _not: { 337 | status: { _eq: 'discontinued' } 338 | } 339 | } 340 | ] 341 | } 342 | }); 343 | ``` 344 | 345 | ### Example 4: Date Range Queries 346 | 347 | ```javascript 348 | // Find orders created between two dates 349 | const startDate = new Date('2024-01-01'); 350 | const endDate = new Date('2024-12-31'); 351 | 352 | const result = await $ctx.$repos.orders.find({ 353 | where: { 354 | createdAt: { _between: [startDate, endDate] } 355 | } 356 | }); 357 | ``` 358 | 359 | ## Filtering by Relations 360 | 361 | Filter records based on related table data. 362 | 363 | ```javascript 364 | // Find products where category name is "Electronics" 365 | const result = await $ctx.$repos.products.find({ 366 | where: { 367 | category: { 368 | name: { _eq: 'Electronics' } 369 | } 370 | } 371 | }); 372 | 373 | // Find orders where customer email contains "@example.com" 374 | const result = await $ctx.$repos.orders.find({ 375 | where: { 376 | customer: { 377 | email: { _contains: '@example.com' } 378 | } 379 | } 380 | }); 381 | ``` 382 | 383 | ## Using Filters in URL Queries 384 | 385 | Filters can be used in URL query strings for REST API calls. 386 | 387 | ```http 388 | # Simple filter 389 | GET /products?filter={"category":{"_eq":"electronics"}} 390 | 391 | # Multiple conditions 392 | GET /products?filter={"price":{"_gte":100},"isActive":{"_eq":true}} 393 | 394 | # Complex filter with logical operators 395 | GET /products?filter={"_and":[{"category":{"_eq":"electronics"}},{"price":{"_between":[100,500]}}]} 396 | 397 | # Text search 398 | GET /products?filter={"name":{"_contains":"phone"}} 399 | ``` 400 | 401 | ## Combining with Sorting and Pagination 402 | 403 | Combine filters with sorting and pagination. 404 | 405 | ```javascript 406 | const result = await $ctx.$repos.products.find({ 407 | where: { 408 | category: { _eq: 'electronics' }, 409 | price: { _between: [100, 500] }, 410 | isActive: { _eq: true } 411 | }, 412 | fields: 'id,name,price', 413 | sort: '-price', 414 | limit: 20, 415 | meta: 'totalCount' 416 | }); 417 | ``` 418 | 419 | ## Best Practices 420 | 421 | 1. **Use appropriate operators** - Choose the right operator for your use case 422 | 2. **Combine conditions logically** - Use `_and` and `_or` to build complex queries 423 | 3. **Use indexes** - Create database indexes on frequently filtered fields 424 | 4. **Limit results** - Always use `limit` for queries that might return many records 425 | 5. **Use text search carefully** - Text search can be slower, use it with other filters 426 | 427 | ## Next Steps 428 | 429 | - See [Repository Methods](./repository-methods/) for complete find() documentation 430 | - Learn about [Context Reference](./context-reference/) for accessing query parameters 431 | - Check [API Lifecycle](./api-lifecycle.md) to understand query processing 432 | 433 | -------------------------------------------------------------------------------- /app/routing-management.md: -------------------------------------------------------------------------------- 1 | # Routing Management 2 | 3 | Routing Management lets you create custom API endpoints that are served by your **backend server**. By default, Enfyra automatically generates REST API endpoints for every table you create (like `/table_definition`). The Routing Manager allows you to create **custom endpoints** like `/products` or `/users` for any purpose you need. 4 | 5 | **Important**: All routes are created and served by the **backend server**, not the frontend app. The frontend consumes these API endpoints via HTTP requests. 6 | 7 | ## Route Properties 8 | 9 | ### Basic Configuration 10 | - **Path**: The custom endpoint path (e.g., `/assets/:id` instead of `/file_definition`) 11 | - **Icon**: Visual identifier for the route (using Lucide icons) 12 | - **Description**: Human-readable description of the route's purpose 13 | - **Status**: Whether the route is enabled or disabled 14 | - **System Route**: Indicates if this is a core system route 15 | 16 | ### Advanced Configuration 17 | - **Main Table**: The primary table this route serves 18 | - **Target Tables**: Additional tables this route can access (also provides automatic CRUD operations) 19 | - **Route Permissions**: Access control rules for this endpoint 20 | - **Handlers**: Custom request processing logic (see [Custom Handlers](hooks-handlers/custom-handlers.md)) 21 | - **Hooks**: Lifecycle events and custom processing 22 | - **Published Methods**: Defines which HTTP methods are public (no authentication required) vs private (role-protected) 23 | 24 | ## Creating Custom Routes 25 | 26 | ### Step 1: Access Route Manager 27 | 1. Navigate to **Settings Routings** 28 | 2. Click **"Create Route"** button 29 | 30 | ### Step 2: Basic Configuration 31 | Configure the essential route properties: 32 | - **Path**: Set your custom endpoint (e.g., `/v1/products`, `/user-profiles`) 33 | - **Icon**: Choose a Lucide icon for visual identification 34 | - **Description**: Add a clear description of the route's purpose 35 | - **Is Enabled**: Toggle to activate the route 36 | 37 | ### Step 3: Link to Main Table 38 | The most important step is connecting your route to a data source: 39 | 1. Click the **relation picker** (pencil icon) next to **Main Table** 40 | 2. Search and select the table this route should serve 41 | 3. **Automatic CRUD Operations**: Once linked, your custom route can provide: 42 | - `GET /your-route` - List all records 43 | - `POST /your-route` - Create new record 44 | - `PATCH /your-route/:id` - Update record 45 | - `DELETE /your-route/:id` - Delete record 46 | 47 | 4. **Published Methods Control**: HTTP methods specified in **Published Methods** become **public** (accessible by guests without authentication). Methods not listed remain **private** and require proper authentication and role permissions. 48 | 49 | The route inherits all the table's fields, validation rules, and relationships without requiring additional configuration. 50 | 51 | See [Relation Picker System](relation-picker.md) for detailed usage of the relation selection interface. 52 | 53 | ### Step 4: Save and Test 54 | 1. Click **Save** to create the route 55 | 2. The custom endpoint becomes active immediately 56 | 3. All data management pages automatically use the new endpoint 57 | 4. API calls throughout the system switch to the custom path 58 | 59 | ### Step 5: Configure Route Permissions (Optional) 60 | After creating the route, you can set up fine-grained access control: 61 | 62 | 1. **Access the edit page** of your newly created route 63 | 2. **Scroll to Route Permissions section** (only available on edit page, not during creation) 64 | 3. **Click "Add Permission"** to open the permission configuration drawer 65 | 4. **Configure permission settings:** 66 | - **Role**: Select which role gets access (single selection via relation picker) 67 | - **Methods**: Choose which methods this role can access (multiple selection): 68 | - **REST API Methods**: `GET`, `POST`, `PUT`, `PATCH`, `DELETE` 69 | - **GraphQL Methods**: `GQL_QUERY`, `GQL_MUTATION` 70 | - **Allowed Users**: Select specific users who bypass role restrictions (multiple selection via relation picker) 71 | - **Is Enabled**: Toggle to activate/deactivate this permission rule 72 | - **Description**: Document the purpose of this permission 73 | 74 | 5. **Save the permission** - it takes effect immediately 75 | 76 | **Important**: Route Permissions operate independently from the main route configuration. Changes to permissions (create, edit, delete) are applied instantly without requiring the "Save" button in the header actions. 77 | 78 | ### Managing Route Permissions 79 | After creating permissions, they appear as a list in the Route Permissions section: 80 | - **Permission entries** display the description (or "No description" if empty) 81 | - **Status indicator** shows if the permission is enabled or disabled 82 | - **Actions** include edit and delete options for each permission 83 | - **Multiple permissions** can be configured for the same route with different roles/methods 84 | - **Instant updates**: All permission changes take effect immediately, separate from route modifications 85 | 86 | See [Relation Picker System](relation-picker.md) for details on selecting roles and users through the relation interface. 87 | 88 | ### GraphQL Permission Control 89 | 90 | GraphQL API access is controlled through two special methods in Route Permissions: 91 | 92 | **`GQL_QUERY` Method:** 93 | - Controls **read access** via GraphQL queries 94 | - When enabled for a role, allows querying table data through GraphQL 95 | - Example: `query { users { data { id name email } } }` 96 | - Independent from REST `GET` permission 97 | 98 | **`GQL_MUTATION` Method:** 99 | - Controls **write access** (create, update, delete) via GraphQL mutations 100 | - When enabled for a role, allows all mutation operations: 101 | - `create_table_name` - Create new records 102 | - `update_table_name` - Update existing records 103 | - `delete_table_name` - Delete records 104 | - Independent from REST `POST`, `PUT`, `PATCH`, `DELETE` permissions 105 | 106 | **Permission Examples:** 107 | 108 | ``` 109 | # Editor Role - Read-only GraphQL access 110 | Role: Editor 111 | Methods: [GQL_QUERY] 112 | Result: Can query data but cannot create/update/delete via GraphQL 113 | 114 | # Admin Role - Full GraphQL access 115 | Role: Admin 116 | Methods: [GQL_QUERY, GQL_MUTATION] 117 | Result: Can query and mutate data via GraphQL 118 | 119 | # Developer Role - Mixed access 120 | Role: Developer 121 | Methods: [GET, POST, GQL_QUERY, GQL_MUTATION] 122 | Result: Full REST and GraphQL access 123 | ``` 124 | 125 | **Important Notes:** 126 | - GraphQL permissions are **separate** from REST permissions 127 | - A role can have GraphQL access without REST access and vice versa 128 | - `GQL_MUTATION` covers all mutation operations (create, update, delete) - you cannot separate them 129 | - `GQL_QUERY` covers all query operations 130 | 131 | For complete GraphQL API documentation, see **[API Lifecycle](../server/api-lifecycle.md)** and **[Query Filtering](../server/query-filtering.md)**. 132 | 133 | ## System Routes 134 | 135 | System routes (marked with "System Route" badge) are core Enfyra endpoints that power the admin interface. These routes are automatically created and should generally not be modified unless you understand the implications. 136 | 137 | Examples include routes for: 138 | - File and storage management 139 | - Route definition management 140 | - Menu configuration 141 | - User and permission systems 142 | 143 | ## Method Publishing and Access Control 144 | 145 | The **Published Methods** field controls the authentication requirements for each HTTP method. 146 | 147 | **For complete details on permissions, roles, and allowedUsers, see [Permission Builder](./permission-builder.md).** 148 | 149 | ### Published Methods vs Private Methods 150 | - **Published Methods**: Public access, no authentication required 151 | - **Unpublished Methods**: Require authentication and proper permissions 152 | 153 | ### Quick Reference 154 | 1. **Published Methods**: Public access 155 | 2. **Allowed Users**: Bypass role restrictions 156 | 3. **Role Permissions**: Standard role-based access 157 | 4. **No Access**: Denied 158 | 159 | ## Custom Route Behavior 160 | 161 | ### Default CRUD Operations 162 | When you create a route and link it to targetTables, Enfyra automatically provides standard CRUD operations: 163 | 164 | **REST API:** 165 | - `GET /your-route` - List records 166 | - `POST /your-route` - Create record 167 | - `PATCH /your-route/:id` - Update record 168 | - `DELETE /your-route/:id` - Delete record 169 | 170 | **GraphQL API:** 171 | - `query { your_table(...) }` - Query records 172 | - `mutation { create_your_table(...) }` - Create record 173 | - `mutation { update_your_table(...) }` - Update record 174 | - `mutation { delete_your_table(...) }` - Delete record 175 | 176 | ### Custom Handlers Override 177 | You can replace any of these default operations with custom business logic by creating handlers: 178 | 179 | 1. **Create the route** with targetTables configured 180 | 2. **Add custom handlers** via **Settings Handlers** to override specific HTTP methods 181 | 3. **Handler takes precedence** - when a handler exists for a route+method, it executes instead of default CRUD 182 | 183 | For detailed handler creation and examples, see [Custom Handlers](hooks-handlers/custom-handlers.md). 184 | 185 | ### Route Status 186 | - **Enabled/Disabled**: Controls whether the route is active 187 | - **System Integration**: System routes are automatically integrated with the admin interface 188 | 189 | ## Custom Endpoint Benefits 190 | 191 | ### API Consistency 192 | Create RESTful endpoints that match your application's naming conventions: 193 | - `/products` instead of `/table_definition` 194 | - `/orders` instead of `/order_table` 195 | 196 | ### Versioning Support 197 | Implement API versioning: 198 | - `/v1/users` 199 | - `/v2/users` 200 | 201 | ### Business Logic Integration 202 | Routes can be enhanced with: 203 | - Custom validation through handlers 204 | - Data transformation via hooks 205 | - Specialized permission rules 206 | 207 | ## Impact on Data Pages 208 | 209 | When you create custom routes, all related functionality automatically adapts: 210 | - **Data management pages** use the custom endpoint 211 | - **Relation pickers** respect custom routes 212 | - **Permission systems** apply to custom paths 213 | - **API calls** throughout the system use the new endpoints 214 | 215 | Changes to routes take effect immediately without requiring application restart. 216 | 217 | ## Best Practices 218 | 219 | ### Naming Conventions 220 | - Use lowercase, hyphen-separated paths: `/user-profiles` 221 | - Include version prefixes for API stability: `/v1/` 222 | - Be descriptive but concise: `/product-categories` not `/pc` 223 | 224 | ### Route Organization 225 | - Group related routes under common prefixes 226 | - Use consistent patterns across your API 227 | - Document the purpose of each custom route 228 | 229 | ### Permission Alignment 230 | - Ensure route permissions align with table permissions 231 | - Test custom routes with different user roles 232 | - Consider the security implications of custom paths 233 | 234 | ## Related Documentation 235 | 236 | - **[Custom Handlers](hooks-handlers/custom-handlers.md)** - Writing JavaScript logic for custom endpoints 237 | - **[Hooks System](hooks-handlers/hooks.md)** - Adding validation and notifications 238 | - **[Package Management](hooks-handlers/package-management.md)** - Installing NPM packages for external integrations 239 | 240 | ## Practical Examples 241 | 242 | - **[User Registration Example](../examples/user-registration-example.md)** - Complete walkthrough including route creation, custom handler, and nodemailer integration -------------------------------------------------------------------------------- /server/file-handling.md: -------------------------------------------------------------------------------- 1 | # File Handling 2 | 3 | Enfyra provides file upload and management capabilities through helpers in the context object. You can upload, update, and delete files with support for folders and storage configurations. 4 | 5 | ## Quick Navigation 6 | 7 | - [Uploaded File Information](#uploaded-file-information) - Access uploaded files 8 | - [Upload Files](#upload-files) - Using $uploadFile helper 9 | - [Update Files](#update-files) - Using $updateFile helper 10 | - [Delete Files](#delete-files) - Using $deleteFile helper 11 | - [Common Patterns](#common-patterns) - Real-world examples 12 | 13 | ## Uploaded File Information 14 | 15 | When a file is uploaded via a request, it's available in the context. 16 | 17 | ### $ctx.$uploadedFile 18 | 19 | Information about the uploaded file (if any). 20 | 21 | ```javascript 22 | if ($ctx.$uploadedFile) { 23 | const filename = $ctx.$uploadedFile.originalname; // Original filename 24 | const mimetype = $ctx.$uploadedFile.mimetype; // MIME type 25 | const size = $ctx.$uploadedFile.size; // File size in bytes 26 | const buffer = $ctx.$uploadedFile.buffer; // File buffer 27 | const fieldname = $ctx.$uploadedFile.fieldname; // Form field name 28 | } 29 | ``` 30 | 31 | ### File Properties 32 | 33 | | Property | Type | Description | 34 | |----------|------|-------------| 35 | | `originalname` | `string` | Original filename from client | 36 | | `mimetype` | `string` | MIME type (e.g., `image/jpeg`, `application/pdf`) | 37 | | `size` | `number` | File size in bytes | 38 | | `buffer` | `Buffer` | Raw file content as Node.js Buffer | 39 | | `fieldname` | `string` | Form field name used for upload | 40 | 41 | ## Upload Files 42 | 43 | Use `$ctx.$helpers.$uploadFile` to upload files to storage. 44 | 45 | ### Basic Upload 46 | 47 | ```javascript 48 | if (!$ctx.$uploadedFile) { 49 | $ctx.$throw['400']('No file uploaded'); 50 | return; 51 | } 52 | 53 | const fileResult = await $ctx.$helpers.$uploadFile({ 54 | originalname: $ctx.$uploadedFile.originalname, 55 | mimetype: $ctx.$uploadedFile.mimetype, 56 | buffer: $ctx.$uploadedFile.buffer, 57 | size: $ctx.$uploadedFile.size 58 | }); 59 | 60 | return fileResult; 61 | ``` 62 | 63 | ### Upload with Options 64 | 65 | ```javascript 66 | const fileResult = await $ctx.$helpers.$uploadFile({ 67 | originalname: 'my-image.jpg', 68 | filename: 'custom-filename.jpg', // Optional: custom filename 69 | mimetype: 'image/jpeg', 70 | buffer: fileBuffer, 71 | size: 1024000, 72 | folder: 123, // Optional: folder ID 73 | storageConfig: 1, // Optional: storage config ID 74 | title: 'My Image', // Optional: custom title 75 | description: 'Image description' // Optional 76 | }); 77 | ``` 78 | 79 | ### Upload Parameters 80 | 81 | | Parameter | Type | Required | Description | 82 | |-----------|------|----------|-------------| 83 | | `originalname` | `string` | No | Original filename | 84 | | `filename` | `string` | No | Custom filename (alternative to originalname) | 85 | | `mimetype` | `string` | Yes | MIME type | 86 | | `buffer` | `Buffer` | Yes | File buffer | 87 | | `size` | `number` | Yes | File size in bytes | 88 | | `encoding` | `string` | No | File encoding (default: 'utf8') | 89 | | `folder` | `number \| { id: number }` | No | Folder ID to store file in | 90 | | `storageConfig` | `number` | No | Storage configuration ID | 91 | | `title` | `string` | No | Custom title (defaults to filename) | 92 | | `description` | `string` | No | File description | 93 | 94 | ### Example: Upload from Request 95 | 96 | ```javascript 97 | // In handler for file upload endpoint 98 | if (!$ctx.$uploadedFile) { 99 | $ctx.$throw['400']('No file uploaded'); 100 | return; 101 | } 102 | 103 | const fileResult = await $ctx.$helpers.$uploadFile({ 104 | originalname: $ctx.$uploadedFile.originalname, 105 | mimetype: $ctx.$uploadedFile.mimetype, 106 | buffer: $ctx.$uploadedFile.buffer, 107 | size: $ctx.$uploadedFile.size, 108 | title: $ctx.$body.title || $ctx.$uploadedFile.originalname, 109 | description: $ctx.$body.description 110 | }); 111 | 112 | $ctx.$logs(`File uploaded: ${fileResult.filename}`); 113 | 114 | return { 115 | success: true, 116 | file: fileResult 117 | }; 118 | ``` 119 | 120 | ## Update Files 121 | 122 | Use `$ctx.$helpers.$updateFile` to update existing files. 123 | 124 | ### Basic Update 125 | 126 | ```javascript 127 | await $ctx.$helpers.$updateFile(fileId, { 128 | buffer: newFileBuffer, 129 | mimetype: 'image/jpeg', 130 | size: newFileSize 131 | }); 132 | ``` 133 | 134 | ### Update with Options 135 | 136 | ```javascript 137 | await $ctx.$helpers.$updateFile(fileId, { 138 | buffer: newFileBuffer, 139 | originalname: 'new-name.jpg', 140 | filename: 'custom-new-name.jpg', 141 | mimetype: 'image/jpeg', 142 | size: 2048000, 143 | folder: 456, 144 | title: 'Updated Title', 145 | description: 'Updated description' 146 | }); 147 | ``` 148 | 149 | ### Update Parameters 150 | 151 | All parameters are optional - only include what you want to update. 152 | 153 | | Parameter | Type | Description | 154 | |-----------|------|-------------| 155 | | `buffer` | `Buffer` | New file buffer | 156 | | `originalname` | `string` | New original filename | 157 | | `filename` | `string` | New custom filename | 158 | | `mimetype` | `string` | New MIME type | 159 | | `size` | `number` | New file size | 160 | | `folder` | `number \| { id: number }` | Move to different folder | 161 | | `title` | `string` | Update title | 162 | | `description` | `string` | Update description | 163 | 164 | ### Example: Update File Metadata 165 | 166 | ```javascript 167 | // Update only metadata, not file content 168 | await $ctx.$helpers.$updateFile($ctx.$params.fileId, { 169 | title: $ctx.$body.title, 170 | description: $ctx.$body.description, 171 | folder: $ctx.$body.folderId 172 | }); 173 | 174 | $ctx.$logs(`File metadata updated: ${$ctx.$params.fileId}`); 175 | 176 | return { success: true }; 177 | ``` 178 | 179 | ## Delete Files 180 | 181 | Use `$ctx.$helpers.$deleteFile` to delete files. 182 | 183 | ### Basic Delete 184 | 185 | ```javascript 186 | await $ctx.$helpers.$deleteFile(fileId); 187 | ``` 188 | 189 | ### Example: Delete File 190 | 191 | ```javascript 192 | // Check if file exists 193 | const fileResult = await $ctx.$repos.file_definition.find({ 194 | where: { id: { _eq: $ctx.$params.fileId } } 195 | }); 196 | 197 | if (fileResult.data.length === 0) { 198 | $ctx.$throw['404']('File not found'); 199 | return; 200 | } 201 | 202 | // Delete the file 203 | await $ctx.$helpers.$deleteFile($ctx.$params.fileId); 204 | 205 | $ctx.$logs(`File deleted: ${$ctx.$params.fileId}`); 206 | 207 | return { success: true, message: 'File deleted successfully' }; 208 | ``` 209 | 210 | ## Common Patterns 211 | 212 | ### Pattern 1: Upload and Create Related Record 213 | 214 | ```javascript 215 | // Upload file 216 | if (!$ctx.$uploadedFile) { 217 | $ctx.$throw['400']('No file uploaded'); 218 | return; 219 | } 220 | 221 | const fileResult = await $ctx.$helpers.$uploadFile({ 222 | originalname: $ctx.$uploadedFile.originalname, 223 | mimetype: $ctx.$uploadedFile.mimetype, 224 | buffer: $ctx.$uploadedFile.buffer, 225 | size: $ctx.$uploadedFile.size 226 | }); 227 | 228 | // Create product with file reference 229 | const productResult = await $ctx.$repos.products.create({ 230 | data: { 231 | name: $ctx.$body.name, 232 | price: $ctx.$body.price, 233 | imageFileId: fileResult.id 234 | } 235 | }); 236 | 237 | return { 238 | product: productResult.data[0], 239 | file: fileResult 240 | }; 241 | ``` 242 | 243 | ### Pattern 2: Validate File Before Upload 244 | 245 | ```javascript 246 | if (!$ctx.$uploadedFile) { 247 | $ctx.$throw['400']('No file uploaded'); 248 | return; 249 | } 250 | 251 | // Validate file type 252 | const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']; 253 | if (!allowedTypes.includes($ctx.$uploadedFile.mimetype)) { 254 | $ctx.$throw['400']('Invalid file type. Only images are allowed.'); 255 | return; 256 | } 257 | 258 | // Validate file size (max 5MB) 259 | const maxSize = 5 * 1024 * 1024; // 5MB 260 | if ($ctx.$uploadedFile.size > maxSize) { 261 | $ctx.$throw['400']('File too large. Maximum size is 5MB.'); 262 | return; 263 | } 264 | 265 | // Upload file 266 | const fileResult = await $ctx.$helpers.$uploadFile({ 267 | originalname: $ctx.$uploadedFile.originalname, 268 | mimetype: $ctx.$uploadedFile.mimetype, 269 | buffer: $ctx.$uploadedFile.buffer, 270 | size: $ctx.$uploadedFile.size 271 | }); 272 | 273 | return fileResult; 274 | ``` 275 | 276 | ### Pattern 3: Upload to Specific Folder 277 | 278 | ```javascript 279 | // Get or create folder 280 | let folder = await $ctx.$repos.folders.find({ 281 | where: { name: { _eq: 'User Uploads' } } 282 | }); 283 | 284 | if (folder.data.length === 0) { 285 | const folderResult = await $ctx.$repos.folders.create({ 286 | data: { 287 | name: 'User Uploads', 288 | userId: $ctx.$user.id 289 | } 290 | }); 291 | folder = folderResult.data[0]; 292 | } else { 293 | folder = folder.data[0]; 294 | } 295 | 296 | // Upload file to folder 297 | const fileResult = await $ctx.$helpers.$uploadFile({ 298 | originalname: $ctx.$uploadedFile.originalname, 299 | mimetype: $ctx.$uploadedFile.mimetype, 300 | buffer: $ctx.$uploadedFile.buffer, 301 | size: $ctx.$uploadedFile.size, 302 | folder: folder.id 303 | }); 304 | 305 | return fileResult; 306 | ``` 307 | 308 | ### Pattern 4: Replace File 309 | 310 | ```javascript 311 | // Get existing file 312 | const existingFile = await $ctx.$repos.file_definition.find({ 313 | where: { id: { _eq: $ctx.$params.fileId } } 314 | }); 315 | 316 | if (existingFile.data.length === 0) { 317 | $ctx.$throw['404']('File not found'); 318 | return; 319 | } 320 | 321 | // Replace with new file 322 | await $ctx.$helpers.$updateFile($ctx.$params.fileId, { 323 | buffer: $ctx.$uploadedFile.buffer, 324 | mimetype: $ctx.$uploadedFile.mimetype, 325 | size: $ctx.$uploadedFile.size, 326 | originalname: $ctx.$uploadedFile.originalname 327 | }); 328 | 329 | $ctx.$logs(`File replaced: ${$ctx.$params.fileId}`); 330 | 331 | return { success: true }; 332 | ``` 333 | 334 | ### Pattern 5: Upload Multiple Files 335 | 336 | ```javascript 337 | // Note: This requires multiple file uploads in the request 338 | // Each file would be processed separately 339 | 340 | const uploadResults = []; 341 | 342 | // Process each uploaded file 343 | // (Implementation depends on how multiple files are sent in request) 344 | 345 | for (const uploadedFile of uploadedFiles) { 346 | const fileResult = await $ctx.$helpers.$uploadFile({ 347 | originalname: uploadedFile.originalname, 348 | mimetype: uploadedFile.mimetype, 349 | buffer: uploadedFile.buffer, 350 | size: uploadedFile.size 351 | }); 352 | 353 | uploadResults.push(fileResult); 354 | } 355 | 356 | return { 357 | success: true, 358 | files: uploadResults, 359 | count: uploadResults.length 360 | }; 361 | ``` 362 | 363 | ## Best Practices 364 | 365 | 1. **Validate files** - Always validate file type and size before uploading 366 | 2. **Handle missing files** - Check if file exists before processing 367 | 3. **Use appropriate folders** - Organize files using folders 368 | 4. **Set file metadata** - Include title and description for better organization 369 | 5. **Error handling** - Wrap file operations in try-catch blocks 370 | 6. **File size limits** - Enforce reasonable file size limits 371 | 7. **File type restrictions** - Only allow safe file types 372 | 373 | ## Next Steps 374 | 375 | - Learn about [Context Reference](./context-reference/) for file-related properties 376 | - See [Error Handling](./error-handling.md) for proper error handling 377 | - Check [Repository Methods](./repository-methods/) for querying file records 378 | 379 | --------------------------------------------------------------------------------