@env-hopper/backend-core 2.0.1-alpha → 2.0.1-alpha.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/dist/index.d.ts +1584 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1806 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -11
- package/prisma/migrations/20250526183023_init/migration.sql +71 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +121 -0
- package/src/db/client.ts +34 -0
- package/src/db/index.ts +17 -0
- package/src/db/syncAppCatalog.ts +67 -0
- package/src/db/tableSyncMagazine.ts +22 -0
- package/src/db/tableSyncPrismaAdapter.ts +202 -0
- package/src/index.ts +96 -3
- package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
- package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
- package/src/modules/appCatalog/service.ts +79 -0
- package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +113 -0
- package/src/modules/assets/assetRestController.ts +309 -0
- package/src/modules/assets/assetUtils.ts +81 -0
- package/src/modules/assets/screenshotRestController.ts +195 -0
- package/src/modules/assets/screenshotRouter.ts +116 -0
- package/src/modules/assets/syncAssets.ts +261 -0
- package/src/modules/auth/auth.ts +51 -0
- package/src/modules/auth/authProviders.ts +108 -0
- package/src/modules/auth/authRouter.ts +77 -0
- package/src/modules/auth/authorizationUtils.ts +114 -0
- package/src/modules/auth/registerAuthRoutes.ts +33 -0
- package/src/modules/icons/iconRestController.ts +190 -0
- package/src/modules/icons/iconRouter.ts +157 -0
- package/src/modules/icons/iconService.ts +73 -0
- package/src/server/controller.ts +102 -29
- package/src/server/ehStaticControllerContract.ts +8 -1
- package/src/server/ehTrpcContext.ts +0 -6
- package/src/types/backend/api.ts +1 -14
- package/src/types/backend/companySpecificBackend.ts +17 -0
- package/src/types/common/appCatalogTypes.ts +167 -0
- package/src/types/common/dataRootTypes.ts +72 -10
- package/src/types/index.ts +2 -0
- package/dist/esm/__tests__/dummy.test.d.ts +0 -1
- package/dist/esm/index.d.ts +0 -7
- package/dist/esm/index.js +0 -9
- package/dist/esm/index.js.map +0 -1
- package/dist/esm/server/controller.d.ts +0 -32
- package/dist/esm/server/controller.js +0 -35
- package/dist/esm/server/controller.js.map +0 -1
- package/dist/esm/server/db.d.ts +0 -2
- package/dist/esm/server/ehStaticControllerContract.d.ts +0 -9
- package/dist/esm/server/ehStaticControllerContract.js +0 -12
- package/dist/esm/server/ehStaticControllerContract.js.map +0 -1
- package/dist/esm/server/ehTrpcContext.d.ts +0 -8
- package/dist/esm/server/ehTrpcContext.js +0 -11
- package/dist/esm/server/ehTrpcContext.js.map +0 -1
- package/dist/esm/types/backend/api.d.ts +0 -71
- package/dist/esm/types/backend/common.d.ts +0 -9
- package/dist/esm/types/backend/dataSources.d.ts +0 -20
- package/dist/esm/types/backend/deployments.d.ts +0 -34
- package/dist/esm/types/common/app/appTypes.d.ts +0 -12
- package/dist/esm/types/common/app/ui/appUiTypes.d.ts +0 -10
- package/dist/esm/types/common/appCatalogTypes.d.ts +0 -16
- package/dist/esm/types/common/dataRootTypes.d.ts +0 -32
- package/dist/esm/types/common/env/envTypes.d.ts +0 -6
- package/dist/esm/types/common/resourceTypes.d.ts +0 -8
- package/dist/esm/types/common/sharedTypes.d.ts +0 -4
- package/dist/esm/types/index.d.ts +0 -11
- package/src/server/db.ts +0 -4
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { AppForCatalog } from '../types/common/appCatalogTypes'
|
|
2
|
+
import { getDbClient } from './client'
|
|
3
|
+
import { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'
|
|
4
|
+
import { tableSyncPrisma } from './tableSyncPrismaAdapter'
|
|
5
|
+
|
|
6
|
+
export interface SyncAppCatalogResult {
|
|
7
|
+
created: number
|
|
8
|
+
updated: number
|
|
9
|
+
deleted: number
|
|
10
|
+
total: number
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Syncs app catalog data to the database using table sync.
|
|
15
|
+
* This will create new apps, update existing ones, and delete any that are no longer in the input.
|
|
16
|
+
*
|
|
17
|
+
* Note: Call connectDb() before and disconnectDb() after if running in a script.
|
|
18
|
+
*/
|
|
19
|
+
export async function syncAppCatalog(
|
|
20
|
+
apps: Array<AppForCatalog>,
|
|
21
|
+
): Promise<SyncAppCatalogResult> {
|
|
22
|
+
const prisma = getDbClient()
|
|
23
|
+
|
|
24
|
+
const sync = tableSyncPrisma({
|
|
25
|
+
prisma,
|
|
26
|
+
...TABLE_SYNC_MAGAZINE.DbAppForCatalog,
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
// Transform AppForCatalog to DbAppForCatalog format
|
|
30
|
+
const dbApps = apps.map((app) => {
|
|
31
|
+
const slug =
|
|
32
|
+
app.slug ||
|
|
33
|
+
app.displayName
|
|
34
|
+
.toLowerCase()
|
|
35
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
36
|
+
.replace(/^-+|-+$/g, '')
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
slug,
|
|
40
|
+
displayName: app.displayName,
|
|
41
|
+
description: app.description,
|
|
42
|
+
access: app.access,
|
|
43
|
+
teams: app.teams ?? [],
|
|
44
|
+
roles: app.roles ?? null,
|
|
45
|
+
approverName: app.approver?.name ?? null,
|
|
46
|
+
approverEmail: app.approver?.email ?? null,
|
|
47
|
+
notes: app.notes ?? null,
|
|
48
|
+
tags: app.tags ?? [],
|
|
49
|
+
appUrl: app.appUrl ?? null,
|
|
50
|
+
links: app.links ?? null,
|
|
51
|
+
iconName: app.iconName ?? null,
|
|
52
|
+
screenshotIds: app.screenshotIds ?? [],
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
const result = await sync.sync(dbApps)
|
|
57
|
+
|
|
58
|
+
// Get actual synced data to calculate stats
|
|
59
|
+
const actual = result.getActual()
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
created: actual.length - apps.length + (apps.length - actual.length),
|
|
63
|
+
updated: 0, // TableSync doesn't expose this directly
|
|
64
|
+
deleted: 0, // TableSync doesn't expose this directly
|
|
65
|
+
total: actual.length,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Prisma } from '@prisma/client'
|
|
2
|
+
import type { ObjectKeys, ScalarKeys } from './tableSyncPrismaAdapter'
|
|
3
|
+
|
|
4
|
+
interface CommonSyncTableInfo<TPrismaModelName extends Prisma.ModelName> {
|
|
5
|
+
prismaModelName: TPrismaModelName
|
|
6
|
+
uniqColumns: Array<ScalarKeys<TPrismaModelName>>
|
|
7
|
+
relationColumns?: Array<ObjectKeys<TPrismaModelName>>
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
type TableSyncMagazineType = Partial<{
|
|
11
|
+
[key in Prisma.ModelName]: CommonSyncTableInfo<key>
|
|
12
|
+
}>
|
|
13
|
+
|
|
14
|
+
export const TABLE_SYNC_MAGAZINE = {
|
|
15
|
+
DbAppForCatalog: {
|
|
16
|
+
prismaModelName: 'DbAppForCatalog',
|
|
17
|
+
uniqColumns: ['slug'],
|
|
18
|
+
},
|
|
19
|
+
} as const satisfies TableSyncMagazineType
|
|
20
|
+
|
|
21
|
+
export type TableSyncMagazine = typeof TABLE_SYNC_MAGAZINE
|
|
22
|
+
export type TableSyncMagazineModelNameKey = keyof TableSyncMagazine
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { tableSync } from '@env-hopper/table-sync'
|
|
2
|
+
import type { Prisma, PrismaClient } from '@prisma/client'
|
|
3
|
+
import type * as runtime from '@prisma/client/runtime/library'
|
|
4
|
+
import { mapValues, omit, pick } from 'radashi'
|
|
5
|
+
|
|
6
|
+
export type ScalarKeys<TPrismaModelName extends Prisma.ModelName> =
|
|
7
|
+
keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']
|
|
8
|
+
export type ObjectKeys<TPrismaModelName extends Prisma.ModelName> =
|
|
9
|
+
keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['objects']
|
|
10
|
+
|
|
11
|
+
export type ScalarFilter<TPrismaModelName extends Prisma.ModelName> = Partial<
|
|
12
|
+
Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']
|
|
13
|
+
>
|
|
14
|
+
|
|
15
|
+
export type GetOperationFns<TModel extends Prisma.ModelName> = {
|
|
16
|
+
[TOperation in keyof Prisma.TypeMap['model']['DbAppForCatalog']['operations']]: (
|
|
17
|
+
args: Prisma.TypeMap['model'][TModel]['operations'][TOperation]['args'],
|
|
18
|
+
) => Promise<
|
|
19
|
+
Prisma.TypeMap['model'][TModel]['operations'][TOperation]['result']
|
|
20
|
+
>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TableSyncParamsPrisma<
|
|
24
|
+
TPrismaClient extends PrismaClient,
|
|
25
|
+
TPrismaModelName extends Prisma.ModelName,
|
|
26
|
+
TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,
|
|
27
|
+
TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,
|
|
28
|
+
> {
|
|
29
|
+
prisma: TPrismaClient
|
|
30
|
+
prismaModelName: TPrismaModelName
|
|
31
|
+
uniqColumns: TUniqColumns
|
|
32
|
+
relationColumns?: TRelationColumns
|
|
33
|
+
where?: ScalarFilter<TPrismaModelName>
|
|
34
|
+
upsertOnly?: boolean
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function getPrismaModelOperations<
|
|
38
|
+
TPrismaClient extends Omit<PrismaClient, runtime.ITXClientDenyList>,
|
|
39
|
+
TPrismaModelName extends Prisma.ModelName,
|
|
40
|
+
>(prisma: TPrismaClient, prismaModelName: TPrismaModelName) {
|
|
41
|
+
const key = (prismaModelName.slice(0, 1).toLowerCase() +
|
|
42
|
+
prismaModelName.slice(1)) as keyof TPrismaClient
|
|
43
|
+
return prisma[key] as GetOperationFns<TPrismaModelName>
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type MakeTFromPrismaModel<TPrismaModelName extends Prisma.ModelName> =
|
|
47
|
+
NonNullable<
|
|
48
|
+
Prisma.TypeMap['model'][TPrismaModelName]['operations']['findUnique']['result']
|
|
49
|
+
>
|
|
50
|
+
|
|
51
|
+
export function tableSyncPrisma<
|
|
52
|
+
TPrismaClient extends PrismaClient,
|
|
53
|
+
TPrismaModelName extends Prisma.ModelName,
|
|
54
|
+
TUniqColumns extends ReadonlyArray<ScalarKeys<TPrismaModelName>>,
|
|
55
|
+
TRelationColumns extends ReadonlyArray<ObjectKeys<TPrismaModelName>>,
|
|
56
|
+
TId extends ScalarKeys<TPrismaModelName> & (string | number) = 'id',
|
|
57
|
+
>(
|
|
58
|
+
params: TableSyncParamsPrisma<
|
|
59
|
+
TPrismaClient,
|
|
60
|
+
TPrismaModelName,
|
|
61
|
+
TUniqColumns,
|
|
62
|
+
TRelationColumns
|
|
63
|
+
>,
|
|
64
|
+
) {
|
|
65
|
+
const {
|
|
66
|
+
prisma,
|
|
67
|
+
prismaModelName,
|
|
68
|
+
uniqColumns,
|
|
69
|
+
where: whereGlobal,
|
|
70
|
+
upsertOnly,
|
|
71
|
+
} = params
|
|
72
|
+
const prismOperations = getPrismaModelOperations(prisma, prismaModelName)
|
|
73
|
+
|
|
74
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
75
|
+
return tableSync<MakeTFromPrismaModel<TPrismaModelName>, TUniqColumns, TId>({
|
|
76
|
+
id: 'id' as TId,
|
|
77
|
+
uniqColumns,
|
|
78
|
+
readAll: async () => {
|
|
79
|
+
const findManyArgs = whereGlobal
|
|
80
|
+
? {
|
|
81
|
+
where: whereGlobal,
|
|
82
|
+
}
|
|
83
|
+
: {}
|
|
84
|
+
return (await prismOperations.findMany(findManyArgs)) as Array<
|
|
85
|
+
MakeTFromPrismaModel<TPrismaModelName>
|
|
86
|
+
>
|
|
87
|
+
},
|
|
88
|
+
writeAll: async (createData, update, deleteIds) => {
|
|
89
|
+
const prismaUniqKey = params.uniqColumns.join('_')
|
|
90
|
+
const relationColumnList =
|
|
91
|
+
params.relationColumns ?? ([] as Array<ObjectKeys<TPrismaModelName>>)
|
|
92
|
+
|
|
93
|
+
return prisma.$transaction(async (tx) => {
|
|
94
|
+
const txOps = getPrismaModelOperations(tx, prismaModelName)
|
|
95
|
+
for (const { data, where } of update) {
|
|
96
|
+
const uniqKeyWhere =
|
|
97
|
+
Object.keys(where).length > 1
|
|
98
|
+
? {
|
|
99
|
+
[prismaUniqKey]: where,
|
|
100
|
+
}
|
|
101
|
+
: where
|
|
102
|
+
|
|
103
|
+
const dataScalar = omit(data, relationColumnList)
|
|
104
|
+
const dataRelations = mapValues(
|
|
105
|
+
pick(data, relationColumnList),
|
|
106
|
+
(value) => {
|
|
107
|
+
return {
|
|
108
|
+
set: value,
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
114
|
+
await txOps.update({
|
|
115
|
+
data: { ...dataScalar, ...dataRelations },
|
|
116
|
+
where: { ...uniqKeyWhere },
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (upsertOnly !== true) {
|
|
121
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
122
|
+
await txOps.deleteMany({
|
|
123
|
+
where: {
|
|
124
|
+
id: {
|
|
125
|
+
in: deleteIds,
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Validate uniqueness of uniqColumns before creating records
|
|
132
|
+
const createDataMapped = createData.map((data) => {
|
|
133
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
134
|
+
const dataScalar = omit(data, relationColumnList)
|
|
135
|
+
|
|
136
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
137
|
+
const onlyRelationColumns = pick(data, relationColumnList)
|
|
138
|
+
const dataRelations = mapValues(onlyRelationColumns, (value) => {
|
|
139
|
+
return {
|
|
140
|
+
connect: value,
|
|
141
|
+
}
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
return { ...dataScalar, ...dataRelations }
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
// Check for duplicates in the data to be created
|
|
148
|
+
if (createDataMapped.length > 0) {
|
|
149
|
+
const uniqKeysInCreate = new Set<string>()
|
|
150
|
+
const duplicateKeys: Array<string> = []
|
|
151
|
+
|
|
152
|
+
for (const data of createDataMapped) {
|
|
153
|
+
const keyParts = params.uniqColumns.map((col) => {
|
|
154
|
+
const value = data[col as keyof typeof data]
|
|
155
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive: unique columns may be nullable in schemas
|
|
156
|
+
return value === null || value === undefined
|
|
157
|
+
? 'null'
|
|
158
|
+
: String(value)
|
|
159
|
+
})
|
|
160
|
+
const key = keyParts.join(':')
|
|
161
|
+
|
|
162
|
+
if (uniqKeysInCreate.has(key)) {
|
|
163
|
+
duplicateKeys.push(key)
|
|
164
|
+
} else {
|
|
165
|
+
uniqKeysInCreate.add(key)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (duplicateKeys.length > 0) {
|
|
170
|
+
const uniqColumnsStr = params.uniqColumns.join(', ')
|
|
171
|
+
throw new Error(
|
|
172
|
+
`Duplicate unique key values found in data to be created. ` +
|
|
173
|
+
`Model: ${prismaModelName}, Unique columns: [${uniqColumnsStr}], ` +
|
|
174
|
+
`Duplicate keys: [${duplicateKeys.join(', ')}]`,
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const results: Array<MakeTFromPrismaModel<TPrismaModelName>> = []
|
|
180
|
+
|
|
181
|
+
if (relationColumnList.length === 0) {
|
|
182
|
+
// @ts-expect-error This is too difficult for me to come up with right types
|
|
183
|
+
const batchResult = await txOps.createManyAndReturn({
|
|
184
|
+
data: createDataMapped,
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
results.push(...batchResult)
|
|
188
|
+
} else {
|
|
189
|
+
for (const dataMappedElement of createDataMapped) {
|
|
190
|
+
// @ts-expect-error too difficult for me
|
|
191
|
+
const newVar = await txOps.create({
|
|
192
|
+
data: dataMappedElement,
|
|
193
|
+
})
|
|
194
|
+
results.push(newVar as MakeTFromPrismaModel<TPrismaModelName>)
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return results
|
|
199
|
+
})
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
// common
|
|
2
2
|
|
|
3
|
-
export {
|
|
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 {
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
EhTrpcContext,
|
|
8
|
+
EhTrpcContextOptions
|
|
9
9
|
} from './server/ehTrpcContext'
|
|
10
10
|
|
|
11
11
|
export { staticControllerContract } from './server/ehStaticControllerContract'
|
|
@@ -15,4 +15,97 @@ 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 {
|
|
29
|
+
registerAuthRoutes
|
|
30
|
+
} from './modules/auth/registerAuthRoutes'
|
|
31
|
+
|
|
32
|
+
export {
|
|
33
|
+
createAuthRouter,
|
|
34
|
+
type AuthRouter
|
|
35
|
+
} from './modules/auth/authRouter'
|
|
36
|
+
|
|
37
|
+
export {
|
|
38
|
+
getAuthProvidersFromEnv,
|
|
39
|
+
getAuthPluginsFromEnv,
|
|
40
|
+
validateAuthConfig
|
|
41
|
+
} from './modules/auth/authProviders'
|
|
42
|
+
|
|
43
|
+
export {
|
|
44
|
+
getUserGroups,
|
|
45
|
+
isMemberOfAnyGroup,
|
|
46
|
+
isMemberOfAllGroups,
|
|
47
|
+
getAdminGroupsFromEnv,
|
|
48
|
+
isAdmin,
|
|
49
|
+
requireAdmin,
|
|
50
|
+
requireGroups,
|
|
51
|
+
type UserWithGroups
|
|
52
|
+
} from './modules/auth/authorizationUtils'
|
|
53
|
+
|
|
54
|
+
// Admin
|
|
55
|
+
export {
|
|
56
|
+
createAdminChatHandler,
|
|
57
|
+
tool,
|
|
58
|
+
type AdminChatHandlerOptions
|
|
59
|
+
} from './modules/admin/chat/createAdminChatHandler'
|
|
60
|
+
|
|
61
|
+
export {
|
|
62
|
+
createDatabaseTools,
|
|
63
|
+
createPrismaDatabaseClient, DEFAULT_ADMIN_SYSTEM_PROMPT, type DatabaseClient
|
|
64
|
+
} from './modules/admin/chat/createDatabaseTools'
|
|
65
|
+
|
|
66
|
+
// Icon management
|
|
67
|
+
export {
|
|
68
|
+
registerIconRestController,
|
|
69
|
+
type IconRestControllerConfig
|
|
70
|
+
} from './modules/icons/iconRestController'
|
|
71
|
+
|
|
72
|
+
export {
|
|
73
|
+
getAssetByName,
|
|
74
|
+
upsertIcon,
|
|
75
|
+
upsertIcons,
|
|
76
|
+
type UpsertIconInput
|
|
77
|
+
} from './modules/icons/iconService'
|
|
78
|
+
|
|
79
|
+
// Asset management (universal for icons, screenshots, etc.)
|
|
80
|
+
export {
|
|
81
|
+
registerAssetRestController,
|
|
82
|
+
type AssetRestControllerConfig
|
|
83
|
+
} from './modules/assets/assetRestController'
|
|
84
|
+
|
|
85
|
+
export {
|
|
86
|
+
registerScreenshotRestController,
|
|
87
|
+
type ScreenshotRestControllerConfig
|
|
88
|
+
} from './modules/assets/screenshotRestController'
|
|
89
|
+
|
|
90
|
+
export {
|
|
91
|
+
createScreenshotRouter
|
|
92
|
+
} from './modules/assets/screenshotRouter'
|
|
93
|
+
|
|
94
|
+
export {
|
|
95
|
+
syncAssets,
|
|
96
|
+
type SyncAssetsConfig
|
|
97
|
+
} from './modules/assets/syncAssets'
|
|
98
|
+
|
|
99
|
+
// App Catalog Admin
|
|
100
|
+
export {
|
|
101
|
+
createAppCatalogAdminRouter
|
|
102
|
+
} from './modules/appCatalogAdmin/appCatalogAdminRouter'
|
|
103
|
+
|
|
104
|
+
// Database utilities
|
|
105
|
+
export {
|
|
106
|
+
connectDb,
|
|
107
|
+
disconnectDb, getDbClient, syncAppCatalog, TABLE_SYNC_MAGAZINE, tableSyncPrisma, type MakeTFromPrismaModel, type ObjectKeys,
|
|
108
|
+
type ScalarFilter, type ScalarKeys, type SyncAppCatalogResult, type TableSyncMagazine,
|
|
109
|
+
type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma
|
|
110
|
+
} from './db'
|
|
111
|
+
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { stepCountIs, streamText, tool } from 'ai'
|
|
2
|
+
import type { LanguageModel, Tool } from 'ai'
|
|
3
|
+
|
|
4
|
+
import type { Request, Response } from 'express'
|
|
5
|
+
|
|
6
|
+
export interface AdminChatHandlerOptions {
|
|
7
|
+
/** The AI model to use (from @ai-sdk/openai, @ai-sdk/anthropic, etc.) */
|
|
8
|
+
model: LanguageModel
|
|
9
|
+
/** System prompt for the AI assistant */
|
|
10
|
+
systemPrompt?: string
|
|
11
|
+
/** Tools available to the AI assistant */
|
|
12
|
+
tools?: Record<string, Tool>
|
|
13
|
+
/**
|
|
14
|
+
* Optional function to validate configuration before processing requests.
|
|
15
|
+
* Should throw an error if configuration is invalid (e.g., missing API key).
|
|
16
|
+
* @example
|
|
17
|
+
* validateConfig: () => {
|
|
18
|
+
* if (!process.env.OPENAI_API_KEY) {
|
|
19
|
+
* throw new Error('OPENAI_API_KEY is not configured')
|
|
20
|
+
* }
|
|
21
|
+
* }
|
|
22
|
+
*/
|
|
23
|
+
validateConfig?: () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface TextPart {
|
|
27
|
+
type: 'text'
|
|
28
|
+
text: string
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface UIMessageInput {
|
|
32
|
+
role: 'user' | 'assistant' | 'system'
|
|
33
|
+
content?: string
|
|
34
|
+
parts?: Array<TextPart | { type: string }>
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface CoreMessage {
|
|
38
|
+
role: 'user' | 'assistant' | 'system'
|
|
39
|
+
content: string
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function convertToCoreMessages(
|
|
43
|
+
messages: Array<UIMessageInput>,
|
|
44
|
+
): Array<CoreMessage> {
|
|
45
|
+
return messages.map((msg) => {
|
|
46
|
+
if (msg.content) {
|
|
47
|
+
return { role: msg.role, content: msg.content }
|
|
48
|
+
}
|
|
49
|
+
// Extract text from parts array (AI SDK v3 format)
|
|
50
|
+
const textContent =
|
|
51
|
+
msg.parts
|
|
52
|
+
?.filter((part): part is TextPart => part.type === 'text')
|
|
53
|
+
.map((part) => part.text)
|
|
54
|
+
.join('') ?? ''
|
|
55
|
+
return { role: msg.role, content: textContent }
|
|
56
|
+
})
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Creates an Express handler for the admin chat endpoint.
|
|
61
|
+
*
|
|
62
|
+
* Usage in thin wrappers:
|
|
63
|
+
*
|
|
64
|
+
* ```typescript
|
|
65
|
+
* // With OpenAI
|
|
66
|
+
* import { openai } from '@ai-sdk/openai'
|
|
67
|
+
* app.post('/api/admin/chat', createAdminChatHandler({
|
|
68
|
+
* model: openai('gpt-4o-mini'),
|
|
69
|
+
* }))
|
|
70
|
+
*
|
|
71
|
+
* // With Claude
|
|
72
|
+
* import { anthropic } from '@ai-sdk/anthropic'
|
|
73
|
+
* app.post('/api/admin/chat', createAdminChatHandler({
|
|
74
|
+
* model: anthropic('claude-sonnet-4-20250514'),
|
|
75
|
+
* }))
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function createAdminChatHandler(options: AdminChatHandlerOptions) {
|
|
79
|
+
const {
|
|
80
|
+
model,
|
|
81
|
+
systemPrompt = 'You are a helpful admin assistant for the Env Hopper application. Help users manage apps, data sources, and MCP server configurations.',
|
|
82
|
+
tools = {},
|
|
83
|
+
validateConfig,
|
|
84
|
+
} = options
|
|
85
|
+
|
|
86
|
+
return async (req: Request, res: Response) => {
|
|
87
|
+
try {
|
|
88
|
+
// Validate configuration if validator provided
|
|
89
|
+
if (validateConfig) {
|
|
90
|
+
validateConfig()
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const { messages } = req.body as { messages: Array<UIMessageInput> }
|
|
94
|
+
const coreMessages = convertToCoreMessages(messages)
|
|
95
|
+
|
|
96
|
+
console.log(
|
|
97
|
+
'[Admin Chat] Received messages:',
|
|
98
|
+
JSON.stringify(coreMessages, null, 2),
|
|
99
|
+
)
|
|
100
|
+
console.log('[Admin Chat] Available tools:', Object.keys(tools))
|
|
101
|
+
|
|
102
|
+
const result = streamText({
|
|
103
|
+
model,
|
|
104
|
+
system: systemPrompt,
|
|
105
|
+
messages: coreMessages,
|
|
106
|
+
tools,
|
|
107
|
+
// Allow up to 5 steps so the model can call tools and then generate a response
|
|
108
|
+
stopWhen: stepCountIs(5),
|
|
109
|
+
onFinish: (event) => {
|
|
110
|
+
console.log('[Admin Chat] Finished:', {
|
|
111
|
+
finishReason: event.finishReason,
|
|
112
|
+
usage: event.usage,
|
|
113
|
+
hasText: !!event.text,
|
|
114
|
+
textLength: event.text.length,
|
|
115
|
+
})
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
// Use UI message stream response which is compatible with AI SDK React hooks
|
|
120
|
+
const response = result.toUIMessageStreamResponse()
|
|
121
|
+
|
|
122
|
+
// Copy headers from the response
|
|
123
|
+
response.headers.forEach((value, key) => {
|
|
124
|
+
res.setHeader(key, value)
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
// Pipe the stream to the response
|
|
128
|
+
if (response.body) {
|
|
129
|
+
const reader = response.body.getReader()
|
|
130
|
+
const pump = async (): Promise<void> => {
|
|
131
|
+
const { done, value } = await reader.read()
|
|
132
|
+
if (done) {
|
|
133
|
+
res.end()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
res.write(value)
|
|
137
|
+
return pump()
|
|
138
|
+
}
|
|
139
|
+
await pump()
|
|
140
|
+
} else {
|
|
141
|
+
console.error('[Admin Chat] No response body')
|
|
142
|
+
res.status(500).json({ error: 'No response from AI model' })
|
|
143
|
+
}
|
|
144
|
+
} catch (error) {
|
|
145
|
+
console.error('[Admin Chat] Error:', error)
|
|
146
|
+
res.status(500).json({ error: 'Failed to process chat request' })
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Re-export tool helper for convenience
|
|
152
|
+
export { tool }
|