@env-hopper/backend-core 2.0.1-alpha → 2.0.1-alpha-20260224145405

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.
Files changed (109) hide show
  1. package/dist/index.d.ts +2059 -0
  2. package/dist/index.d.ts.map +1 -0
  3. package/dist/index.js +2571 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +26 -11
  6. package/prisma/migrations/20250526183023_init/migration.sql +71 -0
  7. package/prisma/migrations/migration_lock.toml +3 -0
  8. package/prisma/schema.prisma +149 -0
  9. package/src/db/client.ts +42 -0
  10. package/src/db/index.ts +21 -0
  11. package/src/db/syncAppCatalog.ts +310 -0
  12. package/src/db/tableSyncMagazine.ts +32 -0
  13. package/src/db/tableSyncPrismaAdapter.ts +203 -0
  14. package/src/generated/prisma/client.d.ts +1 -0
  15. package/src/generated/prisma/client.js +4 -0
  16. package/src/generated/prisma/default.d.ts +1 -0
  17. package/src/generated/prisma/default.js +4 -0
  18. package/src/generated/prisma/edge.d.ts +1 -0
  19. package/src/generated/prisma/edge.js +227 -0
  20. package/src/generated/prisma/index-browser.js +214 -0
  21. package/src/generated/prisma/index.d.ts +4212 -0
  22. package/src/generated/prisma/index.js +248 -0
  23. package/src/generated/prisma/libquery_engine-darwin-arm64.dylib.node +0 -0
  24. package/src/generated/prisma/package.json +183 -0
  25. package/src/generated/prisma/query_engine_bg.js +2 -0
  26. package/src/generated/prisma/query_engine_bg.wasm +0 -0
  27. package/src/generated/prisma/runtime/edge-esm.js +34 -0
  28. package/src/generated/prisma/runtime/edge.js +34 -0
  29. package/src/generated/prisma/runtime/index-browser.d.ts +370 -0
  30. package/src/generated/prisma/runtime/index-browser.js +16 -0
  31. package/src/generated/prisma/runtime/library.d.ts +3982 -0
  32. package/src/generated/prisma/runtime/library.js +146 -0
  33. package/src/generated/prisma/runtime/react-native.js +83 -0
  34. package/src/generated/prisma/runtime/wasm-compiler-edge.js +84 -0
  35. package/src/generated/prisma/runtime/wasm-engine-edge.js +36 -0
  36. package/src/generated/prisma/schema.prisma +49 -0
  37. package/src/generated/prisma/wasm-edge-light-loader.mjs +4 -0
  38. package/src/generated/prisma/wasm-worker-loader.mjs +4 -0
  39. package/src/generated/prisma/wasm.d.ts +1 -0
  40. package/src/generated/prisma/wasm.js +234 -0
  41. package/src/index.ts +109 -1
  42. package/src/middleware/backendResolver.ts +49 -0
  43. package/src/middleware/createEhMiddleware.ts +171 -0
  44. package/src/middleware/database.ts +62 -0
  45. package/src/middleware/featureRegistry.ts +173 -0
  46. package/src/middleware/index.ts +43 -0
  47. package/src/middleware/types.ts +202 -0
  48. package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
  49. package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
  50. package/src/modules/appCatalog/service.ts +130 -0
  51. package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +187 -0
  52. package/src/modules/appCatalogAdmin/catalogBackupController.ts +213 -0
  53. package/src/modules/approvalMethod/approvalMethodRouter.ts +169 -0
  54. package/src/modules/approvalMethod/slugUtils.ts +17 -0
  55. package/src/modules/approvalMethod/syncApprovalMethods.ts +38 -0
  56. package/src/modules/assets/assetRestController.ts +271 -0
  57. package/src/modules/assets/assetUtils.ts +114 -0
  58. package/src/modules/assets/screenshotRestController.ts +195 -0
  59. package/src/modules/assets/screenshotRouter.ts +112 -0
  60. package/src/modules/assets/syncAssets.ts +277 -0
  61. package/src/modules/assets/upsertAsset.ts +46 -0
  62. package/src/modules/auth/auth.ts +51 -0
  63. package/src/modules/auth/authProviders.ts +40 -0
  64. package/src/modules/auth/authRouter.ts +75 -0
  65. package/src/modules/auth/authorizationUtils.ts +132 -0
  66. package/src/modules/auth/devMockUserUtils.ts +49 -0
  67. package/src/modules/auth/registerAuthRoutes.ts +33 -0
  68. package/src/modules/icons/iconRestController.ts +171 -0
  69. package/src/modules/icons/iconRouter.ts +180 -0
  70. package/src/modules/icons/iconService.ts +73 -0
  71. package/src/modules/icons/iconUtils.ts +46 -0
  72. package/src/prisma-json-types.d.ts +34 -0
  73. package/src/server/controller.ts +103 -44
  74. package/src/server/ehStaticControllerContract.ts +8 -1
  75. package/src/server/ehTrpcContext.ts +9 -6
  76. package/src/server/trpcSetup.ts +89 -0
  77. package/src/types/backend/api.ts +1 -14
  78. package/src/types/backend/companySpecificBackend.ts +17 -0
  79. package/src/types/common/appCatalogTypes.ts +56 -10
  80. package/src/types/common/approvalMethodTypes.ts +149 -0
  81. package/src/types/common/dataRootTypes.ts +72 -10
  82. package/src/types/index.ts +3 -0
  83. package/dist/esm/__tests__/dummy.test.d.ts +0 -1
  84. package/dist/esm/index.d.ts +0 -7
  85. package/dist/esm/index.js +0 -9
  86. package/dist/esm/index.js.map +0 -1
  87. package/dist/esm/server/controller.d.ts +0 -32
  88. package/dist/esm/server/controller.js +0 -35
  89. package/dist/esm/server/controller.js.map +0 -1
  90. package/dist/esm/server/db.d.ts +0 -2
  91. package/dist/esm/server/ehStaticControllerContract.d.ts +0 -9
  92. package/dist/esm/server/ehStaticControllerContract.js +0 -12
  93. package/dist/esm/server/ehStaticControllerContract.js.map +0 -1
  94. package/dist/esm/server/ehTrpcContext.d.ts +0 -8
  95. package/dist/esm/server/ehTrpcContext.js +0 -11
  96. package/dist/esm/server/ehTrpcContext.js.map +0 -1
  97. package/dist/esm/types/backend/api.d.ts +0 -71
  98. package/dist/esm/types/backend/common.d.ts +0 -9
  99. package/dist/esm/types/backend/dataSources.d.ts +0 -20
  100. package/dist/esm/types/backend/deployments.d.ts +0 -34
  101. package/dist/esm/types/common/app/appTypes.d.ts +0 -12
  102. package/dist/esm/types/common/app/ui/appUiTypes.d.ts +0 -10
  103. package/dist/esm/types/common/appCatalogTypes.d.ts +0 -16
  104. package/dist/esm/types/common/dataRootTypes.d.ts +0 -32
  105. package/dist/esm/types/common/env/envTypes.d.ts +0 -6
  106. package/dist/esm/types/common/resourceTypes.d.ts +0 -8
  107. package/dist/esm/types/common/sharedTypes.d.ts +0 -4
  108. package/dist/esm/types/index.d.ts +0 -11
  109. package/src/server/db.ts +0 -4
@@ -0,0 +1,234 @@
1
+
2
+ /* !!! This is code generated by Prisma. Do not edit directly. !!!
3
+ /* eslint-disable */
4
+
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+
7
+ const {
8
+ PrismaClientKnownRequestError,
9
+ PrismaClientUnknownRequestError,
10
+ PrismaClientRustPanicError,
11
+ PrismaClientInitializationError,
12
+ PrismaClientValidationError,
13
+ getPrismaClient,
14
+ sqltag,
15
+ empty,
16
+ join,
17
+ raw,
18
+ skip,
19
+ Decimal,
20
+ Debug,
21
+ objectEnumValues,
22
+ makeStrictEnum,
23
+ Extensions,
24
+ warnOnce,
25
+ defineDmmfProperty,
26
+ Public,
27
+ getRuntime,
28
+ createParam,
29
+ } = require('./runtime/wasm-engine-edge.js')
30
+
31
+
32
+ const Prisma = {}
33
+
34
+ exports.Prisma = Prisma
35
+ exports.$Enums = {}
36
+
37
+ /**
38
+ * Prisma Client JS version: 6.17.0
39
+ * Query Engine version: c0aafc03b8ef6cdced8654b9a817999e02457d6a
40
+ */
41
+ Prisma.prismaVersion = {
42
+ client: "6.17.0",
43
+ engine: "c0aafc03b8ef6cdced8654b9a817999e02457d6a"
44
+ }
45
+
46
+ Prisma.PrismaClientKnownRequestError = PrismaClientKnownRequestError;
47
+ Prisma.PrismaClientUnknownRequestError = PrismaClientUnknownRequestError
48
+ Prisma.PrismaClientRustPanicError = PrismaClientRustPanicError
49
+ Prisma.PrismaClientInitializationError = PrismaClientInitializationError
50
+ Prisma.PrismaClientValidationError = PrismaClientValidationError
51
+ Prisma.Decimal = Decimal
52
+
53
+ /**
54
+ * Re-export of sql-template-tag
55
+ */
56
+ Prisma.sql = sqltag
57
+ Prisma.empty = empty
58
+ Prisma.join = join
59
+ Prisma.raw = raw
60
+ Prisma.validator = Public.validator
61
+
62
+ /**
63
+ * Extensions
64
+ */
65
+ Prisma.getExtensionContext = Extensions.getExtensionContext
66
+ Prisma.defineExtension = Extensions.defineExtension
67
+
68
+ /**
69
+ * Shorthand utilities for JSON filtering
70
+ */
71
+ Prisma.DbNull = objectEnumValues.instances.DbNull
72
+ Prisma.JsonNull = objectEnumValues.instances.JsonNull
73
+ Prisma.AnyNull = objectEnumValues.instances.AnyNull
74
+
75
+ Prisma.NullTypes = {
76
+ DbNull: objectEnumValues.classes.DbNull,
77
+ JsonNull: objectEnumValues.classes.JsonNull,
78
+ AnyNull: objectEnumValues.classes.AnyNull
79
+ }
80
+
81
+
82
+
83
+
84
+
85
+ /**
86
+ * Enums
87
+ */
88
+ exports.Prisma.TransactionIsolationLevel = makeStrictEnum({
89
+ ReadUncommitted: 'ReadUncommitted',
90
+ ReadCommitted: 'ReadCommitted',
91
+ RepeatableRead: 'RepeatableRead',
92
+ Serializable: 'Serializable'
93
+ });
94
+
95
+ exports.Prisma.DbAppForCatalogScalarFieldEnum = {
96
+ id: 'id',
97
+ name: 'name',
98
+ description: 'description',
99
+ access: 'access',
100
+ teams: 'teams',
101
+ roles: 'roles',
102
+ approverName: 'approverName',
103
+ approverEmail: 'approverEmail',
104
+ notes: 'notes',
105
+ tags: 'tags',
106
+ appUrl: 'appUrl',
107
+ confluenceUrl: 'confluenceUrl',
108
+ createdAt: 'createdAt',
109
+ updatedAt: 'updatedAt'
110
+ };
111
+
112
+ exports.Prisma.IconScalarFieldEnum = {
113
+ id: 'id',
114
+ name: 'name',
115
+ svgContent: 'svgContent',
116
+ pngBase64: 'pngBase64',
117
+ createdAt: 'createdAt',
118
+ updatedAt: 'updatedAt'
119
+ };
120
+
121
+ exports.Prisma.SortOrder = {
122
+ asc: 'asc',
123
+ desc: 'desc'
124
+ };
125
+
126
+ exports.Prisma.JsonNullValueInput = {
127
+ JsonNull: Prisma.JsonNull
128
+ };
129
+
130
+ exports.Prisma.NullableJsonNullValueInput = {
131
+ DbNull: Prisma.DbNull,
132
+ JsonNull: Prisma.JsonNull
133
+ };
134
+
135
+ exports.Prisma.QueryMode = {
136
+ default: 'default',
137
+ insensitive: 'insensitive'
138
+ };
139
+
140
+ exports.Prisma.JsonNullValueFilter = {
141
+ DbNull: Prisma.DbNull,
142
+ JsonNull: Prisma.JsonNull,
143
+ AnyNull: Prisma.AnyNull
144
+ };
145
+
146
+ exports.Prisma.NullsOrder = {
147
+ first: 'first',
148
+ last: 'last'
149
+ };
150
+
151
+
152
+ exports.Prisma.ModelName = {
153
+ DbAppForCatalog: 'DbAppForCatalog',
154
+ Icon: 'Icon'
155
+ };
156
+ /**
157
+ * Create the Client
158
+ */
159
+ const config = {
160
+ "generator": {
161
+ "name": "client",
162
+ "provider": {
163
+ "fromEnvVar": null,
164
+ "value": "prisma-client-js"
165
+ },
166
+ "output": {
167
+ "value": "/Users/igolovin/src/umbrella/env-hopper/packages/backend-core/src/generated/prisma",
168
+ "fromEnvVar": null
169
+ },
170
+ "config": {
171
+ "engineType": "library"
172
+ },
173
+ "binaryTargets": [
174
+ {
175
+ "fromEnvVar": null,
176
+ "value": "darwin-arm64",
177
+ "native": true
178
+ }
179
+ ],
180
+ "previewFeatures": [],
181
+ "sourceFilePath": "/Users/igolovin/src/umbrella/env-hopper/packages/backend-core/prisma/schema.prisma",
182
+ "isCustomOutput": true
183
+ },
184
+ "relativeEnvPaths": {
185
+ "rootEnvPath": null
186
+ },
187
+ "relativePath": "../../../prisma",
188
+ "clientVersion": "6.17.0",
189
+ "engineVersion": "c0aafc03b8ef6cdced8654b9a817999e02457d6a",
190
+ "datasourceNames": [
191
+ "db"
192
+ ],
193
+ "activeProvider": "postgresql",
194
+ "postinstall": false,
195
+ "inlineDatasources": {
196
+ "db": {
197
+ "url": {
198
+ "fromEnvVar": "EH_CORE_DATABASE_URL",
199
+ "value": null
200
+ }
201
+ }
202
+ },
203
+ "inlineSchema": "// Prisma schema for backend-core library\n// Uses EH_CORE_DATABASE_URL environment variable\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"EH_CORE_DATABASE_URL\")\n}\n\ngenerator client {\n provider = \"prisma-client-js\"\n output = \"../src/generated/prisma\"\n}\n\ngenerator json {\n provider = \"prisma-json-types-generator\"\n}\n\nmodel DbAppForCatalog {\n id String @id @default(cuid())\n name String\n description String\n /// [AccessMethod]\n access Json\n teams String[] @default([])\n /// [AppRole]\n roles Json?\n approverName String?\n approverEmail String?\n notes String?\n tags String[] @default([])\n appUrl String?\n confluenceUrl String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name])\n @@index([tags])\n}\n\nmodel Icon {\n id String @id @default(cuid())\n name String @unique\n svgContent String? @db.Text\n pngBase64 String? @db.Text\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n\n @@index([name])\n}\n",
204
+ "inlineSchemaHash": "b83fbbc0d3c25dcdf35d0f9145415f8093b7124bc747546875740feee17ef2de",
205
+ "copyEngine": true
206
+ }
207
+ config.dirname = '/'
208
+
209
+ config.runtimeDataModel = JSON.parse("{\"models\":{\"DbAppForCatalog\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"access\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"teams\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"roles\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"approverName\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"approverEmail\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"notes\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"tags\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"appUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"confluenceUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"Icon\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"svgContent\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"pngBase64\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
210
+ defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
211
+ config.engineWasm = {
212
+ getRuntime: async () => require('./query_engine_bg.js'),
213
+ getQueryEngineWasmModule: async () => {
214
+ const loader = (await import('#wasm-engine-loader')).default
215
+ const engine = (await loader).default
216
+ return engine
217
+ }
218
+ }
219
+ config.compilerWasm = undefined
220
+
221
+ config.injectableEdgeEnv = () => ({
222
+ parsed: {
223
+ EH_CORE_DATABASE_URL: typeof globalThis !== 'undefined' && globalThis['EH_CORE_DATABASE_URL'] || typeof process !== 'undefined' && process.env && process.env.EH_CORE_DATABASE_URL || undefined
224
+ }
225
+ })
226
+
227
+ if (typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined) {
228
+ Debug.enable(typeof globalThis !== 'undefined' && globalThis['DEBUG'] || typeof process !== 'undefined' && process.env && process.env.DEBUG || undefined)
229
+ }
230
+
231
+ const PrismaClient = getPrismaClient(config)
232
+ exports.PrismaClient = PrismaClient
233
+ Object.assign(exports, Prisma)
234
+
package/src/index.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  // common
2
2
 
3
- export { trpcRouter } from './server/controller'
3
+ export { createTrpcRouter } from './server/controller'
4
4
  export type { TRPCRouter } from './server/controller'
5
5
  export { createEhTrpcContext } from './server/ehTrpcContext'
6
6
  export type {
@@ -15,4 +15,112 @@ export type { EhStaticControllerContract } from './server/ehStaticControllerCont
15
15
 
16
16
  // backend-only
17
17
 
18
+ export type { AppForCatalog } from './types/common/appCatalogTypes'
18
19
  export * from './types/index'
20
+
21
+ // Auth
22
+ export {
23
+ createAuth,
24
+ type AuthConfig,
25
+ type BetterAuth,
26
+ } from './modules/auth/auth'
27
+
28
+ export { registerAuthRoutes } from './modules/auth/registerAuthRoutes'
29
+
30
+ export { createAuthRouter, type AuthRouter } from './modules/auth/authRouter'
31
+
32
+ export {
33
+ getUserGroups,
34
+ isMemberOfAnyGroup,
35
+ isMemberOfAllGroups,
36
+ isAdmin,
37
+ requireAdmin,
38
+ requireGroups,
39
+ type UserWithGroups,
40
+ } from './modules/auth/authorizationUtils'
41
+
42
+ // Admin
43
+ export {
44
+ createAdminChatHandler,
45
+ tool,
46
+ type AdminChatHandlerOptions,
47
+ } from './modules/admin/chat/createAdminChatHandler'
48
+
49
+ export {
50
+ createDatabaseTools,
51
+ createPrismaDatabaseClient,
52
+ DEFAULT_ADMIN_SYSTEM_PROMPT,
53
+ type DatabaseClient,
54
+ } from './modules/admin/chat/createDatabaseTools'
55
+
56
+ // Icon management
57
+ export {
58
+ registerIconRestController,
59
+ type IconRestControllerConfig,
60
+ } from './modules/icons/iconRestController'
61
+
62
+ export {
63
+ getAssetByName,
64
+ upsertIcon,
65
+ upsertIcons,
66
+ type UpsertIconInput,
67
+ } from './modules/icons/iconService'
68
+
69
+ // Asset management (universal for icons, screenshots, etc.)
70
+ export {
71
+ registerAssetRestController,
72
+ type AssetRestControllerConfig,
73
+ } from './modules/assets/assetRestController'
74
+
75
+ export {
76
+ registerScreenshotRestController,
77
+ type ScreenshotRestControllerConfig,
78
+ } from './modules/assets/screenshotRestController'
79
+
80
+ export { createScreenshotRouter } from './modules/assets/screenshotRouter'
81
+
82
+ export { syncAssets, type SyncAssetsConfig } from './modules/assets/syncAssets'
83
+
84
+ // App Catalog Admin
85
+ export { createAppCatalogAdminRouter } from './modules/appCatalogAdmin/appCatalogAdminRouter'
86
+
87
+ // Approval Methods
88
+ export { createApprovalMethodRouter } from './modules/approvalMethod/approvalMethodRouter'
89
+ export {
90
+ syncApprovalMethods,
91
+ type ApprovalMethodSyncInput,
92
+ } from './modules/approvalMethod/syncApprovalMethods'
93
+
94
+ // Database utilities
95
+ export {
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,
111
+ } from './db'
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,171 @@
1
+ import express, { Router } from 'express'
2
+ import * as trpcExpress from '@trpc/server/adapters/express'
3
+ import type {
4
+ EhMiddlewareOptions,
5
+ EhMiddlewareResult,
6
+ MiddlewareContext,
7
+ } from './types'
8
+ import { EhDatabaseManager } from './database'
9
+ import { createBackendResolver } from './backendResolver'
10
+ import { registerFeatures } from './featureRegistry'
11
+ import { createTrpcRouter } from '../server/controller'
12
+ import { createEhTrpcContext } from '../server/ehTrpcContext'
13
+ import { createAuth } from '../modules/auth/auth'
14
+ import { createMockUserFromDevConfig } from '../modules/auth/devMockUserUtils'
15
+
16
+ export async function createEhMiddleware(
17
+ options: EhMiddlewareOptions,
18
+ ): Promise<EhMiddlewareResult> {
19
+ // Normalize options with defaults
20
+ const basePath = options.basePath ?? '/api'
21
+ const normalizedOptions = { ...options, basePath }
22
+
23
+ // Initialize database manager
24
+ const dbManager = new EhDatabaseManager(options.database)
25
+ // Initialize the client (which also sets the global singleton)
26
+ dbManager.getClient()
27
+
28
+ // Create auth instance
29
+ const auth = createAuth({
30
+ appName: options.auth.appName,
31
+ baseURL: options.auth.baseURL,
32
+ secret: options.auth.secret,
33
+ providers: options.auth.providers,
34
+ plugins: options.auth.plugins,
35
+ sessionExpiresIn: options.auth.sessionExpiresIn,
36
+ sessionUpdateAge: options.auth.sessionUpdateAge,
37
+ })
38
+
39
+ // Create tRPC router
40
+ const trpcRouter = createTrpcRouter(auth)
41
+
42
+ // Normalize backend provider to async factory function
43
+ const resolveBackend = createBackendResolver(options.backend)
44
+
45
+ // Get admin groups from config with default
46
+ const adminGroups = options.auth.adminGroups ?? ['env_hopper_ui_super_admins']
47
+
48
+ // Create tRPC context factory
49
+ const createContext = async ({
50
+ req,
51
+ }: trpcExpress.CreateExpressContextOptions) => {
52
+ const companySpecificBackend = await resolveBackend()
53
+
54
+ let user = null
55
+ let userGroups: Array<string> = []
56
+
57
+ // Check if dev mock user is configured
58
+ if (options.auth.devMockUser) {
59
+ user = createMockUserFromDevConfig(options.auth.devMockUser)
60
+ userGroups = options.auth.devMockUser.groups
61
+ } else {
62
+ // Extract user from session
63
+ try {
64
+ const session = await auth.api.getSession({
65
+ headers: req.headers as HeadersInit,
66
+ })
67
+ user = session?.user ?? null
68
+
69
+ // If user is authenticated and Okta is configured, decode groups from access token
70
+ if (user && options.auth.oktaGroupsClaim) {
71
+ try {
72
+ // Get the current access token (auto-refreshes if expired)
73
+ // Note: better-auth requires providerId, but we use 'okta' as default
74
+ const tokenResult = await auth.api.getAccessToken({
75
+ body: {
76
+ providerId: 'okta',
77
+ },
78
+ headers: req.headers as HeadersInit,
79
+ })
80
+
81
+ if (tokenResult.accessToken) {
82
+ // Decode JWT to extract groups claim
83
+ const parts = tokenResult.accessToken.split('.')
84
+ if (parts.length === 3 && parts[1]) {
85
+ const payload = JSON.parse(
86
+ Buffer.from(parts[1], 'base64').toString(),
87
+ )
88
+ const groups = payload[options.auth.oktaGroupsClaim]
89
+ userGroups = Array.isArray(groups) ? groups : []
90
+ }
91
+ }
92
+ } catch (error) {
93
+ console.error('[tRPC Context] Failed to get access token:', error)
94
+ }
95
+ }
96
+ } catch (error) {
97
+ console.error('[tRPC Context] Failed to get session:', error)
98
+ }
99
+ }
100
+
101
+ // Attach groups to user object for authorization checks
102
+ const userWithGroups = user ? { ...user, groups: userGroups } : null
103
+
104
+ return createEhTrpcContext({
105
+ companySpecificBackend,
106
+ user: userWithGroups,
107
+ adminGroups,
108
+ })
109
+ }
110
+
111
+ // Create Express router
112
+ const router = Router()
113
+ router.use(express.json())
114
+
115
+ // Build middleware context for feature registration
116
+ const middlewareContext: MiddlewareContext = {
117
+ auth,
118
+ trpcRouter,
119
+ createContext: async () => {
120
+ const companySpecificBackend = await resolveBackend()
121
+ return createEhTrpcContext({
122
+ companySpecificBackend,
123
+ adminGroups,
124
+ })
125
+ },
126
+ authConfig: options.auth,
127
+ }
128
+
129
+ // Register tRPC middleware (if enabled)
130
+ if (normalizedOptions.features?.trpc !== false) {
131
+ router.use(
132
+ `${basePath}/trpc`,
133
+ trpcExpress.createExpressMiddleware({
134
+ router: trpcRouter,
135
+ createContext,
136
+ }),
137
+ )
138
+ }
139
+
140
+ // Register all enabled features
141
+ registerFeatures(router, normalizedOptions, middlewareContext)
142
+
143
+ // Call onRoutesRegistered hook if provided
144
+ if (options.hooks?.onRoutesRegistered) {
145
+ await options.hooks.onRoutesRegistered(router)
146
+ }
147
+
148
+ return {
149
+ router,
150
+ auth,
151
+ trpcRouter,
152
+
153
+ async connect(): Promise<void> {
154
+ await dbManager.connect()
155
+ if (options.hooks?.onDatabaseConnected) {
156
+ await options.hooks.onDatabaseConnected()
157
+ }
158
+ },
159
+
160
+ async disconnect(): Promise<void> {
161
+ if (options.hooks?.onDatabaseDisconnecting) {
162
+ await options.hooks.onDatabaseDisconnecting()
163
+ }
164
+ await dbManager.disconnect()
165
+ },
166
+
167
+ addRoutes(callback: (router: Router) => void): void {
168
+ callback(router)
169
+ },
170
+ }
171
+ }
@@ -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
+ }