@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "0.2.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-shared-core": "0.2.0",
52
- "@igstack/app-catalog-table-sync": "0.2.0"
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",
@@ -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
  }
@@ -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
- }