@env-hopper/backend-core 2.0.1-alpha.3 → 2.0.1-alpha.5
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 +22 -66
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +151 -89
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/middleware/createEhMiddleware.ts +11 -85
- package/src/middleware/featureRegistry.ts +41 -27
- package/src/middleware/index.ts +25 -0
- package/src/middleware/types.ts +25 -12
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +182 -0
- package/src/modules/icons/iconService.ts +1 -1
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.5",
|
|
4
4
|
"description": "Backend core library for Env Hopper",
|
|
5
5
|
"homepage": "https://github.com/lislon/env-hopper",
|
|
6
6
|
"repository": {
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
"tsyringe": "^4.10.0",
|
|
49
49
|
"yaml": "^2.8.0",
|
|
50
50
|
"zod": "^4.3.5",
|
|
51
|
-
"@env-hopper/table-sync": "2.0.1-alpha
|
|
51
|
+
"@env-hopper/table-sync": "2.0.1-alpha",
|
|
52
52
|
"@env-hopper/shared-core": "2.0.1-alpha.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import express, { Router } from 'express'
|
|
2
2
|
import * as trpcExpress from '@trpc/server/adapters/express'
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
EhMiddlewareOptions,
|
|
5
|
+
EhMiddlewareResult,
|
|
6
|
+
MiddlewareContext,
|
|
7
|
+
} from './types'
|
|
4
8
|
import { EhDatabaseManager } from './database'
|
|
5
9
|
import { createBackendResolver } from './backendResolver'
|
|
6
10
|
import { registerFeatures } from './featureRegistry'
|
|
@@ -8,61 +12,6 @@ import { createTrpcRouter } from '../server/controller'
|
|
|
8
12
|
import { createEhTrpcContext } from '../server/ehTrpcContext'
|
|
9
13
|
import { createAuth } from '../modules/auth/auth'
|
|
10
14
|
|
|
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
15
|
export async function createEhMiddleware(
|
|
67
16
|
options: EhMiddlewareOptions,
|
|
68
17
|
): Promise<EhMiddlewareResult> {
|
|
@@ -70,29 +19,10 @@ export async function createEhMiddleware(
|
|
|
70
19
|
const basePath = options.basePath ?? '/api'
|
|
71
20
|
const normalizedOptions = { ...options, basePath }
|
|
72
21
|
|
|
73
|
-
//
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
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
|
-
}
|
|
22
|
+
// Initialize database manager
|
|
23
|
+
const dbManager = new EhDatabaseManager(options.database)
|
|
24
|
+
// Initialize the client (which also sets the global singleton)
|
|
25
|
+
dbManager.getClient()
|
|
96
26
|
|
|
97
27
|
// Create auth instance
|
|
98
28
|
const auth = createAuth({
|
|
@@ -153,9 +83,7 @@ export async function createEhMiddleware(
|
|
|
153
83
|
trpcRouter,
|
|
154
84
|
|
|
155
85
|
async connect(): Promise<void> {
|
|
156
|
-
|
|
157
|
-
await dbManager.connect()
|
|
158
|
-
}
|
|
86
|
+
await dbManager.connect()
|
|
159
87
|
if (options.hooks?.onDatabaseConnected) {
|
|
160
88
|
await options.hooks.onDatabaseConnected()
|
|
161
89
|
}
|
|
@@ -165,9 +93,7 @@ export async function createEhMiddleware(
|
|
|
165
93
|
if (options.hooks?.onDatabaseDisconnecting) {
|
|
166
94
|
await options.hooks.onDatabaseDisconnecting()
|
|
167
95
|
}
|
|
168
|
-
|
|
169
|
-
await dbManager.disconnect()
|
|
170
|
-
}
|
|
96
|
+
await dbManager.disconnect()
|
|
171
97
|
},
|
|
172
98
|
|
|
173
99
|
addRoutes(callback: (router: Router) => void): void {
|
|
@@ -10,6 +10,14 @@ import { registerAssetRestController } from '../modules/assets/assetRestControll
|
|
|
10
10
|
import { registerScreenshotRestController } from '../modules/assets/screenshotRestController'
|
|
11
11
|
import { createAdminChatHandler } from '../modules/admin/chat/createAdminChatHandler'
|
|
12
12
|
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'
|
|
13
21
|
|
|
14
22
|
interface FeatureRegistration {
|
|
15
23
|
name: keyof EhFeatureToggles
|
|
@@ -22,6 +30,7 @@ interface FeatureRegistration {
|
|
|
22
30
|
) => void
|
|
23
31
|
}
|
|
24
32
|
|
|
33
|
+
// Optional features that can be toggled
|
|
25
34
|
const FEATURES: Array<FeatureRegistration> = [
|
|
26
35
|
{
|
|
27
36
|
name: 'auth',
|
|
@@ -51,33 +60,6 @@ const FEATURES: Array<FeatureRegistration> = [
|
|
|
51
60
|
router.all(`${basePath}/auth/{*any}`, authHandler)
|
|
52
61
|
},
|
|
53
62
|
},
|
|
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
63
|
{
|
|
82
64
|
name: 'adminChat',
|
|
83
65
|
defaultEnabled: false, // Only enabled if adminChat config is provided
|
|
@@ -132,6 +114,38 @@ export function registerFeatures(
|
|
|
132
114
|
EhMiddlewareOptions,
|
|
133
115
|
context: MiddlewareContext,
|
|
134
116
|
): void {
|
|
117
|
+
const basePath = options.basePath
|
|
118
|
+
|
|
119
|
+
// Always-on features (required for core functionality)
|
|
120
|
+
|
|
121
|
+
// Icons
|
|
122
|
+
registerIconRestController(router, {
|
|
123
|
+
basePath: `${basePath}/icons`,
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
// Assets
|
|
127
|
+
registerAssetRestController(router, {
|
|
128
|
+
basePath: `${basePath}/assets`,
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
// Screenshots
|
|
132
|
+
registerScreenshotRestController(router, {
|
|
133
|
+
basePath: `${basePath}/screenshots`,
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
// Catalog backup/restore
|
|
137
|
+
const upload = multer({ storage: multer.memoryStorage() })
|
|
138
|
+
router.get(`${basePath}/catalog/backup/export`, exportCatalog)
|
|
139
|
+
router.post(`${basePath}/catalog/backup/import`, importCatalog)
|
|
140
|
+
router.get(`${basePath}/catalog/backup/assets`, listAssets)
|
|
141
|
+
router.get(`${basePath}/catalog/backup/assets/:name`, exportAsset)
|
|
142
|
+
router.post(
|
|
143
|
+
`${basePath}/catalog/backup/assets`,
|
|
144
|
+
upload.single('file'),
|
|
145
|
+
importAsset,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
// Optional toggleable features
|
|
135
149
|
const toggles = options.features || {}
|
|
136
150
|
|
|
137
151
|
for (const feature of FEATURES) {
|
package/src/middleware/index.ts
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Middleware module for env-hopper backend integration.
|
|
3
|
+
*
|
|
4
|
+
* Provides a batteries-included middleware factory that handles all backend wiring:
|
|
5
|
+
* - Database connection management
|
|
6
|
+
* - Authentication setup
|
|
7
|
+
* - tRPC router configuration
|
|
8
|
+
* - Feature registration (icons, assets, screenshots, admin chat)
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* const eh = await createEhMiddleware({
|
|
13
|
+
* basePath: '/api',
|
|
14
|
+
* database: { host, port, database, username, password, schema },
|
|
15
|
+
* auth: { baseURL, secret, providers },
|
|
16
|
+
* backend: myBackendImplementation,
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* app.use(eh.router)
|
|
20
|
+
* await eh.connect()
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* @module middleware
|
|
24
|
+
*/
|
|
25
|
+
|
|
1
26
|
// Main middleware factory
|
|
2
27
|
export { createEhMiddleware } from './createEhMiddleware'
|
|
3
28
|
|
package/src/middleware/types.ts
CHANGED
|
@@ -57,19 +57,15 @@ export interface EhAdminChatConfig {
|
|
|
57
57
|
|
|
58
58
|
/**
|
|
59
59
|
* Feature toggles for enabling/disabling specific functionality.
|
|
60
|
-
*
|
|
60
|
+
*
|
|
61
|
+
* Note: Icons, assets, screenshots, and catalog backup are always enabled.
|
|
62
|
+
* Only these optional features can be toggled:
|
|
61
63
|
*/
|
|
62
64
|
export interface EhFeatureToggles {
|
|
63
65
|
/** Enable tRPC endpoints (default: true) */
|
|
64
66
|
trpc?: boolean
|
|
65
67
|
/** Enable auth endpoints (default: true) */
|
|
66
68
|
auth?: boolean
|
|
67
|
-
/** Enable icon REST endpoints (default: true) */
|
|
68
|
-
icons?: boolean
|
|
69
|
-
/** Enable asset REST endpoints (default: true) */
|
|
70
|
-
assets?: boolean
|
|
71
|
-
/** Enable screenshot REST endpoints (default: true) */
|
|
72
|
-
screenshots?: boolean
|
|
73
69
|
/** Enable admin chat endpoint (default: true if adminChat config provided) */
|
|
74
70
|
adminChat?: boolean
|
|
75
71
|
/** Enable legacy icon endpoint at /static/icon/:icon (default: false) */
|
|
@@ -117,11 +113,10 @@ export interface EhMiddlewareOptions {
|
|
|
117
113
|
basePath?: string
|
|
118
114
|
|
|
119
115
|
/**
|
|
120
|
-
* Database connection configuration.
|
|
121
|
-
*
|
|
122
|
-
* Can be omitted when these features are disabled and you manage your own database.
|
|
116
|
+
* Database connection configuration (required).
|
|
117
|
+
* Backend-core manages the database for all features.
|
|
123
118
|
*/
|
|
124
|
-
database
|
|
119
|
+
database: EhDatabaseConfig
|
|
125
120
|
|
|
126
121
|
/** Auth configuration (required) */
|
|
127
122
|
auth: EhAuthConfig
|
|
@@ -141,6 +136,22 @@ export interface EhMiddlewareOptions {
|
|
|
141
136
|
|
|
142
137
|
/**
|
|
143
138
|
* Result of middleware initialization.
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* ```typescript
|
|
142
|
+
* const eh = await createEhMiddleware({ ... })
|
|
143
|
+
*
|
|
144
|
+
* // Mount routes
|
|
145
|
+
* app.use(eh.router)
|
|
146
|
+
*
|
|
147
|
+
* // Connect to database
|
|
148
|
+
* await eh.connect()
|
|
149
|
+
*
|
|
150
|
+
* // Cleanup on shutdown
|
|
151
|
+
* process.on('SIGTERM', async () => {
|
|
152
|
+
* await eh.disconnect()
|
|
153
|
+
* })
|
|
154
|
+
* ```
|
|
144
155
|
*/
|
|
145
156
|
export interface EhMiddlewareResult {
|
|
146
157
|
/** Express router with all env-hopper routes */
|
|
@@ -163,5 +174,7 @@ export interface EhMiddlewareResult {
|
|
|
163
174
|
export interface MiddlewareContext {
|
|
164
175
|
auth: BetterAuth
|
|
165
176
|
trpcRouter: TRPCRouter
|
|
166
|
-
createContext: () => Promise<{
|
|
177
|
+
createContext: () => Promise<{
|
|
178
|
+
companySpecificBackend: EhBackendCompanySpecificBackend
|
|
179
|
+
}>
|
|
167
180
|
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import type { Request, Response } from 'express'
|
|
2
|
+
import { getDbClient } from '../../db'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Export the complete app catalog as JSON
|
|
6
|
+
* Includes all fields from DbAppForCatalog
|
|
7
|
+
*/
|
|
8
|
+
export async function exportCatalog(
|
|
9
|
+
_req: Request,
|
|
10
|
+
res: Response,
|
|
11
|
+
): Promise<void> {
|
|
12
|
+
try {
|
|
13
|
+
const prisma = getDbClient()
|
|
14
|
+
|
|
15
|
+
// Fetch all catalog entries
|
|
16
|
+
const apps = await prisma.dbAppForCatalog.findMany({
|
|
17
|
+
orderBy: { slug: 'asc' },
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
res.json({
|
|
21
|
+
version: '1.0',
|
|
22
|
+
exportDate: new Date().toISOString(),
|
|
23
|
+
apps,
|
|
24
|
+
})
|
|
25
|
+
} catch (error) {
|
|
26
|
+
console.error('Error exporting catalog:', error)
|
|
27
|
+
res.status(500).json({ error: 'Failed to export catalog' })
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Import/restore the complete app catalog from JSON
|
|
33
|
+
* Overwrites existing data
|
|
34
|
+
*/
|
|
35
|
+
export async function importCatalog(
|
|
36
|
+
req: Request,
|
|
37
|
+
res: Response,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
try {
|
|
40
|
+
const prisma = getDbClient()
|
|
41
|
+
const { apps } = req.body
|
|
42
|
+
|
|
43
|
+
if (!Array.isArray(apps)) {
|
|
44
|
+
res
|
|
45
|
+
.status(400)
|
|
46
|
+
.json({ error: 'Invalid data format: apps must be an array' })
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Use transaction to ensure atomicity
|
|
51
|
+
await prisma.$transaction(async (tx) => {
|
|
52
|
+
// Delete all existing catalog entries
|
|
53
|
+
await tx.dbAppForCatalog.deleteMany({})
|
|
54
|
+
|
|
55
|
+
// Insert new entries
|
|
56
|
+
for (const app of apps) {
|
|
57
|
+
// Remove id, createdAt, updatedAt to let Prisma generate new ones
|
|
58
|
+
const { id, createdAt, updatedAt, ...appData } = app
|
|
59
|
+
|
|
60
|
+
await tx.dbAppForCatalog.create({
|
|
61
|
+
data: appData,
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
res.json({ success: true, imported: apps.length })
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error('Error importing catalog:', error)
|
|
69
|
+
res.status(500).json({ error: 'Failed to import catalog' })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Export an asset (icon or screenshot) by name
|
|
75
|
+
*/
|
|
76
|
+
export async function exportAsset(req: Request, res: Response): Promise<void> {
|
|
77
|
+
try {
|
|
78
|
+
const { name } = req.params
|
|
79
|
+
const prisma = getDbClient()
|
|
80
|
+
|
|
81
|
+
const asset = await prisma.dbAsset.findUnique({
|
|
82
|
+
where: { name },
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
if (!asset) {
|
|
86
|
+
res.status(404).json({ error: 'Asset not found' })
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Set appropriate content type and send binary data
|
|
91
|
+
res.set('Content-Type', asset.mimeType)
|
|
92
|
+
res.set('Content-Disposition', `attachment; filename="${name}"`)
|
|
93
|
+
res.send(Buffer.from(asset.content))
|
|
94
|
+
} catch (error) {
|
|
95
|
+
console.error('Error exporting asset:', error)
|
|
96
|
+
res.status(500).json({ error: 'Failed to export asset' })
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* List all assets with metadata
|
|
102
|
+
*/
|
|
103
|
+
export async function listAssets(_req: Request, res: Response): Promise<void> {
|
|
104
|
+
try {
|
|
105
|
+
const prisma = getDbClient()
|
|
106
|
+
|
|
107
|
+
const assets = await prisma.dbAsset.findMany({
|
|
108
|
+
select: {
|
|
109
|
+
id: true,
|
|
110
|
+
name: true,
|
|
111
|
+
assetType: true,
|
|
112
|
+
mimeType: true,
|
|
113
|
+
fileSize: true,
|
|
114
|
+
width: true,
|
|
115
|
+
height: true,
|
|
116
|
+
checksum: true,
|
|
117
|
+
},
|
|
118
|
+
orderBy: { name: 'asc' },
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
res.json({ assets })
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('Error listing assets:', error)
|
|
124
|
+
res.status(500).json({ error: 'Failed to list assets' })
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Import an asset (icon or screenshot)
|
|
130
|
+
*/
|
|
131
|
+
export async function importAsset(req: Request, res: Response): Promise<void> {
|
|
132
|
+
try {
|
|
133
|
+
const file = req.file
|
|
134
|
+
const { name, assetType, mimeType, width, height } = req.body
|
|
135
|
+
|
|
136
|
+
if (!file) {
|
|
137
|
+
res.status(400).json({ error: 'No file uploaded' })
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const prisma = getDbClient()
|
|
142
|
+
const crypto = await import('node:crypto')
|
|
143
|
+
|
|
144
|
+
// Calculate checksum
|
|
145
|
+
const checksum = crypto
|
|
146
|
+
.createHash('sha256')
|
|
147
|
+
.update(file.buffer)
|
|
148
|
+
.digest('hex')
|
|
149
|
+
|
|
150
|
+
// Convert Buffer to Uint8Array for Prisma Bytes type
|
|
151
|
+
const content = new Uint8Array(file.buffer)
|
|
152
|
+
|
|
153
|
+
// Upsert asset (update if exists, create if not)
|
|
154
|
+
await prisma.dbAsset.upsert({
|
|
155
|
+
where: { name },
|
|
156
|
+
update: {
|
|
157
|
+
content,
|
|
158
|
+
checksum,
|
|
159
|
+
mimeType: mimeType || file.mimetype,
|
|
160
|
+
fileSize: file.size,
|
|
161
|
+
width: width ? parseInt(width) : null,
|
|
162
|
+
height: height ? parseInt(height) : null,
|
|
163
|
+
assetType: assetType || 'icon',
|
|
164
|
+
},
|
|
165
|
+
create: {
|
|
166
|
+
name,
|
|
167
|
+
content,
|
|
168
|
+
checksum,
|
|
169
|
+
mimeType: mimeType || file.mimetype,
|
|
170
|
+
fileSize: file.size,
|
|
171
|
+
width: width ? parseInt(width) : null,
|
|
172
|
+
height: height ? parseInt(height) : null,
|
|
173
|
+
assetType: assetType || 'icon',
|
|
174
|
+
},
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
res.json({ success: true, name, size: file.size })
|
|
178
|
+
} catch (error) {
|
|
179
|
+
console.error('Error importing asset:', error)
|
|
180
|
+
res.status(500).json({ error: 'Failed to import asset' })
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -47,7 +47,7 @@ export async function upsertIcon(input: UpsertIconInput) {
|
|
|
47
47
|
* This is more efficient than calling upsertIcon multiple times.
|
|
48
48
|
*/
|
|
49
49
|
export async function upsertIcons(icons: Array<UpsertIconInput>) {
|
|
50
|
-
const results = []
|
|
50
|
+
const results: Array<Awaited<ReturnType<typeof upsertIcon>>> = []
|
|
51
51
|
for (const icon of icons) {
|
|
52
52
|
const result = await upsertIcon(icon)
|
|
53
53
|
results.push(result)
|