@env-hopper/backend-core 2.0.1-alpha.1 → 2.0.1-alpha.3
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 +232 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +289 -1
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- 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 +177 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +149 -0
- package/src/middleware/index.ts +18 -0
- package/src/middleware/types.ts +167 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@env-hopper/backend-core",
|
|
3
|
-
"version": "2.0.1-alpha.
|
|
3
|
+
"version": "2.0.1-alpha.3",
|
|
4
4
|
"description": "Backend core library for Env Hopper",
|
|
5
5
|
"homepage": "https://github.com/lislon/env-hopper",
|
|
6
6
|
"repository": {
|
|
@@ -48,8 +48,8 @@
|
|
|
48
48
|
"tsyringe": "^4.10.0",
|
|
49
49
|
"yaml": "^2.8.0",
|
|
50
50
|
"zod": "^4.3.5",
|
|
51
|
-
"@env-hopper/
|
|
52
|
-
"@env-hopper/
|
|
51
|
+
"@env-hopper/table-sync": "2.0.1-alpha.3",
|
|
52
|
+
"@env-hopper/shared-core": "2.0.1-alpha.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@tanstack/vite-config": "^0.4.3",
|
package/src/db/client.ts
CHANGED
|
@@ -13,6 +13,14 @@ export function getDbClient(): PrismaClient {
|
|
|
13
13
|
return prismaClient
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/**
|
|
17
|
+
* Sets the internal Prisma client instance.
|
|
18
|
+
* Used by middleware to bridge with existing getDbClient() usage.
|
|
19
|
+
*/
|
|
20
|
+
export function setDbClient(client: PrismaClient): void {
|
|
21
|
+
prismaClient = client
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Connects to the database.
|
|
18
26
|
* Call this before performing database operations.
|
package/src/db/index.ts
CHANGED
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
// Database connection
|
|
2
|
-
export { connectDb, disconnectDb, getDbClient } from './client'
|
|
2
|
+
export { connectDb, disconnectDb, getDbClient, setDbClient } from './client'
|
|
3
3
|
|
|
4
4
|
// Table sync utilities
|
|
5
5
|
export {
|
|
6
|
-
tableSyncPrisma,
|
|
7
|
-
type
|
|
6
|
+
tableSyncPrisma,
|
|
7
|
+
type MakeTFromPrismaModel,
|
|
8
|
+
type ObjectKeys,
|
|
9
|
+
type ScalarFilter,
|
|
10
|
+
type ScalarKeys,
|
|
11
|
+
type TableSyncParamsPrisma,
|
|
8
12
|
} from './tableSyncPrismaAdapter'
|
|
9
13
|
|
|
10
14
|
export {
|
|
11
15
|
TABLE_SYNC_MAGAZINE,
|
|
12
16
|
type TableSyncMagazine,
|
|
13
|
-
type TableSyncMagazineModelNameKey
|
|
17
|
+
type TableSyncMagazineModelNameKey,
|
|
14
18
|
} from './tableSyncMagazine'
|
|
15
19
|
|
|
16
20
|
// App catalog sync
|
package/src/index.ts
CHANGED
|
@@ -4,8 +4,8 @@ export { createTrpcRouter } from './server/controller'
|
|
|
4
4
|
export type { TRPCRouter } from './server/controller'
|
|
5
5
|
export { createEhTrpcContext } from './server/ehTrpcContext'
|
|
6
6
|
export type {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
EhTrpcContext,
|
|
8
|
+
EhTrpcContextOptions,
|
|
9
9
|
} from './server/ehTrpcContext'
|
|
10
10
|
|
|
11
11
|
export { staticControllerContract } from './server/ehStaticControllerContract'
|
|
@@ -20,92 +20,107 @@ export * from './types/index'
|
|
|
20
20
|
|
|
21
21
|
// Auth
|
|
22
22
|
export {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
createAuth,
|
|
24
|
+
type AuthConfig,
|
|
25
|
+
type BetterAuth,
|
|
26
26
|
} from './modules/auth/auth'
|
|
27
27
|
|
|
28
|
-
export {
|
|
29
|
-
registerAuthRoutes
|
|
30
|
-
} from './modules/auth/registerAuthRoutes'
|
|
28
|
+
export { registerAuthRoutes } from './modules/auth/registerAuthRoutes'
|
|
31
29
|
|
|
32
|
-
export {
|
|
33
|
-
createAuthRouter,
|
|
34
|
-
type AuthRouter
|
|
35
|
-
} from './modules/auth/authRouter'
|
|
30
|
+
export { createAuthRouter, type AuthRouter } from './modules/auth/authRouter'
|
|
36
31
|
|
|
37
32
|
export {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
33
|
+
getAuthProvidersFromEnv,
|
|
34
|
+
getAuthPluginsFromEnv,
|
|
35
|
+
validateAuthConfig,
|
|
41
36
|
} from './modules/auth/authProviders'
|
|
42
37
|
|
|
43
38
|
export {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
getUserGroups,
|
|
40
|
+
isMemberOfAnyGroup,
|
|
41
|
+
isMemberOfAllGroups,
|
|
42
|
+
getAdminGroupsFromEnv,
|
|
43
|
+
isAdmin,
|
|
44
|
+
requireAdmin,
|
|
45
|
+
requireGroups,
|
|
46
|
+
type UserWithGroups,
|
|
52
47
|
} from './modules/auth/authorizationUtils'
|
|
53
48
|
|
|
54
49
|
// Admin
|
|
55
50
|
export {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
51
|
+
createAdminChatHandler,
|
|
52
|
+
tool,
|
|
53
|
+
type AdminChatHandlerOptions,
|
|
59
54
|
} from './modules/admin/chat/createAdminChatHandler'
|
|
60
55
|
|
|
61
56
|
export {
|
|
62
|
-
|
|
63
|
-
|
|
57
|
+
createDatabaseTools,
|
|
58
|
+
createPrismaDatabaseClient,
|
|
59
|
+
DEFAULT_ADMIN_SYSTEM_PROMPT,
|
|
60
|
+
type DatabaseClient,
|
|
64
61
|
} from './modules/admin/chat/createDatabaseTools'
|
|
65
62
|
|
|
66
63
|
// Icon management
|
|
67
64
|
export {
|
|
68
|
-
|
|
69
|
-
|
|
65
|
+
registerIconRestController,
|
|
66
|
+
type IconRestControllerConfig,
|
|
70
67
|
} from './modules/icons/iconRestController'
|
|
71
68
|
|
|
72
69
|
export {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
70
|
+
getAssetByName,
|
|
71
|
+
upsertIcon,
|
|
72
|
+
upsertIcons,
|
|
73
|
+
type UpsertIconInput,
|
|
77
74
|
} from './modules/icons/iconService'
|
|
78
75
|
|
|
79
76
|
// Asset management (universal for icons, screenshots, etc.)
|
|
80
77
|
export {
|
|
81
|
-
|
|
82
|
-
|
|
78
|
+
registerAssetRestController,
|
|
79
|
+
type AssetRestControllerConfig,
|
|
83
80
|
} from './modules/assets/assetRestController'
|
|
84
81
|
|
|
85
82
|
export {
|
|
86
|
-
|
|
87
|
-
|
|
83
|
+
registerScreenshotRestController,
|
|
84
|
+
type ScreenshotRestControllerConfig,
|
|
88
85
|
} from './modules/assets/screenshotRestController'
|
|
89
86
|
|
|
90
|
-
export {
|
|
91
|
-
createScreenshotRouter
|
|
92
|
-
} from './modules/assets/screenshotRouter'
|
|
87
|
+
export { createScreenshotRouter } from './modules/assets/screenshotRouter'
|
|
93
88
|
|
|
94
|
-
export {
|
|
95
|
-
syncAssets,
|
|
96
|
-
type SyncAssetsConfig
|
|
97
|
-
} from './modules/assets/syncAssets'
|
|
89
|
+
export { syncAssets, type SyncAssetsConfig } from './modules/assets/syncAssets'
|
|
98
90
|
|
|
99
91
|
// App Catalog Admin
|
|
100
|
-
export {
|
|
101
|
-
createAppCatalogAdminRouter
|
|
102
|
-
} from './modules/appCatalogAdmin/appCatalogAdminRouter'
|
|
92
|
+
export { createAppCatalogAdminRouter } from './modules/appCatalogAdmin/appCatalogAdminRouter'
|
|
103
93
|
|
|
104
94
|
// Database utilities
|
|
105
95
|
export {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
96
|
+
connectDb,
|
|
97
|
+
disconnectDb,
|
|
98
|
+
getDbClient,
|
|
99
|
+
setDbClient,
|
|
100
|
+
syncAppCatalog,
|
|
101
|
+
TABLE_SYNC_MAGAZINE,
|
|
102
|
+
tableSyncPrisma,
|
|
103
|
+
type MakeTFromPrismaModel,
|
|
104
|
+
type ObjectKeys,
|
|
105
|
+
type ScalarFilter,
|
|
106
|
+
type ScalarKeys,
|
|
107
|
+
type SyncAppCatalogResult,
|
|
108
|
+
type TableSyncMagazine,
|
|
109
|
+
type TableSyncMagazineModelNameKey,
|
|
110
|
+
type TableSyncParamsPrisma,
|
|
110
111
|
} from './db'
|
|
111
112
|
|
|
113
|
+
// Middleware (batteries-included backend setup)
|
|
114
|
+
export {
|
|
115
|
+
createEhMiddleware,
|
|
116
|
+
EhDatabaseManager,
|
|
117
|
+
type EhDatabaseConfig,
|
|
118
|
+
type EhAuthConfig,
|
|
119
|
+
type EhAdminChatConfig,
|
|
120
|
+
type EhFeatureToggles,
|
|
121
|
+
type EhBackendProvider,
|
|
122
|
+
type EhLifecycleHooks,
|
|
123
|
+
type EhMiddlewareOptions,
|
|
124
|
+
type EhMiddlewareResult,
|
|
125
|
+
type MiddlewareContext,
|
|
126
|
+
} from './middleware'
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { EhBackendCompanySpecificBackend } from '../types/backend/companySpecificBackend'
|
|
2
|
+
import type { EhBackendProvider } from './types'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Type guard to check if an object implements EhBackendCompanySpecificBackend.
|
|
6
|
+
*/
|
|
7
|
+
function isBackendInstance(obj: unknown): obj is EhBackendCompanySpecificBackend {
|
|
8
|
+
return (
|
|
9
|
+
typeof obj === 'object' &&
|
|
10
|
+
obj !== null &&
|
|
11
|
+
typeof (obj as EhBackendCompanySpecificBackend).getBootstrapData === 'function' &&
|
|
12
|
+
typeof (obj as EhBackendCompanySpecificBackend).getAvailabilityMatrix ===
|
|
13
|
+
'function' &&
|
|
14
|
+
typeof (obj as EhBackendCompanySpecificBackend).getNameMigrations ===
|
|
15
|
+
'function' &&
|
|
16
|
+
typeof (obj as EhBackendCompanySpecificBackend).getResourceJumps ===
|
|
17
|
+
'function' &&
|
|
18
|
+
typeof (obj as EhBackendCompanySpecificBackend).getResourceJumpsExtended ===
|
|
19
|
+
'function'
|
|
20
|
+
)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Normalizes different backend provider types into a consistent async factory function.
|
|
25
|
+
* Supports:
|
|
26
|
+
* - Direct object implementing EhBackendCompanySpecificBackend
|
|
27
|
+
* - Sync factory function that returns the backend
|
|
28
|
+
* - Async factory function that returns the backend
|
|
29
|
+
*/
|
|
30
|
+
export function createBackendResolver(
|
|
31
|
+
provider: EhBackendProvider,
|
|
32
|
+
): () => Promise<EhBackendCompanySpecificBackend> {
|
|
33
|
+
// If it's already an object with the required methods, wrap it
|
|
34
|
+
if (isBackendInstance(provider)) {
|
|
35
|
+
return async () => provider
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If it's a function, call it and handle both sync and async results
|
|
39
|
+
if (typeof provider === 'function') {
|
|
40
|
+
return async () => {
|
|
41
|
+
const result = provider()
|
|
42
|
+
return result instanceof Promise ? result : result
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
throw new Error(
|
|
47
|
+
'Invalid backend provider: must be an object implementing EhBackendCompanySpecificBackend or a factory function',
|
|
48
|
+
)
|
|
49
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import express, { Router } from 'express'
|
|
2
|
+
import * as trpcExpress from '@trpc/server/adapters/express'
|
|
3
|
+
import type { EhMiddlewareOptions, EhMiddlewareResult, MiddlewareContext } from './types'
|
|
4
|
+
import { EhDatabaseManager } from './database'
|
|
5
|
+
import { createBackendResolver } from './backendResolver'
|
|
6
|
+
import { registerFeatures } from './featureRegistry'
|
|
7
|
+
import { createTrpcRouter } from '../server/controller'
|
|
8
|
+
import { createEhTrpcContext } from '../server/ehTrpcContext'
|
|
9
|
+
import { createAuth } from '../modules/auth/auth'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Creates a fully-configured env-hopper middleware.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* ```typescript
|
|
16
|
+
* // Simple usage with inline backend
|
|
17
|
+
* const eh = await createEhMiddleware({
|
|
18
|
+
* basePath: '/api',
|
|
19
|
+
* database: { url: process.env.DATABASE_URL! },
|
|
20
|
+
* auth: {
|
|
21
|
+
* baseURL: 'http://localhost:4000',
|
|
22
|
+
* secret: process.env.AUTH_SECRET!,
|
|
23
|
+
* },
|
|
24
|
+
* backend: {
|
|
25
|
+
* getBootstrapData: async () => loadStaticData(),
|
|
26
|
+
* getAvailabilityMatrix: async () => ({}),
|
|
27
|
+
* getNameMigrations: async () => false,
|
|
28
|
+
* getResourceJumps: async () => ({ resourceJumps: [], envs: [], lateResolvableParams: [] }),
|
|
29
|
+
* getResourceJumpsExtended: async () => ({ envs: [] }),
|
|
30
|
+
* },
|
|
31
|
+
* })
|
|
32
|
+
*
|
|
33
|
+
* app.use(eh.router)
|
|
34
|
+
* await eh.connect()
|
|
35
|
+
* ```
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // With DI-resolved backend (e.g., tsyringe)
|
|
40
|
+
* const eh = await createEhMiddleware({
|
|
41
|
+
* basePath: '/api',
|
|
42
|
+
* database: {
|
|
43
|
+
* host: cfg.db.host,
|
|
44
|
+
* port: cfg.db.port,
|
|
45
|
+
* database: cfg.db.name,
|
|
46
|
+
* username: cfg.db.username,
|
|
47
|
+
* password: cfg.db.password,
|
|
48
|
+
* schema: cfg.db.schema,
|
|
49
|
+
* },
|
|
50
|
+
* auth: {
|
|
51
|
+
* baseURL: process.env.BETTER_AUTH_URL || 'http://localhost:4000',
|
|
52
|
+
* secret: process.env.BETTER_AUTH_SECRET!,
|
|
53
|
+
* providers: getAuthProvidersFromEnv(),
|
|
54
|
+
* plugins: getAuthPluginsFromEnv(),
|
|
55
|
+
* },
|
|
56
|
+
* // Factory function - resolved fresh per request from DI container
|
|
57
|
+
* backend: () => container.resolve(NateraEhBackend),
|
|
58
|
+
* hooks: {
|
|
59
|
+
* onRoutesRegistered: (router) => {
|
|
60
|
+
* router.get('/health', (_, res) => res.send('ok'))
|
|
61
|
+
* },
|
|
62
|
+
* },
|
|
63
|
+
* })
|
|
64
|
+
* ```
|
|
65
|
+
*/
|
|
66
|
+
export async function createEhMiddleware(
|
|
67
|
+
options: EhMiddlewareOptions,
|
|
68
|
+
): Promise<EhMiddlewareResult> {
|
|
69
|
+
// Normalize options with defaults
|
|
70
|
+
const basePath = options.basePath ?? '/api'
|
|
71
|
+
const normalizedOptions = { ...options, basePath }
|
|
72
|
+
|
|
73
|
+
// Check if database-dependent features are enabled
|
|
74
|
+
const features = options.features ?? {}
|
|
75
|
+
const iconsEnabled = features.icons !== false
|
|
76
|
+
const assetsEnabled = features.assets !== false
|
|
77
|
+
const screenshotsEnabled = features.screenshots !== false
|
|
78
|
+
const legacyIconEnabled = features.legacyIconEndpoint === true
|
|
79
|
+
const needsDatabase = iconsEnabled || assetsEnabled || screenshotsEnabled || legacyIconEnabled
|
|
80
|
+
|
|
81
|
+
// Validate database is provided when needed
|
|
82
|
+
if (needsDatabase && !options.database) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
'Database configuration is required when icons, assets, screenshots, or legacyIconEndpoint features are enabled. ' +
|
|
85
|
+
'Either provide database config or disable these features.',
|
|
86
|
+
)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Initialize database manager only if database config is provided
|
|
90
|
+
let dbManager: EhDatabaseManager | null = null
|
|
91
|
+
if (options.database) {
|
|
92
|
+
dbManager = new EhDatabaseManager(options.database)
|
|
93
|
+
// Initialize the client (which also sets the global singleton)
|
|
94
|
+
dbManager.getClient()
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Create auth instance
|
|
98
|
+
const auth = createAuth({
|
|
99
|
+
appName: options.auth.appName,
|
|
100
|
+
baseURL: options.auth.baseURL,
|
|
101
|
+
secret: options.auth.secret,
|
|
102
|
+
providers: options.auth.providers,
|
|
103
|
+
plugins: options.auth.plugins,
|
|
104
|
+
sessionExpiresIn: options.auth.sessionExpiresIn,
|
|
105
|
+
sessionUpdateAge: options.auth.sessionUpdateAge,
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
// Create tRPC router
|
|
109
|
+
const trpcRouter = createTrpcRouter(auth)
|
|
110
|
+
|
|
111
|
+
// Normalize backend provider to async factory function
|
|
112
|
+
const resolveBackend = createBackendResolver(options.backend)
|
|
113
|
+
|
|
114
|
+
// Create tRPC context factory
|
|
115
|
+
const createContext = async () => {
|
|
116
|
+
const companySpecificBackend = await resolveBackend()
|
|
117
|
+
return createEhTrpcContext({ companySpecificBackend })
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Create Express router
|
|
121
|
+
const router = Router()
|
|
122
|
+
router.use(express.json())
|
|
123
|
+
|
|
124
|
+
// Build middleware context for feature registration
|
|
125
|
+
const middlewareContext: MiddlewareContext = {
|
|
126
|
+
auth,
|
|
127
|
+
trpcRouter,
|
|
128
|
+
createContext,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Register tRPC middleware (if enabled)
|
|
132
|
+
if (normalizedOptions.features?.trpc !== false) {
|
|
133
|
+
router.use(
|
|
134
|
+
`${basePath}/trpc`,
|
|
135
|
+
trpcExpress.createExpressMiddleware({
|
|
136
|
+
router: trpcRouter,
|
|
137
|
+
createContext,
|
|
138
|
+
}),
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Register all enabled features
|
|
143
|
+
registerFeatures(router, normalizedOptions, middlewareContext)
|
|
144
|
+
|
|
145
|
+
// Call onRoutesRegistered hook if provided
|
|
146
|
+
if (options.hooks?.onRoutesRegistered) {
|
|
147
|
+
await options.hooks.onRoutesRegistered(router)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
router,
|
|
152
|
+
auth,
|
|
153
|
+
trpcRouter,
|
|
154
|
+
|
|
155
|
+
async connect(): Promise<void> {
|
|
156
|
+
if (dbManager) {
|
|
157
|
+
await dbManager.connect()
|
|
158
|
+
}
|
|
159
|
+
if (options.hooks?.onDatabaseConnected) {
|
|
160
|
+
await options.hooks.onDatabaseConnected()
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async disconnect(): Promise<void> {
|
|
165
|
+
if (options.hooks?.onDatabaseDisconnecting) {
|
|
166
|
+
await options.hooks.onDatabaseDisconnecting()
|
|
167
|
+
}
|
|
168
|
+
if (dbManager) {
|
|
169
|
+
await dbManager.disconnect()
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
|
|
173
|
+
addRoutes(callback: (router: Router) => void): void {
|
|
174
|
+
callback(router)
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client'
|
|
2
|
+
import type { EhDatabaseConfig } from './types'
|
|
3
|
+
import { setDbClient } from '../db/client'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Formats a database connection URL from structured config.
|
|
7
|
+
*/
|
|
8
|
+
function formatConnectionUrl(config: EhDatabaseConfig): string {
|
|
9
|
+
if ('url' in config) {
|
|
10
|
+
return config.url
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const { host, port, database, username, password, schema = 'public' } = config
|
|
14
|
+
return `postgresql://${username}:${encodeURIComponent(password)}@${host}:${port}/${database}?schema=${schema}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Internal database manager used by the middleware.
|
|
19
|
+
* Handles connection URL formatting and lifecycle.
|
|
20
|
+
*/
|
|
21
|
+
export class EhDatabaseManager {
|
|
22
|
+
private client: PrismaClient | null = null
|
|
23
|
+
private config: EhDatabaseConfig
|
|
24
|
+
|
|
25
|
+
constructor(config: EhDatabaseConfig) {
|
|
26
|
+
this.config = config
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Get or create the Prisma client instance.
|
|
31
|
+
* Uses lazy initialization for flexibility.
|
|
32
|
+
*/
|
|
33
|
+
getClient(): PrismaClient {
|
|
34
|
+
if (!this.client) {
|
|
35
|
+
const datasourceUrl = formatConnectionUrl(this.config)
|
|
36
|
+
|
|
37
|
+
this.client = new PrismaClient({
|
|
38
|
+
datasourceUrl,
|
|
39
|
+
log:
|
|
40
|
+
process.env.NODE_ENV === 'development'
|
|
41
|
+
? ['warn', 'error']
|
|
42
|
+
: ['warn', 'error'],
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
// Bridge with existing backend-core getDbClient() usage
|
|
46
|
+
setDbClient(this.client)
|
|
47
|
+
}
|
|
48
|
+
return this.client
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async connect(): Promise<void> {
|
|
52
|
+
const client = this.getClient()
|
|
53
|
+
await client.$connect()
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async disconnect(): Promise<void> {
|
|
57
|
+
if (this.client) {
|
|
58
|
+
await this.client.$disconnect()
|
|
59
|
+
this.client = null
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import type { Router } from 'express'
|
|
2
|
+
import { toNodeHandler } from 'better-auth/node'
|
|
3
|
+
import type {
|
|
4
|
+
EhFeatureToggles,
|
|
5
|
+
EhMiddlewareOptions,
|
|
6
|
+
MiddlewareContext,
|
|
7
|
+
} from './types'
|
|
8
|
+
import { registerIconRestController } from '../modules/icons/iconRestController'
|
|
9
|
+
import { registerAssetRestController } from '../modules/assets/assetRestController'
|
|
10
|
+
import { registerScreenshotRestController } from '../modules/assets/screenshotRestController'
|
|
11
|
+
import { createAdminChatHandler } from '../modules/admin/chat/createAdminChatHandler'
|
|
12
|
+
import { getAssetByName } from '../modules/icons/iconService'
|
|
13
|
+
|
|
14
|
+
interface FeatureRegistration {
|
|
15
|
+
name: keyof EhFeatureToggles
|
|
16
|
+
defaultEnabled: boolean
|
|
17
|
+
register: (
|
|
18
|
+
router: Router,
|
|
19
|
+
options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
|
|
20
|
+
EhMiddlewareOptions,
|
|
21
|
+
context: MiddlewareContext,
|
|
22
|
+
) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const FEATURES: Array<FeatureRegistration> = [
|
|
26
|
+
{
|
|
27
|
+
name: 'auth',
|
|
28
|
+
defaultEnabled: true,
|
|
29
|
+
register: (router, options, ctx) => {
|
|
30
|
+
const basePath = options.basePath
|
|
31
|
+
|
|
32
|
+
// Explicit session endpoint handler
|
|
33
|
+
router.get(`${basePath}/auth/session`, async (req, res) => {
|
|
34
|
+
try {
|
|
35
|
+
const session = await ctx.auth.api.getSession({
|
|
36
|
+
headers: req.headers as HeadersInit,
|
|
37
|
+
})
|
|
38
|
+
if (session) {
|
|
39
|
+
res.json(session)
|
|
40
|
+
} else {
|
|
41
|
+
res.status(401).json({ error: 'Not authenticated' })
|
|
42
|
+
}
|
|
43
|
+
} catch (error) {
|
|
44
|
+
console.error('[Auth Session Error]', error)
|
|
45
|
+
res.status(500).json({ error: 'Internal server error' })
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
// Use toNodeHandler to adapt better-auth for Express/Node.js
|
|
50
|
+
const authHandler = toNodeHandler(ctx.auth)
|
|
51
|
+
router.all(`${basePath}/auth/{*any}`, authHandler)
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
name: 'icons',
|
|
56
|
+
defaultEnabled: true,
|
|
57
|
+
register: (router, options) => {
|
|
58
|
+
registerIconRestController(router, {
|
|
59
|
+
basePath: `${options.basePath}/icons`,
|
|
60
|
+
})
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'assets',
|
|
65
|
+
defaultEnabled: true,
|
|
66
|
+
register: (router, options) => {
|
|
67
|
+
registerAssetRestController(router, {
|
|
68
|
+
basePath: `${options.basePath}/assets`,
|
|
69
|
+
})
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'screenshots',
|
|
74
|
+
defaultEnabled: true,
|
|
75
|
+
register: (router, options) => {
|
|
76
|
+
registerScreenshotRestController(router, {
|
|
77
|
+
basePath: `${options.basePath}/screenshots`,
|
|
78
|
+
})
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'adminChat',
|
|
83
|
+
defaultEnabled: false, // Only enabled if adminChat config is provided
|
|
84
|
+
register: (router, options) => {
|
|
85
|
+
if (options.adminChat) {
|
|
86
|
+
router.post(
|
|
87
|
+
`${options.basePath}/admin/chat`,
|
|
88
|
+
createAdminChatHandler(options.adminChat),
|
|
89
|
+
)
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: 'legacyIconEndpoint',
|
|
95
|
+
defaultEnabled: false,
|
|
96
|
+
register: (router) => {
|
|
97
|
+
// Legacy endpoint at /static/icon/:icon for backwards compatibility
|
|
98
|
+
router.get('/static/icon/:icon', async (req, res) => {
|
|
99
|
+
const { icon } = req.params
|
|
100
|
+
|
|
101
|
+
if (!icon || !/^[a-z0-9-]+$/i.test(icon)) {
|
|
102
|
+
res.status(400).send('Invalid icon name')
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
const dbIcon = await getAssetByName(icon)
|
|
108
|
+
|
|
109
|
+
if (!dbIcon) {
|
|
110
|
+
res.status(404).send('Icon not found')
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
res.setHeader('Content-Type', dbIcon.mimeType)
|
|
115
|
+
res.setHeader('Cache-Control', 'public, max-age=86400')
|
|
116
|
+
res.send(dbIcon.content)
|
|
117
|
+
} catch (error) {
|
|
118
|
+
console.error('Error fetching icon:', error)
|
|
119
|
+
res.status(404).send('Icon not found')
|
|
120
|
+
}
|
|
121
|
+
})
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
]
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Registers all enabled features on the router.
|
|
128
|
+
*/
|
|
129
|
+
export function registerFeatures(
|
|
130
|
+
router: Router,
|
|
131
|
+
options: Required<Pick<EhMiddlewareOptions, 'basePath'>> &
|
|
132
|
+
EhMiddlewareOptions,
|
|
133
|
+
context: MiddlewareContext,
|
|
134
|
+
): void {
|
|
135
|
+
const toggles = options.features || {}
|
|
136
|
+
|
|
137
|
+
for (const feature of FEATURES) {
|
|
138
|
+
const isEnabled = toggles[feature.name] ?? feature.defaultEnabled
|
|
139
|
+
|
|
140
|
+
// Special case: adminChat is only enabled if config is provided
|
|
141
|
+
if (feature.name === 'adminChat' && !options.adminChat) {
|
|
142
|
+
continue
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (isEnabled) {
|
|
146
|
+
feature.register(router, options, context)
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Main middleware factory
|
|
2
|
+
export { createEhMiddleware } from './createEhMiddleware'
|
|
3
|
+
|
|
4
|
+
// Types
|
|
5
|
+
export type {
|
|
6
|
+
EhDatabaseConfig,
|
|
7
|
+
EhAuthConfig,
|
|
8
|
+
EhAdminChatConfig,
|
|
9
|
+
EhFeatureToggles,
|
|
10
|
+
EhBackendProvider,
|
|
11
|
+
EhLifecycleHooks,
|
|
12
|
+
EhMiddlewareOptions,
|
|
13
|
+
EhMiddlewareResult,
|
|
14
|
+
MiddlewareContext,
|
|
15
|
+
} from './types'
|
|
16
|
+
|
|
17
|
+
// Database manager (for advanced use cases)
|
|
18
|
+
export { EhDatabaseManager } from './database'
|