@env-hopper/backend-core 2.0.1-alpha.2 → 2.0.1-alpha.4
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/dist/index.d.ts +194 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +375 -1
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/db/client.ts +8 -0
- package/src/db/index.ts +8 -4
- package/src/index.ts +67 -52
- package/src/middleware/backendResolver.ts +49 -0
- package/src/middleware/createEhMiddleware.ts +103 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +178 -0
- package/src/middleware/index.ts +43 -0
- package/src/middleware/types.ts +186 -0
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +182 -0
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import type { Router } from 'express'
|
|
2
|
+
import type { LanguageModel, Tool } from 'ai'
|
|
3
|
+
import type { BetterAuthOptions, BetterAuthPlugin } from 'better-auth'
|
|
4
|
+
import type { EhBackendCompanySpecificBackend } from '../types/backend/companySpecificBackend'
|
|
5
|
+
import type { BetterAuth } from '../modules/auth/auth'
|
|
6
|
+
import type { TRPCRouter } from '../server/controller'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Database connection configuration.
|
|
10
|
+
* Supports both connection URL and structured config.
|
|
11
|
+
*/
|
|
12
|
+
export type EhDatabaseConfig =
|
|
13
|
+
| { url: string }
|
|
14
|
+
| {
|
|
15
|
+
host: string
|
|
16
|
+
port: number
|
|
17
|
+
database: string
|
|
18
|
+
username: string
|
|
19
|
+
password: string
|
|
20
|
+
schema?: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Auth configuration for Better Auth integration.
|
|
25
|
+
*/
|
|
26
|
+
export interface EhAuthConfig {
|
|
27
|
+
/** Base URL for auth callbacks (e.g., 'http://localhost:4000') */
|
|
28
|
+
baseURL: string
|
|
29
|
+
/** Secret for signing sessions (min 32 chars in production) */
|
|
30
|
+
secret: string
|
|
31
|
+
/** OAuth providers configuration */
|
|
32
|
+
providers?: BetterAuthOptions['socialProviders']
|
|
33
|
+
/** Additional Better Auth plugins (e.g., Okta) */
|
|
34
|
+
plugins?: Array<BetterAuthPlugin>
|
|
35
|
+
/** Session expiration in seconds (default: 30 days) */
|
|
36
|
+
sessionExpiresIn?: number
|
|
37
|
+
/** Session refresh threshold in seconds (default: 1 day) */
|
|
38
|
+
sessionUpdateAge?: number
|
|
39
|
+
/** Application name shown in auth UI */
|
|
40
|
+
appName?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Admin chat (AI) configuration.
|
|
45
|
+
* When provided, enables the admin/chat endpoint.
|
|
46
|
+
*/
|
|
47
|
+
export interface EhAdminChatConfig {
|
|
48
|
+
/** AI model instance from @ai-sdk/* packages */
|
|
49
|
+
model: LanguageModel
|
|
50
|
+
/** System prompt for the AI assistant */
|
|
51
|
+
systemPrompt?: string
|
|
52
|
+
/** Custom tools available to the AI */
|
|
53
|
+
tools?: Record<string, Tool>
|
|
54
|
+
/** Validation function called before each request */
|
|
55
|
+
validateConfig?: () => void
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Feature toggles for enabling/disabling specific functionality.
|
|
60
|
+
* All features are enabled by default.
|
|
61
|
+
*/
|
|
62
|
+
export interface EhFeatureToggles {
|
|
63
|
+
/** Enable tRPC endpoints (default: true) */
|
|
64
|
+
trpc?: boolean
|
|
65
|
+
/** Enable auth endpoints (default: true) */
|
|
66
|
+
auth?: boolean
|
|
67
|
+
/** Enable icon REST endpoints (default: true) */
|
|
68
|
+
icons?: boolean
|
|
69
|
+
/** Enable asset REST endpoints (default: true) */
|
|
70
|
+
assets?: boolean
|
|
71
|
+
/** Enable screenshot REST endpoints (default: true) */
|
|
72
|
+
screenshots?: boolean
|
|
73
|
+
/** Enable admin chat endpoint (default: true if adminChat config provided) */
|
|
74
|
+
adminChat?: boolean
|
|
75
|
+
/** Enable legacy icon endpoint at /static/icon/:icon (default: false) */
|
|
76
|
+
legacyIconEndpoint?: boolean
|
|
77
|
+
/** Enable catalog backup/restore endpoints (default: true) */
|
|
78
|
+
catalogBackup?: boolean
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Company-specific backend can be provided as:
|
|
83
|
+
* 1. Direct object implementing the interface
|
|
84
|
+
* 2. Factory function called per-request (for DI integration)
|
|
85
|
+
* 3. Async factory function
|
|
86
|
+
*/
|
|
87
|
+
export type EhBackendProvider =
|
|
88
|
+
| EhBackendCompanySpecificBackend
|
|
89
|
+
| (() => EhBackendCompanySpecificBackend)
|
|
90
|
+
| (() => Promise<EhBackendCompanySpecificBackend>)
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Lifecycle hooks for database and middleware events.
|
|
94
|
+
*/
|
|
95
|
+
export interface EhLifecycleHooks {
|
|
96
|
+
/** Called after database connection is established */
|
|
97
|
+
onDatabaseConnected?: () => void | Promise<void>
|
|
98
|
+
/** Called before database disconnection (for cleanup) */
|
|
99
|
+
onDatabaseDisconnecting?: () => void | Promise<void>
|
|
100
|
+
/** Called after all routes are registered - use to add custom routes */
|
|
101
|
+
onRoutesRegistered?: (router: Router) => void | Promise<void>
|
|
102
|
+
/** Custom error handler for middleware errors */
|
|
103
|
+
onError?: (error: Error, context: { path: string }) => void
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Main configuration options for the env-hopper middleware.
|
|
108
|
+
*/
|
|
109
|
+
export interface EhMiddlewareOptions {
|
|
110
|
+
/**
|
|
111
|
+
* Base path prefix for all routes (default: '/api')
|
|
112
|
+
* - tRPC: {basePath}/trpc
|
|
113
|
+
* - Auth: {basePath}/auth (note: auth basePath is hardcoded, this affects where router mounts)
|
|
114
|
+
* - Icons: {basePath}/icons
|
|
115
|
+
* - Assets: {basePath}/assets
|
|
116
|
+
* - Screenshots: {basePath}/screenshots
|
|
117
|
+
* - Admin Chat: {basePath}/admin/chat
|
|
118
|
+
*/
|
|
119
|
+
basePath?: string
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Database connection configuration (required).
|
|
123
|
+
* Backend-core manages the database for all features.
|
|
124
|
+
*/
|
|
125
|
+
database: EhDatabaseConfig
|
|
126
|
+
|
|
127
|
+
/** Auth configuration (required) */
|
|
128
|
+
auth: EhAuthConfig
|
|
129
|
+
|
|
130
|
+
/** Company-specific backend implementation (required) */
|
|
131
|
+
backend: EhBackendProvider
|
|
132
|
+
|
|
133
|
+
/** AI admin chat configuration (optional) */
|
|
134
|
+
adminChat?: EhAdminChatConfig
|
|
135
|
+
|
|
136
|
+
/** Feature toggles (all enabled by default) */
|
|
137
|
+
features?: EhFeatureToggles
|
|
138
|
+
|
|
139
|
+
/** Lifecycle hooks */
|
|
140
|
+
hooks?: EhLifecycleHooks
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Result of middleware initialization.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* ```typescript
|
|
148
|
+
* const eh = await createEhMiddleware({ ... })
|
|
149
|
+
*
|
|
150
|
+
* // Mount routes
|
|
151
|
+
* app.use(eh.router)
|
|
152
|
+
*
|
|
153
|
+
* // Connect to database
|
|
154
|
+
* await eh.connect()
|
|
155
|
+
*
|
|
156
|
+
* // Cleanup on shutdown
|
|
157
|
+
* process.on('SIGTERM', async () => {
|
|
158
|
+
* await eh.disconnect()
|
|
159
|
+
* })
|
|
160
|
+
* ```
|
|
161
|
+
*/
|
|
162
|
+
export interface EhMiddlewareResult {
|
|
163
|
+
/** Express router with all env-hopper routes */
|
|
164
|
+
router: Router
|
|
165
|
+
/** Better Auth instance (for extending auth functionality) */
|
|
166
|
+
auth: BetterAuth
|
|
167
|
+
/** tRPC router (for extending with custom procedures) */
|
|
168
|
+
trpcRouter: TRPCRouter
|
|
169
|
+
/** Connect to database (call during app startup) */
|
|
170
|
+
connect: () => Promise<void>
|
|
171
|
+
/** Disconnect from database (call during app shutdown) */
|
|
172
|
+
disconnect: () => Promise<void>
|
|
173
|
+
/** Add custom routes to the middleware router */
|
|
174
|
+
addRoutes: (callback: (router: Router) => void) => void
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Internal context passed to feature registration functions.
|
|
179
|
+
*/
|
|
180
|
+
export interface MiddlewareContext {
|
|
181
|
+
auth: BetterAuth
|
|
182
|
+
trpcRouter: TRPCRouter
|
|
183
|
+
createContext: () => Promise<{
|
|
184
|
+
companySpecificBackend: EhBackendCompanySpecificBackend
|
|
185
|
+
}>
|
|
186
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Request, Response } from 'express'
|
|
2
|
+
import { getDbClient } from '../../db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Export the complete app catalog as JSON
|
|
6
|
+
* Includes all fields from DbAppForCatalog
|
|
7
|
+
*/
|
|
8
|
+
export async function exportCatalog(
|
|
9
|
+
_req: Request,
|
|
10
|
+
res: Response,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
const prisma = getDbClient()
|
|
14
|
+
|
|
15
|
+
// Fetch all catalog entries
|
|
16
|
+
const apps = await prisma.dbAppForCatalog.findMany({
|
|
17
|
+
orderBy: { slug: 'asc' },
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
res.json({
|
|
21
|
+
version: '1.0',
|
|
22
|
+
exportDate: new Date().toISOString(),
|
|
23
|
+
apps,
|
|
24
|
+
})
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error exporting catalog:', error)
|
|
27
|
+
res.status(500).json({ error: 'Failed to export catalog' })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Import/restore the complete app catalog from JSON
|
|
33
|
+
* Overwrites existing data
|
|
34
|
+
*/
|
|
35
|
+
export async function importCatalog(
|
|
36
|
+
req: Request,
|
|
37
|
+
res: Response,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
const prisma = getDbClient()
|
|
41
|
+
const { apps } = req.body
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(apps)) {
|
|
44
|
+
res
|
|
45
|
+
.status(400)
|
|
46
|
+
.json({ error: 'Invalid data format: apps must be an array' })
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Use transaction to ensure atomicity
|
|
51
|
+
await prisma.$transaction(async (tx) => {
|
|
52
|
+
// Delete all existing catalog entries
|
|
53
|
+
await tx.dbAppForCatalog.deleteMany({})
|
|
54
|
+
|
|
55
|
+
// Insert new entries
|
|
56
|
+
for (const app of apps) {
|
|
57
|
+
// Remove id, createdAt, updatedAt to let Prisma generate new ones
|
|
58
|
+
const { id, createdAt, updatedAt, ...appData } = app
|
|
59
|
+
|
|
60
|
+
await tx.dbAppForCatalog.create({
|
|
61
|
+
data: appData,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
res.json({ success: true, imported: apps.length })
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error importing catalog:', error)
|
|
69
|
+
res.status(500).json({ error: 'Failed to import catalog' })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Export an asset (icon or screenshot) by name
|
|
75
|
+
*/
|
|
76
|
+
export async function exportAsset(req: Request, res: Response): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
const { name } = req.params
|
|
79
|
+
const prisma = getDbClient()
|
|
80
|
+
|
|
81
|
+
const asset = await prisma.dbAsset.findUnique({
|
|
82
|
+
where: { name },
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!asset) {
|
|
86
|
+
res.status(404).json({ error: 'Asset not found' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set appropriate content type and send binary data
|
|
91
|
+
res.set('Content-Type', asset.mimeType)
|
|
92
|
+
res.set('Content-Disposition', `attachment; filename="${name}"`)
|
|
93
|
+
res.send(Buffer.from(asset.content))
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error exporting asset:', error)
|
|
96
|
+
res.status(500).json({ error: 'Failed to export asset' })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List all assets with metadata
|
|
102
|
+
*/
|
|
103
|
+
export async function listAssets(_req: Request, res: Response): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
const prisma = getDbClient()
|
|
106
|
+
|
|
107
|
+
const assets = await prisma.dbAsset.findMany({
|
|
108
|
+
select: {
|
|
109
|
+
id: true,
|
|
110
|
+
name: true,
|
|
111
|
+
assetType: true,
|
|
112
|
+
mimeType: true,
|
|
113
|
+
fileSize: true,
|
|
114
|
+
width: true,
|
|
115
|
+
height: true,
|
|
116
|
+
checksum: true,
|
|
117
|
+
},
|
|
118
|
+
orderBy: { name: 'asc' },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
res.json({ assets })
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error listing assets:', error)
|
|
124
|
+
res.status(500).json({ error: 'Failed to list assets' })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Import an asset (icon or screenshot)
|
|
130
|
+
*/
|
|
131
|
+
export async function importAsset(req: Request, res: Response): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const file = req.file
|
|
134
|
+
const { name, assetType, mimeType, width, height } = req.body
|
|
135
|
+
|
|
136
|
+
if (!file) {
|
|
137
|
+
res.status(400).json({ error: 'No file uploaded' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const prisma = getDbClient()
|
|
142
|
+
const crypto = await import('node:crypto')
|
|
143
|
+
|
|
144
|
+
// Calculate checksum
|
|
145
|
+
const checksum = crypto
|
|
146
|
+
.createHash('sha256')
|
|
147
|
+
.update(file.buffer)
|
|
148
|
+
.digest('hex')
|
|
149
|
+
|
|
150
|
+
// Convert Buffer to Uint8Array for Prisma Bytes type
|
|
151
|
+
const content = new Uint8Array(file.buffer)
|
|
152
|
+
|
|
153
|
+
// Upsert asset (update if exists, create if not)
|
|
154
|
+
await prisma.dbAsset.upsert({
|
|
155
|
+
where: { name },
|
|
156
|
+
update: {
|
|
157
|
+
content,
|
|
158
|
+
checksum,
|
|
159
|
+
mimeType: mimeType || file.mimetype,
|
|
160
|
+
fileSize: file.size,
|
|
161
|
+
width: width ? parseInt(width) : null,
|
|
162
|
+
height: height ? parseInt(height) : null,
|
|
163
|
+
assetType: assetType || 'icon',
|
|
164
|
+
},
|
|
165
|
+
create: {
|
|
166
|
+
name,
|
|
167
|
+
content,
|
|
168
|
+
checksum,
|
|
169
|
+
mimeType: mimeType || file.mimetype,
|
|
170
|
+
fileSize: file.size,
|
|
171
|
+
width: width ? parseInt(width) : null,
|
|
172
|
+
height: height ? parseInt(height) : null,
|
|
173
|
+
assetType: assetType || 'icon',
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
res.json({ success: true, name, size: file.size })
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error importing asset:', error)
|
|
180
|
+
res.status(500).json({ error: 'Failed to import asset' })
|
|
181
|
+
}
|
|
182
|
+
}
|