@igstack/app-catalog-backend-core 0.0.1
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/LICENSE +21 -0
- package/dist/index.d.ts +1934 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2539 -0
- package/dist/index.js.map +1 -0
- package/package.json +84 -0
- package/prisma/migrations/20250526183023_init/migration.sql +71 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +149 -0
- package/src/__tests__/dummy.test.ts +7 -0
- package/src/db/client.ts +42 -0
- package/src/db/index.ts +21 -0
- package/src/db/syncAppCatalog.ts +312 -0
- package/src/db/tableSyncMagazine.ts +32 -0
- package/src/db/tableSyncPrismaAdapter.ts +203 -0
- package/src/index.ts +126 -0
- package/src/middleware/backendResolver.ts +42 -0
- package/src/middleware/createEhMiddleware.ts +171 -0
- package/src/middleware/database.ts +62 -0
- package/src/middleware/featureRegistry.ts +173 -0
- package/src/middleware/index.ts +43 -0
- package/src/middleware/types.ts +202 -0
- package/src/modules/admin/chat/createAdminChatHandler.ts +152 -0
- package/src/modules/admin/chat/createDatabaseTools.ts +261 -0
- package/src/modules/appCatalog/service.ts +130 -0
- package/src/modules/appCatalogAdmin/appCatalogAdminRouter.ts +187 -0
- package/src/modules/appCatalogAdmin/catalogBackupController.ts +213 -0
- package/src/modules/approvalMethod/approvalMethodRouter.ts +169 -0
- package/src/modules/approvalMethod/slugUtils.ts +17 -0
- package/src/modules/approvalMethod/syncApprovalMethods.ts +38 -0
- package/src/modules/assets/assetRestController.ts +271 -0
- package/src/modules/assets/assetUtils.ts +114 -0
- package/src/modules/assets/screenshotRestController.ts +195 -0
- package/src/modules/assets/screenshotRouter.ts +112 -0
- package/src/modules/assets/syncAssets.ts +277 -0
- package/src/modules/assets/upsertAsset.ts +46 -0
- package/src/modules/auth/auth.ts +51 -0
- package/src/modules/auth/authProviders.ts +40 -0
- package/src/modules/auth/authRouter.ts +75 -0
- package/src/modules/auth/authorizationUtils.ts +132 -0
- package/src/modules/auth/devMockUserUtils.ts +49 -0
- package/src/modules/auth/registerAuthRoutes.ts +33 -0
- package/src/modules/icons/iconRestController.ts +171 -0
- package/src/modules/icons/iconRouter.ts +180 -0
- package/src/modules/icons/iconService.ts +73 -0
- package/src/modules/icons/iconUtils.ts +46 -0
- package/src/prisma-json-types.d.ts +34 -0
- package/src/server/controller.ts +47 -0
- package/src/server/ehStaticControllerContract.ts +19 -0
- package/src/server/ehTrpcContext.ts +26 -0
- package/src/server/trpcSetup.ts +89 -0
- package/src/types/backend/api.ts +73 -0
- package/src/types/backend/common.ts +10 -0
- package/src/types/backend/companySpecificBackend.ts +5 -0
- package/src/types/backend/dataSources.ts +25 -0
- package/src/types/backend/deployments.ts +40 -0
- package/src/types/common/app/appTypes.ts +13 -0
- package/src/types/common/app/ui/appUiTypes.ts +12 -0
- package/src/types/common/appCatalogTypes.ts +65 -0
- package/src/types/common/approvalMethodTypes.ts +149 -0
- package/src/types/common/env/envTypes.ts +7 -0
- package/src/types/common/resourceTypes.ts +8 -0
- package/src/types/common/sharedTypes.ts +5 -0
- package/src/types/index.ts +21 -0
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Prisma schema for backend-core library
|
|
2
|
+
// Uses AC_CORE_DATABASE_URL environment variable
|
|
3
|
+
|
|
4
|
+
datasource db {
|
|
5
|
+
provider = "postgresql"
|
|
6
|
+
url = env("AC_CORE_DATABASE_URL")
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
generator client {
|
|
10
|
+
provider = "prisma-client-js"
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
generator json {
|
|
14
|
+
provider = "prisma-json-types-generator"
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ========== Better Auth Models ==========
|
|
18
|
+
model user {
|
|
19
|
+
id String @id
|
|
20
|
+
name String?
|
|
21
|
+
email String? @unique
|
|
22
|
+
emailVerified Boolean @default(false)
|
|
23
|
+
image String?
|
|
24
|
+
role String @default("user") // 'user' | 'admin'
|
|
25
|
+
createdAt DateTime @default(now())
|
|
26
|
+
updatedAt DateTime @updatedAt
|
|
27
|
+
accounts account[]
|
|
28
|
+
sessions session[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
model session {
|
|
32
|
+
id String @id
|
|
33
|
+
token String @unique
|
|
34
|
+
userId String
|
|
35
|
+
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
36
|
+
expiresAt DateTime
|
|
37
|
+
ipAddress String?
|
|
38
|
+
userAgent String?
|
|
39
|
+
createdAt DateTime @default(now())
|
|
40
|
+
updatedAt DateTime @updatedAt
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
model account {
|
|
44
|
+
id String @id
|
|
45
|
+
userId String
|
|
46
|
+
user user @relation(fields: [userId], references: [id], onDelete: Cascade)
|
|
47
|
+
accountId String
|
|
48
|
+
providerId String
|
|
49
|
+
refreshToken String?
|
|
50
|
+
accessToken String?
|
|
51
|
+
accessTokenExpiresAt DateTime?
|
|
52
|
+
refreshTokenExpiresAt DateTime?
|
|
53
|
+
idToken String?
|
|
54
|
+
scope String?
|
|
55
|
+
password String?
|
|
56
|
+
createdAt DateTime @default(now())
|
|
57
|
+
updatedAt DateTime @updatedAt
|
|
58
|
+
|
|
59
|
+
@@unique([providerId, accountId])
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
model verification {
|
|
63
|
+
id String @id
|
|
64
|
+
identifier String
|
|
65
|
+
value String
|
|
66
|
+
expiresAt DateTime
|
|
67
|
+
createdAt DateTime @default(now())
|
|
68
|
+
updatedAt DateTime @updatedAt
|
|
69
|
+
|
|
70
|
+
@@unique([identifier, value])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// ========== EnvHopper Models ==========
|
|
74
|
+
|
|
75
|
+
enum ApprovalMethodType {
|
|
76
|
+
service
|
|
77
|
+
personTeam
|
|
78
|
+
custom
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
model DbApprovalMethod {
|
|
82
|
+
slug String @id
|
|
83
|
+
type ApprovalMethodType
|
|
84
|
+
displayName String
|
|
85
|
+
/// [ApprovalMethodConfig] - Type-specific configuration
|
|
86
|
+
config Json?
|
|
87
|
+
createdAt DateTime @default(now())
|
|
88
|
+
updatedAt DateTime @updatedAt
|
|
89
|
+
|
|
90
|
+
@@unique([type, displayName])
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
model DbAppForCatalog {
|
|
94
|
+
id String @id @default(cuid())
|
|
95
|
+
slug String @unique // URL-friendly identifier for navigation
|
|
96
|
+
displayName String
|
|
97
|
+
description String
|
|
98
|
+
/// [AccessMethod]
|
|
99
|
+
access Json?
|
|
100
|
+
teams String[] @default([])
|
|
101
|
+
/// [AppAccessRequest] - Per-app approval configuration linking to ApprovalMethod
|
|
102
|
+
accessRequest Json?
|
|
103
|
+
notes String?
|
|
104
|
+
tags String[] @default([])
|
|
105
|
+
appUrl String?
|
|
106
|
+
/// [AppLink[]]
|
|
107
|
+
links Json? // Array of {displayName?: string, url: string}
|
|
108
|
+
iconName String? // Optional icon identifier matching DbAsset.name
|
|
109
|
+
screenshotIds String[] @default([]) // Ordered array of DbAsset IDs
|
|
110
|
+
createdAt DateTime @default(now())
|
|
111
|
+
updatedAt DateTime @updatedAt
|
|
112
|
+
|
|
113
|
+
@@index([displayName])
|
|
114
|
+
@@index([slug])
|
|
115
|
+
@@index([tags])
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
model DbAppTagDefinition {
|
|
119
|
+
id String @id @default(cuid())
|
|
120
|
+
prefix String @unique
|
|
121
|
+
displayName String
|
|
122
|
+
description String
|
|
123
|
+
/// [GroupingTagValue[]]
|
|
124
|
+
values Json
|
|
125
|
+
createdAt DateTime @default(now())
|
|
126
|
+
updatedAt DateTime @updatedAt
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
enum AssetType {
|
|
130
|
+
icon
|
|
131
|
+
screenshot
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
model DbAsset {
|
|
135
|
+
id String @id @default(cuid())
|
|
136
|
+
name String @unique // Name with extension (e.g., "jira.svg", "github.png")
|
|
137
|
+
assetType AssetType @default(icon)
|
|
138
|
+
content Bytes // Binary storage for all asset types (SVG, PNG, etc)
|
|
139
|
+
checksum String // SHA-256 of content for deduplication
|
|
140
|
+
mimeType String // e.g., "image/svg+xml", "image/png", "image/jpeg"
|
|
141
|
+
fileSize Int // Size in bytes
|
|
142
|
+
width Int? // Image width in pixels
|
|
143
|
+
height Int? // Image height in pixels
|
|
144
|
+
createdAt DateTime @default(now())
|
|
145
|
+
updatedAt DateTime @updatedAt
|
|
146
|
+
|
|
147
|
+
@@index([name])
|
|
148
|
+
@@index([assetType])
|
|
149
|
+
}
|
package/src/db/client.ts
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client'
|
|
2
|
+
|
|
3
|
+
let prismaClient: PrismaClient | null = null
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gets the internal Prisma client instance.
|
|
7
|
+
* Creates one if it doesn't exist.
|
|
8
|
+
*/
|
|
9
|
+
export function getDbClient(): PrismaClient {
|
|
10
|
+
if (!prismaClient) {
|
|
11
|
+
prismaClient = new PrismaClient()
|
|
12
|
+
}
|
|
13
|
+
return prismaClient
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Sets the internal Prisma client instance.
|
|
18
|
+
* Used by middleware to bridge with existing getDbClient() usage.
|
|
19
|
+
*/
|
|
20
|
+
export function setDbClient(client: PrismaClient): void {
|
|
21
|
+
prismaClient = client
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Connects to the database.
|
|
26
|
+
* Call this before performing database operations.
|
|
27
|
+
*/
|
|
28
|
+
export async function connectDb(): Promise<void> {
|
|
29
|
+
const client = getDbClient()
|
|
30
|
+
await client.$connect()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Disconnects from the database.
|
|
35
|
+
* Call this when done with database operations (e.g., in scripts).
|
|
36
|
+
*/
|
|
37
|
+
export async function disconnectDb(): Promise<void> {
|
|
38
|
+
if (prismaClient) {
|
|
39
|
+
await prismaClient.$disconnect()
|
|
40
|
+
prismaClient = null
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/db/index.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Database connection
|
|
2
|
+
export { connectDb, disconnectDb, getDbClient, setDbClient } from './client'
|
|
3
|
+
|
|
4
|
+
// Table sync utilities
|
|
5
|
+
export {
|
|
6
|
+
tableSyncPrisma,
|
|
7
|
+
type MakeTFromPrismaModel,
|
|
8
|
+
type ObjectKeys,
|
|
9
|
+
type ScalarFilter,
|
|
10
|
+
type ScalarKeys,
|
|
11
|
+
type TableSyncParamsPrisma,
|
|
12
|
+
} from './tableSyncPrismaAdapter'
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
TABLE_SYNC_MAGAZINE,
|
|
16
|
+
type TableSyncMagazine,
|
|
17
|
+
type TableSyncMagazineModelNameKey,
|
|
18
|
+
} from './tableSyncMagazine'
|
|
19
|
+
|
|
20
|
+
// App catalog sync
|
|
21
|
+
export { syncAppCatalog, type SyncAppCatalogResult } from './syncAppCatalog'
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
AppForCatalog,
|
|
3
|
+
GroupingTagDefinition,
|
|
4
|
+
} from '../types/common/appCatalogTypes'
|
|
5
|
+
import { getDbClient } from './client'
|
|
6
|
+
import { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'
|
|
7
|
+
import { tableSyncPrisma } from './tableSyncPrismaAdapter'
|
|
8
|
+
import { readFile, readdir, stat } from 'node:fs/promises'
|
|
9
|
+
import { group } from 'radashi'
|
|
10
|
+
import { upsertAsset } from '../modules/assets/upsertAsset'
|
|
11
|
+
import type { ApprovalMethod } from '../types'
|
|
12
|
+
import type { PrismaClient } from '@prisma/client'
|
|
13
|
+
|
|
14
|
+
export interface SyncAppCatalogResult {
|
|
15
|
+
created: number
|
|
16
|
+
updated: number
|
|
17
|
+
deleted: number
|
|
18
|
+
total: number
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface AssetSyncResult {
|
|
22
|
+
screenshotIds: Array<string>
|
|
23
|
+
iconName: string | null
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function isFileNotFoundError(error: unknown): boolean {
|
|
27
|
+
return (
|
|
28
|
+
error instanceof Error &&
|
|
29
|
+
'code' in error &&
|
|
30
|
+
(error as NodeJS.ErrnoException).code === 'ENOENT'
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function processAssetDirectory(
|
|
35
|
+
dirPath: string,
|
|
36
|
+
appSlug: string,
|
|
37
|
+
assetType: 'screenshot' | 'icon',
|
|
38
|
+
prisma: PrismaClient,
|
|
39
|
+
): Promise<Array<string>> {
|
|
40
|
+
try {
|
|
41
|
+
const files = await readdir(dirPath)
|
|
42
|
+
const assetIds: Array<string> = []
|
|
43
|
+
|
|
44
|
+
for (let i = 0; i < files.length; i++) {
|
|
45
|
+
const fileName = files[i]
|
|
46
|
+
if (!fileName) continue
|
|
47
|
+
|
|
48
|
+
const assetName =
|
|
49
|
+
assetType === 'screenshot'
|
|
50
|
+
? `${appSlug}-screenshot-${i + 1}`
|
|
51
|
+
: `${appSlug}-icon`
|
|
52
|
+
|
|
53
|
+
const id = await upsertAsset({
|
|
54
|
+
prisma,
|
|
55
|
+
buffer: await readFile(`${dirPath}/${fileName}`),
|
|
56
|
+
originalFilename: fileName,
|
|
57
|
+
name: assetName,
|
|
58
|
+
assetType,
|
|
59
|
+
})
|
|
60
|
+
assetIds.push(id)
|
|
61
|
+
|
|
62
|
+
// For icons, only process the first file
|
|
63
|
+
if (assetType === 'icon') {
|
|
64
|
+
break
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return assetIds
|
|
69
|
+
} catch (error: unknown) {
|
|
70
|
+
if (isFileNotFoundError(error)) {
|
|
71
|
+
return []
|
|
72
|
+
}
|
|
73
|
+
throw error
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function syncAppAssets(
|
|
78
|
+
appSlug: string,
|
|
79
|
+
appPath: string,
|
|
80
|
+
prisma: PrismaClient,
|
|
81
|
+
): Promise<AssetSyncResult> {
|
|
82
|
+
const screenshotIds = await processAssetDirectory(
|
|
83
|
+
`${appPath}/screenshots`,
|
|
84
|
+
appSlug,
|
|
85
|
+
'screenshot',
|
|
86
|
+
prisma,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
const iconIds = await processAssetDirectory(
|
|
90
|
+
`${appPath}/icons`,
|
|
91
|
+
appSlug,
|
|
92
|
+
'icon',
|
|
93
|
+
prisma,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
screenshotIds,
|
|
98
|
+
iconName: iconIds.length > 0 ? `${appSlug}-icon` : null,
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function syncAssetsFromFileSystem(
|
|
103
|
+
apps: Array<AppForCatalog>,
|
|
104
|
+
allAppsAssetsPath: string,
|
|
105
|
+
) {
|
|
106
|
+
const appDirectories = await readdir(allAppsAssetsPath)
|
|
107
|
+
const prisma = getDbClient()
|
|
108
|
+
const bySlug = group(apps, (a) => a.slug)
|
|
109
|
+
|
|
110
|
+
for (const appDirName of appDirectories) {
|
|
111
|
+
try {
|
|
112
|
+
const stats = await stat(`${allAppsAssetsPath}/${appDirName}`)
|
|
113
|
+
if (!stats.isDirectory()) {
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
} catch (error: unknown) {
|
|
117
|
+
if (isFileNotFoundError(error)) {
|
|
118
|
+
continue
|
|
119
|
+
}
|
|
120
|
+
throw error
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const appSlug = appDirName
|
|
124
|
+
if (!bySlug[appSlug]) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
`App '${appSlug}' does not exist in the app catalog. Existing apps: ${Object.keys(bySlug).join(', ')}`,
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const { screenshotIds, iconName } = await syncAppAssets(
|
|
132
|
+
appSlug,
|
|
133
|
+
`${allAppsAssetsPath}/${appDirName}`,
|
|
134
|
+
prisma,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
const updateData: {
|
|
138
|
+
screenshotIds?: Array<string>
|
|
139
|
+
iconName?: string | null
|
|
140
|
+
} = {}
|
|
141
|
+
|
|
142
|
+
if (screenshotIds.length > 0) {
|
|
143
|
+
updateData.screenshotIds = screenshotIds
|
|
144
|
+
}
|
|
145
|
+
if (iconName !== null) {
|
|
146
|
+
updateData.iconName = iconName
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (Object.keys(updateData).length > 0) {
|
|
150
|
+
await prisma.dbAppForCatalog.update({
|
|
151
|
+
where: { slug: appSlug },
|
|
152
|
+
data: updateData,
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
} catch (error: unknown) {
|
|
156
|
+
const errorMessage =
|
|
157
|
+
error instanceof Error ? error.message : String(error)
|
|
158
|
+
throw new Error(
|
|
159
|
+
`Error while upserting assets for app '${appSlug}': ${errorMessage}`,
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
// private async syncScreenshots(apps: AppForCatalog[], screenshotsPath: string): Promise<void> {
|
|
164
|
+
// const db = getDbClient();
|
|
165
|
+
//
|
|
166
|
+
// for (const app of apps) {
|
|
167
|
+
// const dirPath = join(screenshotsPath, `${app.slug}-screenshots`);
|
|
168
|
+
//
|
|
169
|
+
// try {
|
|
170
|
+
// const files = await readdir(dirPath);
|
|
171
|
+
// const imageFiles = files.filter(f => /\.(png|jpg|jpeg|gif|webp)$/i.test(f)).sort();
|
|
172
|
+
//
|
|
173
|
+
// if (imageFiles.length === 0) continue;
|
|
174
|
+
//
|
|
175
|
+
// const screenshotIds: string[] = [];
|
|
176
|
+
//
|
|
177
|
+
// for (const filename of imageFiles) {
|
|
178
|
+
// const filePath = join(dirPath, filename);
|
|
179
|
+
// const content = await readFile(filePath);
|
|
180
|
+
// const buffer = Buffer.from(content);
|
|
181
|
+
// const checksum = generateChecksum(buffer);
|
|
182
|
+
// const { width, height } = await getImageDimensions(buffer);
|
|
183
|
+
//
|
|
184
|
+
// const ext = filename.split('.').pop()?.toLowerCase();
|
|
185
|
+
// const mimeType = {
|
|
186
|
+
// png: 'image/png',
|
|
187
|
+
// jpg: 'image/jpeg',
|
|
188
|
+
// jpeg: 'image/jpeg',
|
|
189
|
+
// gif: 'image/gif',
|
|
190
|
+
// webp: 'image/webp',
|
|
191
|
+
// }[ext || ''] || 'application/octet-stream';
|
|
192
|
+
//
|
|
193
|
+
// const asset = await db.dbAsset.upsert({
|
|
194
|
+
// where: { checksum },
|
|
195
|
+
// create: {
|
|
196
|
+
// name: `${app.slug}/${filename}`,
|
|
197
|
+
// assetType: 'screenshot',
|
|
198
|
+
// content: new Uint8Array(buffer),
|
|
199
|
+
// checksum,
|
|
200
|
+
// mimeType,
|
|
201
|
+
// fileSize: buffer.length,
|
|
202
|
+
// width,
|
|
203
|
+
// height,
|
|
204
|
+
// },
|
|
205
|
+
// update: {
|
|
206
|
+
// name: `${app.slug}/${filename}`,
|
|
207
|
+
// content: new Uint8Array(buffer),
|
|
208
|
+
// mimeType,
|
|
209
|
+
// fileSize: buffer.length,
|
|
210
|
+
// width,
|
|
211
|
+
// height,
|
|
212
|
+
// },
|
|
213
|
+
// });
|
|
214
|
+
//
|
|
215
|
+
// screenshotIds.push(asset.id);
|
|
216
|
+
// }
|
|
217
|
+
//
|
|
218
|
+
// await db.dbAppForCatalog.update({
|
|
219
|
+
// where: { slug: app.slug },
|
|
220
|
+
// data: { screenshotIds },
|
|
221
|
+
// });
|
|
222
|
+
//
|
|
223
|
+
// logger.info(`Synced ${screenshotIds.length} screenshots for ${app.slug}`);
|
|
224
|
+
// } catch (error: any) {
|
|
225
|
+
// if (error.code === 'ENOENT') continue; // Skip if directory doesn't exist
|
|
226
|
+
// throw error;
|
|
227
|
+
// }
|
|
228
|
+
// }
|
|
229
|
+
// }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Syncs app catalog data to the database using table sync.
|
|
234
|
+
* This will create new apps, update existing ones, and delete any that are no longer in the input.
|
|
235
|
+
*
|
|
236
|
+
* Note: Call connectDb() before and disconnectDb() after if running in a script.
|
|
237
|
+
*/
|
|
238
|
+
export async function syncAppCatalog(
|
|
239
|
+
apps: Array<AppForCatalog>,
|
|
240
|
+
tagsDefinitions: Array<GroupingTagDefinition>,
|
|
241
|
+
approvalMethods: Array<ApprovalMethod>,
|
|
242
|
+
sreenshotsPath?: string,
|
|
243
|
+
): Promise<SyncAppCatalogResult> {
|
|
244
|
+
try {
|
|
245
|
+
const prisma = getDbClient()
|
|
246
|
+
|
|
247
|
+
await tableSyncPrisma({
|
|
248
|
+
prisma,
|
|
249
|
+
...TABLE_SYNC_MAGAZINE.DbApprovalMethod,
|
|
250
|
+
}).sync(approvalMethods)
|
|
251
|
+
|
|
252
|
+
const sync = tableSyncPrisma({
|
|
253
|
+
prisma,
|
|
254
|
+
...TABLE_SYNC_MAGAZINE.DbAppForCatalog,
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
await tableSyncPrisma({
|
|
258
|
+
prisma,
|
|
259
|
+
...TABLE_SYNC_MAGAZINE.DbAppTagDefinition,
|
|
260
|
+
}).sync(tagsDefinitions)
|
|
261
|
+
|
|
262
|
+
// Transform AppForCatalog to DbAppForCatalog format
|
|
263
|
+
const dbApps = apps.map((app) => {
|
|
264
|
+
const slug =
|
|
265
|
+
app.slug ||
|
|
266
|
+
app.displayName
|
|
267
|
+
.toLowerCase()
|
|
268
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
269
|
+
.replace(/^-+|-+$/g, '')
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
slug,
|
|
273
|
+
displayName: app.displayName,
|
|
274
|
+
description: app.description,
|
|
275
|
+
teams: app.teams ?? [],
|
|
276
|
+
accessRequest: app.accessRequest ?? null,
|
|
277
|
+
notes: app.notes ?? null,
|
|
278
|
+
tags: app.tags ?? [],
|
|
279
|
+
appUrl: app.appUrl ?? null,
|
|
280
|
+
links: app.links ?? null,
|
|
281
|
+
iconName: app.iconName ?? null,
|
|
282
|
+
screenshotIds: app.screenshotIds ?? [],
|
|
283
|
+
}
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
const result = await sync.sync(dbApps)
|
|
287
|
+
|
|
288
|
+
// Get actual synced data to calculate stats
|
|
289
|
+
const actual = result.getActual()
|
|
290
|
+
|
|
291
|
+
if (sreenshotsPath) {
|
|
292
|
+
await syncAssetsFromFileSystem(apps, sreenshotsPath)
|
|
293
|
+
} else {
|
|
294
|
+
console.warn('DO not sync screenhots')
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
created: actual.length - apps.length + (apps.length - actual.length),
|
|
299
|
+
updated: 0, // TableSync doesn't expose this directly
|
|
300
|
+
deleted: 0, // TableSync doesn't expose this directly
|
|
301
|
+
total: actual.length,
|
|
302
|
+
}
|
|
303
|
+
} catch (error) {
|
|
304
|
+
// Wrap error with context
|
|
305
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
306
|
+
const errorStack = error instanceof Error ? error.stack : undefined
|
|
307
|
+
|
|
308
|
+
throw new Error(
|
|
309
|
+
`Error syncing app catalog: ${errorMessage}\n\nDetails:\n${errorStack || 'No stack trace available'}`,
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
id?: ScalarKeys<TPrismaModelName>
|
|
7
|
+
uniqColumns: Array<ScalarKeys<TPrismaModelName>>
|
|
8
|
+
relationColumns?: Array<ObjectKeys<TPrismaModelName>>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
type TableSyncMagazineType = Partial<{
|
|
12
|
+
[key in Prisma.ModelName]: CommonSyncTableInfo<key>
|
|
13
|
+
}>
|
|
14
|
+
|
|
15
|
+
export const TABLE_SYNC_MAGAZINE = {
|
|
16
|
+
DbAppForCatalog: {
|
|
17
|
+
prismaModelName: 'DbAppForCatalog',
|
|
18
|
+
uniqColumns: ['slug'],
|
|
19
|
+
},
|
|
20
|
+
DbAppTagDefinition: {
|
|
21
|
+
prismaModelName: 'DbAppTagDefinition',
|
|
22
|
+
uniqColumns: ['prefix'],
|
|
23
|
+
},
|
|
24
|
+
DbApprovalMethod: {
|
|
25
|
+
id: 'slug',
|
|
26
|
+
prismaModelName: 'DbApprovalMethod',
|
|
27
|
+
uniqColumns: ['slug'],
|
|
28
|
+
},
|
|
29
|
+
} as const satisfies TableSyncMagazineType
|
|
30
|
+
|
|
31
|
+
export type TableSyncMagazine = typeof TABLE_SYNC_MAGAZINE
|
|
32
|
+
export type TableSyncMagazineModelNameKey = keyof TableSyncMagazine
|