@igstack/app-catalog-backend-core 0.2.0 → 0.3.0
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 +239 -1235
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +441 -2367
- package/dist/index.js.map +1 -1
- package/package.json +7 -4
- package/prisma.config.ts +13 -0
- package/src/index.ts +0 -30
- package/src/middleware/featureRegistry.ts +0 -38
- package/src/server/controller.ts +0 -16
- package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +0 -187
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +0 -213
- package/src/modules/approvalMethod/approvalMethodRouter.ts +0 -169
- package/src/modules/approvalMethod/slugUtils.ts +0 -17
- package/src/modules/approvalMethod/syncApprovalMethods.ts +0 -38
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-backend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Backend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
"files": [
|
|
28
28
|
"dist",
|
|
29
29
|
"src",
|
|
30
|
-
"prisma"
|
|
30
|
+
"prisma",
|
|
31
|
+
"prisma.config.ts"
|
|
31
32
|
],
|
|
32
33
|
"dependencies": {
|
|
33
34
|
"@kubernetes/client-node": "^1.2.0",
|
|
35
|
+
"@prisma/adapter-pg": "^7.4.2",
|
|
34
36
|
"@prisma/client": "^7.4.2",
|
|
35
37
|
"@trpc/client": "^11.4.2",
|
|
36
38
|
"@trpc/server": "^11.4.2",
|
|
@@ -48,8 +50,8 @@
|
|
|
48
50
|
"tsyringe": "^4.10.0",
|
|
49
51
|
"yaml": "^2.8.0",
|
|
50
52
|
"zod": "^4.3.5",
|
|
51
|
-
"@igstack/app-catalog-
|
|
52
|
-
"@igstack/app-catalog-
|
|
53
|
+
"@igstack/app-catalog-table-sync": "0.3.0",
|
|
54
|
+
"@igstack/app-catalog-shared-core": "0.3.0"
|
|
53
55
|
},
|
|
54
56
|
"devDependencies": {
|
|
55
57
|
"@tanstack/vite-config": "^0.4.3",
|
|
@@ -59,6 +61,7 @@
|
|
|
59
61
|
"@types/jsonwebtoken": "^9.0.9",
|
|
60
62
|
"@types/multer": "^1.4.12",
|
|
61
63
|
"@types/node": "^24.3.0",
|
|
64
|
+
"@types/pg": "^8.18.0",
|
|
62
65
|
"esbuild": "^0.25.5",
|
|
63
66
|
"prisma": "^7.4.2",
|
|
64
67
|
"prisma-json-types-generator": "^4.1.1",
|
package/prisma.config.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defineConfig } from 'prisma/config'
|
|
2
|
+
|
|
3
|
+
// Note: Environment variables should be loaded by the calling context
|
|
4
|
+
// (e.g., via dotenv-defaults in the application entry point)
|
|
5
|
+
// The Prisma CLI will use the AC_CORE_DATABASE_URL from the environment
|
|
6
|
+
// where it's executed (e.g., examples/backend-example/.env.defaults)
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
schema: 'prisma/schema.prisma',
|
|
10
|
+
datasource: {
|
|
11
|
+
url: process.env.AC_CORE_DATABASE_URL ?? '',
|
|
12
|
+
},
|
|
13
|
+
})
|
package/src/index.ts
CHANGED
|
@@ -39,20 +39,6 @@ export {
|
|
|
39
39
|
type UserWithGroups,
|
|
40
40
|
} from './modules/auth/authorizationUtils'
|
|
41
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
42
|
// Icon management
|
|
57
43
|
export {
|
|
58
44
|
registerIconRestController,
|
|
@@ -77,26 +63,14 @@ export {
|
|
|
77
63
|
type ScreenshotRestControllerConfig,
|
|
78
64
|
} from './modules/assets/screenshotRestController'
|
|
79
65
|
|
|
80
|
-
export { createScreenshotRouter } from './modules/assets/screenshotRouter'
|
|
81
|
-
|
|
82
66
|
export { syncAssets, type SyncAssetsConfig } from './modules/assets/syncAssets'
|
|
83
67
|
|
|
84
|
-
// App Catalog Admin
|
|
85
|
-
export { createAppCatalogAdminRouter } from './modules/appCatalogAdmin/appCatalogAdminRouter'
|
|
86
|
-
|
|
87
68
|
// App Catalog utilities
|
|
88
69
|
export {
|
|
89
70
|
checkAllLinks,
|
|
90
71
|
printLinkCheckReport,
|
|
91
72
|
} from './modules/appCatalog/checkLinks'
|
|
92
73
|
|
|
93
|
-
// Approval Methods
|
|
94
|
-
export { createApprovalMethodRouter } from './modules/approvalMethod/approvalMethodRouter'
|
|
95
|
-
export {
|
|
96
|
-
syncApprovalMethods,
|
|
97
|
-
type ApprovalMethodSyncInput,
|
|
98
|
-
} from './modules/approvalMethod/syncApprovalMethods'
|
|
99
|
-
|
|
100
74
|
// Database utilities
|
|
101
75
|
export {
|
|
102
76
|
connectDb,
|
|
@@ -116,9 +90,6 @@ export {
|
|
|
116
90
|
type TableSyncParamsPrisma,
|
|
117
91
|
} from './db'
|
|
118
92
|
|
|
119
|
-
// Prisma types (re-export from generated client for external packages)
|
|
120
|
-
export type { Prisma, PrismaClient } from './db/prisma'
|
|
121
|
-
|
|
122
93
|
// Middleware (batteries-included backend setup)
|
|
123
94
|
export {
|
|
124
95
|
createEhMiddleware,
|
|
@@ -126,7 +97,6 @@ export {
|
|
|
126
97
|
injectCustomScripts,
|
|
127
98
|
type EhDatabaseConfig,
|
|
128
99
|
type EhAuthConfig,
|
|
129
|
-
type EhAdminChatConfig,
|
|
130
100
|
type EhFeatureToggles,
|
|
131
101
|
type EhBackendProvider,
|
|
132
102
|
type EhLifecycleHooks,
|
|
@@ -8,16 +8,7 @@ import type {
|
|
|
8
8
|
import { registerIconRestController } from '../modules/icons/iconRestController'
|
|
9
9
|
import { registerAssetRestController } from '../modules/assets/assetRestController'
|
|
10
10
|
import { registerScreenshotRestController } from '../modules/assets/screenshotRestController'
|
|
11
|
-
import { createAdminChatHandler } from '../modules/admin/chat/createAdminChatHandler'
|
|
12
11
|
import { getAssetByName } from '../modules/icons/iconService'
|
|
13
|
-
import {
|
|
14
|
-
exportAsset,
|
|
15
|
-
exportCatalog,
|
|
16
|
-
importAsset,
|
|
17
|
-
importCatalog,
|
|
18
|
-
listAssets,
|
|
19
|
-
} from '../modules/appCatalogAdmin/catalogBackupController'
|
|
20
|
-
import multer from 'multer'
|
|
21
12
|
import { createMockSessionResponse } from '../modules/auth/devMockUserUtils'
|
|
22
13
|
|
|
23
14
|
interface FeatureRegistration {
|
|
@@ -70,18 +61,6 @@ const FEATURES: Array<FeatureRegistration> = [
|
|
|
70
61
|
router.all(`${basePath}/auth/{*any}`, authHandler)
|
|
71
62
|
},
|
|
72
63
|
},
|
|
73
|
-
{
|
|
74
|
-
name: 'adminChat',
|
|
75
|
-
defaultEnabled: false, // Only enabled if adminChat config is provided
|
|
76
|
-
register: (router, options) => {
|
|
77
|
-
if (options.adminChat) {
|
|
78
|
-
router.post(
|
|
79
|
-
`${options.basePath}/admin/chat`,
|
|
80
|
-
createAdminChatHandler(options.adminChat),
|
|
81
|
-
)
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
},
|
|
85
64
|
{
|
|
86
65
|
name: 'legacyIconEndpoint',
|
|
87
66
|
defaultEnabled: false,
|
|
@@ -143,29 +122,12 @@ export function registerFeatures(
|
|
|
143
122
|
basePath: `${basePath}/screenshots`,
|
|
144
123
|
})
|
|
145
124
|
|
|
146
|
-
// Catalog backup/restore
|
|
147
|
-
const upload = multer({ storage: multer.memoryStorage() })
|
|
148
|
-
router.get(`${basePath}/catalog/backup/export`, exportCatalog)
|
|
149
|
-
router.post(`${basePath}/catalog/backup/import`, importCatalog)
|
|
150
|
-
router.get(`${basePath}/catalog/backup/assets`, listAssets)
|
|
151
|
-
router.get(`${basePath}/catalog/backup/assets/:name`, exportAsset)
|
|
152
|
-
router.post(
|
|
153
|
-
`${basePath}/catalog/backup/assets`,
|
|
154
|
-
upload.single('file'),
|
|
155
|
-
importAsset,
|
|
156
|
-
)
|
|
157
|
-
|
|
158
125
|
// Optional toggleable features
|
|
159
126
|
const toggles = options.features || {}
|
|
160
127
|
|
|
161
128
|
for (const feature of FEATURES) {
|
|
162
129
|
const isEnabled = toggles[feature.name] ?? feature.defaultEnabled
|
|
163
130
|
|
|
164
|
-
// Special case: adminChat is only enabled if config is provided
|
|
165
|
-
if (feature.name === 'adminChat' && !options.adminChat) {
|
|
166
|
-
continue
|
|
167
|
-
}
|
|
168
|
-
|
|
169
131
|
if (isEnabled) {
|
|
170
132
|
feature.register(router, options, context)
|
|
171
133
|
}
|
package/src/server/controller.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { getAppCatalogData } from '../modules/appCatalog/service'
|
|
2
2
|
import type { AppCatalogData } from '../types'
|
|
3
3
|
|
|
4
|
-
import { createAppCatalogAdminRouter } from '../modules/appCatalogAdmin/appCatalogAdminRouter.js'
|
|
5
|
-
import { createApprovalMethodRouter } from '../modules/approvalMethod/approvalMethodRouter.js'
|
|
6
|
-
import { createScreenshotRouter } from '../modules/assets/screenshotRouter.js'
|
|
7
4
|
import type { BetterAuth } from '../modules/auth/auth'
|
|
8
5
|
import { createAuthRouter } from '../modules/auth/authRouter.js'
|
|
9
|
-
import { createIconRouter } from '../modules/icons/iconRouter.js'
|
|
10
6
|
import { publicProcedure, router, t } from './trpcSetup'
|
|
11
7
|
|
|
12
8
|
/**
|
|
@@ -32,18 +28,6 @@ export function createTrpcRouter(auth?: BetterAuth) {
|
|
|
32
28
|
},
|
|
33
29
|
),
|
|
34
30
|
|
|
35
|
-
// Icon management routes
|
|
36
|
-
icon: createIconRouter(),
|
|
37
|
-
|
|
38
|
-
// Screenshot management routes
|
|
39
|
-
screenshot: createScreenshotRouter(),
|
|
40
|
-
|
|
41
|
-
// App catalog admin routes
|
|
42
|
-
appCatalogAdmin: createAppCatalogAdminRouter(),
|
|
43
|
-
|
|
44
|
-
// Approval method routes
|
|
45
|
-
approvalMethod: createApprovalMethodRouter(),
|
|
46
|
-
|
|
47
31
|
// Auth routes (requires auth instance)
|
|
48
32
|
auth: createAuthRouter(t, auth),
|
|
49
33
|
})
|
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod'
|
|
2
|
-
import { getDbClient } from '../../db'
|
|
3
|
-
import { adminProcedure, router } from '../../server/trpcSetup'
|
|
4
|
-
import type { AppAccessRequest } from '../../types'
|
|
5
|
-
|
|
6
|
-
// Zod schema for access method (simplified for now - you can expand this)
|
|
7
|
-
const AccessMethodSchema = z
|
|
8
|
-
.object({
|
|
9
|
-
type: z.enum([
|
|
10
|
-
'bot',
|
|
11
|
-
'ticketing',
|
|
12
|
-
'email',
|
|
13
|
-
'self-service',
|
|
14
|
-
'documentation',
|
|
15
|
-
'manual',
|
|
16
|
-
]),
|
|
17
|
-
})
|
|
18
|
-
.loose()
|
|
19
|
-
|
|
20
|
-
const AppLinkSchema = z.object({
|
|
21
|
-
displayName: z.string().optional(),
|
|
22
|
-
url: z.url(),
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
// New AppAccessRequest schema
|
|
26
|
-
const AppRoleSchema = z.object({
|
|
27
|
-
name: z.string(),
|
|
28
|
-
description: z.string().optional(),
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
const ApproverContactSchema = z.object({
|
|
32
|
-
displayName: z.string(),
|
|
33
|
-
contact: z.string().optional(),
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
const ApprovalUrlSchema = z.object({
|
|
37
|
-
label: z.string().optional(),
|
|
38
|
-
url: z.url(),
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
const AppAccessRequestSchema = z.object({
|
|
42
|
-
approvalMethodId: z.string(),
|
|
43
|
-
comments: z.string().optional(),
|
|
44
|
-
requestPrompt: z.string().optional(),
|
|
45
|
-
postApprovalInstructions: z.string().optional(),
|
|
46
|
-
roles: z.array(AppRoleSchema).optional(),
|
|
47
|
-
approvers: z.array(ApproverContactSchema).optional(),
|
|
48
|
-
urls: z.array(ApprovalUrlSchema).optional(),
|
|
49
|
-
whoToReachOut: z.string().optional(),
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
const CreateAppForCatalogSchema = z.object({
|
|
53
|
-
slug: z
|
|
54
|
-
.string()
|
|
55
|
-
.min(1)
|
|
56
|
-
.regex(/^[a-z0-9-]+$/, 'Slug must be lowercase alphanumeric with hyphens'),
|
|
57
|
-
displayName: z.string().min(1),
|
|
58
|
-
description: z.string(),
|
|
59
|
-
access: AccessMethodSchema.optional(),
|
|
60
|
-
teams: z.array(z.string()).optional(),
|
|
61
|
-
accessRequest: AppAccessRequestSchema.optional(),
|
|
62
|
-
notes: z.string().optional(),
|
|
63
|
-
tags: z.array(z.string()).optional(),
|
|
64
|
-
appUrl: z.url().optional(),
|
|
65
|
-
links: z.array(AppLinkSchema).optional(),
|
|
66
|
-
iconName: z.string().optional(),
|
|
67
|
-
screenshotIds: z.array(z.string()).optional(),
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
const UpdateAppForCatalogSchema = CreateAppForCatalogSchema.partial().extend({
|
|
71
|
-
id: z.string(),
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
export function createAppCatalogAdminRouter() {
|
|
75
|
-
const prisma = getDbClient()
|
|
76
|
-
return router({
|
|
77
|
-
list: adminProcedure.query(async () => {
|
|
78
|
-
return prisma.dbAppForCatalog.findMany({
|
|
79
|
-
orderBy: { displayName: 'asc' },
|
|
80
|
-
})
|
|
81
|
-
}),
|
|
82
|
-
|
|
83
|
-
getById: adminProcedure
|
|
84
|
-
.input(z.object({ id: z.string() }))
|
|
85
|
-
.query(async ({ input }) => {
|
|
86
|
-
return prisma.dbAppForCatalog.findUnique({
|
|
87
|
-
where: { id: input.id },
|
|
88
|
-
})
|
|
89
|
-
}),
|
|
90
|
-
|
|
91
|
-
getBySlug: adminProcedure
|
|
92
|
-
.input(z.object({ slug: z.string() }))
|
|
93
|
-
.query(async ({ input }) => {
|
|
94
|
-
return prisma.dbAppForCatalog.findUnique({
|
|
95
|
-
where: { slug: input.slug },
|
|
96
|
-
})
|
|
97
|
-
}),
|
|
98
|
-
|
|
99
|
-
create: adminProcedure
|
|
100
|
-
.input(CreateAppForCatalogSchema)
|
|
101
|
-
.mutation(async ({ input }) => {
|
|
102
|
-
// Type assertion needed because Zod's passthrough() creates index signatures
|
|
103
|
-
// that don't structurally match Prisma's typed JSON fields.
|
|
104
|
-
// This is safe because Zod validates the input shape.
|
|
105
|
-
return prisma.dbAppForCatalog.create({
|
|
106
|
-
data: {
|
|
107
|
-
slug: input.slug,
|
|
108
|
-
displayName: input.displayName,
|
|
109
|
-
description: input.description,
|
|
110
|
-
teams: input.teams ?? [],
|
|
111
|
-
accessRequest: input.accessRequest as AppAccessRequest | undefined,
|
|
112
|
-
notes: input.notes,
|
|
113
|
-
tags: input.tags ?? [],
|
|
114
|
-
appUrl: input.appUrl,
|
|
115
|
-
links: input.links as Array<{ displayName?: string; url: string }>,
|
|
116
|
-
iconName: input.iconName,
|
|
117
|
-
screenshotIds: input.screenshotIds ?? [],
|
|
118
|
-
},
|
|
119
|
-
})
|
|
120
|
-
}),
|
|
121
|
-
|
|
122
|
-
update: adminProcedure
|
|
123
|
-
.input(UpdateAppForCatalogSchema)
|
|
124
|
-
.mutation(async ({ input }) => {
|
|
125
|
-
const { id, ...updateData } = input
|
|
126
|
-
|
|
127
|
-
// Type assertion needed because Zod's passthrough() creates index signatures
|
|
128
|
-
return prisma.dbAppForCatalog.update({
|
|
129
|
-
where: { id },
|
|
130
|
-
data: {
|
|
131
|
-
...(updateData.slug !== undefined && { slug: updateData.slug }),
|
|
132
|
-
...(updateData.displayName !== undefined && {
|
|
133
|
-
displayName: updateData.displayName,
|
|
134
|
-
}),
|
|
135
|
-
...(updateData.description !== undefined && {
|
|
136
|
-
description: updateData.description,
|
|
137
|
-
}),
|
|
138
|
-
...(updateData.teams !== undefined && { teams: updateData.teams }),
|
|
139
|
-
...(updateData.accessRequest !== undefined && {
|
|
140
|
-
accessRequest: updateData.accessRequest as
|
|
141
|
-
| AppAccessRequest
|
|
142
|
-
| undefined,
|
|
143
|
-
}),
|
|
144
|
-
...(updateData.notes !== undefined && { notes: updateData.notes }),
|
|
145
|
-
...(updateData.tags !== undefined && { tags: updateData.tags }),
|
|
146
|
-
...(updateData.appUrl !== undefined && {
|
|
147
|
-
appUrl: updateData.appUrl,
|
|
148
|
-
}),
|
|
149
|
-
...(updateData.links !== undefined && {
|
|
150
|
-
links: updateData.links as Array<{
|
|
151
|
-
displayName?: string
|
|
152
|
-
url: string
|
|
153
|
-
}>,
|
|
154
|
-
}),
|
|
155
|
-
...(updateData.iconName !== undefined && {
|
|
156
|
-
iconName: updateData.iconName,
|
|
157
|
-
}),
|
|
158
|
-
...(updateData.screenshotIds !== undefined && {
|
|
159
|
-
screenshotIds: updateData.screenshotIds,
|
|
160
|
-
}),
|
|
161
|
-
},
|
|
162
|
-
})
|
|
163
|
-
}),
|
|
164
|
-
|
|
165
|
-
updateScreenshots: adminProcedure
|
|
166
|
-
.input(
|
|
167
|
-
z.object({
|
|
168
|
-
id: z.string(),
|
|
169
|
-
screenshotIds: z.array(z.string()),
|
|
170
|
-
}),
|
|
171
|
-
)
|
|
172
|
-
.mutation(async ({ input }) => {
|
|
173
|
-
return prisma.dbAppForCatalog.update({
|
|
174
|
-
where: { id: input.id },
|
|
175
|
-
data: { screenshotIds: input.screenshotIds },
|
|
176
|
-
})
|
|
177
|
-
}),
|
|
178
|
-
|
|
179
|
-
delete: adminProcedure
|
|
180
|
-
.input(z.object({ id: z.string() }))
|
|
181
|
-
.mutation(async ({ input }) => {
|
|
182
|
-
return prisma.dbAppForCatalog.delete({
|
|
183
|
-
where: { id: input.id },
|
|
184
|
-
})
|
|
185
|
-
}),
|
|
186
|
-
})
|
|
187
|
-
}
|
|
@@ -1,213 +0,0 @@
|
|
|
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 and DbApprovalMethod
|
|
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
|
-
// Fetch all approval methods
|
|
21
|
-
const approvalMethods = await prisma.dbApprovalMethod.findMany({
|
|
22
|
-
orderBy: { displayName: 'asc' },
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
res.json({
|
|
26
|
-
version: '2.0',
|
|
27
|
-
exportDate: new Date().toISOString(),
|
|
28
|
-
apps,
|
|
29
|
-
approvalMethods,
|
|
30
|
-
})
|
|
31
|
-
} catch (error) {
|
|
32
|
-
console.error('Error exporting catalog:', error)
|
|
33
|
-
res.status(500).json({ error: 'Failed to export catalog' })
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* Import/restore the complete app catalog from JSON
|
|
39
|
-
* Overwrites existing data
|
|
40
|
-
*/
|
|
41
|
-
export async function importCatalog(
|
|
42
|
-
req: Request,
|
|
43
|
-
res: Response,
|
|
44
|
-
): Promise<void> {
|
|
45
|
-
try {
|
|
46
|
-
const prisma = getDbClient()
|
|
47
|
-
const { apps, approvalMethods } = req.body
|
|
48
|
-
|
|
49
|
-
if (!Array.isArray(apps)) {
|
|
50
|
-
res
|
|
51
|
-
.status(400)
|
|
52
|
-
.json({ error: 'Invalid data format: apps must be an array' })
|
|
53
|
-
return
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Use transaction to ensure atomicity
|
|
57
|
-
await prisma.$transaction(async (tx) => {
|
|
58
|
-
// Delete all existing approval methods first (due to potential FK references)
|
|
59
|
-
if (Array.isArray(approvalMethods)) {
|
|
60
|
-
await tx.dbApprovalMethod.deleteMany({})
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// Delete all existing catalog entries
|
|
64
|
-
await tx.dbAppForCatalog.deleteMany({})
|
|
65
|
-
|
|
66
|
-
// Insert approval methods first
|
|
67
|
-
if (Array.isArray(approvalMethods)) {
|
|
68
|
-
for (const method of approvalMethods) {
|
|
69
|
-
// Remove id, createdAt, updatedAt to let Prisma generate new ones
|
|
70
|
-
const { id, createdAt, updatedAt, ...methodData } = method
|
|
71
|
-
|
|
72
|
-
await tx.dbApprovalMethod.create({
|
|
73
|
-
data: methodData,
|
|
74
|
-
})
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Insert app catalog entries
|
|
79
|
-
for (const app of apps) {
|
|
80
|
-
// Remove id, createdAt, updatedAt to let Prisma generate new ones
|
|
81
|
-
const { id, createdAt, updatedAt, ...appData } = app
|
|
82
|
-
|
|
83
|
-
await tx.dbAppForCatalog.create({
|
|
84
|
-
data: appData,
|
|
85
|
-
})
|
|
86
|
-
}
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
res.json({
|
|
90
|
-
success: true,
|
|
91
|
-
imported: {
|
|
92
|
-
apps: apps.length,
|
|
93
|
-
approvalMethods: Array.isArray(approvalMethods)
|
|
94
|
-
? approvalMethods.length
|
|
95
|
-
: 0,
|
|
96
|
-
},
|
|
97
|
-
})
|
|
98
|
-
} catch (error) {
|
|
99
|
-
console.error('Error importing catalog:', error)
|
|
100
|
-
res.status(500).json({ error: 'Failed to import catalog' })
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
/**
|
|
105
|
-
* Export an asset (icon or screenshot) by name
|
|
106
|
-
*/
|
|
107
|
-
export async function exportAsset(req: Request, res: Response): Promise<void> {
|
|
108
|
-
try {
|
|
109
|
-
const { name } = req.params
|
|
110
|
-
const prisma = getDbClient()
|
|
111
|
-
|
|
112
|
-
const asset = await prisma.dbAsset.findUnique({
|
|
113
|
-
where: { name },
|
|
114
|
-
})
|
|
115
|
-
|
|
116
|
-
if (!asset) {
|
|
117
|
-
res.status(404).json({ error: 'Asset not found' })
|
|
118
|
-
return
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// Set appropriate content type and send binary data
|
|
122
|
-
res.set('Content-Type', asset.mimeType)
|
|
123
|
-
res.set('Content-Disposition', `attachment; filename="${name}"`)
|
|
124
|
-
res.send(Buffer.from(asset.content))
|
|
125
|
-
} catch (error) {
|
|
126
|
-
console.error('Error exporting asset:', error)
|
|
127
|
-
res.status(500).json({ error: 'Failed to export asset' })
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* List all assets with metadata
|
|
133
|
-
*/
|
|
134
|
-
export async function listAssets(_req: Request, res: Response): Promise<void> {
|
|
135
|
-
try {
|
|
136
|
-
const prisma = getDbClient()
|
|
137
|
-
|
|
138
|
-
const assets = await prisma.dbAsset.findMany({
|
|
139
|
-
select: {
|
|
140
|
-
id: true,
|
|
141
|
-
name: true,
|
|
142
|
-
assetType: true,
|
|
143
|
-
mimeType: true,
|
|
144
|
-
fileSize: true,
|
|
145
|
-
width: true,
|
|
146
|
-
height: true,
|
|
147
|
-
checksum: true,
|
|
148
|
-
},
|
|
149
|
-
orderBy: { name: 'asc' },
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
res.json({ assets })
|
|
153
|
-
} catch (error) {
|
|
154
|
-
console.error('Error listing assets:', error)
|
|
155
|
-
res.status(500).json({ error: 'Failed to list assets' })
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Import an asset (icon or screenshot)
|
|
161
|
-
*/
|
|
162
|
-
export async function importAsset(req: Request, res: Response): Promise<void> {
|
|
163
|
-
try {
|
|
164
|
-
const file = req.file
|
|
165
|
-
const { name, assetType, mimeType, width, height } = req.body
|
|
166
|
-
|
|
167
|
-
if (!file) {
|
|
168
|
-
res.status(400).json({ error: 'No file uploaded' })
|
|
169
|
-
return
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const prisma = getDbClient()
|
|
173
|
-
const crypto = await import('node:crypto')
|
|
174
|
-
|
|
175
|
-
// Calculate checksum
|
|
176
|
-
const checksum = crypto
|
|
177
|
-
.createHash('sha256')
|
|
178
|
-
.update(file.buffer)
|
|
179
|
-
.digest('hex')
|
|
180
|
-
|
|
181
|
-
// Convert Buffer to Uint8Array for Prisma Bytes type
|
|
182
|
-
const content = new Uint8Array(file.buffer)
|
|
183
|
-
|
|
184
|
-
// Upsert asset (update if exists, create if not)
|
|
185
|
-
await prisma.dbAsset.upsert({
|
|
186
|
-
where: { name },
|
|
187
|
-
update: {
|
|
188
|
-
content,
|
|
189
|
-
checksum,
|
|
190
|
-
mimeType: mimeType || file.mimetype,
|
|
191
|
-
fileSize: file.size,
|
|
192
|
-
width: width ? parseInt(width) : null,
|
|
193
|
-
height: height ? parseInt(height) : null,
|
|
194
|
-
assetType: assetType || 'icon',
|
|
195
|
-
},
|
|
196
|
-
create: {
|
|
197
|
-
name,
|
|
198
|
-
content,
|
|
199
|
-
checksum,
|
|
200
|
-
mimeType: mimeType || file.mimetype,
|
|
201
|
-
fileSize: file.size,
|
|
202
|
-
width: width ? parseInt(width) : null,
|
|
203
|
-
height: height ? parseInt(height) : null,
|
|
204
|
-
assetType: assetType || 'icon',
|
|
205
|
-
},
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
res.json({ success: true, name, size: file.size })
|
|
209
|
-
} catch (error) {
|
|
210
|
-
console.error('Error importing asset:', error)
|
|
211
|
-
res.status(500).json({ error: 'Failed to import asset' })
|
|
212
|
-
}
|
|
213
|
-
}
|