@bloomneo/appkit 1.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +902 -0
- package/bin/appkit.js +71 -0
- package/bin/commands/generate.js +1050 -0
- package/bin/templates/backend/README.md.template +39 -0
- package/bin/templates/backend/api.http.template +0 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +507 -0
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +61 -0
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +2539 -0
- package/bin/templates/backend/package.json.template +34 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.http.template +29 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.route.ts.template +36 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.service.ts.template +88 -0
- package/bin/templates/backend/src/api/features/welcome/welcome.types.ts.template +18 -0
- package/bin/templates/backend/src/api/lib/api-router.ts.template +84 -0
- package/bin/templates/backend/src/api/server.ts.template +188 -0
- package/bin/templates/backend/tsconfig.api.json.template +24 -0
- package/bin/templates/backend/tsconfig.json.template +40 -0
- package/bin/templates/feature/feature.http.template +63 -0
- package/bin/templates/feature/feature.route.ts.template +36 -0
- package/bin/templates/feature/feature.service.ts.template +81 -0
- package/bin/templates/feature/feature.types.ts.template +23 -0
- package/bin/templates/feature-db/feature.http.template +63 -0
- package/bin/templates/feature-db/feature.model.ts.template +74 -0
- package/bin/templates/feature-db/feature.route.ts.template +58 -0
- package/bin/templates/feature-db/feature.service.ts.template +231 -0
- package/bin/templates/feature-db/feature.types.ts.template +25 -0
- package/bin/templates/feature-db/schema-addition.prisma.template +9 -0
- package/bin/templates/feature-db/seeding/README.md.template +57 -0
- package/bin/templates/feature-db/seeding/feature.seed.js.template +67 -0
- package/bin/templates/feature-user/schema-addition.prisma.template +19 -0
- package/bin/templates/feature-user/user.http.template +157 -0
- package/bin/templates/feature-user/user.model.ts.template +244 -0
- package/bin/templates/feature-user/user.route.ts.template +379 -0
- package/bin/templates/feature-user/user.seed.js.template +182 -0
- package/bin/templates/feature-user/user.service.ts.template +426 -0
- package/bin/templates/feature-user/user.types.ts.template +127 -0
- package/dist/auth/auth.d.ts +182 -0
- package/dist/auth/auth.d.ts.map +1 -0
- package/dist/auth/auth.js +477 -0
- package/dist/auth/auth.js.map +1 -0
- package/dist/auth/defaults.d.ts +104 -0
- package/dist/auth/defaults.d.ts.map +1 -0
- package/dist/auth/defaults.js +374 -0
- package/dist/auth/defaults.js.map +1 -0
- package/dist/auth/index.d.ts +70 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +94 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/cache/cache.d.ts +118 -0
- package/dist/cache/cache.d.ts.map +1 -0
- package/dist/cache/cache.js +249 -0
- package/dist/cache/cache.js.map +1 -0
- package/dist/cache/defaults.d.ts +63 -0
- package/dist/cache/defaults.d.ts.map +1 -0
- package/dist/cache/defaults.js +193 -0
- package/dist/cache/defaults.js.map +1 -0
- package/dist/cache/index.d.ts +101 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +203 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/strategies/memory.d.ts +138 -0
- package/dist/cache/strategies/memory.d.ts.map +1 -0
- package/dist/cache/strategies/memory.js +348 -0
- package/dist/cache/strategies/memory.js.map +1 -0
- package/dist/cache/strategies/redis.d.ts +105 -0
- package/dist/cache/strategies/redis.d.ts.map +1 -0
- package/dist/cache/strategies/redis.js +318 -0
- package/dist/cache/strategies/redis.js.map +1 -0
- package/dist/config/config.d.ts +62 -0
- package/dist/config/config.d.ts.map +1 -0
- package/dist/config/config.js +107 -0
- package/dist/config/config.js.map +1 -0
- package/dist/config/defaults.d.ts +44 -0
- package/dist/config/defaults.d.ts.map +1 -0
- package/dist/config/defaults.js +217 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/index.d.ts +105 -0
- package/dist/config/index.d.ts.map +1 -0
- package/dist/config/index.js +163 -0
- package/dist/config/index.js.map +1 -0
- package/dist/database/adapters/mongoose.d.ts +106 -0
- package/dist/database/adapters/mongoose.d.ts.map +1 -0
- package/dist/database/adapters/mongoose.js +480 -0
- package/dist/database/adapters/mongoose.js.map +1 -0
- package/dist/database/adapters/prisma.d.ts +106 -0
- package/dist/database/adapters/prisma.d.ts.map +1 -0
- package/dist/database/adapters/prisma.js +494 -0
- package/dist/database/adapters/prisma.js.map +1 -0
- package/dist/database/defaults.d.ts +87 -0
- package/dist/database/defaults.d.ts.map +1 -0
- package/dist/database/defaults.js +271 -0
- package/dist/database/defaults.js.map +1 -0
- package/dist/database/index.d.ts +137 -0
- package/dist/database/index.d.ts.map +1 -0
- package/dist/database/index.js +490 -0
- package/dist/database/index.js.map +1 -0
- package/dist/email/defaults.d.ts +100 -0
- package/dist/email/defaults.d.ts.map +1 -0
- package/dist/email/defaults.js +400 -0
- package/dist/email/defaults.js.map +1 -0
- package/dist/email/email.d.ts +139 -0
- package/dist/email/email.d.ts.map +1 -0
- package/dist/email/email.js +316 -0
- package/dist/email/email.js.map +1 -0
- package/dist/email/index.d.ts +176 -0
- package/dist/email/index.d.ts.map +1 -0
- package/dist/email/index.js +251 -0
- package/dist/email/index.js.map +1 -0
- package/dist/email/strategies/console.d.ts +90 -0
- package/dist/email/strategies/console.d.ts.map +1 -0
- package/dist/email/strategies/console.js +268 -0
- package/dist/email/strategies/console.js.map +1 -0
- package/dist/email/strategies/resend.d.ts +84 -0
- package/dist/email/strategies/resend.d.ts.map +1 -0
- package/dist/email/strategies/resend.js +266 -0
- package/dist/email/strategies/resend.js.map +1 -0
- package/dist/email/strategies/smtp.d.ts +77 -0
- package/dist/email/strategies/smtp.d.ts.map +1 -0
- package/dist/email/strategies/smtp.js +286 -0
- package/dist/email/strategies/smtp.js.map +1 -0
- package/dist/error/defaults.d.ts +40 -0
- package/dist/error/defaults.d.ts.map +1 -0
- package/dist/error/defaults.js +75 -0
- package/dist/error/defaults.js.map +1 -0
- package/dist/error/error.d.ts +140 -0
- package/dist/error/error.d.ts.map +1 -0
- package/dist/error/error.js +200 -0
- package/dist/error/error.js.map +1 -0
- package/dist/error/index.d.ts +145 -0
- package/dist/error/index.d.ts.map +1 -0
- package/dist/error/index.js +145 -0
- package/dist/error/index.js.map +1 -0
- package/dist/event/defaults.d.ts +111 -0
- package/dist/event/defaults.d.ts.map +1 -0
- package/dist/event/defaults.js +378 -0
- package/dist/event/defaults.js.map +1 -0
- package/dist/event/event.d.ts +171 -0
- package/dist/event/event.d.ts.map +1 -0
- package/dist/event/event.js +391 -0
- package/dist/event/event.js.map +1 -0
- package/dist/event/index.d.ts +173 -0
- package/dist/event/index.d.ts.map +1 -0
- package/dist/event/index.js +302 -0
- package/dist/event/index.js.map +1 -0
- package/dist/event/strategies/memory.d.ts +122 -0
- package/dist/event/strategies/memory.d.ts.map +1 -0
- package/dist/event/strategies/memory.js +331 -0
- package/dist/event/strategies/memory.js.map +1 -0
- package/dist/event/strategies/redis.d.ts +115 -0
- package/dist/event/strategies/redis.d.ts.map +1 -0
- package/dist/event/strategies/redis.js +434 -0
- package/dist/event/strategies/redis.js.map +1 -0
- package/dist/index.d.ts +58 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +72 -0
- package/dist/index.js.map +1 -0
- package/dist/logger/defaults.d.ts +67 -0
- package/dist/logger/defaults.d.ts.map +1 -0
- package/dist/logger/defaults.js +213 -0
- package/dist/logger/defaults.js.map +1 -0
- package/dist/logger/index.d.ts +84 -0
- package/dist/logger/index.d.ts.map +1 -0
- package/dist/logger/index.js +101 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/logger/logger.d.ts +165 -0
- package/dist/logger/logger.d.ts.map +1 -0
- package/dist/logger/logger.js +843 -0
- package/dist/logger/logger.js.map +1 -0
- package/dist/logger/transports/console.d.ts +102 -0
- package/dist/logger/transports/console.d.ts.map +1 -0
- package/dist/logger/transports/console.js +276 -0
- package/dist/logger/transports/console.js.map +1 -0
- package/dist/logger/transports/database.d.ts +153 -0
- package/dist/logger/transports/database.d.ts.map +1 -0
- package/dist/logger/transports/database.js +539 -0
- package/dist/logger/transports/database.js.map +1 -0
- package/dist/logger/transports/file.d.ts +146 -0
- package/dist/logger/transports/file.d.ts.map +1 -0
- package/dist/logger/transports/file.js +464 -0
- package/dist/logger/transports/file.js.map +1 -0
- package/dist/logger/transports/http.d.ts +128 -0
- package/dist/logger/transports/http.d.ts.map +1 -0
- package/dist/logger/transports/http.js +401 -0
- package/dist/logger/transports/http.js.map +1 -0
- package/dist/logger/transports/webhook.d.ts +152 -0
- package/dist/logger/transports/webhook.d.ts.map +1 -0
- package/dist/logger/transports/webhook.js +485 -0
- package/dist/logger/transports/webhook.js.map +1 -0
- package/dist/queue/defaults.d.ts +66 -0
- package/dist/queue/defaults.d.ts.map +1 -0
- package/dist/queue/defaults.js +205 -0
- package/dist/queue/defaults.js.map +1 -0
- package/dist/queue/index.d.ts +124 -0
- package/dist/queue/index.d.ts.map +1 -0
- package/dist/queue/index.js +116 -0
- package/dist/queue/index.js.map +1 -0
- package/dist/queue/queue.d.ts +156 -0
- package/dist/queue/queue.d.ts.map +1 -0
- package/dist/queue/queue.js +387 -0
- package/dist/queue/queue.js.map +1 -0
- package/dist/queue/transports/database.d.ts +165 -0
- package/dist/queue/transports/database.d.ts.map +1 -0
- package/dist/queue/transports/database.js +595 -0
- package/dist/queue/transports/database.js.map +1 -0
- package/dist/queue/transports/memory.d.ts +143 -0
- package/dist/queue/transports/memory.d.ts.map +1 -0
- package/dist/queue/transports/memory.js +415 -0
- package/dist/queue/transports/memory.js.map +1 -0
- package/dist/queue/transports/redis.d.ts +203 -0
- package/dist/queue/transports/redis.d.ts.map +1 -0
- package/dist/queue/transports/redis.js +744 -0
- package/dist/queue/transports/redis.js.map +1 -0
- package/dist/security/defaults.d.ts +64 -0
- package/dist/security/defaults.d.ts.map +1 -0
- package/dist/security/defaults.js +159 -0
- package/dist/security/defaults.js.map +1 -0
- package/dist/security/index.d.ts +110 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +160 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/security.d.ts +138 -0
- package/dist/security/security.d.ts.map +1 -0
- package/dist/security/security.js +419 -0
- package/dist/security/security.js.map +1 -0
- package/dist/storage/defaults.d.ts +79 -0
- package/dist/storage/defaults.d.ts.map +1 -0
- package/dist/storage/defaults.js +358 -0
- package/dist/storage/defaults.js.map +1 -0
- package/dist/storage/index.d.ts +153 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +242 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/storage.d.ts +151 -0
- package/dist/storage/storage.d.ts.map +1 -0
- package/dist/storage/storage.js +439 -0
- package/dist/storage/storage.js.map +1 -0
- package/dist/storage/strategies/local.d.ts +117 -0
- package/dist/storage/strategies/local.d.ts.map +1 -0
- package/dist/storage/strategies/local.js +368 -0
- package/dist/storage/strategies/local.js.map +1 -0
- package/dist/storage/strategies/r2.d.ts +130 -0
- package/dist/storage/strategies/r2.d.ts.map +1 -0
- package/dist/storage/strategies/r2.js +470 -0
- package/dist/storage/strategies/r2.js.map +1 -0
- package/dist/storage/strategies/s3.d.ts +121 -0
- package/dist/storage/strategies/s3.d.ts.map +1 -0
- package/dist/storage/strategies/s3.js +461 -0
- package/dist/storage/strategies/s3.js.map +1 -0
- package/dist/util/defaults.d.ts +77 -0
- package/dist/util/defaults.d.ts.map +1 -0
- package/dist/util/defaults.js +193 -0
- package/dist/util/defaults.js.map +1 -0
- package/dist/util/index.d.ts +97 -0
- package/dist/util/index.d.ts.map +1 -0
- package/dist/util/index.js +165 -0
- package/dist/util/index.js.map +1 -0
- package/dist/util/util.d.ts +145 -0
- package/dist/util/util.d.ts.map +1 -0
- package/dist/util/util.js +481 -0
- package/dist/util/util.js.map +1 -0
- package/package.json +234 -0
|
@@ -0,0 +1,2539 @@
|
|
|
1
|
+
# VoilaJSX AppKit - LLM Guide: Core Philosophy & Universal Patterns 🤖
|
|
2
|
+
|
|
3
|
+
> **Essential foundation for AI agents to generate perfect VoilaJSX AppKit
|
|
4
|
+
> code**
|
|
5
|
+
|
|
6
|
+
## CRITICAL: Read This First
|
|
7
|
+
|
|
8
|
+
This section establishes the core patterns that ALL VoilaJSX AppKit code must
|
|
9
|
+
follow. Every example is production-ready. Every pattern is tested. Every rule
|
|
10
|
+
is absolute.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## WHEN TO USE VoilaJSX AppKit
|
|
15
|
+
|
|
16
|
+
✅ **ALWAYS use AppKit when:**
|
|
17
|
+
|
|
18
|
+
- Building Node.js backend applications
|
|
19
|
+
- Need authentication with roles/permissions
|
|
20
|
+
- Require file storage, caching, queuing, or email
|
|
21
|
+
- Want environment-driven configuration
|
|
22
|
+
- Building multi-tenant or multi-org applications
|
|
23
|
+
- Need production-ready security features
|
|
24
|
+
- Want zero-configuration startup
|
|
25
|
+
|
|
26
|
+
❌ **NEVER use AppKit when:**
|
|
27
|
+
|
|
28
|
+
- Building frontend React/Vue applications
|
|
29
|
+
- Creating CLI tools or desktop applications
|
|
30
|
+
- Need real-time WebSocket servers as primary feature
|
|
31
|
+
- Building pure microservices with single responsibility
|
|
32
|
+
- Working with non-Node.js environments
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## COMPLETE MODULE REFERENCE (All 12 Modules)
|
|
37
|
+
|
|
38
|
+
**MEMORIZE this table - it's your single source of truth:**
|
|
39
|
+
|
|
40
|
+
| Module | Import | Class | Object Name | Optional Param | When to Use Param |
|
|
41
|
+
| ------------ | ----------------------------------------------------------- | --------------- | ----------- | ----------------- | --------------------------------- |
|
|
42
|
+
| **Util** | `import { utilClass } from '@bloomneo/appkit/util'` | `utilClass` | `util` | ❌ None | N/A |
|
|
43
|
+
| **Config** | `import { configClass } from '@bloomneo/appkit/config'` | `configClass` | `config` | ❌ None | N/A |
|
|
44
|
+
| **Auth** | `import { authClass } from '@bloomneo/appkit/auth'` | `authClass` | `auth` | ❌ None | N/A |
|
|
45
|
+
| **Logger** | `import { loggerClass } from '@bloomneo/appkit/logger'` | `loggerClass` | `logger` | ✅ `(component?)` | Component-specific logging |
|
|
46
|
+
| **Database** | `import { databaseClass } from '@bloomneo/appkit/database'` | `databaseClass` | `database` | ❌ None | N/A |
|
|
47
|
+
| **Cache** | `import { cacheClass } from '@bloomneo/appkit/cache'` | `cacheClass` | `cache` | ✅ `(namespace?)` | Custom namespace (default: 'app') |
|
|
48
|
+
| **Storage** | `import { storageClass } from '@bloomneo/appkit/storage'` | `storageClass` | `storage` | ❌ None | N/A |
|
|
49
|
+
| **Queue** | `import { queueClass } from '@bloomneo/appkit/queue'` | `queueClass` | `queue` | ❌ None | N/A |
|
|
50
|
+
| **Email** | `import { emailClass } from '@bloomneo/appkit/email'` | `emailClass` | `email` | ❌ None | N/A |
|
|
51
|
+
| **Error** | `import { errorClass } from '@bloomneo/appkit/error'` | `errorClass` | `error` | ❌ None | N/A |
|
|
52
|
+
| **Security** | `import { securityClass } from '@bloomneo/appkit/security'` | `securityClass` | `security` | ❌ None | N/A |
|
|
53
|
+
| **Event** | `import { eventClass } from '@bloomneo/appkit/event'` | `eventClass` | `event` | ✅ `(namespace?)` | Namespace isolation for events |
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## THE ONE FUNCTION RULE
|
|
58
|
+
|
|
59
|
+
**EVERY module follows the same pattern:**
|
|
60
|
+
|
|
61
|
+
```javascript
|
|
62
|
+
// ALWAYS use this pattern for EVERY module
|
|
63
|
+
const module = moduleClass.get();
|
|
64
|
+
|
|
65
|
+
// Standard usage - MEMORIZE these exact patterns
|
|
66
|
+
const util = utilClass.get();
|
|
67
|
+
const config = configClass.get();
|
|
68
|
+
const auth = authClass.get();
|
|
69
|
+
const database = databaseClass.get();
|
|
70
|
+
const storage = storageClass.get();
|
|
71
|
+
const queue = queueClass.get();
|
|
72
|
+
const email = emailClass.get();
|
|
73
|
+
const error = errorClass.get();
|
|
74
|
+
const security = securityClass.get();
|
|
75
|
+
|
|
76
|
+
// With optional parameters
|
|
77
|
+
const logger = loggerClass.get('api'); // Component-specific
|
|
78
|
+
const cache = cacheClass.get(); // Default 'app' namespace
|
|
79
|
+
const userCache = cacheClass.get('users'); // Custom namespace
|
|
80
|
+
const event = eventClass.get('notifications'); // Event namespace
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**RULE: Never call constructors directly. Always use .get()**
|
|
84
|
+
|
|
85
|
+
❌ **NEVER do this:**
|
|
86
|
+
|
|
87
|
+
```javascript
|
|
88
|
+
new AuthClass();
|
|
89
|
+
new DatabaseService();
|
|
90
|
+
new ConfigManager();
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
✅ **ALWAYS do this:**
|
|
94
|
+
|
|
95
|
+
```javascript
|
|
96
|
+
const auth = authClass.get();
|
|
97
|
+
const database = databaseClass.get();
|
|
98
|
+
const config = configClass.get();
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## MODULE CATEGORIES & DECISION TREE
|
|
104
|
+
|
|
105
|
+
### **Infrastructure (4 modules) - Use First**
|
|
106
|
+
|
|
107
|
+
- **Auth**: JWT tokens, role-based permissions, middleware
|
|
108
|
+
- **Database**: Multi-tenant database operations, ORM integration
|
|
109
|
+
- **Security**: CSRF protection, rate limiting, input sanitization, encryption
|
|
110
|
+
- **Error**: HTTP error handling, status codes, middleware
|
|
111
|
+
|
|
112
|
+
### **Data & Communication (5 modules) - Use Second**
|
|
113
|
+
|
|
114
|
+
- **Cache**: Redis/Memory caching with namespaces
|
|
115
|
+
- **Storage**: File storage (local/S3/R2), CDN integration
|
|
116
|
+
- **Queue**: Background job processing, scheduled tasks
|
|
117
|
+
- **Email**: Multi-provider email sending, templates
|
|
118
|
+
- **Event**: Real-time events, pub/sub messaging
|
|
119
|
+
|
|
120
|
+
### **Developer Experience (3 modules) - Use Third**
|
|
121
|
+
|
|
122
|
+
- **Util**: Safe object access, array operations, string utilities, performance
|
|
123
|
+
helpers
|
|
124
|
+
- **Config**: Environment variable parsing, application configuration
|
|
125
|
+
- **Logger**: Structured logging, multiple transports
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## IMPORT PATTERNS
|
|
130
|
+
|
|
131
|
+
**ALWAYS use direct module imports for best tree-shaking:**
|
|
132
|
+
|
|
133
|
+
✅ **BEST (Perfect tree-shaking):**
|
|
134
|
+
|
|
135
|
+
```javascript
|
|
136
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
137
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
138
|
+
import { configClass } from '@bloomneo/appkit/config';
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
✅ **GOOD (Still tree-shakable):**
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
import { utilClass, authClass, configClass } from '@bloomneo/appkit';
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
❌ **AVOID (Poor tree-shaking):**
|
|
148
|
+
|
|
149
|
+
```javascript
|
|
150
|
+
import * as appkit from '@bloomneo/appkit';
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
---
|
|
154
|
+
|
|
155
|
+
## ENVIRONMENT-DRIVEN SCALING
|
|
156
|
+
|
|
157
|
+
**AppKit automatically scales based on environment variables:**
|
|
158
|
+
|
|
159
|
+
### **Development (Zero Config)**
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
# No environment variables needed
|
|
163
|
+
npm start
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
- Memory cache/queue
|
|
167
|
+
- Local file storage
|
|
168
|
+
- Console logging
|
|
169
|
+
- Single database
|
|
170
|
+
|
|
171
|
+
### **Production (Auto-Detection)**
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
# Set these - everything scales automatically
|
|
175
|
+
REDIS_URL=redis://... # → Distributed cache/queue
|
|
176
|
+
DATABASE_URL=postgres://... # → Database logging/queue
|
|
177
|
+
AWS_S3_BUCKET=bucket # → Cloud storage
|
|
178
|
+
RESEND_API_KEY=re_... # → Email service
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## UNIVERSAL ERROR HANDLING PATTERNS
|
|
184
|
+
|
|
185
|
+
### **Pattern 1: Safe Access with Defaults**
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
// ALWAYS use util.get() for safe property access
|
|
189
|
+
const util = utilClass.get();
|
|
190
|
+
|
|
191
|
+
❌ const name = user.profile.name; // Can crash
|
|
192
|
+
✅ const name = util.get(user, 'profile.name', 'Guest'); // Never crashes
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### **Pattern 2: Try-Catch with Specific Errors**
|
|
196
|
+
|
|
197
|
+
```javascript
|
|
198
|
+
const error = errorClass.get();
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
await someOperation();
|
|
202
|
+
} catch (err) {
|
|
203
|
+
if (err.message.includes('validation')) {
|
|
204
|
+
throw error.badRequest('Invalid input data');
|
|
205
|
+
}
|
|
206
|
+
if (err.message.includes('permission')) {
|
|
207
|
+
throw error.forbidden('Access denied');
|
|
208
|
+
}
|
|
209
|
+
if (err.message.includes('not found')) {
|
|
210
|
+
throw error.notFound('Resource not found');
|
|
211
|
+
}
|
|
212
|
+
// Default to server error
|
|
213
|
+
throw error.serverError('Operation failed');
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### **Pattern 3: Startup Validation**
|
|
218
|
+
|
|
219
|
+
```javascript
|
|
220
|
+
// ALWAYS validate configuration at app startup
|
|
221
|
+
try {
|
|
222
|
+
const auth = authClass.get();
|
|
223
|
+
const config = configClass.get();
|
|
224
|
+
|
|
225
|
+
// Validate required config
|
|
226
|
+
config.getRequired('database.url');
|
|
227
|
+
|
|
228
|
+
console.log('✅ App validation passed');
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('❌ App validation failed:', error.message);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## VARIABLE NAMING CONVENTIONS
|
|
238
|
+
|
|
239
|
+
**ALWAYS use singular naming that matches the module name for clarity:**
|
|
240
|
+
|
|
241
|
+
### **Standard Pattern**
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const util = utilClass.get(); // Singular: 'util'
|
|
245
|
+
const config = configClass.get(); // Singular: 'config'
|
|
246
|
+
const auth = authClass.get(); // Singular: 'auth'
|
|
247
|
+
const database = databaseClass.get(); // Singular: 'database'
|
|
248
|
+
const storage = storageClass.get(); // Singular: 'storage'
|
|
249
|
+
const queue = queueClass.get(); // Singular: 'queue'
|
|
250
|
+
const email = emailClass.get(); // Singular: 'email'
|
|
251
|
+
const error = errorClass.get(); // Singular: 'error'
|
|
252
|
+
const security = securityClass.get(); // Singular: 'security'
|
|
253
|
+
const logger = loggerClass.get(); // Singular: 'logger'
|
|
254
|
+
const cache = cacheClass.get(); // Singular: 'cache' + namespace
|
|
255
|
+
const event = eventClass.get(); // Singular: 'event'
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
### **Namespaced Instances**
|
|
259
|
+
|
|
260
|
+
```javascript
|
|
261
|
+
// Logger, Cache, and Event allow parameters for organization
|
|
262
|
+
|
|
263
|
+
// Logger - component-specific logging
|
|
264
|
+
const logger = loggerClass.get('api');
|
|
265
|
+
const dbLogger = loggerClass.get('database');
|
|
266
|
+
|
|
267
|
+
// Cache - ALWAYS add namespace suffix
|
|
268
|
+
const userCache = cacheClass.get('users');
|
|
269
|
+
const sessionCache = cacheClass.get('sessions');
|
|
270
|
+
|
|
271
|
+
// Event - namespace for event isolation
|
|
272
|
+
const userEvent = eventClass.get('users');
|
|
273
|
+
const orderEvent = eventClass.get('orders');
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## FRAMEWORK INTEGRATION PATTERNS
|
|
279
|
+
|
|
280
|
+
### **Express Pattern**
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
import express from 'express';
|
|
284
|
+
import { authClass, errorClass, loggerClass } from '@bloomneo/appkit';
|
|
285
|
+
|
|
286
|
+
const app = express();
|
|
287
|
+
const auth = authClass.get();
|
|
288
|
+
const error = errorClass.get();
|
|
289
|
+
const logger = loggerClass.get('app');
|
|
290
|
+
|
|
291
|
+
// ALWAYS use this middleware order
|
|
292
|
+
app.use(express.json());
|
|
293
|
+
app.use(auth.requireLogin()); // Auth first
|
|
294
|
+
app.use('/api', routes); // Routes second
|
|
295
|
+
app.use(error.handleErrors()); // Error handling LAST
|
|
296
|
+
|
|
297
|
+
// ALWAYS use asyncRoute wrapper
|
|
298
|
+
app.post(
|
|
299
|
+
'/users',
|
|
300
|
+
error.asyncRoute(async (req, res) => {
|
|
301
|
+
// Errors automatically handled
|
|
302
|
+
})
|
|
303
|
+
);
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
### **Fastify Pattern**
|
|
307
|
+
|
|
308
|
+
```javascript
|
|
309
|
+
import Fastify from 'fastify';
|
|
310
|
+
import { authClass, errorClass } from '@bloomneo/appkit';
|
|
311
|
+
|
|
312
|
+
const fastify = Fastify();
|
|
313
|
+
const auth = authClass.get();
|
|
314
|
+
const error = errorClass.get();
|
|
315
|
+
|
|
316
|
+
// ALWAYS set error handler
|
|
317
|
+
fastify.setErrorHandler((error, request, reply) => {
|
|
318
|
+
const appError = error.statusCode ? error : error.serverError(error.message);
|
|
319
|
+
reply.status(appError.statusCode).send({
|
|
320
|
+
error: appError.type,
|
|
321
|
+
message: appError.message,
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// ALWAYS use preHandler for auth
|
|
326
|
+
fastify.get(
|
|
327
|
+
'/protected',
|
|
328
|
+
{
|
|
329
|
+
preHandler: auth.requireRole('admin.tenant'),
|
|
330
|
+
},
|
|
331
|
+
async (request, reply) => {
|
|
332
|
+
// Route handler
|
|
333
|
+
}
|
|
334
|
+
);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
## MODULE INTERACTION RULES
|
|
340
|
+
|
|
341
|
+
### **Dependency Order (ALWAYS follow this order):**
|
|
342
|
+
|
|
343
|
+
1. **Util** - No dependencies, use first
|
|
344
|
+
2. **Config** - No dependencies, use for configuration
|
|
345
|
+
3. **Logger** - Depends on Config
|
|
346
|
+
4. **Error** - Depends on Config and Logger
|
|
347
|
+
5. **Auth** - Depends on Config and Error
|
|
348
|
+
6. **Security** - Depends on Config and Error
|
|
349
|
+
7. **Database** - Depends on Config, Logger, and Error
|
|
350
|
+
8. **Cache** - Depends on Config and Logger
|
|
351
|
+
9. **Storage** - Depends on Config, Logger, and Error
|
|
352
|
+
10. **Email** - Depends on Config, Logger, and Error
|
|
353
|
+
11. **Queue** - Depends on Config, Logger, and Error
|
|
354
|
+
12. **Event** - Depends on Config, Logger, and Error
|
|
355
|
+
|
|
356
|
+
### **Safe Initialization Pattern**
|
|
357
|
+
|
|
358
|
+
```javascript
|
|
359
|
+
// ALWAYS initialize in this order
|
|
360
|
+
async function initializeApp() {
|
|
361
|
+
try {
|
|
362
|
+
// 1. Core utilities first
|
|
363
|
+
const config = configClass.get();
|
|
364
|
+
const logger = loggerClass.get('init');
|
|
365
|
+
|
|
366
|
+
// 2. Validate configuration
|
|
367
|
+
config.getRequired('database.url');
|
|
368
|
+
|
|
369
|
+
// 3. Initialize database
|
|
370
|
+
const database = databaseClass.get();
|
|
371
|
+
|
|
372
|
+
// 4. Initialize other services
|
|
373
|
+
const cache = cacheClass.get('app');
|
|
374
|
+
const queue = queueClass.get();
|
|
375
|
+
|
|
376
|
+
logger.info('✅ App initialized successfully');
|
|
377
|
+
} catch (error) {
|
|
378
|
+
console.error('❌ App initialization failed:', error.message);
|
|
379
|
+
process.exit(1);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
---
|
|
385
|
+
|
|
386
|
+
## TESTING PATTERNS
|
|
387
|
+
|
|
388
|
+
### **Module Reset Between Tests**
|
|
389
|
+
|
|
390
|
+
```javascript
|
|
391
|
+
import {
|
|
392
|
+
utilClass,
|
|
393
|
+
loggerClass,
|
|
394
|
+
cacheClass,
|
|
395
|
+
configClass,
|
|
396
|
+
} from '@bloomneo/appkit';
|
|
397
|
+
|
|
398
|
+
describe('App Tests', () => {
|
|
399
|
+
afterEach(async () => {
|
|
400
|
+
// ALWAYS reset module state between tests
|
|
401
|
+
utilClass.clearCache();
|
|
402
|
+
await loggerClass.clear();
|
|
403
|
+
await cacheClass.clear();
|
|
404
|
+
configClass.clearCache();
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test('should process data safely', () => {
|
|
408
|
+
const util = utilClass.get();
|
|
409
|
+
const result = util.get({ user: { name: 'John' } }, 'user.name');
|
|
410
|
+
expect(result).toBe('John');
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
---
|
|
416
|
+
|
|
417
|
+
## PRODUCTION DEPLOYMENT PATTERNS
|
|
418
|
+
|
|
419
|
+
### **Environment Validation**
|
|
420
|
+
|
|
421
|
+
```javascript
|
|
422
|
+
// ALWAYS validate environment at startup
|
|
423
|
+
function validateProductionEnv() {
|
|
424
|
+
if (process.env.NODE_ENV !== 'production') return;
|
|
425
|
+
|
|
426
|
+
const required = ['VOILA_AUTH_SECRET', 'DATABASE_URL', 'REDIS_URL'];
|
|
427
|
+
const missing = required.filter((key) => !process.env[key]);
|
|
428
|
+
|
|
429
|
+
if (missing.length > 0) {
|
|
430
|
+
console.error('❌ Missing required environment variables:', missing);
|
|
431
|
+
process.exit(1);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
console.log('✅ Production environment validated');
|
|
435
|
+
}
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### **Graceful Shutdown**
|
|
439
|
+
|
|
440
|
+
```javascript
|
|
441
|
+
// ALWAYS implement graceful shutdown
|
|
442
|
+
async function gracefulShutdown() {
|
|
443
|
+
console.log('🔄 Shutting down gracefully...');
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
await database.disconnect();
|
|
447
|
+
await queue.close();
|
|
448
|
+
await logger.flush();
|
|
449
|
+
|
|
450
|
+
console.log('✅ Shutdown complete');
|
|
451
|
+
process.exit(0);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
console.error('❌ Shutdown error:', error);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
459
|
+
process.on('SIGINT', gracefulShutdown);
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
# Essential Modules 🚀
|
|
463
|
+
|
|
464
|
+
> **Core modules every application needs - Util, Config, Auth, Logger**
|
|
465
|
+
|
|
466
|
+
## 🛠️ UTIL MODULE - ALL 12 METHODS
|
|
467
|
+
|
|
468
|
+
### When to Use
|
|
469
|
+
|
|
470
|
+
✅ **Safe property access, array operations, string utilities, performance
|
|
471
|
+
helpers**
|
|
472
|
+
❌ **Complex data transformations, DOM manipulation, heavy math**
|
|
473
|
+
|
|
474
|
+
### Core Pattern
|
|
475
|
+
|
|
476
|
+
```javascript
|
|
477
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
478
|
+
const util = utilClass.get();
|
|
479
|
+
```
|
|
480
|
+
|
|
481
|
+
### Complete API (All 12 Methods)
|
|
482
|
+
|
|
483
|
+
```javascript
|
|
484
|
+
// 1. get() - Safe property access (NEVER crashes)
|
|
485
|
+
const name = util.get(user, 'profile.name', 'Guest');
|
|
486
|
+
const items = util.get(response, 'data.items', []);
|
|
487
|
+
const nested = util.get(obj, 'users[0].addresses[1].city', 'N/A');
|
|
488
|
+
|
|
489
|
+
// 2. isEmpty() - Universal empty check
|
|
490
|
+
if (util.isEmpty(req.body.email)) throw error.badRequest('Email required');
|
|
491
|
+
util.isEmpty(null); // → true
|
|
492
|
+
util.isEmpty({}); // → true
|
|
493
|
+
util.isEmpty([]); // → true
|
|
494
|
+
util.isEmpty(''); // → true
|
|
495
|
+
util.isEmpty(' '); // → true (whitespace only)
|
|
496
|
+
util.isEmpty(0); // → false (number is not empty)
|
|
497
|
+
util.isEmpty(false); // → false (boolean is not empty)
|
|
498
|
+
|
|
499
|
+
// 3. slugify() - URL-safe strings
|
|
500
|
+
const slug = util.slugify('Product Name!'); // → 'product-name'
|
|
501
|
+
const userSlug = util.slugify('User@Email.com'); // → 'user-email-com'
|
|
502
|
+
|
|
503
|
+
// 4. chunk() - Split arrays into batches
|
|
504
|
+
const batches = util.chunk(largeArray, 100);
|
|
505
|
+
util.chunk([1, 2, 3, 4, 5, 6], 2); // → [[1,2], [3,4], [5,6]]
|
|
506
|
+
|
|
507
|
+
// 5. debounce() - Prevent excessive function calls
|
|
508
|
+
const debouncedSearch = util.debounce(searchAPI, 300);
|
|
509
|
+
const saveSettings = util.debounce(saveToStorage, 1000);
|
|
510
|
+
|
|
511
|
+
// 6. pick() - Extract specific object properties
|
|
512
|
+
const publicUser = util.pick(user, ['id', 'name', 'email']);
|
|
513
|
+
|
|
514
|
+
// 7. unique() - Remove duplicates
|
|
515
|
+
const uniqueIds = util.unique([1, 2, 2, 3, 3, 4]); // → [1, 2, 3, 4]
|
|
516
|
+
|
|
517
|
+
// 8. clamp() - Constrain numbers to range
|
|
518
|
+
const volume = util.clamp(userInput, 0, 1); // Audio volume
|
|
519
|
+
util.clamp(150, 0, 100); // → 100 (max limit)
|
|
520
|
+
util.clamp(-10, 0, 100); // → 0 (min limit)
|
|
521
|
+
|
|
522
|
+
// 9. formatBytes() - Human-readable file sizes
|
|
523
|
+
const size = util.formatBytes(1048576); // → '1 MB'
|
|
524
|
+
util.formatBytes(1024); // → '1 KB'
|
|
525
|
+
|
|
526
|
+
// 10. truncate() - Smart text cutting
|
|
527
|
+
const preview = util.truncate(longText, { length: 100, preserveWords: true });
|
|
528
|
+
|
|
529
|
+
// 11. sleep() - Promise-based delays
|
|
530
|
+
await util.sleep(1000); // Wait 1 second
|
|
531
|
+
|
|
532
|
+
// 12. uuid() - Generate unique identifiers
|
|
533
|
+
const sessionId = util.uuid(); // → 'f47ac10b-58cc-4372-a567-0e02b2c3d479'
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
---
|
|
537
|
+
|
|
538
|
+
## ⚙️ CONFIG MODULE
|
|
539
|
+
|
|
540
|
+
### When to Use
|
|
541
|
+
|
|
542
|
+
✅ **Environment variables, application settings, startup validation**
|
|
543
|
+
❌ **Runtime configuration changes, user preferences**
|
|
544
|
+
|
|
545
|
+
### Core Pattern
|
|
546
|
+
|
|
547
|
+
```javascript
|
|
548
|
+
import { configClass } from '@bloomneo/appkit/config';
|
|
549
|
+
const config = configClass.get();
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
### Environment Variable Convention
|
|
553
|
+
|
|
554
|
+
**UPPER_SNAKE_CASE** automatically becomes nested dot notation:
|
|
555
|
+
|
|
556
|
+
```bash
|
|
557
|
+
# Environment Variable → Config Path
|
|
558
|
+
DATABASE_HOST=localhost → config.get('database.host')
|
|
559
|
+
DATABASE_CONNECTION_POOL_SIZE=10 → config.get('database.connection.pool_size')
|
|
560
|
+
STRIPE_API_KEYS_PUBLIC=pk_test_123 → config.get('stripe.api.keys.public')
|
|
561
|
+
FEATURES_ANALYTICS_ENABLED=true → config.get('features.analytics.enabled')
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
### Essential Patterns
|
|
565
|
+
|
|
566
|
+
```javascript
|
|
567
|
+
// 1. Required values (throws if missing)
|
|
568
|
+
const dbUrl = config.getRequired('database.url');
|
|
569
|
+
const authSecret = config.getRequired('auth.secret');
|
|
570
|
+
|
|
571
|
+
// 2. Optional with defaults
|
|
572
|
+
const port = config.get('server.port', 3000);
|
|
573
|
+
const timeout = config.get('api.timeout', 5000);
|
|
574
|
+
|
|
575
|
+
// 3. Environment detection
|
|
576
|
+
const isDev = config.isDevelopment();
|
|
577
|
+
const isProd = config.isProduction();
|
|
578
|
+
|
|
579
|
+
// 4. Startup validation
|
|
580
|
+
try {
|
|
581
|
+
config.getRequired('database.url');
|
|
582
|
+
config.getRequired('auth.secret');
|
|
583
|
+
|
|
584
|
+
if (config.isProduction()) {
|
|
585
|
+
config.getRequired('redis.url');
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
console.log('✅ Configuration validation passed');
|
|
589
|
+
} catch (error) {
|
|
590
|
+
console.error('❌ Configuration validation failed:', error.message);
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
597
|
+
## 🔐 AUTH MODULE
|
|
598
|
+
|
|
599
|
+
### When to Use
|
|
600
|
+
|
|
601
|
+
✅ **Dual token system, JWT operations, role-level permissions, API security, password hashing**
|
|
602
|
+
❌ **Frontend authentication, OAuth providers, session storage**
|
|
603
|
+
|
|
604
|
+
### Core Pattern
|
|
605
|
+
|
|
606
|
+
```javascript
|
|
607
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
608
|
+
const auth = authClass.get();
|
|
609
|
+
```
|
|
610
|
+
|
|
611
|
+
### Dual Token System (CRITICAL - Two Distinct Types)
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
// ✅ LOGIN TOKENS - For user authentication (mobile/web)
|
|
615
|
+
const loginToken = auth.generateLoginToken({
|
|
616
|
+
userId: 123, // REQUIRED - unique user identifier
|
|
617
|
+
role: 'admin', // REQUIRED - role name (admin, user, moderator)
|
|
618
|
+
level: 'tenant', // REQUIRED - level within role (basic, tenant, org, system)
|
|
619
|
+
}, '7d'); // Short-medium expiry
|
|
620
|
+
|
|
621
|
+
// ✅ API TOKENS - For service authentication (webhooks/integrations)
|
|
622
|
+
const apiToken = auth.generateApiToken({
|
|
623
|
+
keyId: 'webhook_service', // REQUIRED - service identifier
|
|
624
|
+
role: 'service', // REQUIRED - role name
|
|
625
|
+
level: 'external', // REQUIRED - level within role
|
|
626
|
+
}, '1y'); // Long expiry
|
|
627
|
+
|
|
628
|
+
// ❌ WRONG - Don't mix these up
|
|
629
|
+
auth.generateLoginToken({ keyId: 'test' }); // keyId is for API tokens
|
|
630
|
+
auth.generateApiToken({ userId: 123 }); // userId is for login tokens
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### Role Hierarchy (Built-in Inheritance)
|
|
634
|
+
|
|
635
|
+
```
|
|
636
|
+
admin.system > admin.org > admin.tenant >
|
|
637
|
+
moderator.manage > moderator.approve > moderator.review >
|
|
638
|
+
user.max > user.pro > user.basic
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
### Essential Auth Patterns
|
|
642
|
+
|
|
643
|
+
```javascript
|
|
644
|
+
// 1. User route protection (login tokens only)
|
|
645
|
+
app.get('/profile', auth.requireLoginToken(), handler);
|
|
646
|
+
app.get('/admin', auth.requireLoginToken(), auth.requireUserRoles(['admin.tenant']), handler);
|
|
647
|
+
|
|
648
|
+
// 2. API route protection (API tokens only)
|
|
649
|
+
app.post('/webhook/data', auth.requireApiToken(), handler);
|
|
650
|
+
|
|
651
|
+
// 3. Safe user extraction (works with both token types)
|
|
652
|
+
const user = auth.user(req);
|
|
653
|
+
if (!user) throw error.unauthorized('Authentication required');
|
|
654
|
+
|
|
655
|
+
// 4. Role hierarchy checking
|
|
656
|
+
const userRoleLevel = `${user.role}.${user.level}`;
|
|
657
|
+
if (!auth.hasRole(userRoleLevel, 'admin.tenant')) {
|
|
658
|
+
throw error.forbidden('Admin access required');
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 5. Permission checking (action:scope format)
|
|
662
|
+
if (!auth.can(user, 'manage:tenant')) {
|
|
663
|
+
throw error.forbidden('Insufficient permissions');
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// 6. Password handling
|
|
667
|
+
const hashedPassword = await auth.hashPassword(plainPassword);
|
|
668
|
+
const isValid = await auth.comparePassword(plainPassword, hashedPassword);
|
|
669
|
+
|
|
670
|
+
// 7. Token operations (works with both types)
|
|
671
|
+
const payload = auth.verifyToken(token);
|
|
672
|
+
```
|
|
673
|
+
|
|
674
|
+
### Complete Authentication Flow
|
|
675
|
+
|
|
676
|
+
```javascript
|
|
677
|
+
// User login endpoint (generates login token)
|
|
678
|
+
app.post(
|
|
679
|
+
'/auth/login',
|
|
680
|
+
error.asyncRoute(async (req, res) => {
|
|
681
|
+
const { email, password } = req.body;
|
|
682
|
+
|
|
683
|
+
if (util.isEmpty(email)) throw error.badRequest('Email required');
|
|
684
|
+
if (util.isEmpty(password)) throw error.badRequest('Password required');
|
|
685
|
+
|
|
686
|
+
const user = await database.user.findUnique({ where: { email } });
|
|
687
|
+
if (!user) throw error.unauthorized('Invalid credentials');
|
|
688
|
+
|
|
689
|
+
const isValid = await auth.comparePassword(password, user.password);
|
|
690
|
+
if (!isValid) throw error.unauthorized('Invalid credentials');
|
|
691
|
+
|
|
692
|
+
// Generate login token for user authentication
|
|
693
|
+
const loginToken = auth.generateLoginToken({
|
|
694
|
+
userId: user.id,
|
|
695
|
+
role: user.role,
|
|
696
|
+
level: user.level,
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
res.json({
|
|
700
|
+
token: loginToken,
|
|
701
|
+
user: util.pick(user, ['id', 'email', 'name']),
|
|
702
|
+
});
|
|
703
|
+
})
|
|
704
|
+
);
|
|
705
|
+
|
|
706
|
+
// API token creation endpoint (admin-only)
|
|
707
|
+
app.post(
|
|
708
|
+
'/admin/api-tokens',
|
|
709
|
+
auth.requireLoginToken(),
|
|
710
|
+
auth.requireUserRoles(['admin.tenant']),
|
|
711
|
+
error.asyncRoute(async (req, res) => {
|
|
712
|
+
const { keyId, permissions } = req.body;
|
|
713
|
+
|
|
714
|
+
// Generate API token for service authentication
|
|
715
|
+
const apiToken = auth.generateApiToken({
|
|
716
|
+
keyId,
|
|
717
|
+
role: 'service',
|
|
718
|
+
level: 'external',
|
|
719
|
+
permissions,
|
|
720
|
+
}, '1y');
|
|
721
|
+
|
|
722
|
+
// Store token info in database (store hash, not plain token)
|
|
723
|
+
const hashedToken = await auth.hashPassword(apiToken);
|
|
724
|
+
await database.apiToken.create({
|
|
725
|
+
data: { keyId, token: hashedToken, permissions },
|
|
726
|
+
});
|
|
727
|
+
|
|
728
|
+
res.json({ apiToken }); // Return once for client to save
|
|
729
|
+
})
|
|
730
|
+
);
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
---
|
|
734
|
+
|
|
735
|
+
## 📝 LOGGER MODULE
|
|
736
|
+
|
|
737
|
+
### When to Use
|
|
738
|
+
|
|
739
|
+
✅ **Structured logging, error tracking, performance monitoring**
|
|
740
|
+
❌ **Simple console.log, debugging only**
|
|
741
|
+
|
|
742
|
+
### Core Pattern
|
|
743
|
+
|
|
744
|
+
```javascript
|
|
745
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
746
|
+
const logger = loggerClass.get();
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
### Auto-Transport Selection
|
|
750
|
+
|
|
751
|
+
```bash
|
|
752
|
+
# Development → Console
|
|
753
|
+
# Production with DATABASE_URL → Database
|
|
754
|
+
# Production with REDIS_URL → Redis
|
|
755
|
+
# Production with VOILA_LOGGER_HTTP_URL → HTTP endpoint
|
|
756
|
+
```
|
|
757
|
+
|
|
758
|
+
### Essential Patterns
|
|
759
|
+
|
|
760
|
+
```javascript
|
|
761
|
+
// 1. Component-specific loggers
|
|
762
|
+
const logger = loggerClass.get('api');
|
|
763
|
+
const dbLogger = loggerClass.get('database');
|
|
764
|
+
|
|
765
|
+
// 2. Structured logging with context
|
|
766
|
+
logger.info('User created', {
|
|
767
|
+
userId: user.id,
|
|
768
|
+
email: user.email,
|
|
769
|
+
timestamp: Date.now(),
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// 3. Error logging
|
|
773
|
+
try {
|
|
774
|
+
await riskyOperation();
|
|
775
|
+
} catch (err) {
|
|
776
|
+
logger.error('Operation failed', {
|
|
777
|
+
error: err.message,
|
|
778
|
+
stack: err.stack,
|
|
779
|
+
userId: req.user?.id,
|
|
780
|
+
});
|
|
781
|
+
throw error.serverError('Operation failed');
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// 4. Request logging middleware
|
|
785
|
+
app.use((req, res, next) => {
|
|
786
|
+
req.requestId = util.uuid();
|
|
787
|
+
req.logger = logger.child({
|
|
788
|
+
requestId: req.requestId,
|
|
789
|
+
method: req.method,
|
|
790
|
+
url: req.url,
|
|
791
|
+
});
|
|
792
|
+
|
|
793
|
+
const startTime = Date.now();
|
|
794
|
+
res.on('finish', () => {
|
|
795
|
+
req.logger.info('Request completed', {
|
|
796
|
+
statusCode: res.statusCode,
|
|
797
|
+
duration: Date.now() - startTime,
|
|
798
|
+
});
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
next();
|
|
802
|
+
});
|
|
803
|
+
```
|
|
804
|
+
|
|
805
|
+
---
|
|
806
|
+
|
|
807
|
+
## ESSENTIAL MODULES INTEGRATION
|
|
808
|
+
|
|
809
|
+
```javascript
|
|
810
|
+
// Complete API endpoint using all 4 essential modules
|
|
811
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
812
|
+
import { configClass } from '@bloomneo/appkit/config';
|
|
813
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
814
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
815
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
816
|
+
|
|
817
|
+
const util = utilClass.get();
|
|
818
|
+
const config = configClass.get();
|
|
819
|
+
const auth = authClass.get();
|
|
820
|
+
const logger = loggerClass.get('api');
|
|
821
|
+
const error = errorClass.get();
|
|
822
|
+
|
|
823
|
+
// User profile update endpoint
|
|
824
|
+
app.put(
|
|
825
|
+
'/api/profile',
|
|
826
|
+
auth.requireLogin(),
|
|
827
|
+
error.asyncRoute(async (req, res) => {
|
|
828
|
+
const user = auth.user(req);
|
|
829
|
+
const requestLogger = logger.child({
|
|
830
|
+
userId: user.userId,
|
|
831
|
+
requestId: util.uuid(),
|
|
832
|
+
});
|
|
833
|
+
|
|
834
|
+
// Safe data extraction
|
|
835
|
+
const name = util.get(req.body, 'name', '').trim();
|
|
836
|
+
const bio = util.get(req.body, 'bio', '').trim();
|
|
837
|
+
|
|
838
|
+
// Input validation
|
|
839
|
+
if (util.isEmpty(name)) {
|
|
840
|
+
requestLogger.warn('Profile update failed - empty name');
|
|
841
|
+
throw error.badRequest('Name is required');
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// Length validation from config
|
|
845
|
+
const maxBioLength = config.get('user.bio.maxLength', 500);
|
|
846
|
+
if (bio.length > maxBioLength) {
|
|
847
|
+
throw error.badRequest(`Bio too long (max ${maxBioLength} characters)`);
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Create slug for profile URL
|
|
851
|
+
const slug = util.slugify(name);
|
|
852
|
+
|
|
853
|
+
// Update user data
|
|
854
|
+
const updatedUser = await database.user.update({
|
|
855
|
+
where: { id: user.userId },
|
|
856
|
+
data: { name, bio: util.isEmpty(bio) ? null : bio, slug },
|
|
857
|
+
});
|
|
858
|
+
|
|
859
|
+
requestLogger.info('Profile updated successfully');
|
|
860
|
+
|
|
861
|
+
res.json({
|
|
862
|
+
success: true,
|
|
863
|
+
user: util.pick(updatedUser, ['id', 'name', 'bio', 'slug']),
|
|
864
|
+
});
|
|
865
|
+
})
|
|
866
|
+
);
|
|
867
|
+
```
|
|
868
|
+
|
|
869
|
+
# Infrastructure Modules 📊
|
|
870
|
+
|
|
871
|
+
> **Data persistence, communication, and background processing - Database,
|
|
872
|
+
> Cache, Storage, Queue, Email**
|
|
873
|
+
|
|
874
|
+
## 🗄️ DATABASE MODULE
|
|
875
|
+
|
|
876
|
+
### When to Use
|
|
877
|
+
|
|
878
|
+
✅ **Persistent data, user data, content, multi-tenant apps, cross-cloud
|
|
879
|
+
orgs**
|
|
880
|
+
❌ **Temporary data, files, caching, session storage**
|
|
881
|
+
|
|
882
|
+
### Core Pattern
|
|
883
|
+
|
|
884
|
+
```javascript
|
|
885
|
+
import { databaseClass } from '@bloomneo/appkit/database';
|
|
886
|
+
const database = databaseClass.get();
|
|
887
|
+
```
|
|
888
|
+
|
|
889
|
+
### Progressive Scaling (CRITICAL)
|
|
890
|
+
|
|
891
|
+
```javascript
|
|
892
|
+
// Day 1: Single database
|
|
893
|
+
const database = await databaseClass.get();
|
|
894
|
+
const users = await database.user.findMany();
|
|
895
|
+
|
|
896
|
+
// Month 6: Multi-tenant (zero code changes!)
|
|
897
|
+
// Just add: VOILA_DB_TENANT=auto
|
|
898
|
+
const database = await databaseClass.get(); // Now auto-filtered by tenant
|
|
899
|
+
|
|
900
|
+
// Year 1: Multi-org (still zero code changes!)
|
|
901
|
+
// Add: ORG_ACME=postgresql://acme.aws.com/db
|
|
902
|
+
const acmeDatabase = await databaseClass.org('acme').get();
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
### Essential API (3 Core Patterns)
|
|
906
|
+
|
|
907
|
+
```javascript
|
|
908
|
+
// 1. Normal user access (single or tenant-filtered)
|
|
909
|
+
const database = await databaseClass.get();
|
|
910
|
+
const users = await database.user.findMany(); // Auto-filtered if tenant mode
|
|
911
|
+
|
|
912
|
+
// 2. Admin access (all tenants)
|
|
913
|
+
const dbTenants = await databaseClass.getTenants();
|
|
914
|
+
const allUsers = await dbTenants.user.findMany(); // Cross-tenant
|
|
915
|
+
|
|
916
|
+
// 3. Organization-specific access
|
|
917
|
+
const acmeDatabase = await databaseClass.org('acme').get();
|
|
918
|
+
const acmeUsers = await acmeDatabase.user.findMany();
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
### MANDATORY Schema Requirements
|
|
922
|
+
|
|
923
|
+
```sql
|
|
924
|
+
-- ✅ EVERY table MUST include tenant_id from Day 1 (nullable for future)
|
|
925
|
+
CREATE TABLE users (
|
|
926
|
+
id uuid PRIMARY KEY,
|
|
927
|
+
email text UNIQUE,
|
|
928
|
+
name text,
|
|
929
|
+
tenant_id text, -- MANDATORY: nullable for future
|
|
930
|
+
created_at timestamp DEFAULT now(),
|
|
931
|
+
|
|
932
|
+
INDEX idx_users_tenant (tenant_id) -- MANDATORY: performance index
|
|
933
|
+
);
|
|
934
|
+
|
|
935
|
+
CREATE TABLE posts (
|
|
936
|
+
id uuid PRIMARY KEY,
|
|
937
|
+
title text,
|
|
938
|
+
content text,
|
|
939
|
+
user_id uuid REFERENCES users(id),
|
|
940
|
+
tenant_id text, -- MANDATORY: on EVERY table
|
|
941
|
+
created_at timestamp DEFAULT now(),
|
|
942
|
+
|
|
943
|
+
INDEX idx_posts_tenant (tenant_id) -- MANDATORY: on EVERY table
|
|
944
|
+
);
|
|
945
|
+
```
|
|
946
|
+
|
|
947
|
+
---
|
|
948
|
+
|
|
949
|
+
## 💾 CACHE MODULE
|
|
950
|
+
|
|
951
|
+
### When to Use
|
|
952
|
+
|
|
953
|
+
✅ **Speed up database queries, session data, API responses, computed
|
|
954
|
+
results**
|
|
955
|
+
❌ **Permanent data, files, transactions, sensitive data without encryption**
|
|
956
|
+
|
|
957
|
+
### Core Pattern
|
|
958
|
+
|
|
959
|
+
```javascript
|
|
960
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
961
|
+
const cache = cacheClass.get(); // Default 'app' namespace
|
|
962
|
+
```
|
|
963
|
+
|
|
964
|
+
### Auto-Strategy Detection
|
|
965
|
+
|
|
966
|
+
```bash
|
|
967
|
+
# Development → Memory cache with LRU
|
|
968
|
+
# Production → Redis (if REDIS_URL) → Memory
|
|
969
|
+
```
|
|
970
|
+
|
|
971
|
+
### Essential API (5 Core Methods)
|
|
972
|
+
|
|
973
|
+
```javascript
|
|
974
|
+
// 1. set() - Store with TTL (seconds)
|
|
975
|
+
await cache.set('user:123', userData, 3600); // 1 hour TTL
|
|
976
|
+
|
|
977
|
+
// 2. get() - Retrieve (null if not found/expired)
|
|
978
|
+
const user = await cache.get('user:123');
|
|
979
|
+
|
|
980
|
+
// 3. getOrSet() - Get or compute and cache
|
|
981
|
+
const weather = await cache.getOrSet(
|
|
982
|
+
`weather:${city}`,
|
|
983
|
+
async () => {
|
|
984
|
+
return await fetchWeatherAPI(city); // Only runs on cache miss
|
|
985
|
+
},
|
|
986
|
+
1800 // 30 minutes
|
|
987
|
+
);
|
|
988
|
+
|
|
989
|
+
// 4. delete() - Remove specific key
|
|
990
|
+
await cache.delete('user:123');
|
|
991
|
+
|
|
992
|
+
// 5. clear() - Clear entire namespace
|
|
993
|
+
await cache.clear();
|
|
994
|
+
```
|
|
995
|
+
|
|
996
|
+
### Namespace Isolation
|
|
997
|
+
|
|
998
|
+
```javascript
|
|
999
|
+
// ALWAYS use specific namespaces - completely isolated
|
|
1000
|
+
const userCache = cacheClass.get('users');
|
|
1001
|
+
const sessionCache = cacheClass.get('sessions');
|
|
1002
|
+
const apiCache = cacheClass.get('external-api');
|
|
1003
|
+
|
|
1004
|
+
await userCache.set('123', userData);
|
|
1005
|
+
await sessionCache.set('123', sessionData); // Different from user:123
|
|
1006
|
+
```
|
|
1007
|
+
|
|
1008
|
+
---
|
|
1009
|
+
|
|
1010
|
+
## 📁 STORAGE MODULE
|
|
1011
|
+
|
|
1012
|
+
### When to Use
|
|
1013
|
+
|
|
1014
|
+
✅ **File uploads, documents, images, videos, CDN integration**
|
|
1015
|
+
❌ **Configuration data, temporary data, session storage**
|
|
1016
|
+
|
|
1017
|
+
### Core Pattern
|
|
1018
|
+
|
|
1019
|
+
```javascript
|
|
1020
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
1021
|
+
const storage = storageClass.get();
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Auto-Strategy Detection
|
|
1025
|
+
|
|
1026
|
+
```bash
|
|
1027
|
+
# Development → Local files in ./uploads/
|
|
1028
|
+
# Production → R2 (if CLOUDFLARE_R2_BUCKET) → S3 (if AWS_S3_BUCKET) → Local
|
|
1029
|
+
```
|
|
1030
|
+
|
|
1031
|
+
### Essential API (4 Core Methods)
|
|
1032
|
+
|
|
1033
|
+
```javascript
|
|
1034
|
+
// 1. put() - Upload files
|
|
1035
|
+
await storage.put('avatars/user123.jpg', imageBuffer);
|
|
1036
|
+
await storage.put('docs/contract.pdf', pdfBuffer, {
|
|
1037
|
+
contentType: 'application/pdf',
|
|
1038
|
+
cacheControl: 'public, max-age=3600',
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
// 2. get() - Download files
|
|
1042
|
+
const buffer = await storage.get('avatars/user123.jpg');
|
|
1043
|
+
if (!buffer) throw error.notFound('File not found');
|
|
1044
|
+
|
|
1045
|
+
// 3. delete() - Remove files
|
|
1046
|
+
await storage.delete('temp/old-file.jpg');
|
|
1047
|
+
|
|
1048
|
+
// 4. url() - Get public URLs
|
|
1049
|
+
const url = storage.url('avatars/user123.jpg');
|
|
1050
|
+
// Local: /uploads/avatars/user123.jpg
|
|
1051
|
+
// S3: https://bucket.s3.region.amazonaws.com/avatars/user123.jpg
|
|
1052
|
+
// R2: https://cdn.example.com/avatars/user123.jpg
|
|
1053
|
+
```
|
|
1054
|
+
|
|
1055
|
+
---
|
|
1056
|
+
|
|
1057
|
+
## 🚀 QUEUE MODULE
|
|
1058
|
+
|
|
1059
|
+
### When to Use
|
|
1060
|
+
|
|
1061
|
+
✅ **Background jobs, emails, file processing, webhooks, scheduled tasks**
|
|
1062
|
+
❌ **Real-time operations, simple sync operations, immediate responses**
|
|
1063
|
+
|
|
1064
|
+
### Core Pattern
|
|
1065
|
+
|
|
1066
|
+
```javascript
|
|
1067
|
+
import { queueClass } from '@bloomneo/appkit/queue';
|
|
1068
|
+
const queue = queueClass.get();
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### Auto-Transport Detection
|
|
1072
|
+
|
|
1073
|
+
```bash
|
|
1074
|
+
# Development → Memory queue
|
|
1075
|
+
# Production → Redis (if REDIS_URL) → Database (if DATABASE_URL) → Memory
|
|
1076
|
+
```
|
|
1077
|
+
|
|
1078
|
+
### Essential API (3 Core Methods)
|
|
1079
|
+
|
|
1080
|
+
```javascript
|
|
1081
|
+
// 1. add() - Add jobs to queue
|
|
1082
|
+
await queue.add('email', {
|
|
1083
|
+
to: 'user@example.com',
|
|
1084
|
+
subject: 'Welcome!',
|
|
1085
|
+
body: 'Thanks for signing up',
|
|
1086
|
+
});
|
|
1087
|
+
|
|
1088
|
+
await queue.add(
|
|
1089
|
+
'image-resize',
|
|
1090
|
+
{
|
|
1091
|
+
input: 'uploads/large.jpg',
|
|
1092
|
+
output: 'thumbnails/thumb.jpg',
|
|
1093
|
+
width: 200,
|
|
1094
|
+
},
|
|
1095
|
+
{
|
|
1096
|
+
delay: 5000, // Start in 5 seconds
|
|
1097
|
+
attempts: 3, // Retry 3 times
|
|
1098
|
+
}
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
// 2. process() - Handle jobs
|
|
1102
|
+
queue.process('email', async (data) => {
|
|
1103
|
+
await sendEmail(data.to, data.subject, data.body);
|
|
1104
|
+
return { sent: true };
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
queue.process('image-resize', async (data) => {
|
|
1108
|
+
await resizeImage(data.input, data.output, data.width);
|
|
1109
|
+
return { resized: true };
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
// 3. schedule() - Delayed jobs
|
|
1113
|
+
await queue.schedule(
|
|
1114
|
+
'reminder',
|
|
1115
|
+
{
|
|
1116
|
+
userId: 123,
|
|
1117
|
+
message: 'Your trial ends soon!',
|
|
1118
|
+
},
|
|
1119
|
+
7 * 24 * 60 * 60 * 1000
|
|
1120
|
+
); // 7 days
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
---
|
|
1124
|
+
|
|
1125
|
+
## 📧 EMAIL MODULE
|
|
1126
|
+
|
|
1127
|
+
### When to Use
|
|
1128
|
+
|
|
1129
|
+
✅ **Transactional emails, notifications, templates, multi-provider support**
|
|
1130
|
+
❌ **Marketing emails, bulk campaigns, newsletter management**
|
|
1131
|
+
|
|
1132
|
+
### Core Pattern
|
|
1133
|
+
|
|
1134
|
+
```javascript
|
|
1135
|
+
import { emailClass } from '@bloomneo/appkit/email';
|
|
1136
|
+
const email = emailClass.get();
|
|
1137
|
+
```
|
|
1138
|
+
|
|
1139
|
+
### Auto-Provider Detection
|
|
1140
|
+
|
|
1141
|
+
```bash
|
|
1142
|
+
# Production → Resend (if RESEND_API_KEY) → SMTP (if SMTP_HOST) → Console
|
|
1143
|
+
```
|
|
1144
|
+
|
|
1145
|
+
### Essential API (3 Core Methods)
|
|
1146
|
+
|
|
1147
|
+
```javascript
|
|
1148
|
+
// 1. send() - Basic email sending
|
|
1149
|
+
await email.send({
|
|
1150
|
+
to: 'user@example.com',
|
|
1151
|
+
subject: 'Welcome to our app!',
|
|
1152
|
+
text: 'Thanks for signing up.',
|
|
1153
|
+
html: '<h1>Thanks for signing up!</h1>',
|
|
1154
|
+
});
|
|
1155
|
+
|
|
1156
|
+
// 2. sendTemplate() - Template-based emails
|
|
1157
|
+
await email.sendTemplate('welcome', {
|
|
1158
|
+
to: user.email,
|
|
1159
|
+
name: user.name,
|
|
1160
|
+
activationLink: `https://app.com/activate/${user.token}`,
|
|
1161
|
+
});
|
|
1162
|
+
|
|
1163
|
+
// 3. Queue integration (recommended for production)
|
|
1164
|
+
await queue.add('email', {
|
|
1165
|
+
template: 'password-reset',
|
|
1166
|
+
to: user.email,
|
|
1167
|
+
resetLink: resetUrl,
|
|
1168
|
+
});
|
|
1169
|
+
|
|
1170
|
+
queue.process('email', async (data) => {
|
|
1171
|
+
if (data.template) {
|
|
1172
|
+
return await email.sendTemplate(data.template, data);
|
|
1173
|
+
} else {
|
|
1174
|
+
return await email.send(data);
|
|
1175
|
+
}
|
|
1176
|
+
});
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
---
|
|
1180
|
+
|
|
1181
|
+
## INFRASTRUCTURE INTEGRATION
|
|
1182
|
+
|
|
1183
|
+
```javascript
|
|
1184
|
+
// File upload with complete infrastructure integration
|
|
1185
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
1186
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
1187
|
+
import { databaseClass } from '@bloomneo/appkit/database';
|
|
1188
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
1189
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
1190
|
+
import { queueClass } from '@bloomneo/appkit/queue';
|
|
1191
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
1192
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
1193
|
+
|
|
1194
|
+
const util = utilClass.get();
|
|
1195
|
+
const auth = authClass.get();
|
|
1196
|
+
const storage = storageClass.get();
|
|
1197
|
+
const queue = queueClass.get();
|
|
1198
|
+
const error = errorClass.get();
|
|
1199
|
+
const logger = loggerClass.get('upload');
|
|
1200
|
+
|
|
1201
|
+
// File upload endpoint
|
|
1202
|
+
app.post(
|
|
1203
|
+
'/api/upload',
|
|
1204
|
+
auth.requireLogin(),
|
|
1205
|
+
upload.single('file'),
|
|
1206
|
+
error.asyncRoute(async (req, res) => {
|
|
1207
|
+
const user = auth.user(req);
|
|
1208
|
+
const userCache = cacheClass.get('users');
|
|
1209
|
+
|
|
1210
|
+
if (!req.file) {
|
|
1211
|
+
throw error.badRequest('No file uploaded');
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1214
|
+
// 1. Store file in storage (auto-detects Local/S3/R2)
|
|
1215
|
+
const fileKey = `uploads/${user.userId}/${Date.now()}-${util.slugify(req.file.originalname)}`;
|
|
1216
|
+
await storage.put(fileKey, req.file.buffer, {
|
|
1217
|
+
contentType: req.file.mimetype,
|
|
1218
|
+
});
|
|
1219
|
+
|
|
1220
|
+
// 2. Save to database (auto-filtered by tenant if enabled)
|
|
1221
|
+
const database = await databaseClass.get();
|
|
1222
|
+
const file = await database.file.create({
|
|
1223
|
+
data: {
|
|
1224
|
+
key: fileKey,
|
|
1225
|
+
name: req.file.originalname,
|
|
1226
|
+
size: req.file.size,
|
|
1227
|
+
contentType: req.file.mimetype,
|
|
1228
|
+
userId: user.userId,
|
|
1229
|
+
},
|
|
1230
|
+
});
|
|
1231
|
+
|
|
1232
|
+
// 3. Queue background processing
|
|
1233
|
+
await queue.add('process-upload', {
|
|
1234
|
+
fileId: file.id,
|
|
1235
|
+
fileKey: fileKey,
|
|
1236
|
+
userId: user.userId,
|
|
1237
|
+
});
|
|
1238
|
+
|
|
1239
|
+
// 4. Clear user's file cache
|
|
1240
|
+
await userCache.delete(`files:${user.userId}`);
|
|
1241
|
+
|
|
1242
|
+
// 5. Log activity
|
|
1243
|
+
logger.info('File uploaded', {
|
|
1244
|
+
fileId: file.id,
|
|
1245
|
+
userId: user.userId,
|
|
1246
|
+
size: util.formatBytes(req.file.size),
|
|
1247
|
+
});
|
|
1248
|
+
|
|
1249
|
+
res.json({
|
|
1250
|
+
success: true,
|
|
1251
|
+
file: {
|
|
1252
|
+
id: file.id,
|
|
1253
|
+
url: storage.url(fileKey),
|
|
1254
|
+
name: req.file.originalname,
|
|
1255
|
+
size: util.formatBytes(req.file.size),
|
|
1256
|
+
},
|
|
1257
|
+
});
|
|
1258
|
+
})
|
|
1259
|
+
);
|
|
1260
|
+
|
|
1261
|
+
// Background file processor
|
|
1262
|
+
queue.process('process-upload', async (data) => {
|
|
1263
|
+
const database = await databaseClass.get();
|
|
1264
|
+
const email = emailClass.get();
|
|
1265
|
+
const processingLogger = loggerClass.get('processing');
|
|
1266
|
+
|
|
1267
|
+
try {
|
|
1268
|
+
// Get file buffer for processing
|
|
1269
|
+
const buffer = await storage.get(data.fileKey);
|
|
1270
|
+
|
|
1271
|
+
// Process file (resize, convert, scan, etc.)
|
|
1272
|
+
const processedBuffer = await processFile(buffer);
|
|
1273
|
+
|
|
1274
|
+
// Store processed version
|
|
1275
|
+
const processedKey = data.fileKey.replace('uploads/', 'processed/');
|
|
1276
|
+
await storage.put(processedKey, processedBuffer);
|
|
1277
|
+
|
|
1278
|
+
// Update database
|
|
1279
|
+
await database.file.update({
|
|
1280
|
+
where: { id: data.fileId },
|
|
1281
|
+
data: {
|
|
1282
|
+
processed: true,
|
|
1283
|
+
processedKey,
|
|
1284
|
+
processedAt: new Date(),
|
|
1285
|
+
},
|
|
1286
|
+
});
|
|
1287
|
+
|
|
1288
|
+
// Clear cache
|
|
1289
|
+
const userCache = cacheClass.get('users');
|
|
1290
|
+
await userCache.delete(`files:${data.userId}`);
|
|
1291
|
+
|
|
1292
|
+
// Send notification email
|
|
1293
|
+
await queue.add('file-processed-email', {
|
|
1294
|
+
userId: data.userId,
|
|
1295
|
+
fileId: data.fileId,
|
|
1296
|
+
});
|
|
1297
|
+
|
|
1298
|
+
processingLogger.info('File processed successfully', {
|
|
1299
|
+
fileId: data.fileId,
|
|
1300
|
+
userId: data.userId,
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
return { processed: true, processedKey };
|
|
1304
|
+
} catch (error) {
|
|
1305
|
+
processingLogger.error('File processing failed', {
|
|
1306
|
+
fileId: data.fileId,
|
|
1307
|
+
error: error.message,
|
|
1308
|
+
});
|
|
1309
|
+
throw error;
|
|
1310
|
+
}
|
|
1311
|
+
});
|
|
1312
|
+
```
|
|
1313
|
+
|
|
1314
|
+
# System Modules 🛡️
|
|
1315
|
+
|
|
1316
|
+
> **Application security, error handling, and real-time communication - Error,
|
|
1317
|
+
> Security, Event**
|
|
1318
|
+
|
|
1319
|
+
## ⚠️ ERROR MODULE
|
|
1320
|
+
|
|
1321
|
+
### When to Use
|
|
1322
|
+
|
|
1323
|
+
✅ **HTTP APIs, status codes, middleware integration, client responses**
|
|
1324
|
+
❌ **CLI applications, non-HTTP servers, simple utilities**
|
|
1325
|
+
|
|
1326
|
+
### Core Pattern
|
|
1327
|
+
|
|
1328
|
+
```javascript
|
|
1329
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
1330
|
+
const error = errorClass.get();
|
|
1331
|
+
```
|
|
1332
|
+
|
|
1333
|
+
### HTTP Status Code Mapping (CRITICAL)
|
|
1334
|
+
|
|
1335
|
+
```javascript
|
|
1336
|
+
// ✅ CORRECT - Use these exact error types for specific situations
|
|
1337
|
+
error.badRequest('message'); // 400 - Client input errors
|
|
1338
|
+
error.unauthorized('message'); // 401 - Authentication required
|
|
1339
|
+
error.forbidden('message'); // 403 - Access denied (user authenticated, no permission)
|
|
1340
|
+
error.notFound('message'); // 404 - Resource doesn't exist
|
|
1341
|
+
error.conflict('message'); // 409 - Business logic conflicts (duplicate email)
|
|
1342
|
+
error.serverError('message'); // 500 - Internal server errors
|
|
1343
|
+
|
|
1344
|
+
// ❌ WRONG - Don't use wrong error types
|
|
1345
|
+
throw error.serverError('Email required'); // Should be badRequest
|
|
1346
|
+
throw error.badRequest('Database failed'); // Should be serverError
|
|
1347
|
+
throw error.unauthorized('Admin required'); // Should be forbidden
|
|
1348
|
+
```
|
|
1349
|
+
|
|
1350
|
+
### Essential API (4 Core Patterns)
|
|
1351
|
+
|
|
1352
|
+
```javascript
|
|
1353
|
+
// 1. Input validation (400 errors)
|
|
1354
|
+
if (!req.body.email) {
|
|
1355
|
+
throw error.badRequest('Email is required');
|
|
1356
|
+
}
|
|
1357
|
+
if (!email.includes('@')) {
|
|
1358
|
+
throw error.badRequest('Invalid email format');
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
// 2. Authentication checks (401 errors)
|
|
1362
|
+
if (!token) {
|
|
1363
|
+
throw error.unauthorized('Authentication token required');
|
|
1364
|
+
}
|
|
1365
|
+
if (tokenExpired) {
|
|
1366
|
+
throw error.unauthorized('Session expired. Please login again.');
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
// 3. Permission checks (403 errors)
|
|
1370
|
+
if (!user.isAdmin) {
|
|
1371
|
+
throw error.forbidden('Admin access required');
|
|
1372
|
+
}
|
|
1373
|
+
if (user.status === 'suspended') {
|
|
1374
|
+
throw error.forbidden('Account suspended');
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// 4. Resource checks (404 errors)
|
|
1378
|
+
const user = await database.user.findUnique({ where: { id } });
|
|
1379
|
+
if (!user) {
|
|
1380
|
+
throw error.notFound('User not found');
|
|
1381
|
+
}
|
|
1382
|
+
```
|
|
1383
|
+
|
|
1384
|
+
### Framework Integration
|
|
1385
|
+
|
|
1386
|
+
```javascript
|
|
1387
|
+
// Express - Error handling middleware (MUST be last)
|
|
1388
|
+
app.use(error.handleErrors());
|
|
1389
|
+
|
|
1390
|
+
// Express - Async route wrapper
|
|
1391
|
+
app.post(
|
|
1392
|
+
'/users',
|
|
1393
|
+
error.asyncRoute(async (req, res) => {
|
|
1394
|
+
// Errors automatically handled
|
|
1395
|
+
if (!req.body.email) throw error.badRequest('Email required');
|
|
1396
|
+
res.json({ success: true });
|
|
1397
|
+
})
|
|
1398
|
+
);
|
|
1399
|
+
|
|
1400
|
+
// Fastify - Error handler setup
|
|
1401
|
+
fastify.setErrorHandler((err, request, reply) => {
|
|
1402
|
+
const appError = err.statusCode ? err : error.serverError(err.message);
|
|
1403
|
+
reply.status(appError.statusCode).send({
|
|
1404
|
+
error: appError.type,
|
|
1405
|
+
message: appError.message,
|
|
1406
|
+
});
|
|
1407
|
+
});
|
|
1408
|
+
```
|
|
1409
|
+
|
|
1410
|
+
---
|
|
1411
|
+
|
|
1412
|
+
## 🔒 SECURITY MODULE
|
|
1413
|
+
|
|
1414
|
+
### When to Use
|
|
1415
|
+
|
|
1416
|
+
✅ **Web forms, CSRF protection, rate limiting, input sanitization,
|
|
1417
|
+
encryption**
|
|
1418
|
+
❌ **CLI applications, API-only services, read-only applications**
|
|
1419
|
+
|
|
1420
|
+
### Core Pattern
|
|
1421
|
+
|
|
1422
|
+
```javascript
|
|
1423
|
+
import { securityClass } from '@bloomneo/appkit/security';
|
|
1424
|
+
const security = securityClass.get();
|
|
1425
|
+
```
|
|
1426
|
+
|
|
1427
|
+
### Required Environment Variables
|
|
1428
|
+
|
|
1429
|
+
```bash
|
|
1430
|
+
# CRITICAL - Required for startup
|
|
1431
|
+
VOILA_SECURITY_CSRF_SECRET=your-csrf-secret-key-2024-minimum-32-chars
|
|
1432
|
+
VOILA_SECURITY_ENCRYPTION_KEY=64-char-hex-key-for-aes256-encryption
|
|
1433
|
+
```
|
|
1434
|
+
|
|
1435
|
+
### Essential API (4 Core Methods)
|
|
1436
|
+
|
|
1437
|
+
```javascript
|
|
1438
|
+
// 1. CSRF Protection (CRITICAL: Session middleware MUST come first)
|
|
1439
|
+
app.use(session({ secret: process.env.SESSION_SECRET }));
|
|
1440
|
+
app.use(security.forms()); // CSRF protection for all routes
|
|
1441
|
+
|
|
1442
|
+
// Generate CSRF token for forms
|
|
1443
|
+
app.get('/form', (req, res) => {
|
|
1444
|
+
const csrfToken = req.csrfToken();
|
|
1445
|
+
res.render('form', { csrfToken });
|
|
1446
|
+
});
|
|
1447
|
+
|
|
1448
|
+
// 2. Rate Limiting
|
|
1449
|
+
app.use('/api', security.requests()); // Default: 100 requests per 15 minutes
|
|
1450
|
+
app.use('/auth', security.requests(5, 3600000)); // 5 requests per hour
|
|
1451
|
+
|
|
1452
|
+
// 3. Input Sanitization
|
|
1453
|
+
const safeName = security.input(req.body.name, { maxLength: 50 });
|
|
1454
|
+
const safeEmail = security.input(req.body.email?.toLowerCase());
|
|
1455
|
+
const safeHtml = security.html(req.body.content, {
|
|
1456
|
+
allowedTags: ['p', 'b', 'i', 'a'],
|
|
1457
|
+
});
|
|
1458
|
+
|
|
1459
|
+
// 4. Data Encryption (AES-256-GCM)
|
|
1460
|
+
const encryptedSSN = security.encrypt(user.ssn);
|
|
1461
|
+
const encryptedPhone = security.encrypt(user.phone);
|
|
1462
|
+
|
|
1463
|
+
// Decrypt for authorized access
|
|
1464
|
+
const originalSSN = security.decrypt(encryptedSSN);
|
|
1465
|
+
const originalPhone = security.decrypt(encryptedPhone);
|
|
1466
|
+
```
|
|
1467
|
+
|
|
1468
|
+
---
|
|
1469
|
+
|
|
1470
|
+
## 🚀 EVENT MODULE
|
|
1471
|
+
|
|
1472
|
+
### When to Use
|
|
1473
|
+
|
|
1474
|
+
✅ **Real-time features, WebSocket connections, pub/sub messaging, live
|
|
1475
|
+
notifications**
|
|
1476
|
+
❌ **HTTP APIs, file transfers, database operations, background jobs**
|
|
1477
|
+
|
|
1478
|
+
### Core Pattern
|
|
1479
|
+
|
|
1480
|
+
```javascript
|
|
1481
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
1482
|
+
const event = eventClass.get();
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
### Auto-Strategy Detection
|
|
1486
|
+
|
|
1487
|
+
```bash
|
|
1488
|
+
# Development → Memory-based event emitter
|
|
1489
|
+
# Production → Redis pub/sub (if REDIS_URL) → Memory
|
|
1490
|
+
```
|
|
1491
|
+
|
|
1492
|
+
### Essential API (6 Core Methods)
|
|
1493
|
+
|
|
1494
|
+
```javascript
|
|
1495
|
+
// 1. on() - Listen to events
|
|
1496
|
+
event.on('user.login', (data) => {
|
|
1497
|
+
console.log(`User ${data.userId} logged in`);
|
|
1498
|
+
});
|
|
1499
|
+
|
|
1500
|
+
// 2. emit() - Send events
|
|
1501
|
+
await event.emit('user.login', {
|
|
1502
|
+
userId: 123,
|
|
1503
|
+
timestamp: Date.now(),
|
|
1504
|
+
ip: req.ip,
|
|
1505
|
+
});
|
|
1506
|
+
|
|
1507
|
+
// 3. Wildcard patterns
|
|
1508
|
+
event.on('user.*', (eventName, data) => {
|
|
1509
|
+
console.log(`User event: ${eventName}`, data);
|
|
1510
|
+
});
|
|
1511
|
+
|
|
1512
|
+
// 4. once() - One-time listeners
|
|
1513
|
+
event.once('app.ready', () => {
|
|
1514
|
+
console.log('Application is ready');
|
|
1515
|
+
});
|
|
1516
|
+
|
|
1517
|
+
// 5. off() - Remove listeners
|
|
1518
|
+
event.off('user.login'); // Remove all listeners
|
|
1519
|
+
event.off('user.login', specificHandler); // Remove specific listener
|
|
1520
|
+
|
|
1521
|
+
// 6. namespace() - Isolated event channels
|
|
1522
|
+
const userEvent = eventClass.get('users');
|
|
1523
|
+
const orderEvent = eventClass.get('orders');
|
|
1524
|
+
|
|
1525
|
+
userEvent.emit('created', data); // → users:created
|
|
1526
|
+
orderEvent.emit('created', data); // → orders:created
|
|
1527
|
+
```
|
|
1528
|
+
|
|
1529
|
+
---
|
|
1530
|
+
|
|
1531
|
+
## SYSTEM MODULES INTEGRATION
|
|
1532
|
+
|
|
1533
|
+
```javascript
|
|
1534
|
+
// Secure real-time application with all system modules
|
|
1535
|
+
import express from 'express';
|
|
1536
|
+
import session from 'express-session';
|
|
1537
|
+
import { createServer } from 'http';
|
|
1538
|
+
import { Server } from 'socket.io';
|
|
1539
|
+
|
|
1540
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
1541
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
1542
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
1543
|
+
import { securityClass } from '@bloomneo/appkit/security';
|
|
1544
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
1545
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
1546
|
+
|
|
1547
|
+
const app = express();
|
|
1548
|
+
const server = createServer(app);
|
|
1549
|
+
const io = new Server(server);
|
|
1550
|
+
|
|
1551
|
+
const util = utilClass.get();
|
|
1552
|
+
const auth = authClass.get();
|
|
1553
|
+
const error = errorClass.get();
|
|
1554
|
+
const security = securityClass.get();
|
|
1555
|
+
const event = eventClass.get('app');
|
|
1556
|
+
const logger = loggerClass.get('app');
|
|
1557
|
+
|
|
1558
|
+
// Security middleware setup (ORDER CRITICAL)
|
|
1559
|
+
app.use(express.json({ limit: '10mb' }));
|
|
1560
|
+
app.use(
|
|
1561
|
+
session({
|
|
1562
|
+
secret: config.getRequired('session.secret'),
|
|
1563
|
+
resave: false,
|
|
1564
|
+
saveUninitialized: false,
|
|
1565
|
+
cookie: { secure: config.isProduction(), httpOnly: true },
|
|
1566
|
+
})
|
|
1567
|
+
);
|
|
1568
|
+
app.use(security.forms()); // CSRF protection
|
|
1569
|
+
app.use('/api', security.requests(100, 900000)); // Rate limiting
|
|
1570
|
+
|
|
1571
|
+
// Real-time secure messaging endpoint
|
|
1572
|
+
app.post(
|
|
1573
|
+
'/api/messages',
|
|
1574
|
+
auth.requireLogin(),
|
|
1575
|
+
error.asyncRoute(async (req, res) => {
|
|
1576
|
+
const user = auth.user(req);
|
|
1577
|
+
const { content, roomId } = req.body;
|
|
1578
|
+
|
|
1579
|
+
// Input validation
|
|
1580
|
+
if (util.isEmpty(content)) {
|
|
1581
|
+
throw error.badRequest('Message content required');
|
|
1582
|
+
}
|
|
1583
|
+
if (util.isEmpty(roomId)) {
|
|
1584
|
+
throw error.badRequest('Room ID required');
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Sanitize content
|
|
1588
|
+
const safeContent = security.html(content, {
|
|
1589
|
+
allowedTags: ['b', 'i', 'em', 'strong'],
|
|
1590
|
+
maxLength: 1000,
|
|
1591
|
+
});
|
|
1592
|
+
|
|
1593
|
+
// Check permissions
|
|
1594
|
+
const database = databaseClass.get();
|
|
1595
|
+
const room = await database.room.findFirst({
|
|
1596
|
+
where: {
|
|
1597
|
+
id: roomId,
|
|
1598
|
+
members: { some: { userId: user.userId } },
|
|
1599
|
+
},
|
|
1600
|
+
});
|
|
1601
|
+
|
|
1602
|
+
if (!room) {
|
|
1603
|
+
throw error.forbidden('Access to room denied');
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
// Save message
|
|
1607
|
+
const message = await database.message.create({
|
|
1608
|
+
data: {
|
|
1609
|
+
content: safeContent,
|
|
1610
|
+
userId: user.userId,
|
|
1611
|
+
roomId,
|
|
1612
|
+
tenant_id: user.tenant_id,
|
|
1613
|
+
},
|
|
1614
|
+
include: {
|
|
1615
|
+
user: { select: { id: true, name: true } },
|
|
1616
|
+
},
|
|
1617
|
+
});
|
|
1618
|
+
|
|
1619
|
+
// Emit real-time event
|
|
1620
|
+
await event.emit('message.created', {
|
|
1621
|
+
messageId: message.id,
|
|
1622
|
+
content: message.content,
|
|
1623
|
+
user: message.user,
|
|
1624
|
+
roomId,
|
|
1625
|
+
timestamp: message.createdAt,
|
|
1626
|
+
});
|
|
1627
|
+
|
|
1628
|
+
// Send via WebSocket
|
|
1629
|
+
io.to(`room:${roomId}`).emit('new-message', {
|
|
1630
|
+
id: message.id,
|
|
1631
|
+
content: message.content,
|
|
1632
|
+
user: message.user,
|
|
1633
|
+
timestamp: message.createdAt,
|
|
1634
|
+
});
|
|
1635
|
+
|
|
1636
|
+
logger.info('Message sent', {
|
|
1637
|
+
messageId: message.id,
|
|
1638
|
+
userId: user.userId,
|
|
1639
|
+
roomId,
|
|
1640
|
+
});
|
|
1641
|
+
|
|
1642
|
+
res.json({
|
|
1643
|
+
success: true,
|
|
1644
|
+
message: {
|
|
1645
|
+
id: message.id,
|
|
1646
|
+
content: message.content,
|
|
1647
|
+
user: message.user,
|
|
1648
|
+
timestamp: message.createdAt,
|
|
1649
|
+
},
|
|
1650
|
+
});
|
|
1651
|
+
})
|
|
1652
|
+
);
|
|
1653
|
+
|
|
1654
|
+
// WebSocket authentication and real-time handling
|
|
1655
|
+
io.use(async (socket, next) => {
|
|
1656
|
+
try {
|
|
1657
|
+
const token = socket.handshake.auth.token;
|
|
1658
|
+
const user = auth.verifyToken(token);
|
|
1659
|
+
socket.user = user;
|
|
1660
|
+
next();
|
|
1661
|
+
} catch (err) {
|
|
1662
|
+
next(new Error('Authentication failed'));
|
|
1663
|
+
}
|
|
1664
|
+
});
|
|
1665
|
+
|
|
1666
|
+
io.on('connection', (socket) => {
|
|
1667
|
+
const user = socket.user;
|
|
1668
|
+
|
|
1669
|
+
logger.info('User connected', {
|
|
1670
|
+
userId: user.userId,
|
|
1671
|
+
socketId: socket.id,
|
|
1672
|
+
});
|
|
1673
|
+
|
|
1674
|
+
// Join user to their rooms
|
|
1675
|
+
socket.join(`user:${user.userId}`);
|
|
1676
|
+
|
|
1677
|
+
socket.on('join-room', async (roomId) => {
|
|
1678
|
+
// Verify user can join room
|
|
1679
|
+
const database = databaseClass.get();
|
|
1680
|
+
const room = await database.room.findFirst({
|
|
1681
|
+
where: {
|
|
1682
|
+
id: roomId,
|
|
1683
|
+
members: { some: { userId: user.userId } },
|
|
1684
|
+
},
|
|
1685
|
+
});
|
|
1686
|
+
|
|
1687
|
+
if (room) {
|
|
1688
|
+
await socket.join(`room:${roomId}`);
|
|
1689
|
+
socket.emit('joined-room', { roomId });
|
|
1690
|
+
|
|
1691
|
+
logger.info('User joined room', {
|
|
1692
|
+
userId: user.userId,
|
|
1693
|
+
roomId,
|
|
1694
|
+
});
|
|
1695
|
+
} else {
|
|
1696
|
+
socket.emit('error', { message: 'Access denied to room' });
|
|
1697
|
+
}
|
|
1698
|
+
});
|
|
1699
|
+
|
|
1700
|
+
socket.on('disconnect', () => {
|
|
1701
|
+
logger.info('User disconnected', { userId: user.userId });
|
|
1702
|
+
});
|
|
1703
|
+
});
|
|
1704
|
+
|
|
1705
|
+
// Event listeners for cross-service communication
|
|
1706
|
+
event.on('user.registered', async (data) => {
|
|
1707
|
+
// Send welcome notification
|
|
1708
|
+
io.to(`user:${data.userId}`).emit('notification', {
|
|
1709
|
+
type: 'welcome',
|
|
1710
|
+
message: 'Welcome to our platform!',
|
|
1711
|
+
timestamp: new Date(),
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
1714
|
+
|
|
1715
|
+
// Error handling middleware (MUST be last)
|
|
1716
|
+
app.use(error.handleErrors());
|
|
1717
|
+
|
|
1718
|
+
server.listen(3000, () => {
|
|
1719
|
+
logger.info('🚀 Server started with real-time support', { port: 3000 });
|
|
1720
|
+
});
|
|
1721
|
+
```
|
|
1722
|
+
|
|
1723
|
+
# Production & Complete Examples 🚀
|
|
1724
|
+
|
|
1725
|
+
> **Production deployment, complete application examples, and comprehensive
|
|
1726
|
+
> guidance**
|
|
1727
|
+
|
|
1728
|
+
## ENVIRONMENT VALIDATION
|
|
1729
|
+
|
|
1730
|
+
### Production Environment Variables
|
|
1731
|
+
|
|
1732
|
+
```bash
|
|
1733
|
+
# ✅ Framework (Required in production)
|
|
1734
|
+
NODE_ENV=production
|
|
1735
|
+
VOILA_AUTH_SECRET=your-super-secure-jwt-secret-key-minimum-32-chars
|
|
1736
|
+
VOILA_SECURITY_CSRF_SECRET=your-csrf-secret-key-minimum-32-chars
|
|
1737
|
+
VOILA_SECURITY_ENCRYPTION_KEY=64-char-hex-encryption-key-for-aes256
|
|
1738
|
+
|
|
1739
|
+
# ✅ Services (Required)
|
|
1740
|
+
DATABASE_URL=postgresql://user:password@host:5432/database
|
|
1741
|
+
REDIS_URL=redis://user:password@host:6379
|
|
1742
|
+
|
|
1743
|
+
# ✅ Email (Choose one)
|
|
1744
|
+
RESEND_API_KEY=re_your_api_key
|
|
1745
|
+
# OR SMTP_HOST=smtp.gmail.com
|
|
1746
|
+
|
|
1747
|
+
# ✅ Storage (Choose one)
|
|
1748
|
+
CLOUDFLARE_R2_BUCKET=your-bucket
|
|
1749
|
+
# OR AWS_S3_BUCKET=your-bucket
|
|
1750
|
+
|
|
1751
|
+
# ✅ Application
|
|
1752
|
+
APP_NAME=Your Production App
|
|
1753
|
+
APP_URL=https://yourapp.com
|
|
1754
|
+
```
|
|
1755
|
+
|
|
1756
|
+
### Startup Validation Pattern
|
|
1757
|
+
|
|
1758
|
+
```javascript
|
|
1759
|
+
// ALWAYS validate environment at startup
|
|
1760
|
+
function validateProductionEnv() {
|
|
1761
|
+
if (process.env.NODE_ENV !== 'production') return;
|
|
1762
|
+
|
|
1763
|
+
const required = ['VOILA_AUTH_SECRET', 'DATABASE_URL', 'REDIS_URL'];
|
|
1764
|
+
const missing = required.filter((key) => !process.env[key]);
|
|
1765
|
+
|
|
1766
|
+
if (missing.length > 0) {
|
|
1767
|
+
console.error('❌ Missing required environment variables:', missing);
|
|
1768
|
+
process.exit(1);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
console.log('✅ Production environment validated');
|
|
1772
|
+
}
|
|
1773
|
+
|
|
1774
|
+
// App initialization
|
|
1775
|
+
async function initializeApp() {
|
|
1776
|
+
try {
|
|
1777
|
+
validateProductionEnv();
|
|
1778
|
+
|
|
1779
|
+
const config = configClass.get();
|
|
1780
|
+
const logger = loggerClass.get('init');
|
|
1781
|
+
|
|
1782
|
+
// Validate configuration
|
|
1783
|
+
config.getRequired('database.url');
|
|
1784
|
+
config.getRequired('auth.secret');
|
|
1785
|
+
|
|
1786
|
+
// Initialize database
|
|
1787
|
+
const database = databaseClass.get();
|
|
1788
|
+
await database.$queryRaw`SELECT 1`;
|
|
1789
|
+
|
|
1790
|
+
logger.info('✅ App initialized successfully');
|
|
1791
|
+
} catch (error) {
|
|
1792
|
+
console.error('❌ App initialization failed:', error.message);
|
|
1793
|
+
process.exit(1);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
```
|
|
1797
|
+
|
|
1798
|
+
---
|
|
1799
|
+
|
|
1800
|
+
## GRACEFUL SHUTDOWN
|
|
1801
|
+
|
|
1802
|
+
```javascript
|
|
1803
|
+
// ALWAYS implement graceful shutdown
|
|
1804
|
+
async function gracefulShutdown(signal) {
|
|
1805
|
+
console.log(`🔄 Received ${signal}, shutting down gracefully...`);
|
|
1806
|
+
|
|
1807
|
+
try {
|
|
1808
|
+
// Close server first (stop accepting new connections)
|
|
1809
|
+
if (server) {
|
|
1810
|
+
await new Promise((resolve) => server.close(resolve));
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
// Close services in reverse dependency order
|
|
1814
|
+
await queueClass.get().close(); // Stop processing jobs
|
|
1815
|
+
await databaseClass.disconnect(); // Close DB connections
|
|
1816
|
+
await loggerClass.get().flush(); // Write remaining logs
|
|
1817
|
+
|
|
1818
|
+
console.log('✅ Graceful shutdown completed');
|
|
1819
|
+
process.exit(0);
|
|
1820
|
+
} catch (error) {
|
|
1821
|
+
console.error('❌ Shutdown error:', error);
|
|
1822
|
+
process.exit(1);
|
|
1823
|
+
}
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
1827
|
+
process.on('SIGINT', gracefulShutdown);
|
|
1828
|
+
process.on('SIGUSR2', gracefulShutdown); // For PM2
|
|
1829
|
+
```
|
|
1830
|
+
|
|
1831
|
+
---
|
|
1832
|
+
|
|
1833
|
+
## MODULE INITIALIZATION ORDER
|
|
1834
|
+
|
|
1835
|
+
```javascript
|
|
1836
|
+
// ALWAYS follow this exact order:
|
|
1837
|
+
// 1. Util (no dependencies)
|
|
1838
|
+
// 2. Config (no dependencies)
|
|
1839
|
+
// 3. Logger (depends on Config)
|
|
1840
|
+
// 4. Error (depends on Config + Logger)
|
|
1841
|
+
// 5. Auth (depends on Config + Error)
|
|
1842
|
+
// 6. Security (depends on Config + Error)
|
|
1843
|
+
// 7. Database (depends on Config + Logger + Error)
|
|
1844
|
+
// 8. Cache (depends on Config + Logger)
|
|
1845
|
+
// 9. Storage (depends on Config + Logger + Error)
|
|
1846
|
+
// 10. Email (depends on Config + Logger + Error)
|
|
1847
|
+
// 11. Queue (depends on Config + Logger + Error)
|
|
1848
|
+
// 12. Event (depends on Config + Logger + Error)
|
|
1849
|
+
|
|
1850
|
+
async function initializeApp() {
|
|
1851
|
+
try {
|
|
1852
|
+
// 1-2. Core utilities first
|
|
1853
|
+
const config = configClass.get();
|
|
1854
|
+
const util = utilClass.get();
|
|
1855
|
+
|
|
1856
|
+
// 3-4. Logging and error handling
|
|
1857
|
+
const logger = loggerClass.get('init');
|
|
1858
|
+
const error = errorClass.get();
|
|
1859
|
+
|
|
1860
|
+
// 5-6. Security modules
|
|
1861
|
+
const auth = authClass.get();
|
|
1862
|
+
const security = securityClass.get();
|
|
1863
|
+
|
|
1864
|
+
// 7. Database
|
|
1865
|
+
const database = databaseClass.get();
|
|
1866
|
+
|
|
1867
|
+
// 8-12. Infrastructure services
|
|
1868
|
+
const cache = cacheClass.get('app');
|
|
1869
|
+
const storage = storageClass.get();
|
|
1870
|
+
const email = emailClass.get();
|
|
1871
|
+
const queue = queueClass.get();
|
|
1872
|
+
const event = eventClass.get('app');
|
|
1873
|
+
|
|
1874
|
+
logger.info('✅ All modules initialized successfully');
|
|
1875
|
+
return {
|
|
1876
|
+
config,
|
|
1877
|
+
util,
|
|
1878
|
+
logger,
|
|
1879
|
+
error,
|
|
1880
|
+
auth,
|
|
1881
|
+
security,
|
|
1882
|
+
database,
|
|
1883
|
+
cache,
|
|
1884
|
+
storage,
|
|
1885
|
+
email,
|
|
1886
|
+
queue,
|
|
1887
|
+
event,
|
|
1888
|
+
};
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
console.error('❌ Module initialization failed:', error.message);
|
|
1891
|
+
process.exit(1);
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
```
|
|
1895
|
+
|
|
1896
|
+
---
|
|
1897
|
+
|
|
1898
|
+
## COMPLETE PRODUCTION APPLICATION
|
|
1899
|
+
|
|
1900
|
+
```javascript
|
|
1901
|
+
// Complete production-ready Express application
|
|
1902
|
+
import express from 'express';
|
|
1903
|
+
import session from 'express-session';
|
|
1904
|
+
import multer from 'multer';
|
|
1905
|
+
import { createServer } from 'http';
|
|
1906
|
+
|
|
1907
|
+
// Import all AppKit modules
|
|
1908
|
+
import { utilClass } from '@bloomneo/appkit/util';
|
|
1909
|
+
import { configClass } from '@bloomneo/appkit/config';
|
|
1910
|
+
import { authClass } from '@bloomneo/appkit/auth';
|
|
1911
|
+
import { loggerClass } from '@bloomneo/appkit/logger';
|
|
1912
|
+
import { errorClass } from '@bloomneo/appkit/error';
|
|
1913
|
+
import { securityClass } from '@bloomneo/appkit/security';
|
|
1914
|
+
import { databaseClass } from '@bloomneo/appkit/database';
|
|
1915
|
+
import { cacheClass } from '@bloomneo/appkit/cache';
|
|
1916
|
+
import { storageClass } from '@bloomneo/appkit/storage';
|
|
1917
|
+
import { queueClass } from '@bloomneo/appkit/queue';
|
|
1918
|
+
import { emailClass } from '@bloomneo/appkit/email';
|
|
1919
|
+
import { eventClass } from '@bloomneo/appkit/event';
|
|
1920
|
+
|
|
1921
|
+
const app = express();
|
|
1922
|
+
const server = createServer(app);
|
|
1923
|
+
const upload = multer({ storage: multer.memoryStorage() });
|
|
1924
|
+
|
|
1925
|
+
// Initialize modules in correct order
|
|
1926
|
+
const util = utilClass.get();
|
|
1927
|
+
const config = configClass.get();
|
|
1928
|
+
const logger = loggerClass.get('app');
|
|
1929
|
+
const error = errorClass.get();
|
|
1930
|
+
const auth = authClass.get();
|
|
1931
|
+
const security = securityClass.get();
|
|
1932
|
+
const storage = storageClass.get();
|
|
1933
|
+
const queue = queueClass.get();
|
|
1934
|
+
const email = emailClass.get();
|
|
1935
|
+
const event = eventClass.get('app');
|
|
1936
|
+
|
|
1937
|
+
// Startup validation
|
|
1938
|
+
validateProductionEnv();
|
|
1939
|
+
await initializeApp();
|
|
1940
|
+
|
|
1941
|
+
// Middleware setup (ORDER CRITICAL)
|
|
1942
|
+
app.use(express.json({ limit: '10mb' }));
|
|
1943
|
+
app.use(
|
|
1944
|
+
session({
|
|
1945
|
+
secret: config.getRequired('session.secret'),
|
|
1946
|
+
resave: false,
|
|
1947
|
+
saveUninitialized: false,
|
|
1948
|
+
cookie: {
|
|
1949
|
+
secure: config.isProduction(),
|
|
1950
|
+
httpOnly: true,
|
|
1951
|
+
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
|
1952
|
+
},
|
|
1953
|
+
})
|
|
1954
|
+
);
|
|
1955
|
+
|
|
1956
|
+
app.use(security.forms()); // CSRF protection
|
|
1957
|
+
app.use('/api', security.requests(100, 900000)); // Rate limiting
|
|
1958
|
+
|
|
1959
|
+
// Request logging
|
|
1960
|
+
app.use((req, res, next) => {
|
|
1961
|
+
req.requestId = util.uuid();
|
|
1962
|
+
req.logger = logger.child({
|
|
1963
|
+
requestId: req.requestId,
|
|
1964
|
+
method: req.method,
|
|
1965
|
+
url: req.url,
|
|
1966
|
+
});
|
|
1967
|
+
|
|
1968
|
+
const startTime = Date.now();
|
|
1969
|
+
res.on('finish', () => {
|
|
1970
|
+
req.logger.info('Request completed', {
|
|
1971
|
+
statusCode: res.statusCode,
|
|
1972
|
+
duration: Date.now() - startTime,
|
|
1973
|
+
});
|
|
1974
|
+
});
|
|
1975
|
+
|
|
1976
|
+
next();
|
|
1977
|
+
});
|
|
1978
|
+
|
|
1979
|
+
// Health check
|
|
1980
|
+
app.get('/health', async (req, res) => {
|
|
1981
|
+
try {
|
|
1982
|
+
const database = databaseClass.get();
|
|
1983
|
+
await database.$queryRaw`SELECT 1`;
|
|
1984
|
+
|
|
1985
|
+
res.json({
|
|
1986
|
+
status: 'ok',
|
|
1987
|
+
timestamp: new Date().toISOString(),
|
|
1988
|
+
environment: config.getEnvironment(),
|
|
1989
|
+
});
|
|
1990
|
+
} catch (error) {
|
|
1991
|
+
res.status(503).json({
|
|
1992
|
+
status: 'error',
|
|
1993
|
+
error: error.message,
|
|
1994
|
+
});
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
|
|
1998
|
+
// Authentication endpoints
|
|
1999
|
+
app.post(
|
|
2000
|
+
'/auth/register',
|
|
2001
|
+
error.asyncRoute(async (req, res) => {
|
|
2002
|
+
const { email, password, name } = req.body;
|
|
2003
|
+
|
|
2004
|
+
// Input validation
|
|
2005
|
+
if (util.isEmpty(email)) throw error.badRequest('Email required');
|
|
2006
|
+
if (!email.includes('@')) throw error.badRequest('Invalid email format');
|
|
2007
|
+
if (util.isEmpty(password)) throw error.badRequest('Password required');
|
|
2008
|
+
if (password.length < 8)
|
|
2009
|
+
throw error.badRequest('Password must be 8+ characters');
|
|
2010
|
+
if (util.isEmpty(name)) throw error.badRequest('Name required');
|
|
2011
|
+
|
|
2012
|
+
// Sanitize inputs
|
|
2013
|
+
const safeEmail = security.input(email.toLowerCase());
|
|
2014
|
+
const safeName = security.input(name, { maxLength: 50 });
|
|
2015
|
+
|
|
2016
|
+
// Check for existing user
|
|
2017
|
+
const database = databaseClass.get();
|
|
2018
|
+
const existingUser = await database.user.findUnique({
|
|
2019
|
+
where: { email: safeEmail },
|
|
2020
|
+
});
|
|
2021
|
+
|
|
2022
|
+
if (existingUser) {
|
|
2023
|
+
throw error.conflict('Email already registered');
|
|
2024
|
+
}
|
|
2025
|
+
|
|
2026
|
+
// Create user
|
|
2027
|
+
const hashedPassword = await auth.hashPassword(password);
|
|
2028
|
+
const user = await database.user.create({
|
|
2029
|
+
data: {
|
|
2030
|
+
email: safeEmail,
|
|
2031
|
+
name: safeName,
|
|
2032
|
+
password: hashedPassword,
|
|
2033
|
+
role: 'user',
|
|
2034
|
+
level: 'basic',
|
|
2035
|
+
},
|
|
2036
|
+
});
|
|
2037
|
+
|
|
2038
|
+
// Generate token
|
|
2039
|
+
const token = auth.signToken({
|
|
2040
|
+
userId: user.id,
|
|
2041
|
+
role: user.role,
|
|
2042
|
+
level: user.level,
|
|
2043
|
+
});
|
|
2044
|
+
|
|
2045
|
+
// Queue welcome email
|
|
2046
|
+
await queue.add('welcome-email', {
|
|
2047
|
+
userId: user.id,
|
|
2048
|
+
email: user.email,
|
|
2049
|
+
name: user.name,
|
|
2050
|
+
});
|
|
2051
|
+
|
|
2052
|
+
// Emit event
|
|
2053
|
+
await event.emit('user.registered', {
|
|
2054
|
+
userId: user.id,
|
|
2055
|
+
email: user.email,
|
|
2056
|
+
timestamp: new Date(),
|
|
2057
|
+
});
|
|
2058
|
+
|
|
2059
|
+
req.logger.info('User registered', { userId: user.id });
|
|
2060
|
+
|
|
2061
|
+
res.json({
|
|
2062
|
+
success: true,
|
|
2063
|
+
token,
|
|
2064
|
+
user: util.pick(user, ['id', 'email', 'name']),
|
|
2065
|
+
});
|
|
2066
|
+
})
|
|
2067
|
+
);
|
|
2068
|
+
|
|
2069
|
+
app.post(
|
|
2070
|
+
'/auth/login',
|
|
2071
|
+
error.asyncRoute(async (req, res) => {
|
|
2072
|
+
const { email, password } = req.body;
|
|
2073
|
+
|
|
2074
|
+
if (util.isEmpty(email)) throw error.badRequest('Email required');
|
|
2075
|
+
if (util.isEmpty(password)) throw error.badRequest('Password required');
|
|
2076
|
+
|
|
2077
|
+
const database = databaseClass.get();
|
|
2078
|
+
const user = await database.user.findUnique({
|
|
2079
|
+
where: { email: email.toLowerCase() },
|
|
2080
|
+
});
|
|
2081
|
+
|
|
2082
|
+
if (!user) throw error.unauthorized('Invalid credentials');
|
|
2083
|
+
|
|
2084
|
+
const isValid = await auth.comparePassword(password, user.password);
|
|
2085
|
+
if (!isValid) throw error.unauthorized('Invalid credentials');
|
|
2086
|
+
|
|
2087
|
+
const token = auth.signToken({
|
|
2088
|
+
userId: user.id,
|
|
2089
|
+
role: user.role,
|
|
2090
|
+
level: user.level,
|
|
2091
|
+
});
|
|
2092
|
+
|
|
2093
|
+
// Cache user data
|
|
2094
|
+
const userCache = cacheClass.get('users');
|
|
2095
|
+
await userCache.set(`user:${user.id}`, user, 3600);
|
|
2096
|
+
|
|
2097
|
+
req.logger.info('User logged in', { userId: user.id });
|
|
2098
|
+
|
|
2099
|
+
res.json({
|
|
2100
|
+
success: true,
|
|
2101
|
+
token,
|
|
2102
|
+
user: util.pick(user, ['id', 'email', 'name']),
|
|
2103
|
+
});
|
|
2104
|
+
})
|
|
2105
|
+
);
|
|
2106
|
+
|
|
2107
|
+
// File upload endpoint
|
|
2108
|
+
app.post(
|
|
2109
|
+
'/api/upload',
|
|
2110
|
+
auth.requireLogin(),
|
|
2111
|
+
upload.single('file'),
|
|
2112
|
+
error.asyncRoute(async (req, res) => {
|
|
2113
|
+
if (!req.file) throw error.badRequest('No file uploaded');
|
|
2114
|
+
|
|
2115
|
+
const user = auth.user(req);
|
|
2116
|
+
|
|
2117
|
+
// Validate file
|
|
2118
|
+
const allowedTypes = ['image/jpeg', 'image/png', 'application/pdf'];
|
|
2119
|
+
if (!allowedTypes.includes(req.file.mimetype)) {
|
|
2120
|
+
throw error.badRequest('File type not allowed');
|
|
2121
|
+
}
|
|
2122
|
+
|
|
2123
|
+
const maxSize = 10 * 1024 * 1024; // 10MB
|
|
2124
|
+
if (req.file.size > maxSize) {
|
|
2125
|
+
throw error.badRequest('File too large (max 10MB)');
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
// Store file
|
|
2129
|
+
const fileKey = `uploads/${user.userId}/${Date.now()}-${util.slugify(req.file.originalname)}`;
|
|
2130
|
+
await storage.put(fileKey, req.file.buffer, {
|
|
2131
|
+
contentType: req.file.mimetype,
|
|
2132
|
+
});
|
|
2133
|
+
|
|
2134
|
+
// Save to database
|
|
2135
|
+
const database = databaseClass.get();
|
|
2136
|
+
const file = await database.file.create({
|
|
2137
|
+
data: {
|
|
2138
|
+
key: fileKey,
|
|
2139
|
+
name: req.file.originalname,
|
|
2140
|
+
size: req.file.size,
|
|
2141
|
+
contentType: req.file.mimetype,
|
|
2142
|
+
userId: user.userId,
|
|
2143
|
+
},
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
// Queue processing
|
|
2147
|
+
await queue.add('process-file', {
|
|
2148
|
+
fileId: file.id,
|
|
2149
|
+
fileKey,
|
|
2150
|
+
userId: user.userId,
|
|
2151
|
+
});
|
|
2152
|
+
|
|
2153
|
+
req.logger.info('File uploaded', {
|
|
2154
|
+
fileId: file.id,
|
|
2155
|
+
size: util.formatBytes(req.file.size),
|
|
2156
|
+
});
|
|
2157
|
+
|
|
2158
|
+
res.json({
|
|
2159
|
+
success: true,
|
|
2160
|
+
file: {
|
|
2161
|
+
id: file.id,
|
|
2162
|
+
url: storage.url(fileKey),
|
|
2163
|
+
name: req.file.originalname,
|
|
2164
|
+
size: util.formatBytes(req.file.size),
|
|
2165
|
+
},
|
|
2166
|
+
});
|
|
2167
|
+
})
|
|
2168
|
+
);
|
|
2169
|
+
|
|
2170
|
+
// Protected user endpoint
|
|
2171
|
+
app.get(
|
|
2172
|
+
'/api/profile',
|
|
2173
|
+
auth.requireLogin(),
|
|
2174
|
+
error.asyncRoute(async (req, res) => {
|
|
2175
|
+
const user = auth.user(req);
|
|
2176
|
+
const userCache = cacheClass.get('users');
|
|
2177
|
+
|
|
2178
|
+
// Try cache first
|
|
2179
|
+
let userData = await userCache.get(`user:${user.userId}`);
|
|
2180
|
+
|
|
2181
|
+
if (!userData) {
|
|
2182
|
+
const database = databaseClass.get();
|
|
2183
|
+
userData = await database.user.findUnique({
|
|
2184
|
+
where: { id: user.userId },
|
|
2185
|
+
select: { id: true, email: true, name: true, createdAt: true },
|
|
2186
|
+
});
|
|
2187
|
+
|
|
2188
|
+
if (userData) {
|
|
2189
|
+
await userCache.set(`user:${user.userId}`, userData, 3600);
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
|
|
2193
|
+
if (!userData) throw error.notFound('User not found');
|
|
2194
|
+
|
|
2195
|
+
res.json(userData);
|
|
2196
|
+
})
|
|
2197
|
+
);
|
|
2198
|
+
|
|
2199
|
+
// Admin endpoint
|
|
2200
|
+
app.get(
|
|
2201
|
+
'/api/admin/users',
|
|
2202
|
+
auth.requireRole('admin.tenant'),
|
|
2203
|
+
error.asyncRoute(async (req, res) => {
|
|
2204
|
+
const dbTenants = databaseClass.getTenants();
|
|
2205
|
+
const users = await dbTenants.user.findMany({
|
|
2206
|
+
select: {
|
|
2207
|
+
id: true,
|
|
2208
|
+
email: true,
|
|
2209
|
+
name: true,
|
|
2210
|
+
role: true,
|
|
2211
|
+
level: true,
|
|
2212
|
+
createdAt: true,
|
|
2213
|
+
},
|
|
2214
|
+
});
|
|
2215
|
+
|
|
2216
|
+
res.json(users);
|
|
2217
|
+
})
|
|
2218
|
+
);
|
|
2219
|
+
|
|
2220
|
+
// Background job processors
|
|
2221
|
+
queue.process('welcome-email', async (data) => {
|
|
2222
|
+
try {
|
|
2223
|
+
await email.send({
|
|
2224
|
+
to: data.email,
|
|
2225
|
+
subject: `Welcome to ${config.get('app.name', 'Our Platform')}!`,
|
|
2226
|
+
html: `
|
|
2227
|
+
<h1>Welcome ${data.name}!</h1>
|
|
2228
|
+
<p>Thanks for joining us. We're excited to have you on board!</p>
|
|
2229
|
+
<a href="${config.get('app.url')}/dashboard">Get Started</a>
|
|
2230
|
+
`,
|
|
2231
|
+
});
|
|
2232
|
+
|
|
2233
|
+
logger.info('Welcome email sent', { userId: data.userId });
|
|
2234
|
+
return { sent: true };
|
|
2235
|
+
} catch (error) {
|
|
2236
|
+
logger.error('Welcome email failed', {
|
|
2237
|
+
userId: data.userId,
|
|
2238
|
+
error: error.message,
|
|
2239
|
+
});
|
|
2240
|
+
throw error;
|
|
2241
|
+
}
|
|
2242
|
+
});
|
|
2243
|
+
|
|
2244
|
+
queue.process('process-file', async (data) => {
|
|
2245
|
+
const processingLogger = loggerClass.get('processing');
|
|
2246
|
+
|
|
2247
|
+
try {
|
|
2248
|
+
// Simulate file processing
|
|
2249
|
+
await util.sleep(2000);
|
|
2250
|
+
|
|
2251
|
+
const database = databaseClass.get();
|
|
2252
|
+
await database.file.update({
|
|
2253
|
+
where: { id: data.fileId },
|
|
2254
|
+
data: { processed: true, processedAt: new Date() },
|
|
2255
|
+
});
|
|
2256
|
+
|
|
2257
|
+
processingLogger.info('File processed', { fileId: data.fileId });
|
|
2258
|
+
return { processed: true };
|
|
2259
|
+
} catch (error) {
|
|
2260
|
+
processingLogger.error('File processing failed', {
|
|
2261
|
+
fileId: data.fileId,
|
|
2262
|
+
error: error.message,
|
|
2263
|
+
});
|
|
2264
|
+
throw error;
|
|
2265
|
+
}
|
|
2266
|
+
});
|
|
2267
|
+
|
|
2268
|
+
// Error handling middleware (MUST be last)
|
|
2269
|
+
app.use(error.handleErrors());
|
|
2270
|
+
|
|
2271
|
+
// Start server
|
|
2272
|
+
const port = config.get('server.port', 3000);
|
|
2273
|
+
const host = config.get('server.host', '0.0.0.0');
|
|
2274
|
+
|
|
2275
|
+
server.listen(port, host, () => {
|
|
2276
|
+
logger.info('🌟 Server ready', { port, host });
|
|
2277
|
+
});
|
|
2278
|
+
|
|
2279
|
+
// Graceful shutdown
|
|
2280
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
2281
|
+
process.on('SIGINT', gracefulShutdown);
|
|
2282
|
+
```
|
|
2283
|
+
|
|
2284
|
+
---
|
|
2285
|
+
|
|
2286
|
+
## TESTING PATTERNS
|
|
2287
|
+
|
|
2288
|
+
### Test Setup
|
|
2289
|
+
|
|
2290
|
+
```javascript
|
|
2291
|
+
import {
|
|
2292
|
+
utilClass,
|
|
2293
|
+
loggerClass,
|
|
2294
|
+
cacheClass,
|
|
2295
|
+
configClass,
|
|
2296
|
+
databaseClass,
|
|
2297
|
+
} from '@bloomneo/appkit';
|
|
2298
|
+
|
|
2299
|
+
describe('AppKit Application', () => {
|
|
2300
|
+
beforeEach(() => {
|
|
2301
|
+
// Force test configuration
|
|
2302
|
+
configClass.reset({
|
|
2303
|
+
database: { url: 'memory://test' },
|
|
2304
|
+
cache: { strategy: 'memory' },
|
|
2305
|
+
logging: { level: 'silent' },
|
|
2306
|
+
});
|
|
2307
|
+
});
|
|
2308
|
+
|
|
2309
|
+
afterEach(async () => {
|
|
2310
|
+
// ALWAYS reset module state between tests
|
|
2311
|
+
utilClass.clearCache();
|
|
2312
|
+
await loggerClass.clear();
|
|
2313
|
+
await cacheClass.clear();
|
|
2314
|
+
configClass.clearCache();
|
|
2315
|
+
await databaseClass.clear();
|
|
2316
|
+
});
|
|
2317
|
+
|
|
2318
|
+
test('should handle user registration', async () => {
|
|
2319
|
+
const util = utilClass.get();
|
|
2320
|
+
const auth = authClass.get();
|
|
2321
|
+
|
|
2322
|
+
const userData = {
|
|
2323
|
+
email: 'test@example.com',
|
|
2324
|
+
name: 'Test User',
|
|
2325
|
+
};
|
|
2326
|
+
|
|
2327
|
+
const email = util.get(userData, 'email');
|
|
2328
|
+
expect(email).toBe('test@example.com');
|
|
2329
|
+
|
|
2330
|
+
const token = auth.signToken({
|
|
2331
|
+
userId: 123,
|
|
2332
|
+
role: 'user',
|
|
2333
|
+
level: 'basic',
|
|
2334
|
+
});
|
|
2335
|
+
|
|
2336
|
+
expect(token).toBeDefined();
|
|
2337
|
+
});
|
|
2338
|
+
});
|
|
2339
|
+
```
|
|
2340
|
+
|
|
2341
|
+
---
|
|
2342
|
+
|
|
2343
|
+
## DOCKER DEPLOYMENT
|
|
2344
|
+
|
|
2345
|
+
### Dockerfile
|
|
2346
|
+
|
|
2347
|
+
```dockerfile
|
|
2348
|
+
FROM node:18-alpine
|
|
2349
|
+
|
|
2350
|
+
WORKDIR /app
|
|
2351
|
+
|
|
2352
|
+
# Copy package files
|
|
2353
|
+
COPY package*.json ./
|
|
2354
|
+
RUN npm ci --only=production
|
|
2355
|
+
|
|
2356
|
+
# Copy application
|
|
2357
|
+
COPY . .
|
|
2358
|
+
|
|
2359
|
+
# Health check
|
|
2360
|
+
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
|
2361
|
+
CMD node -e "require('http').get('http://localhost:3000/health', (res) => { process.exit(res.statusCode === 200 ? 0 : 1) })"
|
|
2362
|
+
|
|
2363
|
+
EXPOSE 3000
|
|
2364
|
+
CMD ["node", "server.js"]
|
|
2365
|
+
```
|
|
2366
|
+
|
|
2367
|
+
### Docker Compose
|
|
2368
|
+
|
|
2369
|
+
```yaml
|
|
2370
|
+
version: '3.8'
|
|
2371
|
+
services:
|
|
2372
|
+
app:
|
|
2373
|
+
build: .
|
|
2374
|
+
environment:
|
|
2375
|
+
- NODE_ENV=production
|
|
2376
|
+
- DATABASE_URL=postgresql://user:pass@postgres:5432/app
|
|
2377
|
+
- REDIS_URL=redis://redis:6379
|
|
2378
|
+
- VOILA_AUTH_SECRET=${VOILA_AUTH_SECRET}
|
|
2379
|
+
- VOILA_SECURITY_CSRF_SECRET=${VOILA_SECURITY_CSRF_SECRET}
|
|
2380
|
+
ports:
|
|
2381
|
+
- '3000:3000'
|
|
2382
|
+
depends_on:
|
|
2383
|
+
- postgres
|
|
2384
|
+
- redis
|
|
2385
|
+
restart: unless-stopped
|
|
2386
|
+
|
|
2387
|
+
postgres:
|
|
2388
|
+
image: postgres:15-alpine
|
|
2389
|
+
environment:
|
|
2390
|
+
POSTGRES_DB: app
|
|
2391
|
+
POSTGRES_USER: user
|
|
2392
|
+
POSTGRES_PASSWORD: pass
|
|
2393
|
+
volumes:
|
|
2394
|
+
- postgres_data:/var/lib/postgresql/data
|
|
2395
|
+
|
|
2396
|
+
redis:
|
|
2397
|
+
image: redis:7-alpine
|
|
2398
|
+
volumes:
|
|
2399
|
+
- redis_data:/data
|
|
2400
|
+
|
|
2401
|
+
volumes:
|
|
2402
|
+
postgres_data:
|
|
2403
|
+
redis_data:
|
|
2404
|
+
```
|
|
2405
|
+
|
|
2406
|
+
---
|
|
2407
|
+
|
|
2408
|
+
## COMMON LLM MISTAKES TO AVOID
|
|
2409
|
+
|
|
2410
|
+
```javascript
|
|
2411
|
+
// ❌ WRONG - Unsafe property access
|
|
2412
|
+
const name = user.profile.name; // Can crash on undefined
|
|
2413
|
+
|
|
2414
|
+
// ✅ CORRECT - Safe access with default
|
|
2415
|
+
const name = util.get(user, 'profile.name', 'Guest');
|
|
2416
|
+
|
|
2417
|
+
// ❌ WRONG - Missing required token fields
|
|
2418
|
+
auth.signToken({ userId: 123 }); // Missing role/level
|
|
2419
|
+
|
|
2420
|
+
// ✅ CORRECT - All required fields
|
|
2421
|
+
auth.signToken({ userId: 123, role: 'admin', level: 'tenant' });
|
|
2422
|
+
|
|
2423
|
+
// ❌ WRONG - Wrong error types
|
|
2424
|
+
throw error.serverError('Email required'); // Should be badRequest
|
|
2425
|
+
|
|
2426
|
+
// ✅ CORRECT - Semantic error types
|
|
2427
|
+
throw error.badRequest('Email required');
|
|
2428
|
+
throw error.unauthorized('Login required');
|
|
2429
|
+
throw error.forbidden('Admin access required');
|
|
2430
|
+
|
|
2431
|
+
// ❌ WRONG - Missing tenant_id in schema
|
|
2432
|
+
CREATE TABLE users (id, email, name); // Will break multi-tenancy
|
|
2433
|
+
|
|
2434
|
+
// ✅ CORRECT - Future-proof schema
|
|
2435
|
+
CREATE TABLE users (
|
|
2436
|
+
id uuid PRIMARY KEY,
|
|
2437
|
+
email text,
|
|
2438
|
+
name text,
|
|
2439
|
+
tenant_id text, -- MANDATORY
|
|
2440
|
+
INDEX idx_users_tenant (tenant_id) -- MANDATORY
|
|
2441
|
+
);
|
|
2442
|
+
|
|
2443
|
+
// ❌ WRONG - No startup validation
|
|
2444
|
+
app.listen(3000, () => {
|
|
2445
|
+
console.log('Server started'); // No validation!
|
|
2446
|
+
});
|
|
2447
|
+
|
|
2448
|
+
// ✅ CORRECT - Validate before starting
|
|
2449
|
+
validateProductionEnv();
|
|
2450
|
+
initializeApp().then(() => {
|
|
2451
|
+
app.listen(3000, () => {
|
|
2452
|
+
console.log('✅ Server ready');
|
|
2453
|
+
});
|
|
2454
|
+
});
|
|
2455
|
+
|
|
2456
|
+
// ❌ WRONG - No graceful shutdown
|
|
2457
|
+
process.exit(0); // Abrupt shutdown
|
|
2458
|
+
|
|
2459
|
+
// ✅ CORRECT - Graceful shutdown
|
|
2460
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
2461
|
+
|
|
2462
|
+
// ❌ WRONG - No test cleanup
|
|
2463
|
+
test('should work', () => {
|
|
2464
|
+
// No cleanup between tests!
|
|
2465
|
+
});
|
|
2466
|
+
|
|
2467
|
+
// ✅ CORRECT - Proper test cleanup
|
|
2468
|
+
afterEach(async () => {
|
|
2469
|
+
await loggerClass.clear();
|
|
2470
|
+
configClass.clearCache();
|
|
2471
|
+
});
|
|
2472
|
+
```
|
|
2473
|
+
|
|
2474
|
+
---
|
|
2475
|
+
|
|
2476
|
+
## 📋 COMPREHENSIVE CHECKLIST FOR LLMs
|
|
2477
|
+
|
|
2478
|
+
### **Module Usage**
|
|
2479
|
+
|
|
2480
|
+
- [ ] Always use `moduleClass.get()` pattern
|
|
2481
|
+
- [ ] Use exact object names from reference table (singular, matches module
|
|
2482
|
+
name)
|
|
2483
|
+
- [ ] Use namespace suffixes only for cache
|
|
2484
|
+
(`userCache = cacheClass.get('users')`)
|
|
2485
|
+
- [ ] Follow dependency order for initialization
|
|
2486
|
+
|
|
2487
|
+
### **Essential Modules**
|
|
2488
|
+
|
|
2489
|
+
- [ ] Use `util.get()` for safe property access
|
|
2490
|
+
- [ ] Include `userId`, `role`, `level` in JWT tokens (all required)
|
|
2491
|
+
- [ ] Use `config.getRequired()` for critical configuration
|
|
2492
|
+
- [ ] Follow UPPER_SNAKE_CASE → dot.notation convention
|
|
2493
|
+
- [ ] Use component-specific loggers (`loggerClass.get('component')`)
|
|
2494
|
+
- [ ] Log with structured data, not just messages
|
|
2495
|
+
|
|
2496
|
+
### **Infrastructure Modules**
|
|
2497
|
+
|
|
2498
|
+
- [ ] Include `tenant_id` field in ALL database tables with index
|
|
2499
|
+
- [ ] Use appropriate database access pattern (normal vs admin vs org)
|
|
2500
|
+
- [ ] Use specific cache namespaces with getOrSet pattern
|
|
2501
|
+
- [ ] Queue heavy operations instead of blocking requests
|
|
2502
|
+
- [ ] Handle file uploads with proper error handling
|
|
2503
|
+
- [ ] Cache frequently accessed data with appropriate TTL
|
|
2504
|
+
|
|
2505
|
+
### **System Modules**
|
|
2506
|
+
|
|
2507
|
+
- [ ] Use semantic error types (`badRequest`, `unauthorized`, `forbidden`,
|
|
2508
|
+
`notFound`, `serverError`)
|
|
2509
|
+
- [ ] Put session middleware BEFORE CSRF protection
|
|
2510
|
+
- [ ] Put error handling middleware LAST in Express
|
|
2511
|
+
- [ ] Wrap async routes with `error.asyncRoute()`
|
|
2512
|
+
- [ ] Always sanitize user input with `security.input()` and `security.html()`
|
|
2513
|
+
- [ ] Set required security environment variables
|
|
2514
|
+
|
|
2515
|
+
### **Production Deployment**
|
|
2516
|
+
|
|
2517
|
+
- [ ] Validate environment variables at startup
|
|
2518
|
+
- [ ] Implement graceful shutdown for all production apps
|
|
2519
|
+
- [ ] Set up proper middleware order (session → CSRF → rate limiting → error
|
|
2520
|
+
handling)
|
|
2521
|
+
- [ ] Test database connections before starting server
|
|
2522
|
+
- [ ] Use structured logging with request IDs
|
|
2523
|
+
- [ ] Handle Docker signals properly (SIGTERM, SIGINT)
|
|
2524
|
+
- [ ] Set all required environment variables for production
|
|
2525
|
+
|
|
2526
|
+
### **Testing**
|
|
2527
|
+
|
|
2528
|
+
- [ ] Reset module state between tests (`utilClass.clearCache()`, etc.)
|
|
2529
|
+
- [ ] Use proper test cleanup in afterEach
|
|
2530
|
+
- [ ] Force test configuration with `configClass.reset()`
|
|
2531
|
+
- [ ] Clean up resources (database, cache, logger)
|
|
2532
|
+
|
|
2533
|
+
### **Framework Integration**
|
|
2534
|
+
|
|
2535
|
+
- [ ] Use `auth.requireLogin()` and `auth.requireRole()` middleware correctly
|
|
2536
|
+
- [ ] Apply rate limiting on public endpoints
|
|
2537
|
+
- [ ] Encrypt sensitive data before storing in database
|
|
2538
|
+
- [ ] Log important operations with structured data
|
|
2539
|
+
- [ ] Implement proper health check endpoints
|