@bloomneo/appkit 1.5.1 → 1.5.2
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/AGENTS.md +195 -0
- package/CHANGELOG.md +253 -0
- package/README.md +147 -799
- package/bin/commands/generate.js +7 -7
- package/cookbook/README.md +26 -0
- package/cookbook/api-key-service.ts +106 -0
- package/cookbook/auth-protected-crud.ts +112 -0
- package/cookbook/file-upload-pipeline.ts +113 -0
- package/cookbook/multi-tenant-saas.ts +87 -0
- package/cookbook/real-time-chat.ts +121 -0
- package/dist/auth/auth.d.ts +21 -4
- package/dist/auth/auth.d.ts.map +1 -1
- package/dist/auth/auth.js +56 -44
- package/dist/auth/auth.js.map +1 -1
- package/dist/auth/defaults.d.ts +1 -1
- package/dist/auth/defaults.js +35 -35
- package/dist/cache/cache.d.ts +29 -6
- package/dist/cache/cache.d.ts.map +1 -1
- package/dist/cache/cache.js +72 -44
- package/dist/cache/cache.js.map +1 -1
- package/dist/cache/defaults.js +25 -25
- package/dist/cache/index.d.ts +19 -10
- package/dist/cache/index.d.ts.map +1 -1
- package/dist/cache/index.js +21 -18
- package/dist/cache/index.js.map +1 -1
- package/dist/config/defaults.d.ts +1 -1
- package/dist/config/defaults.js +8 -8
- package/dist/config/index.d.ts +3 -3
- package/dist/config/index.js +4 -4
- package/dist/database/adapters/mongoose.js +2 -2
- package/dist/database/adapters/prisma.js +2 -2
- package/dist/database/defaults.d.ts +1 -1
- package/dist/database/defaults.js +4 -4
- package/dist/database/index.js +2 -2
- package/dist/database/index.js.map +1 -1
- package/dist/email/defaults.js +20 -20
- package/dist/error/defaults.d.ts +1 -1
- package/dist/error/defaults.js +12 -12
- package/dist/error/error.d.ts +12 -0
- package/dist/error/error.d.ts.map +1 -1
- package/dist/error/error.js +19 -0
- package/dist/error/error.js.map +1 -1
- package/dist/error/index.d.ts +14 -3
- package/dist/error/index.d.ts.map +1 -1
- package/dist/error/index.js +14 -3
- package/dist/error/index.js.map +1 -1
- package/dist/event/defaults.js +30 -30
- package/dist/logger/defaults.d.ts +1 -1
- package/dist/logger/defaults.js +40 -40
- package/dist/logger/index.d.ts +1 -0
- package/dist/logger/index.d.ts.map +1 -1
- package/dist/logger/index.js.map +1 -1
- package/dist/logger/logger.d.ts +8 -0
- package/dist/logger/logger.d.ts.map +1 -1
- package/dist/logger/logger.js +13 -3
- package/dist/logger/logger.js.map +1 -1
- package/dist/logger/transports/console.js +1 -1
- package/dist/logger/transports/http.d.ts +1 -1
- package/dist/logger/transports/http.js +1 -1
- package/dist/logger/transports/webhook.d.ts +1 -1
- package/dist/logger/transports/webhook.js +1 -1
- package/dist/queue/defaults.d.ts +2 -2
- package/dist/queue/defaults.js +38 -38
- package/dist/security/defaults.d.ts +1 -1
- package/dist/security/defaults.js +29 -29
- package/dist/security/index.d.ts +1 -1
- package/dist/security/index.js +3 -3
- package/dist/security/security.d.ts +1 -1
- package/dist/security/security.js +4 -4
- package/dist/storage/defaults.js +19 -19
- package/dist/util/defaults.d.ts +1 -1
- package/dist/util/defaults.js +34 -34
- package/dist/util/env.d.ts +35 -0
- package/dist/util/env.d.ts.map +1 -0
- package/dist/util/env.js +50 -0
- package/dist/util/env.js.map +1 -0
- package/dist/util/errors.d.ts +52 -0
- package/dist/util/errors.d.ts.map +1 -0
- package/dist/util/errors.js +82 -0
- package/dist/util/errors.js.map +1 -0
- package/examples/.env.example +80 -0
- package/examples/README.md +16 -0
- package/examples/auth.ts +228 -0
- package/examples/cache.ts +36 -0
- package/examples/config.ts +45 -0
- package/examples/database.ts +69 -0
- package/examples/email.ts +53 -0
- package/examples/error.ts +50 -0
- package/examples/event.ts +42 -0
- package/examples/logger.ts +41 -0
- package/examples/queue.ts +58 -0
- package/examples/security.ts +46 -0
- package/examples/storage.ts +44 -0
- package/examples/util.ts +47 -0
- package/llms.txt +591 -0
- package/package.json +19 -10
- package/src/auth/README.md +850 -0
- package/src/cache/README.md +756 -0
- package/src/config/README.md +604 -0
- package/src/database/README.md +818 -0
- package/src/email/README.md +759 -0
- package/src/error/README.md +660 -0
- package/src/event/README.md +729 -0
- package/src/logger/README.md +435 -0
- package/src/queue/README.md +851 -0
- package/src/security/README.md +612 -0
- package/src/storage/README.md +1008 -0
- package/src/util/README.md +955 -0
- package/bin/templates/backend/docs/APPKIT_CLI.md +0 -507
- package/bin/templates/backend/docs/APPKIT_COMMENTS_GUIDELINES.md +0 -61
- package/bin/templates/backend/docs/APPKIT_LLM_GUIDE.md +0 -2539
|
@@ -0,0 +1,818 @@
|
|
|
1
|
+
# @bloomneo/appkit - Database Module 💾
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@bloomneo/appkit)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://www.typescriptlang.org/)
|
|
6
|
+
|
|
7
|
+
> Ultra-simple database wrapper with automatic tenant isolation and progressive
|
|
8
|
+
> multi-organization support that grows with your needs
|
|
9
|
+
|
|
10
|
+
**One simple function** - `databaseClass.get()` - handles everything from single
|
|
11
|
+
databases to complex multi-org, multi-tenant architectures. **Zero configuration
|
|
12
|
+
needed**, production-ready by default, with **mandatory future-proofing** built
|
|
13
|
+
in.
|
|
14
|
+
|
|
15
|
+
## 🚀 Why Choose AppKit Database?
|
|
16
|
+
|
|
17
|
+
- **⚡ One Function** - `databaseClass.get()` handles all use cases, environment
|
|
18
|
+
controls behavior
|
|
19
|
+
- **🔧 Zero Configuration** - Just `DATABASE_URL`, everything else is optional
|
|
20
|
+
- **📈 Progressive Scaling** - Start simple, add tenants/orgs with zero code
|
|
21
|
+
changes
|
|
22
|
+
- **🛡️ Future-Proof Schema** - Mandatory `tenant_id` field prevents migration
|
|
23
|
+
pain
|
|
24
|
+
- **🔥 Hot Reload** - Change `.env` file, connections update instantly
|
|
25
|
+
- **🌍 Multi-Cloud Ready** - Each org can use different cloud providers
|
|
26
|
+
- **🤖 LLM-Optimized** - Clear variable naming patterns for AI code generation
|
|
27
|
+
|
|
28
|
+
## 📦 Installation
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
npm install @bloomneo/appkit
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Database-Specific Dependencies
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# PostgreSQL/MySQL/SQLite with Prisma
|
|
38
|
+
npm install @bloomneo/appkit @prisma/client
|
|
39
|
+
|
|
40
|
+
# MongoDB with Mongoose
|
|
41
|
+
npm install @bloomneo/appkit mongoose
|
|
42
|
+
|
|
43
|
+
# Multi-database setup (both ORMs)
|
|
44
|
+
npm install @bloomneo/appkit @prisma/client mongoose
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## 🏃♂️ Quick Start (30 seconds)
|
|
48
|
+
|
|
49
|
+
### Single Database (Day 1)
|
|
50
|
+
|
|
51
|
+
```typescript
|
|
52
|
+
import { database } from '@bloomneo/appkit/database';
|
|
53
|
+
|
|
54
|
+
// PostgreSQL/MySQL with Prisma
|
|
55
|
+
const database = await databaseClass.get();
|
|
56
|
+
const users = await database.user.findMany();
|
|
57
|
+
|
|
58
|
+
// MongoDB with Mongoose
|
|
59
|
+
const database = await databaseClass.get();
|
|
60
|
+
const users = await database.User.find();
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Multi-Tenant (Month 6 - Zero Code Changes!)
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# Add to .env file - code stays exactly the same
|
|
67
|
+
BLOOM_DB_TENANT=auto
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
// Same code, now tenant-filtered automatically
|
|
72
|
+
const database = await databaseClass.get(); // User's tenant data only
|
|
73
|
+
|
|
74
|
+
// Prisma (SQL databases)
|
|
75
|
+
const users = await database.user.findMany(); // Auto-filtered by tenant
|
|
76
|
+
|
|
77
|
+
// Mongoose (MongoDB)
|
|
78
|
+
const users = await database.User.find(); // Auto-filtered by tenant
|
|
79
|
+
|
|
80
|
+
// Admin access to all tenants
|
|
81
|
+
const dbTenants = await databaseClass.getTenants();
|
|
82
|
+
const allUsers = await dbTenants.user.findMany(); // Prisma - All tenant data
|
|
83
|
+
const allUsers = await dbTenants.User.find(); // Mongoose - All tenant data
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Multi-Organization (Year 1 - Still Zero Code Changes!)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
# Add org-specific databases to .env
|
|
90
|
+
ORG_ACME=postgresql://acme.aws.com/prod # PostgreSQL on AWS
|
|
91
|
+
ORG_TECH=mongodb://tech.azure.com/prod # MongoDB on Azure
|
|
92
|
+
ORG_STARTUP=mysql://startup.gcp.com/prod # MySQL on GCP
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
// Same code, now org-aware with auto-adapter detection
|
|
97
|
+
const acmedatabase = await databaseClass.org('acme').get(); // Uses Prisma for PostgreSQL
|
|
98
|
+
const techdatabase = await databaseClass.org('tech').get(); // Uses Mongoose for MongoDB
|
|
99
|
+
const startupdatabase = await databaseClass.org('startup').get(); // Uses Prisma for MySQL
|
|
100
|
+
|
|
101
|
+
// Different database queries, same simple API
|
|
102
|
+
const acmeUsers = await acmedatabase.user.findMany(); // Prisma syntax
|
|
103
|
+
const techUsers = await techdatabase.User.find(); // Mongoose syntax
|
|
104
|
+
const startupUsers = await startupdatabase.user.findMany(); // Prisma syntax
|
|
105
|
+
|
|
106
|
+
// Org admin access
|
|
107
|
+
const acmeDbTenants = await databaseClass.org('acme').getTenants();
|
|
108
|
+
const techDbTenants = await databaseClass.org('tech').getTenants();
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**That's it!** Your code never changes, only your environment evolves.
|
|
112
|
+
|
|
113
|
+
## 🎯 Core API
|
|
114
|
+
|
|
115
|
+
### **One Function Rule: `databaseClass.get()`**
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
// Normal user access (single tenant or their specific tenant)
|
|
119
|
+
const database = await databaseClass.get();
|
|
120
|
+
|
|
121
|
+
// Admin access to all tenants
|
|
122
|
+
const dbTenants = await databaseClass.getTenants();
|
|
123
|
+
|
|
124
|
+
// Organization-specific access
|
|
125
|
+
const acmedatabase = await databaseClass.org('acme').get();
|
|
126
|
+
const acmeDbTenants = await databaseClass.org('acme').getTenants();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### **LLM-Friendly Variable Naming**
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
// Standard patterns for AI code generation:
|
|
133
|
+
const database = await databaseClass.get(); // Single/tenant user data
|
|
134
|
+
const dbTenants = await databaseClass.getTenants(); // All tenants (admin)
|
|
135
|
+
const acmedatabase = await databaseClass.org('acme').get(); // Acme org data
|
|
136
|
+
const acmeDbTenants = await databaseClass.org('acme').getTenants(); // All Acme tenants
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## 🛡️ Mandatory Future-Proofing
|
|
140
|
+
|
|
141
|
+
### **Required Schema Pattern**
|
|
142
|
+
|
|
143
|
+
**EVERY table/collection MUST include `tenant_id` field from Day 1:**
|
|
144
|
+
|
|
145
|
+
#### **SQL Databases (Prisma)**
|
|
146
|
+
|
|
147
|
+
```sql
|
|
148
|
+
-- ✅ CORRECT: Future-proof schema
|
|
149
|
+
CREATE TABLE users (
|
|
150
|
+
id uuid PRIMARY KEY,
|
|
151
|
+
email text UNIQUE,
|
|
152
|
+
name text,
|
|
153
|
+
tenant_id text, -- MANDATORY: nullable for future compatibility
|
|
154
|
+
created_at timestamp DEFAULT now(),
|
|
155
|
+
|
|
156
|
+
INDEX idx_users_tenant (tenant_id) -- MANDATORY: performance index
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
CREATE TABLE posts (
|
|
160
|
+
id uuid PRIMARY KEY,
|
|
161
|
+
title text,
|
|
162
|
+
content text,
|
|
163
|
+
user_id uuid REFERENCES users(id),
|
|
164
|
+
tenant_id text, -- MANDATORY: on EVERY table
|
|
165
|
+
created_at timestamp DEFAULT now(),
|
|
166
|
+
|
|
167
|
+
INDEX idx_posts_tenant (tenant_id) -- MANDATORY: on EVERY table
|
|
168
|
+
);
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
```prisma
|
|
172
|
+
// Prisma schema example
|
|
173
|
+
model User {
|
|
174
|
+
id String @id @default(cuid())
|
|
175
|
+
email String @unique
|
|
176
|
+
name String
|
|
177
|
+
tenant_id String? // MANDATORY: nullable for future use
|
|
178
|
+
createdAt DateTime @default(now())
|
|
179
|
+
|
|
180
|
+
@@index([tenant_id]) // MANDATORY: performance index
|
|
181
|
+
@@map("users")
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
model Post {
|
|
185
|
+
id String @id @default(cuid())
|
|
186
|
+
title String
|
|
187
|
+
content String
|
|
188
|
+
userId String
|
|
189
|
+
tenant_id String? // MANDATORY: on EVERY table
|
|
190
|
+
createdAt DateTime @default(now())
|
|
191
|
+
|
|
192
|
+
user User @relation(fields: [userId], references: [id])
|
|
193
|
+
|
|
194
|
+
@@index([tenant_id]) // MANDATORY: on EVERY table
|
|
195
|
+
@@map("posts")
|
|
196
|
+
}
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
#### **MongoDB (Mongoose)**
|
|
200
|
+
|
|
201
|
+
```javascript
|
|
202
|
+
// Mongoose schema example
|
|
203
|
+
const userSchema = new Schema({
|
|
204
|
+
email: { type: String, unique: true, required: true },
|
|
205
|
+
name: { type: String, required: true },
|
|
206
|
+
tenant_id: { type: String, index: true }, // MANDATORY: indexed for performance
|
|
207
|
+
createdAt: { type: Date, default: Date.now },
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// MANDATORY: Index for performance
|
|
211
|
+
userSchema.index({ tenant_id: 1 });
|
|
212
|
+
|
|
213
|
+
const postSchema = new Schema({
|
|
214
|
+
title: { type: String, required: true },
|
|
215
|
+
content: { type: String, required: true },
|
|
216
|
+
userId: { type: Schema.Types.ObjectId, ref: 'User', required: true },
|
|
217
|
+
tenant_id: { type: String, index: true }, // MANDATORY: on EVERY schema
|
|
218
|
+
createdAt: { type: Date, default: Date.now },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
// MANDATORY: Index on every schema
|
|
222
|
+
postSchema.index({ tenant_id: 1 });
|
|
223
|
+
|
|
224
|
+
export const User = model('User', userSchema);
|
|
225
|
+
export const Post = model('Post', postSchema);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### **Why Mandatory `tenant_id`?**
|
|
229
|
+
|
|
230
|
+
- ✅ **Zero Migration Pain** - Enable multi-tenancy later with just environment
|
|
231
|
+
variables
|
|
232
|
+
- ✅ **Performance Ready** - Indexes in place from day 1
|
|
233
|
+
- ✅ **No Data Restructuring** - Never need to alter table schemas
|
|
234
|
+
- ✅ **Gradual Adoption** - Start single-tenant, scale when needed
|
|
235
|
+
|
|
236
|
+
## 🌍 Environment Configuration
|
|
237
|
+
|
|
238
|
+
### **Minimal Setup (2 Variables)**
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
# Required: Main database connection
|
|
242
|
+
DATABASE_URL=postgresql://localhost:5432/myapp # PostgreSQL
|
|
243
|
+
# OR
|
|
244
|
+
DATABASE_URL=mongodb://localhost:27017/myapp # MongoDB
|
|
245
|
+
# OR
|
|
246
|
+
DATABASE_URL=mysql://localhost:3306/myapp # MySQL
|
|
247
|
+
|
|
248
|
+
# Optional: Enable tenant mode (auto-detects from requests)
|
|
249
|
+
BLOOM_DB_TENANT=auto
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### **Multi-Database & Multi-Organization Setup**
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Fallback database
|
|
256
|
+
DATABASE_URL=postgresql://localhost:5432/main
|
|
257
|
+
|
|
258
|
+
# Organization-specific databases (any provider)
|
|
259
|
+
ORG_ACME=postgresql://acme.aws.com/prod # PostgreSQL on AWS
|
|
260
|
+
ORG_TECH=mongodb://tech.azure.com/db # MongoDB on Azure
|
|
261
|
+
ORG_STARTUP=mysql://startup.gcp.com/prod # MySQL on GCP
|
|
262
|
+
ORG_LOCAL=sqlite:///local/dev.db # SQLite for development
|
|
263
|
+
ORG_LEGACY=mongodb://legacy.onprem.com/data # On-premise MongoDB
|
|
264
|
+
|
|
265
|
+
# Enable tenant mode within each org
|
|
266
|
+
BLOOM_DB_TENANT=auto
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### **Hot Reload Magic**
|
|
270
|
+
|
|
271
|
+
```bash
|
|
272
|
+
# Change .env file while app is running:
|
|
273
|
+
echo "ORG_NEWCLIENT=postgresql://newclient.com/db" >> .env
|
|
274
|
+
|
|
275
|
+
# Connections update instantly - no server restart needed! 🔥
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
## 💡 Real-World Examples
|
|
279
|
+
|
|
280
|
+
### **Progressive Scaling Journey**
|
|
281
|
+
|
|
282
|
+
```typescript
|
|
283
|
+
/**
|
|
284
|
+
* Day 1: Simple blog application
|
|
285
|
+
*/
|
|
286
|
+
async function getBlogPosts() {
|
|
287
|
+
const database = await databaseClass.get();
|
|
288
|
+
return await database.posts.findMany({
|
|
289
|
+
include: { user: true },
|
|
290
|
+
orderBy: { createdAt: 'desc' },
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Month 6: Add team workspaces (zero code changes!)
|
|
296
|
+
* Just add: BLOOM_DB_TENANT=auto to .env
|
|
297
|
+
*/
|
|
298
|
+
async function getBlogPosts() {
|
|
299
|
+
const database = await databaseClass.get(); // Now auto-filters by tenant
|
|
300
|
+
return await database.posts.findMany({
|
|
301
|
+
include: { user: true },
|
|
302
|
+
orderBy: { createdAt: 'desc' },
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Year 1: Multi-organization SaaS (still zero code changes!)
|
|
308
|
+
* Just add org URLs to .env
|
|
309
|
+
*/
|
|
310
|
+
async function getBlogPosts() {
|
|
311
|
+
const database = await databaseClass.get(); // Now org + tenant aware
|
|
312
|
+
return await database.posts.findMany({
|
|
313
|
+
include: { user: true },
|
|
314
|
+
orderBy: { createdAt: 'desc' },
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Admin dashboard (any time)
|
|
320
|
+
*/
|
|
321
|
+
async function getAllOrgPosts(orgId) {
|
|
322
|
+
const dbTenants = await databaseClass.org(orgId).getTenants();
|
|
323
|
+
return await dbTenants.posts.findMany({
|
|
324
|
+
include: { user: true },
|
|
325
|
+
orderBy: { createdAt: 'desc' },
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### **Multi-Tenant API Endpoints**
|
|
331
|
+
|
|
332
|
+
```typescript
|
|
333
|
+
import { database } from '@bloomneo/appkit/database';
|
|
334
|
+
|
|
335
|
+
// User endpoints - auto-filtered by tenant
|
|
336
|
+
app.get('/api/users', async (req, res) => {
|
|
337
|
+
const database = await databaseClass.get();
|
|
338
|
+
const users = await database.user.findMany();
|
|
339
|
+
res.json(users); // Only user's tenant data
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
app.post('/api/users', async (req, res) => {
|
|
343
|
+
const database = await databaseClass.get();
|
|
344
|
+
const user = await database.user.create({
|
|
345
|
+
data: req.body, // tenant_id added automatically
|
|
346
|
+
});
|
|
347
|
+
res.json(user);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
// Admin endpoints - see all tenant data
|
|
351
|
+
app.get('/api/admin/users', requireRole('admin'), async (req, res) => {
|
|
352
|
+
const dbTenants = await databaseClass.getTenants();
|
|
353
|
+
const users = await dbTenants.user.findMany({
|
|
354
|
+
include: { _count: { select: { posts: true } } },
|
|
355
|
+
});
|
|
356
|
+
res.json(users); // All tenants data
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
// Organization management
|
|
360
|
+
app.get('/api/orgs/:orgId/users', requireRole('admin'), async (req, res) => {
|
|
361
|
+
const { orgId } = req.params;
|
|
362
|
+
const orgdatabase = await databaseClass.org(orgId).get();
|
|
363
|
+
const users = await orgdatabase.user.findMany();
|
|
364
|
+
res.json(users); // Specific org data
|
|
365
|
+
});
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### **Multi-Cloud Enterprise Setup**
|
|
369
|
+
|
|
370
|
+
```typescript
|
|
371
|
+
/**
|
|
372
|
+
* Enterprise deployment with different cloud providers and databases per organization
|
|
373
|
+
*/
|
|
374
|
+
|
|
375
|
+
// Environment configuration supports any database provider:
|
|
376
|
+
const envConfig = `
|
|
377
|
+
# System/Admin database
|
|
378
|
+
DATABASE_URL=postgresql://admin.company.com/system
|
|
379
|
+
|
|
380
|
+
# Customer organizations on different clouds and databases
|
|
381
|
+
ORG_ENTERPRISE_CORP=postgresql://enterprise.dedicated.aws.com/prod
|
|
382
|
+
ORG_TECH_STARTUP=mongodb://tech.shared.azure.com/startup_db
|
|
383
|
+
ORG_LOCAL_BUSINESS=mysql://local.gcp.com:3306/business_db
|
|
384
|
+
ORG_DEV_TESTING=sqlite:///tmp/testing.db
|
|
385
|
+
|
|
386
|
+
# Enable tenant mode across all orgs
|
|
387
|
+
BLOOM_DB_TENANT=auto
|
|
388
|
+
`;
|
|
389
|
+
|
|
390
|
+
// Code remains identical regardless of backend:
|
|
391
|
+
async function getUserData(orgId, userId) {
|
|
392
|
+
const database = await databaseClass.org(orgId).get();
|
|
393
|
+
|
|
394
|
+
// Works with any database type - AppKit handles the differences
|
|
395
|
+
if (database.user?.findUnique) {
|
|
396
|
+
// Prisma client (PostgreSQL, MySQL, SQLite)
|
|
397
|
+
return await database.user.findUnique({
|
|
398
|
+
where: { id: userId },
|
|
399
|
+
include: { posts: true, profile: true },
|
|
400
|
+
});
|
|
401
|
+
} else if (database.User?.findOne) {
|
|
402
|
+
// Mongoose client (MongoDB)
|
|
403
|
+
return await database.User.findOne({ _id: userId })
|
|
404
|
+
.populate('posts')
|
|
405
|
+
.populate('profile');
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
## 🔧 Automatic Context Detection
|
|
411
|
+
|
|
412
|
+
### **Tenant Detection Sources** (when `BLOOM_DB_TENANT=auto`)
|
|
413
|
+
|
|
414
|
+
```typescript
|
|
415
|
+
// AppKit automatically detects tenant from:
|
|
416
|
+
const tenantId =
|
|
417
|
+
req.headers['x-tenant-id'] || // API header (recommended)
|
|
418
|
+
req.user?.tenant_id || // Authenticated user metadata
|
|
419
|
+
req.params?.tenantId || // URL parameter
|
|
420
|
+
req.query?.tenant || // Query parameter
|
|
421
|
+
req.subdomain || // Subdomain (team.app.com)
|
|
422
|
+
null; // Single tenant mode
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
### **Organization Detection Sources**
|
|
426
|
+
|
|
427
|
+
```typescript
|
|
428
|
+
// AppKit automatically detects organization from:
|
|
429
|
+
const orgId =
|
|
430
|
+
req.headers['x-org-id'] || // API header (recommended)
|
|
431
|
+
req.user?.org_id || // Authenticated user metadata
|
|
432
|
+
req.params?.orgId || // URL parameter
|
|
433
|
+
req.query?.org || // Query parameter
|
|
434
|
+
req.subdomain || // Subdomain (acme.app.com)
|
|
435
|
+
null; // Single org mode
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### **Manual Override** (when needed)
|
|
439
|
+
|
|
440
|
+
```typescript
|
|
441
|
+
// Override auto-detection when needed
|
|
442
|
+
const specificTenantdatabase = await databaseClass.get({
|
|
443
|
+
tenant: 'specific-tenant',
|
|
444
|
+
});
|
|
445
|
+
const specificOrgdatabase = await databaseClass.org('specific-org').get();
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
## 🚀 Framework Integration
|
|
449
|
+
|
|
450
|
+
### **Express.js**
|
|
451
|
+
|
|
452
|
+
```typescript
|
|
453
|
+
import express from 'express';
|
|
454
|
+
import { database } from '@bloomneo/appkit/database';
|
|
455
|
+
|
|
456
|
+
const app = express();
|
|
457
|
+
|
|
458
|
+
// Simple route - auto-detects tenant from request
|
|
459
|
+
app.get('/users', async (req, res) => {
|
|
460
|
+
const database = await databaseClass.get();
|
|
461
|
+
const users = await database.user.findMany();
|
|
462
|
+
res.json(users);
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
// Admin route - access all tenants
|
|
466
|
+
app.get('/admin/users', requireAdmin, async (req, res) => {
|
|
467
|
+
const dbTenants = await databaseClass.getTenants();
|
|
468
|
+
const users = await dbTenants.user.findMany();
|
|
469
|
+
res.json(users);
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### **Fastify**
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
import Fastify from 'fastify';
|
|
477
|
+
import { database } from '@bloomneo/appkit/database';
|
|
478
|
+
|
|
479
|
+
const fastify = Fastify();
|
|
480
|
+
|
|
481
|
+
fastify.get('/users', async (request, reply) => {
|
|
482
|
+
const database = await databaseClass.get();
|
|
483
|
+
const users = await database.user.findMany();
|
|
484
|
+
return users;
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
fastify.get(
|
|
488
|
+
'/admin/users',
|
|
489
|
+
{ preHandler: requireAdmin },
|
|
490
|
+
async (request, reply) => {
|
|
491
|
+
const dbTenants = await databaseClass.getTenants();
|
|
492
|
+
const users = await dbTenants.user.findMany();
|
|
493
|
+
return users;
|
|
494
|
+
}
|
|
495
|
+
);
|
|
496
|
+
```
|
|
497
|
+
|
|
498
|
+
### **Next.js API Routes**
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
// pages/api/users.ts
|
|
502
|
+
import { database } from '@bloomneo/appkit/database';
|
|
503
|
+
|
|
504
|
+
export default async function handler(req, res) {
|
|
505
|
+
const database = await databaseClass.get();
|
|
506
|
+
|
|
507
|
+
if (req.method === 'GET') {
|
|
508
|
+
const users = await database.user.findMany();
|
|
509
|
+
res.json(users);
|
|
510
|
+
} else if (req.method === 'POST') {
|
|
511
|
+
const user = await database.user.create({ data: req.body });
|
|
512
|
+
res.json(user);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// pages/api/admin/users.ts
|
|
517
|
+
import { database } from '@bloomneo/appkit/database';
|
|
518
|
+
|
|
519
|
+
export default async function handler(req, res) {
|
|
520
|
+
const dbTenants = await databaseClass.getTenants();
|
|
521
|
+
const users = await dbTenants.user.findMany();
|
|
522
|
+
res.json(users);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
## 🛠️ Advanced Features
|
|
527
|
+
|
|
528
|
+
### **Health Monitoring**
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
// System health check
|
|
532
|
+
const health = await databaseClass.health();
|
|
533
|
+
console.log(health);
|
|
534
|
+
// {
|
|
535
|
+
// healthy: true,
|
|
536
|
+
// connections: 3,
|
|
537
|
+
// timestamp: "2024-01-15T10:30:00.000Z"
|
|
538
|
+
// }
|
|
539
|
+
```
|
|
540
|
+
|
|
541
|
+
### **Tenant Management**
|
|
542
|
+
|
|
543
|
+
```typescript
|
|
544
|
+
// List all tenants
|
|
545
|
+
const tenants = await databaseClass.list();
|
|
546
|
+
console.log(tenants); // ['team-alpha', 'team-beta', 'team-gamma']
|
|
547
|
+
|
|
548
|
+
// Check if tenant exists
|
|
549
|
+
const exists = await databaseClass.exists('team-alpha');
|
|
550
|
+
console.log(exists); // true
|
|
551
|
+
|
|
552
|
+
// Create tenant (validates ID format)
|
|
553
|
+
await databaseClass.create('new-team');
|
|
554
|
+
|
|
555
|
+
// Delete tenant (requires confirmation)
|
|
556
|
+
await databaseClass.delete('old-team', { confirm: true });
|
|
557
|
+
```
|
|
558
|
+
|
|
559
|
+
### **Connection Management**
|
|
560
|
+
|
|
561
|
+
```typescript
|
|
562
|
+
// Graceful shutdown
|
|
563
|
+
process.on('SIGTERM', async () => {
|
|
564
|
+
await databaseClass.disconnect();
|
|
565
|
+
process.exit(0);
|
|
566
|
+
});
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
## 📊 Performance & Scaling
|
|
570
|
+
|
|
571
|
+
### **Connection Pooling**
|
|
572
|
+
|
|
573
|
+
- **Automatic caching** - Connections reused per org/tenant combination
|
|
574
|
+
- **Hot reload** - New .env configurations picked up instantly
|
|
575
|
+
- **Memory efficient** - Connections shared across requests
|
|
576
|
+
|
|
577
|
+
### **Database Performance**
|
|
578
|
+
|
|
579
|
+
- **Mandatory indexes** - `tenant_id` indexed on all tables from day 1
|
|
580
|
+
- **Query optimization** - Automatic tenant filtering at database level
|
|
581
|
+
- **Connection limits** - Respects database provider connection pools
|
|
582
|
+
|
|
583
|
+
### **Scaling Characteristics**
|
|
584
|
+
|
|
585
|
+
- **Single tenant**: 1 connection per app
|
|
586
|
+
- **Multi-tenant**: 1 connection (shared filtering)
|
|
587
|
+
- **Multi-org**: 1 connection per organization
|
|
588
|
+
- **Multi-org + tenant**: 1 connection per org (shared tenant filtering)
|
|
589
|
+
|
|
590
|
+
## 🔍 Migration Guide
|
|
591
|
+
|
|
592
|
+
### **From Direct Prisma**
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
// Before: Direct Prisma usage
|
|
596
|
+
import { PrismaClient } from '@prisma/client';
|
|
597
|
+
const prisma = new PrismaClient();
|
|
598
|
+
const users = await prisma.user.findMany();
|
|
599
|
+
|
|
600
|
+
// After: AppKit Database
|
|
601
|
+
import { database } from '@bloomneo/appkit/database';
|
|
602
|
+
const database = await databaseClass.get();
|
|
603
|
+
const users = await database.user.findMany();
|
|
604
|
+
```
|
|
605
|
+
|
|
606
|
+
### **From Manual Multi-Tenancy**
|
|
607
|
+
|
|
608
|
+
```typescript
|
|
609
|
+
// Before: Manual tenant filtering everywhere
|
|
610
|
+
const users = await prisma.user.findMany({
|
|
611
|
+
where: { tenant_id: getTenantId(req) },
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
// After: Automatic tenant filtering
|
|
615
|
+
const database = await databaseClass.get();
|
|
616
|
+
const users = await database.user.findMany(); // tenant_id added automatically
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
### **Schema Migration**
|
|
620
|
+
|
|
621
|
+
```sql
|
|
622
|
+
-- Add tenant_id to existing tables
|
|
623
|
+
ALTER TABLE users ADD COLUMN tenant_id text;
|
|
624
|
+
ALTER TABLE posts ADD COLUMN tenant_id text;
|
|
625
|
+
ALTER TABLE comments ADD COLUMN tenant_id text;
|
|
626
|
+
|
|
627
|
+
-- Add performance indexes
|
|
628
|
+
CREATE INDEX idx_users_tenant ON users(tenant_id);
|
|
629
|
+
CREATE INDEX idx_posts_tenant ON posts(tenant_id);
|
|
630
|
+
CREATE INDEX idx_comments_tenant ON comments(tenant_id);
|
|
631
|
+
|
|
632
|
+
-- Set existing data to null (single tenant mode)
|
|
633
|
+
-- No data changes needed - null = single tenant
|
|
634
|
+
```
|
|
635
|
+
|
|
636
|
+
## 🤖 LLM Guidelines
|
|
637
|
+
|
|
638
|
+
### **Variable Naming Patterns**
|
|
639
|
+
|
|
640
|
+
```typescript
|
|
641
|
+
// ✅ Standard patterns for AI code generation:
|
|
642
|
+
|
|
643
|
+
// Normal user access (single or tenant mode)
|
|
644
|
+
const database = await databaseClass.get();
|
|
645
|
+
|
|
646
|
+
// Admin access to all tenants
|
|
647
|
+
const dbTenants = await databaseClass.getTenants();
|
|
648
|
+
|
|
649
|
+
// Organization-specific access (use org name in variable)
|
|
650
|
+
const acmedatabase = await databaseClass.org('acme').get();
|
|
651
|
+
const techdatabase = await databaseClass.org('tech').get();
|
|
652
|
+
const startupdatabase = await databaseClass.org('startup').get();
|
|
653
|
+
|
|
654
|
+
// Organization admin access (use org name + DbTenants)
|
|
655
|
+
const acmeDbTenants = await databaseClass.org('acme').getTenants();
|
|
656
|
+
const techDbTenants = await databaseClass.org('tech').getTenants();
|
|
657
|
+
```
|
|
658
|
+
|
|
659
|
+
### **Common Patterns**
|
|
660
|
+
|
|
661
|
+
```typescript
|
|
662
|
+
// ✅ User data access
|
|
663
|
+
const database = await databaseClass.get();
|
|
664
|
+
const users = await database.user.findMany();
|
|
665
|
+
|
|
666
|
+
// ✅ Admin functionality
|
|
667
|
+
const dbTenants = await databaseClass.getTenants();
|
|
668
|
+
const allUsers = await dbTenants.user.findMany();
|
|
669
|
+
|
|
670
|
+
// ✅ Organization management
|
|
671
|
+
const acmedatabase = await databaseClass.org('acme').get();
|
|
672
|
+
const acmeUsers = await acmedatabase.user.findMany();
|
|
673
|
+
|
|
674
|
+
// ✅ Cross-tenant analytics (admin)
|
|
675
|
+
const acmeDbTenants = await databaseClass.org('acme').getTenants();
|
|
676
|
+
const analytics = await acmeDbTenants.user.groupBy({
|
|
677
|
+
by: ['tenant_id'],
|
|
678
|
+
_count: true,
|
|
679
|
+
});
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
### **Schema Requirements**
|
|
683
|
+
|
|
684
|
+
```typescript
|
|
685
|
+
// ✅ ALWAYS include in ALL models (SQL):
|
|
686
|
+
model AnyTable {
|
|
687
|
+
id String @id @default(cuid())
|
|
688
|
+
// ... your fields ...
|
|
689
|
+
tenant_id String? // MANDATORY: nullable for future use
|
|
690
|
+
|
|
691
|
+
@@index([tenant_id]) // MANDATORY: performance index
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// ✅ ALWAYS include in ALL schemas (MongoDB):
|
|
695
|
+
const anySchema = new Schema({
|
|
696
|
+
// ... your fields ...
|
|
697
|
+
tenant_id: { type: String, index: true } // MANDATORY: indexed
|
|
698
|
+
});
|
|
699
|
+
anySchema.index({ tenant_id: 1 }); // MANDATORY: performance index
|
|
700
|
+
```
|
|
701
|
+
|
|
702
|
+
## 🚨 Common Mistakes to Avoid
|
|
703
|
+
|
|
704
|
+
### **❌ Schema Mistakes**
|
|
705
|
+
|
|
706
|
+
```sql
|
|
707
|
+
-- ❌ DON'T: Missing tenant_id field
|
|
708
|
+
CREATE TABLE users (
|
|
709
|
+
id uuid PRIMARY KEY,
|
|
710
|
+
email text,
|
|
711
|
+
name text
|
|
712
|
+
-- Missing tenant_id - will need painful migration later
|
|
713
|
+
);
|
|
714
|
+
|
|
715
|
+
-- ✅ DO: Always include tenant_id (SQL)
|
|
716
|
+
CREATE TABLE users (
|
|
717
|
+
id uuid PRIMARY KEY,
|
|
718
|
+
email text,
|
|
719
|
+
name text,
|
|
720
|
+
tenant_id text, -- Future-proof from day 1
|
|
721
|
+
INDEX idx_tenant (tenant_id)
|
|
722
|
+
);
|
|
723
|
+
```
|
|
724
|
+
|
|
725
|
+
```javascript
|
|
726
|
+
// ❌ DON'T: Missing tenant_id field (MongoDB)
|
|
727
|
+
const userSchema = new Schema({
|
|
728
|
+
email: String,
|
|
729
|
+
name: String,
|
|
730
|
+
// Missing tenant_id - will need painful migration later
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
// ✅ DO: Always include tenant_id (MongoDB)
|
|
734
|
+
const userSchema = new Schema({
|
|
735
|
+
email: String,
|
|
736
|
+
name: String,
|
|
737
|
+
tenant_id: { type: String, index: true }, // Future-proof from day 1
|
|
738
|
+
});
|
|
739
|
+
userSchema.index({ tenant_id: 1 });
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
### **❌ API Usage Mistakes**
|
|
743
|
+
|
|
744
|
+
```typescript
|
|
745
|
+
// ❌ DON'T: Hard-code tenant access (any database)
|
|
746
|
+
const users = await prisma.user.findMany({
|
|
747
|
+
where: { tenant_id: 'hardcoded-tenant' },
|
|
748
|
+
});
|
|
749
|
+
const users = await User.find({ tenant_id: 'hardcoded-tenant' });
|
|
750
|
+
|
|
751
|
+
// ✅ DO: Use databaseClass.get() for automatic filtering
|
|
752
|
+
const database = await databaseClass.get();
|
|
753
|
+
const users = await database.user.findMany(); // Prisma - Auto-filtered
|
|
754
|
+
const users = await database.User.find(); // Mongoose - Auto-filtered
|
|
755
|
+
|
|
756
|
+
// ❌ DON'T: Mix access patterns
|
|
757
|
+
const database = await databaseClass.get();
|
|
758
|
+
const admindatabase = await databaseClass.getTenants();
|
|
759
|
+
const users = await database.user.findMany(); // Which database am I using?
|
|
760
|
+
|
|
761
|
+
// ✅ DO: Clear variable naming
|
|
762
|
+
const database = await databaseClass.get(); // User data
|
|
763
|
+
const dbTenants = await databaseClass.getTenants(); // Admin data
|
|
764
|
+
const users = await database.user.findMany(); // Clear intent (Prisma)
|
|
765
|
+
const users = await database.User.find(); // Clear intent (Mongoose)
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
## 🔧 Troubleshooting
|
|
769
|
+
|
|
770
|
+
### **Database Connection Issues**
|
|
771
|
+
|
|
772
|
+
```typescript
|
|
773
|
+
// Check configuration
|
|
774
|
+
import { database } from '@bloomneo/appkit/database';
|
|
775
|
+
|
|
776
|
+
const health = await databaseClass.health();
|
|
777
|
+
if (!health.healthy) {
|
|
778
|
+
console.error('Database issue:', health.error);
|
|
779
|
+
}
|
|
780
|
+
```
|
|
781
|
+
|
|
782
|
+
### **Missing tenant_id Fields**
|
|
783
|
+
|
|
784
|
+
```bash
|
|
785
|
+
# Development warning will show:
|
|
786
|
+
# Model 'User' missing required field 'tenant_id'
|
|
787
|
+
# Add: tenant_id String? @map("tenant_id") to your Prisma schema
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### **Environment Validation**
|
|
791
|
+
|
|
792
|
+
```typescript
|
|
793
|
+
import { getConfigSummary } from '@bloomneo/appkit/database/defaults';
|
|
794
|
+
|
|
795
|
+
console.log(getConfigSummary());
|
|
796
|
+
// Shows current configuration, validation status, and warnings
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
## 📈 Roadmap
|
|
800
|
+
|
|
801
|
+
- **Vector Search Support** - Built-in pgvector integration
|
|
802
|
+
- **Read Replicas** - Automatic read/write splitting
|
|
803
|
+
- **Connection Pooling** - Advanced connection management
|
|
804
|
+
- **Schema Migrations** - Automated tenant-aware migrations
|
|
805
|
+
- **Analytics Dashboard** - Built-in multi-tenant analytics
|
|
806
|
+
|
|
807
|
+
## 📄 License
|
|
808
|
+
|
|
809
|
+
MIT © [Bloomneo](https://github.com/bloomneo)
|
|
810
|
+
|
|
811
|
+
---
|
|
812
|
+
|
|
813
|
+
<p align="center">
|
|
814
|
+
<strong>Built for developers who value simplicity and future-proof architecture</strong><br>
|
|
815
|
+
<a href="https://github.com/bloomneo/appkit">⭐ Star us on GitHub</a> •
|
|
816
|
+
<a href="https://discord.gg/bloomneo">💬 Join our Discord</a> •
|
|
817
|
+
<a href="https://twitter.com/bloomneo">🐦 Follow on Twitter</a>
|
|
818
|
+
</p>
|