,
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 |
202 |
203 |
204 |
205 | Create User
206 |
207 |
208 |
209 |
210 |
211 | Delete Selected
212 |
213 |
214 |
215 |
216 | ```
217 |
218 | ### Table Actions
219 |
220 | ```vue
221 |
222 |
223 |
224 |
225 | Edit
226 |
227 |
228 |
229 | Delete
230 |
231 |
232 |
233 |
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 |
--------------------------------------------------------------------------------