@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.
@@ -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
+ }