@igstack/app-catalog-backend-core 0.3.1-alpha-20260405015231 → 0.4.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.
Files changed (72) hide show
  1. package/dist/db/syncAppCatalog.d.mts +3 -5
  2. package/dist/db/syncAppCatalog.d.mts.map +1 -1
  3. package/dist/db/syncAppCatalog.mjs +49 -57
  4. package/dist/db/syncAppCatalog.mjs.map +1 -1
  5. package/dist/db/tableSyncMagazine.d.mts +3 -7
  6. package/dist/db/tableSyncMagazine.d.mts.map +1 -1
  7. package/dist/db/tableSyncMagazine.mjs +3 -7
  8. package/dist/db/tableSyncMagazine.mjs.map +1 -1
  9. package/dist/db/tableSyncPrismaAdapter.mjs.map +1 -1
  10. package/dist/generated/prisma/client.mjs.map +1 -1
  11. package/dist/generated/prisma/internal/class.d.mts +5 -17
  12. package/dist/generated/prisma/internal/class.d.mts.map +1 -1
  13. package/dist/generated/prisma/internal/class.mjs +4 -4
  14. package/dist/generated/prisma/internal/class.mjs.map +1 -1
  15. package/dist/generated/prisma/internal/prismaNamespace.d.mts +46 -132
  16. package/dist/generated/prisma/internal/prismaNamespace.d.mts.map +1 -1
  17. package/dist/generated/prisma/models/DbResource.d.mts +2433 -0
  18. package/dist/generated/prisma/models/DbResource.d.mts.map +1 -0
  19. package/dist/generated/prisma/models/SourceReference.d.mts +90 -90
  20. package/dist/generated/prisma/models/SourceReference.d.mts.map +1 -1
  21. package/dist/generated/prisma/models.d.mts +2 -3
  22. package/dist/index.d.mts +3 -4
  23. package/dist/modules/appCatalog/checkLinks.mjs +1 -1
  24. package/dist/modules/appCatalog/checkLinks.mjs.map +1 -1
  25. package/dist/modules/appCatalog/service.mjs +26 -34
  26. package/dist/modules/appCatalog/service.mjs.map +1 -1
  27. package/dist/modules/assets/screenshotRestController.mjs +2 -2
  28. package/dist/modules/assets/screenshotRestController.mjs.map +1 -1
  29. package/dist/modules/assets/syncAssets.mjs +4 -4
  30. package/dist/modules/assets/syncAssets.mjs.map +1 -1
  31. package/dist/modules/lighthouseKeeper/tools.mjs +1 -1
  32. package/dist/modules/lighthouseKeeper/tools.mjs.map +1 -1
  33. package/dist/server/controller.d.mts +2 -2
  34. package/dist/server/controller.mjs.map +1 -1
  35. package/dist/types/common/appCatalogTypes.d.mts +26 -9
  36. package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
  37. package/dist/types/common/approvalMethodTypes.d.mts +5 -1
  38. package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
  39. package/package.json +3 -3
  40. package/prisma/schema.prisma +53 -62
  41. package/src/db/syncAppCatalog.ts +68 -73
  42. package/src/db/tableSyncMagazine.ts +3 -7
  43. package/src/db/tableSyncPrismaAdapter.ts +1 -1
  44. package/src/generated/prisma/browser.ts +2 -7
  45. package/src/generated/prisma/client.ts +2 -7
  46. package/src/generated/prisma/internal/class.ts +8 -18
  47. package/src/generated/prisma/internal/prismaNamespace.ts +43 -131
  48. package/src/generated/prisma/internal/prismaNamespaceBrowser.ts +7 -20
  49. package/src/generated/prisma/models/DbResource.ts +2701 -0
  50. package/src/generated/prisma/models/SourceReference.ts +89 -89
  51. package/src/generated/prisma/models.ts +1 -2
  52. package/src/index.ts +1 -1
  53. package/src/modules/appCatalog/checkLinks.ts +7 -7
  54. package/src/modules/appCatalog/service.ts +51 -62
  55. package/src/modules/assets/screenshotRestController.ts +2 -2
  56. package/src/modules/assets/screenshotRouter.ts +2 -2
  57. package/src/modules/assets/syncAssets.ts +4 -4
  58. package/src/modules/lighthouseKeeper/tools.ts +1 -1
  59. package/src/prisma-json-types.d.ts +8 -8
  60. package/src/server/controller.ts +2 -2
  61. package/src/types/common/appCatalogTypes.ts +28 -9
  62. package/src/types/common/approvalMethodTypes.ts +6 -0
  63. package/src/types/index.ts +0 -1
  64. package/dist/generated/prisma/models/DbAppForCatalog.d.mts +0 -1778
  65. package/dist/generated/prisma/models/DbAppForCatalog.d.mts.map +0 -1
  66. package/dist/generated/prisma/models/DbSubResource.d.mts +0 -1468
  67. package/dist/generated/prisma/models/DbSubResource.d.mts.map +0 -1
  68. package/dist/types/common/subResourceTypes.d.mts +0 -24
  69. package/dist/types/common/subResourceTypes.d.mts.map +0 -1
  70. package/src/generated/prisma/models/DbAppForCatalog.ts +0 -2014
  71. package/src/generated/prisma/models/DbSubResource.ts +0 -1692
  72. package/src/types/common/subResourceTypes.ts +0 -20
@@ -115,7 +115,7 @@ async function syncScreenshotsFromDirectory(prisma, screenshotsDir) {
115
115
  });
116
116
  }
117
117
  for (const [appId, screenshots] of screenshotsByApp) try {
118
- if (!await prisma.dbAppForCatalog.findUnique({
118
+ if (!await prisma.dbResource.findUnique({
119
119
  where: { slug: appId },
120
120
  select: { id: true }
121
121
  })) {
@@ -131,8 +131,8 @@ async function syncScreenshotsFromDirectory(prisma, screenshotsDir) {
131
131
  assetType: "screenshot"
132
132
  } });
133
133
  if (existing) {
134
- const existingApp = await prisma.dbAppForCatalog.findUnique({ where: { slug: appId } });
135
- if (existingApp && !existingApp.screenshotIds.includes(existing.id)) await prisma.dbAppForCatalog.update({
134
+ const existingApp = await prisma.dbResource.findUnique({ where: { slug: appId } });
135
+ if (existingApp && !existingApp.screenshotIds.includes(existing.id)) await prisma.dbResource.update({
136
136
  where: { slug: appId },
137
137
  data: { screenshotIds: [...existingApp.screenshotIds, existing.id] }
138
138
  });
@@ -156,7 +156,7 @@ async function syncScreenshotsFromDirectory(prisma, screenshotsDir) {
156
156
  width: width ?? null,
157
157
  height: height ?? null
158
158
  } });
159
- await prisma.dbAppForCatalog.update({
159
+ await prisma.dbResource.update({
160
160
  where: { slug: appId },
161
161
  data: { screenshotIds: { push: asset.id } }
162
162
  });
@@ -1 +1 @@
1
- {"version":3,"file":"syncAssets.mjs","names":[],"sources":["../../../src/modules/assets/syncAssets.ts"],"sourcesContent":["import { readFileSync, readdirSync } from 'node:fs'\nimport { extname, join } from 'node:path'\nimport { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from './assetUtils'\n\nexport interface SyncAssetsConfig {\n /**\n * Directory containing icon files to sync\n */\n iconsDir?: string\n\n /**\n * Directory containing screenshot files to sync\n */\n screenshotsDir?: string\n}\n\n/**\n * Sync local asset files (icons and screenshots) from directories into the database.\n *\n * This function allows consuming applications to sync asset files without directly\n * exposing the Prisma client. It handles:\n * - Icon files: Assigned to apps by matching filename to icon name patterns\n * - Screenshot files: Assigned to apps by matching filename to app ID (format: <app-id>_screenshot_<no>.<ext>)\n *\n * @param config Configuration with paths to icon and screenshot directories\n */\nexport async function syncAssets(config: SyncAssetsConfig): Promise<{\n iconsUpserted: number\n screenshotsUpserted: number\n}> {\n const prisma = getDbClient()\n let iconsUpserted = 0\n let screenshotsUpserted = 0\n\n // Sync icons from local/icons directory\n if (config.iconsDir) {\n console.log(`📁 Syncing icons from ${config.iconsDir}...`)\n iconsUpserted = await syncIconsFromDirectory(prisma, config.iconsDir)\n console.log(` ✓ Upserted ${iconsUpserted} icons`)\n }\n\n // Sync screenshots from local/screenshots directory\n if (config.screenshotsDir) {\n console.log(`📷 Syncing screenshots from ${config.screenshotsDir}...`)\n screenshotsUpserted = await syncScreenshotsFromDirectory(\n prisma,\n config.screenshotsDir,\n )\n console.log(\n ` ✓ Upserted ${screenshotsUpserted} screenshots and assigned to apps`,\n )\n }\n\n return {\n iconsUpserted,\n screenshotsUpserted,\n }\n}\n\n/**\n * Sync icon files from a directory\n */\nasync function syncIconsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n iconsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(iconsDir)\n\n for (const file of files) {\n const filePath = join(iconsDir, file)\n const ext = extname(file).toLowerCase().slice(1) // Remove leading dot\n\n // Skip non-image files\n if (!['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n continue\n }\n\n try {\n const content = readFileSync(filePath)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n const iconName = file.replace(/\\.[^/.]+$/, '') // Remove extension\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n continue // Already synced\n }\n\n // Extract dimensions for raster images\n let width: number | null = null\n let height: number | null = null\n if (!ext.includes('svg')) {\n const { width: w, height: h } = await getImageDimensions(buffer)\n width = w ?? null\n height = h ?? null\n }\n\n // Determine MIME type\n const mimeType =\n {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n }[ext] || 'application/octet-stream'\n\n await prisma.dbAsset.create({\n data: {\n name: iconName,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync icon ${file}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading icons directory:`, error)\n }\n\n return count\n}\n\n/**\n * Sync screenshot files from a directory and assign to apps\n */\nasync function syncScreenshotsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n screenshotsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(screenshotsDir)\n\n // Group screenshots by app ID\n const screenshotsByApp = new Map<string, { path: string; ext: string }[]>()\n\n for (const file of files) {\n // Parse filename: <app-id>_screenshot_<no>.<ext>\n const match = file.match(/^(.+?)_screenshot_(\\d+)\\.([^.]+)$/)\n if (!match || !match[1] || !match[3]) {\n continue\n }\n\n const appId = match[1]\n const ext = match[3]\n if (!screenshotsByApp.has(appId)) {\n screenshotsByApp.set(appId, [])\n }\n screenshotsByApp.get(appId)!.push({\n path: join(screenshotsDir, file),\n ext,\n })\n }\n\n // Process each app's screenshots\n for (const [appId, screenshots] of screenshotsByApp) {\n try {\n // Check if app exists\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n select: { id: true },\n })\n\n if (!app) {\n console.warn(` ⚠ App not found: ${appId}`)\n continue\n }\n\n // Sync screenshots for this app\n for (const screenshot of screenshots) {\n try {\n const content = readFileSync(screenshot.path)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n\n // Check if screenshot with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'screenshot' },\n })\n\n if (existing) {\n // Link to app via screenshotIds array if not already linked\n const existingApp = await prisma.dbAppForCatalog.findUnique({\n where: { slug: appId },\n })\n if (\n existingApp &&\n !existingApp.screenshotIds.includes(existing.id)\n ) {\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: [...existingApp.screenshotIds, existing.id],\n },\n })\n }\n continue\n }\n\n // Extract dimensions\n const { width, height } = await getImageDimensions(buffer)\n\n // Determine MIME type\n const mimeType =\n {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n }[screenshot.ext.toLowerCase()] || 'application/octet-stream'\n\n // Create screenshot asset\n const asset = await prisma.dbAsset.create({\n data: {\n name: `${appId}-screenshot-${Date.now()}`,\n assetType: 'screenshot',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width: width ?? null,\n height: height ?? null,\n },\n })\n\n // Link screenshot to app via screenshotIds array\n await prisma.dbAppForCatalog.update({\n where: { slug: appId },\n data: {\n screenshotIds: {\n push: asset.id,\n },\n },\n })\n\n count++\n } catch (error) {\n console.warn(\n ` ⚠ Failed to sync screenshot ${screenshot.path}:`,\n error,\n )\n }\n }\n } catch (error) {\n console.warn(` ⚠ Failed to process app ${appId}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading screenshots directory:`, error)\n }\n\n return count\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA2BA,eAAsB,WAAW,QAG9B;CACD,MAAM,SAAS,aAAa;CAC5B,IAAI,gBAAgB;CACpB,IAAI,sBAAsB;AAG1B,KAAI,OAAO,UAAU;AACnB,UAAQ,IAAI,yBAAyB,OAAO,SAAS,KAAK;AAC1D,kBAAgB,MAAM,uBAAuB,QAAQ,OAAO,SAAS;AACrE,UAAQ,IAAI,iBAAiB,cAAc,QAAQ;;AAIrD,KAAI,OAAO,gBAAgB;AACzB,UAAQ,IAAI,+BAA+B,OAAO,eAAe,KAAK;AACtE,wBAAsB,MAAM,6BAC1B,QACA,OAAO,eACR;AACD,UAAQ,IACN,iBAAiB,oBAAoB,mCACtC;;AAGH,QAAO;EACL;EACA;EACD;;;;;AAMH,eAAe,uBACb,QACA,UACiB;CACjB,IAAI,QAAQ;AAEZ,KAAI;EACF,MAAM,QAAQ,YAAY,SAAS;AAEnC,OAAK,MAAM,QAAQ,OAAO;GACxB,MAAM,WAAW,KAAK,UAAU,KAAK;GACrC,MAAM,MAAM,QAAQ,KAAK,CAAC,aAAa,CAAC,MAAM,EAAE;AAGhD,OAAI,CAAC;IAAC;IAAO;IAAO;IAAQ;IAAO;IAAQ;IAAM,CAAC,SAAS,IAAI,CAC7D;AAGF,OAAI;IACF,MAAM,UAAU,aAAa,SAAS;IACtC,MAAM,SAAS,OAAO,KAAK,QAAQ;IACnC,MAAM,WAAW,iBAAiB,OAAO;IACzC,MAAM,WAAW,KAAK,QAAQ,aAAa,GAAG;AAO9C,QAJiB,MAAM,OAAO,QAAQ,UAAU,EAC9C,OAAO;KAAE;KAAU,WAAW;KAAQ,EACvC,CAAC,CAGA;IAIF,IAAI,QAAuB;IAC3B,IAAI,SAAwB;AAC5B,QAAI,CAAC,IAAI,SAAS,MAAM,EAAE;KACxB,MAAM,EAAE,OAAO,GAAG,QAAQ,MAAM,MAAM,mBAAmB,OAAO;AAChE,aAAQ,KAAK;AACb,cAAS,KAAK;;IAIhB,MAAM,WACJ;KACE,KAAK;KACL,KAAK;KACL,MAAM;KACN,KAAK;KACL,MAAM;KACN,KAAK;KACN,CAAC,QAAQ;AAEZ,UAAM,OAAO,QAAQ,OAAO,EAC1B,MAAM;KACJ,MAAM;KACN,WAAW;KACX,SAAS,IAAI,WAAW,OAAO;KAC/B;KACA;KACA,UAAU,OAAO;KACjB;KACA;KACD,EACF,CAAC;AAEF;YACO,OAAO;AACd,YAAQ,KAAK,2BAA2B,KAAK,IAAI,MAAM;;;UAGpD,OAAO;AACd,UAAQ,MAAM,sCAAsC,MAAM;;AAG5D,QAAO;;;;;AAMT,eAAe,6BACb,QACA,gBACiB;CACjB,IAAI,QAAQ;AAEZ,KAAI;EACF,MAAM,QAAQ,YAAY,eAAe;EAGzC,MAAM,mCAAmB,IAAI,KAA8C;AAE3E,OAAK,MAAM,QAAQ,OAAO;GAExB,MAAM,QAAQ,KAAK,MAAM,oCAAoC;AAC7D,OAAI,CAAC,SAAS,CAAC,MAAM,MAAM,CAAC,MAAM,GAChC;GAGF,MAAM,QAAQ,MAAM;GACpB,MAAM,MAAM,MAAM;AAClB,OAAI,CAAC,iBAAiB,IAAI,MAAM,CAC9B,kBAAiB,IAAI,OAAO,EAAE,CAAC;AAEjC,oBAAiB,IAAI,MAAM,CAAE,KAAK;IAChC,MAAM,KAAK,gBAAgB,KAAK;IAChC;IACD,CAAC;;AAIJ,OAAK,MAAM,CAAC,OAAO,gBAAgB,iBACjC,KAAI;AAOF,OAAI,CALQ,MAAM,OAAO,gBAAgB,WAAW;IAClD,OAAO,EAAE,MAAM,OAAO;IACtB,QAAQ,EAAE,IAAI,MAAM;IACrB,CAAC,EAEQ;AACR,YAAQ,KAAK,sBAAsB,QAAQ;AAC3C;;AAIF,QAAK,MAAM,cAAc,YACvB,KAAI;IACF,MAAM,UAAU,aAAa,WAAW,KAAK;IAC7C,MAAM,SAAS,OAAO,KAAK,QAAQ;IACnC,MAAM,WAAW,iBAAiB,OAAO;IAGzC,MAAM,WAAW,MAAM,OAAO,QAAQ,UAAU,EAC9C,OAAO;KAAE;KAAU,WAAW;KAAc,EAC7C,CAAC;AAEF,QAAI,UAAU;KAEZ,MAAM,cAAc,MAAM,OAAO,gBAAgB,WAAW,EAC1D,OAAO,EAAE,MAAM,OAAO,EACvB,CAAC;AACF,SACE,eACA,CAAC,YAAY,cAAc,SAAS,SAAS,GAAG,CAEhD,OAAM,OAAO,gBAAgB,OAAO;MAClC,OAAO,EAAE,MAAM,OAAO;MACtB,MAAM,EACJ,eAAe,CAAC,GAAG,YAAY,eAAe,SAAS,GAAG,EAC3D;MACF,CAAC;AAEJ;;IAIF,MAAM,EAAE,OAAO,WAAW,MAAM,mBAAmB,OAAO;IAG1D,MAAM,WACJ;KACE,KAAK;KACL,KAAK;KACL,MAAM;KACN,KAAK;KACL,MAAM;KACP,CAAC,WAAW,IAAI,aAAa,KAAK;IAGrC,MAAM,QAAQ,MAAM,OAAO,QAAQ,OAAO,EACxC,MAAM;KACJ,MAAM,GAAG,MAAM,cAAc,KAAK,KAAK;KACvC,WAAW;KACX,SAAS,IAAI,WAAW,OAAO;KAC/B;KACA;KACA,UAAU,OAAO;KACjB,OAAO,SAAS;KAChB,QAAQ,UAAU;KACnB,EACF,CAAC;AAGF,UAAM,OAAO,gBAAgB,OAAO;KAClC,OAAO,EAAE,MAAM,OAAO;KACtB,MAAM,EACJ,eAAe,EACb,MAAM,MAAM,IACb,EACF;KACF,CAAC;AAEF;YACO,OAAO;AACd,YAAQ,KACN,iCAAiC,WAAW,KAAK,IACjD,MACD;;WAGE,OAAO;AACd,WAAQ,KAAK,6BAA6B,MAAM,IAAI,MAAM;;UAGvD,OAAO;AACd,UAAQ,MAAM,4CAA4C,MAAM;;AAGlE,QAAO"}
1
+ {"version":3,"file":"syncAssets.mjs","names":[],"sources":["../../../src/modules/assets/syncAssets.ts"],"sourcesContent":["import { readFileSync, readdirSync } from 'node:fs'\nimport { extname, join } from 'node:path'\nimport { getDbClient } from '../../db'\nimport { generateChecksum, getImageDimensions } from './assetUtils'\n\nexport interface SyncAssetsConfig {\n /**\n * Directory containing icon files to sync\n */\n iconsDir?: string\n\n /**\n * Directory containing screenshot files to sync\n */\n screenshotsDir?: string\n}\n\n/**\n * Sync local asset files (icons and screenshots) from directories into the database.\n *\n * This function allows consuming applications to sync asset files without directly\n * exposing the Prisma client. It handles:\n * - Icon files: Assigned to apps by matching filename to icon name patterns\n * - Screenshot files: Assigned to apps by matching filename to app ID (format: <app-id>_screenshot_<no>.<ext>)\n *\n * @param config Configuration with paths to icon and screenshot directories\n */\nexport async function syncAssets(config: SyncAssetsConfig): Promise<{\n iconsUpserted: number\n screenshotsUpserted: number\n}> {\n const prisma = getDbClient()\n let iconsUpserted = 0\n let screenshotsUpserted = 0\n\n // Sync icons from local/icons directory\n if (config.iconsDir) {\n console.log(`📁 Syncing icons from ${config.iconsDir}...`)\n iconsUpserted = await syncIconsFromDirectory(prisma, config.iconsDir)\n console.log(` ✓ Upserted ${iconsUpserted} icons`)\n }\n\n // Sync screenshots from local/screenshots directory\n if (config.screenshotsDir) {\n console.log(`📷 Syncing screenshots from ${config.screenshotsDir}...`)\n screenshotsUpserted = await syncScreenshotsFromDirectory(\n prisma,\n config.screenshotsDir,\n )\n console.log(\n ` ✓ Upserted ${screenshotsUpserted} screenshots and assigned to apps`,\n )\n }\n\n return {\n iconsUpserted,\n screenshotsUpserted,\n }\n}\n\n/**\n * Sync icon files from a directory\n */\nasync function syncIconsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n iconsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(iconsDir)\n\n for (const file of files) {\n const filePath = join(iconsDir, file)\n const ext = extname(file).toLowerCase().slice(1) // Remove leading dot\n\n // Skip non-image files\n if (!['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg'].includes(ext)) {\n continue\n }\n\n try {\n const content = readFileSync(filePath)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n const iconName = file.replace(/\\.[^/.]+$/, '') // Remove extension\n\n // Check if asset with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'icon' },\n })\n\n if (existing) {\n continue // Already synced\n }\n\n // Extract dimensions for raster images\n let width: number | null = null\n let height: number | null = null\n if (!ext.includes('svg')) {\n const { width: w, height: h } = await getImageDimensions(buffer)\n width = w ?? null\n height = h ?? null\n }\n\n // Determine MIME type\n const mimeType =\n {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n }[ext] || 'application/octet-stream'\n\n await prisma.dbAsset.create({\n data: {\n name: iconName,\n assetType: 'icon',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width,\n height,\n },\n })\n\n count++\n } catch (error) {\n console.warn(` ⚠ Failed to sync icon ${file}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading icons directory:`, error)\n }\n\n return count\n}\n\n/**\n * Sync screenshot files from a directory and assign to apps\n */\nasync function syncScreenshotsFromDirectory(\n prisma: ReturnType<typeof getDbClient>,\n screenshotsDir: string,\n): Promise<number> {\n let count = 0\n\n try {\n const files = readdirSync(screenshotsDir)\n\n // Group screenshots by app ID\n const screenshotsByApp = new Map<string, { path: string; ext: string }[]>()\n\n for (const file of files) {\n // Parse filename: <app-id>_screenshot_<no>.<ext>\n const match = file.match(/^(.+?)_screenshot_(\\d+)\\.([^.]+)$/)\n if (!match || !match[1] || !match[3]) {\n continue\n }\n\n const appId = match[1]\n const ext = match[3]\n if (!screenshotsByApp.has(appId)) {\n screenshotsByApp.set(appId, [])\n }\n screenshotsByApp.get(appId)!.push({\n path: join(screenshotsDir, file),\n ext,\n })\n }\n\n // Process each app's screenshots\n for (const [appId, screenshots] of screenshotsByApp) {\n try {\n // Check if app exists\n const app = await prisma.dbResource.findUnique({\n where: { slug: appId },\n select: { id: true },\n })\n\n if (!app) {\n console.warn(` ⚠ App not found: ${appId}`)\n continue\n }\n\n // Sync screenshots for this app\n for (const screenshot of screenshots) {\n try {\n const content = readFileSync(screenshot.path)\n const buffer = Buffer.from(content)\n const checksum = generateChecksum(buffer)\n\n // Check if screenshot with same checksum already exists\n const existing = await prisma.dbAsset.findFirst({\n where: { checksum, assetType: 'screenshot' },\n })\n\n if (existing) {\n // Link to app via screenshotIds array if not already linked\n const existingApp = await prisma.dbResource.findUnique({\n where: { slug: appId },\n })\n if (\n existingApp &&\n !existingApp.screenshotIds.includes(existing.id)\n ) {\n await prisma.dbResource.update({\n where: { slug: appId },\n data: {\n screenshotIds: [...existingApp.screenshotIds, existing.id],\n },\n })\n }\n continue\n }\n\n // Extract dimensions\n const { width, height } = await getImageDimensions(buffer)\n\n // Determine MIME type\n const mimeType =\n {\n png: 'image/png',\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n gif: 'image/gif',\n webp: 'image/webp',\n }[screenshot.ext.toLowerCase()] || 'application/octet-stream'\n\n // Create screenshot asset\n const asset = await prisma.dbAsset.create({\n data: {\n name: `${appId}-screenshot-${Date.now()}`,\n assetType: 'screenshot',\n content: new Uint8Array(buffer),\n checksum,\n mimeType,\n fileSize: buffer.length,\n width: width ?? null,\n height: height ?? null,\n },\n })\n\n // Link screenshot to app via screenshotIds array\n await prisma.dbResource.update({\n where: { slug: appId },\n data: {\n screenshotIds: {\n push: asset.id,\n },\n },\n })\n\n count++\n } catch (error) {\n console.warn(\n ` ⚠ Failed to sync screenshot ${screenshot.path}:`,\n error,\n )\n }\n }\n } catch (error) {\n console.warn(` ⚠ Failed to process app ${appId}:`, error)\n }\n }\n } catch (error) {\n console.error(` ❌ Error reading screenshots directory:`, error)\n }\n\n return count\n}\n"],"mappings":"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"}
@@ -21,7 +21,7 @@ function createAppCatalogAITools(databaseConfig) {
21
21
  inputSchema: z.object({ slug: z.string().describe("The app slug to fetch") }),
22
22
  execute: async ({ slug }) => {
23
23
  try {
24
- const app = await prisma.dbAppForCatalog.findUnique({
24
+ const app = await prisma.dbResource.findUnique({
25
25
  where: { slug },
26
26
  include: { sourceRefs: true }
27
27
  });
@@ -1 +1 @@
1
- {"version":3,"file":"tools.mjs","names":[],"sources":["../../../src/modules/lighthouseKeeper/tools.ts"],"sourcesContent":["import type { Tool } from 'ai'\nimport { z } from 'zod'\nimport { PrismaClient } from '../../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\nimport type { AcDatabaseConfig } from '../../middleware/types.js'\n\n// ============================================================================\n// Database URL Helper\n// ============================================================================\n\nfunction getDatabaseUrl(config: AcDatabaseConfig): string {\n if ('url' in config) return config.url\n const { host, port, database, username, password, schema } = config\n const schemaParam = schema ? `?schema=${schema}` : ''\n return `postgresql://${username}:${password}@${host}:${port}/${database}${schemaParam}`\n}\n\n// ============================================================================\n// AI Tools for App Catalog\n// ============================================================================\n\n/**\n * Create AI tools for working with app catalog cards.\n * These tools allow the AI to fetch and inspect app information.\n */\nexport function createAppCatalogAITools(\n databaseConfig: AcDatabaseConfig,\n): Record<string, Tool> {\n const databaseUrl = getDatabaseUrl(databaseConfig)\n const pool = new pg.Pool({ connectionString: databaseUrl })\n const adapter = new PrismaPg(pool)\n const prisma = new PrismaClient({ adapter })\n\n const getAppCardSchema = z.object({\n slug: z.string().describe('The app slug to fetch'),\n })\n\n type GetAppCardInput = z.infer<typeof getAppCardSchema>\n\n const getAppCard: Tool<GetAppCardInput, unknown> = {\n description:\n 'Fetch complete app catalog card information by ID. Returns title, description, tags, access request details, links, and all other app metadata.',\n inputSchema: getAppCardSchema,\n execute: async ({ slug }: { slug: string }) => {\n try {\n const app = await prisma.dbAppForCatalog.findUnique({\n where: { slug },\n include: {\n sourceRefs: true,\n },\n })\n\n if (!app) {\n return { error: `App not found with slug: ${slug}` }\n }\n\n // Return structured card data\n return {\n success: true,\n card: {\n id: app.id,\n slug: app.slug,\n displayName: app.displayName,\n description: app.description,\n teams: app.teams,\n tags: app.tags,\n appUrl: app.appUrl,\n links: app.links,\n iconName: app.iconName,\n sources: app.sourceRefs.map((ref) => ref.url),\n notes: app.notes,\n accessRequest: app.accessRequest,\n deprecated: app.deprecated,\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n },\n }\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : 'Failed to fetch app',\n }\n }\n },\n }\n\n return {\n getAppCard,\n }\n}\n\n// ============================================================================\n// System Prompt\n// ============================================================================\n\n/**\n * Default system prompt for AI working with app catalog cards.\n * Use this when configuring lighthouseKeeper for card updates.\n */\nexport const APP_CATALOG_AI_SYSTEM_PROMPT = `You are an assistant that helps update app catalog cards based on information from provided links.\n\nWhen given a link and an app ID:\n1. Use getAppCard to fetch the current card information\n2. Analyze the provided link for relevant information\n3. Output a new card structure with updated information based on the link\n\nReturn the updated card as a structured JSON object matching the card format:\n- displayName: App title\n- description: Full description\n- tags: Array of tags\n- teams: Array of team names\n- appUrl: Main application URL\n- links: Array of {url, title} objects for related links\n- sources: Array of source URLs (documentation, Confluence pages, etc.)\n- accessRequest: Object with approvalMethodSlug, comments, requestPrompt, postApprovalInstructions, roles, approverPersonSlugs, urls\n- notes: Additional notes\n- deprecated: Optional deprecation info with type, replacementSlug, comment\n\nDo NOT save the card to the database. Only output the updated structure.`\n"],"mappings":";;;;;;AAWA,SAAS,eAAe,QAAkC;AACxD,KAAI,SAAS,OAAQ,QAAO,OAAO;CACnC,MAAM,EAAE,MAAM,MAAM,UAAU,UAAU,UAAU,WAAW;AAE7D,QAAO,gBAAgB,SAAS,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,WAD3C,SAAS,WAAW,WAAW;;;;;;AAYrD,SAAgB,wBACd,gBACsB;CACtB,MAAM,cAAc,eAAe,eAAe;CAGlD,MAAM,SAAS,IAAI,aAAa,EAAE,SADlB,IAAI,SADP,IAAI,GAAG,KAAK,EAAE,kBAAkB,aAAa,CAAC,CACzB,EACS,CAAC;AAsD5C,QAAO,EACL,YA/CiD;EACjD,aACE;EACF,aATuB,EAAE,OAAO,EAChC,MAAM,EAAE,QAAQ,CAAC,SAAS,wBAAwB,EACnD,CAAC;EAQA,SAAS,OAAO,EAAE,WAA6B;AAC7C,OAAI;IACF,MAAM,MAAM,MAAM,OAAO,gBAAgB,WAAW;KAClD,OAAO,EAAE,MAAM;KACf,SAAS,EACP,YAAY,MACb;KACF,CAAC;AAEF,QAAI,CAAC,IACH,QAAO,EAAE,OAAO,4BAA4B,QAAQ;AAItD,WAAO;KACL,SAAS;KACT,MAAM;MACJ,IAAI,IAAI;MACR,MAAM,IAAI;MACV,aAAa,IAAI;MACjB,aAAa,IAAI;MACjB,OAAO,IAAI;MACX,MAAM,IAAI;MACV,QAAQ,IAAI;MACZ,OAAO,IAAI;MACX,UAAU,IAAI;MACd,SAAS,IAAI,WAAW,KAAK,QAAQ,IAAI,IAAI;MAC7C,OAAO,IAAI;MACX,eAAe,IAAI;MACnB,YAAY,IAAI;MAChB,WAAW,IAAI;MACf,WAAW,IAAI;MAChB;KACF;YACM,OAAO;AACd,WAAO,EACL,OAAO,iBAAiB,QAAQ,MAAM,UAAU,uBACjD;;;EAGN,EAIA;;;;;;AAWH,MAAa,+BAA+B"}
1
+ {"version":3,"file":"tools.mjs","names":[],"sources":["../../../src/modules/lighthouseKeeper/tools.ts"],"sourcesContent":["import type { Tool } from 'ai'\nimport { z } from 'zod'\nimport { PrismaClient } from '../../generated/prisma/client'\nimport { PrismaPg } from '@prisma/adapter-pg'\nimport pg from 'pg'\nimport type { AcDatabaseConfig } from '../../middleware/types.js'\n\n// ============================================================================\n// Database URL Helper\n// ============================================================================\n\nfunction getDatabaseUrl(config: AcDatabaseConfig): string {\n if ('url' in config) return config.url\n const { host, port, database, username, password, schema } = config\n const schemaParam = schema ? `?schema=${schema}` : ''\n return `postgresql://${username}:${password}@${host}:${port}/${database}${schemaParam}`\n}\n\n// ============================================================================\n// AI Tools for App Catalog\n// ============================================================================\n\n/**\n * Create AI tools for working with app catalog cards.\n * These tools allow the AI to fetch and inspect app information.\n */\nexport function createAppCatalogAITools(\n databaseConfig: AcDatabaseConfig,\n): Record<string, Tool> {\n const databaseUrl = getDatabaseUrl(databaseConfig)\n const pool = new pg.Pool({ connectionString: databaseUrl })\n const adapter = new PrismaPg(pool)\n const prisma = new PrismaClient({ adapter })\n\n const getAppCardSchema = z.object({\n slug: z.string().describe('The app slug to fetch'),\n })\n\n type GetAppCardInput = z.infer<typeof getAppCardSchema>\n\n const getAppCard: Tool<GetAppCardInput, unknown> = {\n description:\n 'Fetch complete app catalog card information by ID. Returns title, description, tags, access request details, links, and all other app metadata.',\n inputSchema: getAppCardSchema,\n execute: async ({ slug }: { slug: string }) => {\n try {\n const app = await prisma.dbResource.findUnique({\n where: { slug },\n include: {\n sourceRefs: true,\n },\n })\n\n if (!app) {\n return { error: `App not found with slug: ${slug}` }\n }\n\n // Return structured card data\n return {\n success: true,\n card: {\n id: app.id,\n slug: app.slug,\n displayName: app.displayName,\n description: app.description,\n teams: app.teams,\n tags: app.tags,\n appUrl: app.appUrl,\n links: app.links,\n iconName: app.iconName,\n sources: app.sourceRefs.map((ref) => ref.url),\n notes: app.notes,\n accessRequest: app.accessRequest,\n deprecated: app.deprecated,\n createdAt: app.createdAt,\n updatedAt: app.updatedAt,\n },\n }\n } catch (error) {\n return {\n error: error instanceof Error ? error.message : 'Failed to fetch app',\n }\n }\n },\n }\n\n return {\n getAppCard,\n }\n}\n\n// ============================================================================\n// System Prompt\n// ============================================================================\n\n/**\n * Default system prompt for AI working with app catalog cards.\n * Use this when configuring lighthouseKeeper for card updates.\n */\nexport const APP_CATALOG_AI_SYSTEM_PROMPT = `You are an assistant that helps update app catalog cards based on information from provided links.\n\nWhen given a link and an app ID:\n1. Use getAppCard to fetch the current card information\n2. Analyze the provided link for relevant information\n3. Output a new card structure with updated information based on the link\n\nReturn the updated card as a structured JSON object matching the card format:\n- displayName: App title\n- description: Full description\n- tags: Array of tags\n- teams: Array of team names\n- appUrl: Main application URL\n- links: Array of {url, title} objects for related links\n- sources: Array of source URLs (documentation, Confluence pages, etc.)\n- accessRequest: Object with approvalMethodSlug, comments, requestPrompt, postApprovalInstructions, roles, approverPersonSlugs, urls\n- notes: Additional notes\n- deprecated: Optional deprecation info with type, replacementSlug, comment\n\nDo NOT save the card to the database. Only output the updated structure.`\n"],"mappings":";;;;;;AAWA,SAAS,eAAe,QAAkC;AACxD,KAAI,SAAS,OAAQ,QAAO,OAAO;CACnC,MAAM,EAAE,MAAM,MAAM,UAAU,UAAU,UAAU,WAAW;AAE7D,QAAO,gBAAgB,SAAS,GAAG,SAAS,GAAG,KAAK,GAAG,KAAK,GAAG,WAD3C,SAAS,WAAW,WAAW;;;;;;AAYrD,SAAgB,wBACd,gBACsB;CACtB,MAAM,cAAc,eAAe,eAAe;CAGlD,MAAM,SAAS,IAAI,aAAa,EAAE,SADlB,IAAI,SADP,IAAI,GAAG,KAAK,EAAE,kBAAkB,aAAa,CAAC,CACzB,EACS,CAAC;AAsD5C,QAAO,EACL,YA/CiD;EACjD,aACE;EACF,aATuB,EAAE,OAAO,EAChC,MAAM,EAAE,QAAQ,CAAC,SAAS,wBAAwB,EACnD,CAAC;EAQA,SAAS,OAAO,EAAE,WAA6B;AAC7C,OAAI;IACF,MAAM,MAAM,MAAM,OAAO,WAAW,WAAW;KAC7C,OAAO,EAAE,MAAM;KACf,SAAS,EACP,YAAY,MACb;KACF,CAAC;AAEF,QAAI,CAAC,IACH,QAAO,EAAE,OAAO,4BAA4B,QAAQ;AAItD,WAAO;KACL,SAAS;KACT,MAAM;MACJ,IAAI,IAAI;MACR,MAAM,IAAI;MACV,aAAa,IAAI;MACjB,aAAa,IAAI;MACjB,OAAO,IAAI;MACX,MAAM,IAAI;MACV,QAAQ,IAAI;MACZ,OAAO,IAAI;MACX,UAAU,IAAI;MACd,SAAS,IAAI,WAAW,KAAK,QAAQ,IAAI,IAAI;MAC7C,OAAO,IAAI;MACX,eAAe,IAAI;MACnB,YAAY,IAAI;MAChB,WAAW,IAAI;MACf,WAAW,IAAI;MAChB;KACF;YACM,OAAO;AACd,WAAO,EACL,OAAO,iBAAiB,QAAQ,MAAM,UAAU,uBACjD;;;EAGN,EAIA;;;;;;AAWH,MAAa,+BAA+B"}
@@ -1,4 +1,4 @@
1
- import { AppCatalogData, AppForCatalog } from "../types/common/appCatalogTypes.mjs";
1
+ import { AppCatalogData, Resource } from "../types/common/appCatalogTypes.mjs";
2
2
  import { AcTrpcContext } from "./acTrpcContext.mjs";
3
3
  import { BetterAuth } from "../modules/auth/auth.mjs";
4
4
  import * as _trpc_server0 from "@trpc/server";
@@ -40,7 +40,7 @@ declare function createTrpcRouter(auth?: BetterAuth, options?: {
40
40
  aiPrompt?: string | null | undefined;
41
41
  };
42
42
  };
43
- output: AppForCatalog;
43
+ output: Resource;
44
44
  meta: object;
45
45
  }>;
46
46
  }>>;
@@ -1 +1 @@
1
- {"version":3,"file":"controller.mjs","names":["updateAppService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { AppForCatalog } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: { devLoginEnabled?: boolean },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<AppForCatalog> => {\n return updateAppService(input)\n }),\n }),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;AAYA,MAAM,uBAAuB,EAAE,OAAO;CACpC,IAAI,EAAE,QAAQ;CACd,MAAM,EACH,OAAO;EACN,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,cAAc,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU;EACtD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU;EAC7B,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;EACvC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;EAC3C,CAAC,CACD,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC,SAAS,GAAG,EACxC,SAAS,+BACV,CAAC;CACL,CAAC;;;;;AAMF,SAAgB,iBACd,MACA,SACA;AACA,QAAO,OAAO;EACZ,YAAY,OAAO;GACjB,SAAS,gBAAgB,MACvB,OAAO,EAAE,UAAmC;IAC1C,MAAM,WAAW,MAAM,mBAAmB;IAC1C,MAAM,WAAW,MAAM,IAAI,uBAAuB,kBAAkB;AAEpE,WAAO;KACL,GAAG;KACH,GAAI,YAAY,EAAE,UAAU;KAC7B;KAEJ;GACD,WAAW,gBACR,MAAM,qBAAqB,CAC3B,SAAS,OAAO,EAAE,YAAoC;AACrD,WAAOA,UAAiB,MAAM;KAC9B;GACL,CAAC;EAGF,MAAM,iBAAiB,GAAG,MAAM,QAAQ;EACzC,CAAC"}
1
+ {"version":3,"file":"controller.mjs","names":["updateAppService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { Resource } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: { devLoginEnabled?: boolean },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAppService(input)\n }),\n }),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;AAYA,MAAM,uBAAuB,EAAE,OAAO;CACpC,IAAI,EAAE,QAAQ;CACd,MAAM,EACH,OAAO;EACN,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,cAAc,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU;EACtD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU;EAC7B,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;EACvC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;EAC3C,CAAC,CACD,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC,SAAS,GAAG,EACxC,SAAS,+BACV,CAAC;CACL,CAAC;;;;;AAMF,SAAgB,iBACd,MACA,SACA;AACA,QAAO,OAAO;EACZ,YAAY,OAAO;GACjB,SAAS,gBAAgB,MACvB,OAAO,EAAE,UAAmC;IAC1C,MAAM,WAAW,MAAM,mBAAmB;IAC1C,MAAM,WAAW,MAAM,IAAI,uBAAuB,kBAAkB;AAEpE,WAAO;KACL,GAAG;KACH,GAAI,YAAY,EAAE,UAAU;KAC7B;KAEJ;GACD,WAAW,gBACR,MAAM,qBAAqB,CAC3B,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOA,UAAiB,MAAM;KAC9B;GACL,CAAC;EAGF,MAAM,iBAAiB,GAAG,MAAM,QAAQ;EACzC,CAAC"}
@@ -1,13 +1,12 @@
1
1
  import { AppAccessRequest, ApprovalMethod } from "./approvalMethodTypes.mjs";
2
2
  import { Group, Person } from "./personGroupTypes.mjs";
3
- import { SubResource } from "./subResourceTypes.mjs";
4
3
 
5
4
  //#region src/types/common/appCatalogTypes.d.ts
6
5
  /**
7
- * A tier variant of an app (e.g., prod/dev environments).
6
+ * A tier variant of a resource (e.g., prod/dev environments).
8
7
  * Each tier can have its own URL and access process.
9
8
  */
10
- interface AppTierVariant {
9
+ interface TierVariant {
11
10
  tierSlug: string;
12
11
  displayName?: string;
13
12
  description?: string;
@@ -24,11 +23,14 @@ interface SourceReference {
24
23
  parseDate: string | null;
25
24
  }
26
25
  /**
27
- * Application entry in the catalog
26
+ * Resource entry in the catalog (application or sub-resource).
27
+ * Unified model: applications have no parentSlug; sub-resources have parentSlug.
28
28
  */
29
- interface AppForCatalog {
29
+ interface Resource {
30
30
  id: string;
31
31
  slug: string;
32
+ /** Discriminator: "application" for top-level apps, "sub-resource" for children, etc. */
33
+ type?: string;
32
34
  displayName: string;
33
35
  abbreviation?: string;
34
36
  nicknames?: string[];
@@ -55,7 +57,23 @@ interface AppForCatalog {
55
57
  /** URL health issues detected by automated scanning */
56
58
  urlIssues?: string[];
57
59
  /** Optional tier variants (e.g., prod/dev) with per-tier URLs and access */
58
- tiers?: AppTierVariant[];
60
+ tiers?: TierVariant[];
61
+ /** Slug of parent resource (undefined for top-level applications) */
62
+ parentSlug?: string;
63
+ /** Tier slug (e.g. "prod", "dev") — for sub-resources */
64
+ tier?: string;
65
+ /** Groups tier variants visually */
66
+ familySlug?: string;
67
+ /** Alternative identifiers */
68
+ aliases?: string[];
69
+ /** Person slug of the owner */
70
+ ownerPersonSlug?: string;
71
+ /** Group slugs of access maintainers */
72
+ accessMaintainerGroupSlugs?: string[];
73
+ /** Free-text access comments */
74
+ accessComments?: string;
75
+ /** Arbitrary extra data */
76
+ extra?: Record<string, unknown>;
59
77
  }
60
78
  interface AppCategory {
61
79
  id: string;
@@ -84,14 +102,13 @@ interface AppVersionInfo {
84
102
  coreVersion?: VersionInfo;
85
103
  }
86
104
  interface AppCatalogData {
87
- apps: AppForCatalog[];
105
+ resources: Resource[];
88
106
  tagsDefinitions: GroupingTagDefinition[];
89
107
  approvalMethods: AppApprovalMethod[];
90
108
  persons: Person[];
91
109
  groups: Group[];
92
- subResources: SubResource[];
93
110
  versions?: AppVersionInfo;
94
111
  }
95
112
  //#endregion
96
- export { AppApprovalMethod, AppCatalogData, AppCategory, AppForCatalog, AppTierVariant, AppVersionInfo, GroupingTagDefinition, GroupingTagValue, SourceReference, VersionInfo };
113
+ export { AppApprovalMethod, AppCatalogData, AppCategory, AppVersionInfo, GroupingTagDefinition, GroupingTagValue, Resource, SourceReference, TierVariant, VersionInfo };
97
114
  //# sourceMappingURL=appCatalogTypes.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"appCatalogTypes.d.mts","names":[],"sources":["../../../src/types/common/appCatalogTypes.ts"],"mappings":";;;;;;;;;UAoBiB,cAAA;EACf,QAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA;EACA,aAAA,GAAgB,gBAAA;AAAA;;;;;UAWD,eAAA;EACf,UAAA;EACA,GAAA;EACA,SAAA;AAAA;;;;UAMe,aAAA;EACf,EAAA;EACA,IAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA,GAAgB,gBAAA;EAChB,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;IAAU,GAAA;IAAa,KAAA;EAAA;EACvB,QAAA;EACA,aAAA;EACA,OAAA,cAAqB,eAAA;EACrB,UAAA;IAAA,0HAEE,IAAA,iCAEA;IAAA,eAAA,WAKF;IAHE,OAAA;EAAA;EAOM;EAJR,QAAA;EAIsB;EAFtB,SAAA;EAM0B;EAJ1B,KAAA,GAAQ,cAAA;AAAA;AAAA,UAIO,WAAA;EACf,EAAA;EACA,IAAA;AAAA;AAAA,UAGe,qBAAA;EACf,MAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;AAAA,KAGL,gBAAA,8BAA8C,CAAA,eAC/C,IAAA,CAAK,CAAA,EAAG,IAAA;AAAA,KAGA,iBAAA,GAAoB,gBAAA,CAC9B,cAAA;AAAA,UAIe,gBAAA;EACf,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,WAAA;EACf,WAAA;EACA,GAAA;AAAA;AAAA,UAGe,cAAA;EACf,OAAA,GAAU,WAAA;EACV,QAAA,GAAW,WAAA;EACX,WAAA,GAAc,WAAA;AAAA;AAAA,UAGC,cAAA;EACf,IAAA,EAAM,aAAA;EACN,eAAA,EAAiB,qBAAA;EACjB,eAAA,EAAiB,iBAAA;EACjB,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,KAAA;EACR,YAAA,EAAc,WAAA;EACd,QAAA,GAAW,cAAA;AAAA"}
1
+ {"version":3,"file":"appCatalogTypes.d.mts","names":[],"sources":["../../../src/types/common/appCatalogTypes.ts"],"mappings":";;;;;;;;UAmBiB,WAAA;EACf,QAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA;EACA,aAAA,GAAgB,gBAAA;AAAA;;;;;UAWD,eAAA;EACf,UAAA;EACA,GAAA;EACA,SAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAEA;EAAA,IAAA;EACA,WAAA;EACA,YAAA;EACA,SAAA;EACA,WAAA;EACA,KAAA;EACA,aAAA,GAAgB,gBAAA;EAChB,KAAA;EACA,IAAA;EACA,MAAA;EACA,KAAA;IAAU,GAAA;IAAa,KAAA;EAAA;EACvB,QAAA;EACA,aAAA;EACA,OAAA,cAAqB,eAAA;EACrB,UAAA;IAEE,0HAAA,IAAA,iCAIA;IAFA,eAAA,WAOF;IALE,OAAA;EAAA;EAWF;EARA,QAAA;EAYA;EAVA,SAAA;EAcA;EAZA,KAAA,GAAQ,WAAA;EAgBR;EAZA,UAAA;EAcQ;EAZR,IAAA;EAYc;EAVd,UAAA;EAc0B;EAZ1B,OAAA;EAaA;EAXA,eAAA;EAee;EAbf,0BAAA;;EAEA,cAAA;EAYA;EAVA,KAAA,GAAQ,MAAA;AAAA;AAAA,UAIO,WAAA;EACf,EAAA;EACA,IAAA;AAAA;AAAA,UAGe,qBAAA;EACf,MAAA;EACA,WAAA;EACA,WAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;AAAA,KAGL,gBAAA,8BAA8C,CAAA,eAC/C,IAAA,CAAK,CAAA,EAAG,IAAA;AAAA,KAGA,iBAAA,GAAoB,gBAAA,CAC9B,cAAA;AAAA,UAIe,gBAAA;EACf,KAAA;EACA,WAAA;EACA,WAAA;AAAA;AAAA,UAGe,WAAA;EACf,WAAA;EACA,GAAA;AAAA;AAAA,UAGe,cAAA;EACf,OAAA,GAAU,WAAA;EACV,QAAA,GAAW,WAAA;EACX,WAAA,GAAc,WAAA;AAAA;AAAA,UAGC,cAAA;EACf,SAAA,EAAW,QAAA;EACX,eAAA,EAAiB,qBAAA;EACjB,eAAA,EAAiB,iBAAA;EACjB,OAAA,EAAS,MAAA;EACT,MAAA,EAAQ,KAAA;EACR,QAAA,GAAW,cAAA;AAAA"}
@@ -116,6 +116,10 @@ interface AppAccessRequest {
116
116
  */
117
117
  urls?: ApprovalUrl[];
118
118
  }
119
+ /** @deprecated Use AppAccessRequest instead (same type, new canonical name) */
120
+ type AccessRequest = AppAccessRequest;
121
+ /** @deprecated Use AppRole instead (same type, new canonical name) */
122
+ type Role = AppRole;
119
123
  //#endregion
120
- export { AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, ServiceConfig };
124
+ export { AccessRequest, AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, Role, ServiceConfig };
121
125
  //# sourceMappingURL=approvalMethodTypes.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;AAAA;;;AAcF;UARiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;AAaF;;;AAAA,UAPiB,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EADwB;;;EAKxB,WAAA;EAUY;;;EANZ,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;UAWG,OAAA;EAZX;;;EAgBJ,WAAA;EAfwB;AAW1B;;EAQE,WAAA;EARsB;;;EAYtB,UAAA;AAAA;;AAMF;;UAAiB,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;;UAMe,gBAAA;EASf;;;EALA,kBAAA;EAkBQ;;;EAbR,QAAA;EAsBkB;;;EAlBlB,aAAA;;;;EAIA,wBAAA;;;;EAKA,KAAA,GAAQ,OAAA;;;;EAKR,mBAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA"}
1
+ {"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;AAAA;;;AAcF;UARiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;AAaF;;;AAAA,UAPiB,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EADwB;;;EAKxB,WAAA;EAUY;;;EANZ,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;;UAWG,OAAA;EAZX;;;EAgBJ,WAAA;EAfwB;AAW1B;;EAQE,WAAA;EARsB;;;EAYtB,UAAA;AAAA;;AAMF;;UAAiB,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;;UAMe,gBAAA;EASf;;;EALA,kBAAA;EAkBQ;;;EAbR,QAAA;EAsBkB;;AAIpB;EAtBE,aAAA;;;;EAIA,wBAAA;EAqBc;;;EAhBd,KAAA,GAAQ,OAAA;;;;EAKR,mBAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA;;KAIG,aAAA,GAAgB,gBAAA;;KAGhB,IAAA,GAAO,OAAA"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "0.3.1-alpha-20260405015231",
3
+ "version": "0.4.0",
4
4
  "description": "Backend core library for App Catalog",
5
5
  "homepage": "https://github.com/lislon/app-catalog",
6
6
  "repository": {
@@ -45,8 +45,8 @@
45
45
  "tsyringe": "^4.10.0",
46
46
  "yaml": "^2.8.0",
47
47
  "zod": "^4.3.5",
48
- "@igstack/app-catalog-shared-core": "0.3.1-alpha-20260405015231",
49
- "@igstack/app-catalog-table-sync": "0.3.1-alpha-20260405015231"
48
+ "@igstack/app-catalog-shared-core": "0.4.0",
49
+ "@igstack/app-catalog-table-sync": "0.4.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tanstack/vite-config": "^0.4.3",
@@ -83,22 +83,22 @@ enum ApprovalMethodType {
83
83
  // ========== Person & Group Models ==========
84
84
 
85
85
  model DbPerson {
86
- slug String @id // email or synthetic
86
+ slug String @id // email or synthetic
87
87
  firstName String
88
88
  lastName String
89
- email String? @unique
89
+ email String? @unique
90
90
  memberships DbGroupMembership[]
91
- createdAt DateTime @default(now())
92
- updatedAt DateTime @updatedAt
91
+ createdAt DateTime @default(now())
92
+ updatedAt DateTime @updatedAt
93
93
  }
94
94
 
95
95
  model DbGroup {
96
- slug String @id
96
+ slug String @id
97
97
  displayName String?
98
- email String? // team alias
98
+ email String? // team alias
99
99
  memberships DbGroupMembership[]
100
- createdAt DateTime @default(now())
101
- updatedAt DateTime @updatedAt
100
+ createdAt DateTime @default(now())
101
+ updatedAt DateTime @updatedAt
102
102
  }
103
103
 
104
104
  model DbGroupMembership {
@@ -124,62 +124,53 @@ model DbApprovalMethod {
124
124
  @@unique([type, displayName])
125
125
  }
126
126
 
127
- model DbAppForCatalog {
128
- id String @id @default(cuid())
129
- slug String @unique // URL-friendly identifier for navigation
130
- displayName String
131
- abbreviation String? // Optional short abbreviation (e.g. K8s, ECR, LV)
132
- nicknames String[] @default([]) // Alternative names / AKA
133
- description String
127
+ model DbResource {
128
+ id String @id @default(cuid())
129
+ slug String @unique // URL-friendly identifier for navigation
130
+ type String @default("application") // "application" | "sub-resource" | etc.
131
+ displayName String
132
+ abbreviation String? // Optional short abbreviation (e.g. K8s, ECR, LV)
133
+ nicknames String[] @default([]) // Alternative names / AKA
134
+ description String
134
135
  /// [AccessMethod]
135
- access Json?
136
- teams String[] @default([])
137
- /// [AppAccessRequest] - Per-app approval configuration linking to ApprovalMethod
138
- accessRequest Json?
139
- notes String?
140
- tags String[] @default([])
141
- appUrl String?
136
+ access Json?
137
+ teams String[] @default([])
138
+ /// [AccessRequest] - Per-resource approval configuration linking to ApprovalMethod
139
+ accessRequest Json?
140
+ notes String?
141
+ tags String[] @default([])
142
+ appUrl String?
142
143
  /// [AppLink[]]
143
- links Json? // Array of {displayName?: string, url: string}
144
- iconName String? // Optional icon identifier matching DbAsset.name
145
- screenshotIds String[] @default([]) // Ordered array of DbAsset IDs
144
+ links Json? // Array of {displayName?: string, url: string}
145
+ iconName String? // Optional icon identifier matching DbAsset.name
146
+ screenshotIds String[] @default([]) // Ordered array of DbAsset IDs
146
147
  /// [AppDeprecation] - Deprecation info with optional replacement slug
147
- deprecated Json?
148
- aiPrompt String?
149
- urlIssues String[] @default([])
150
- /// [AppTierVariant[]]
151
- tiers Json?
152
- sourceRefs SourceReference[]
153
- subResources DbSubResource[]
154
- createdAt DateTime @default(now())
155
- updatedAt DateTime @updatedAt
156
-
157
- @@index([displayName])
158
- @@index([slug])
159
- @@index([tags])
160
- }
161
-
162
- model DbSubResource {
163
- id String @id @default(cuid())
164
- slug String @unique
165
- displayName String
166
- description String?
167
- appSlug String
168
- app DbAppForCatalog @relation(fields: [appSlug], references: [slug], onDelete: Cascade)
169
- familySlug String? // groups tier variants visually
170
- tierSlug String?
171
- aliases String[] @default([])
148
+ deprecated Json?
149
+ aiPrompt String?
150
+ urlIssues String[] @default([])
151
+ /// [TierVariant[]]
152
+ tiers Json?
153
+ sourceRefs SourceReference[]
154
+ // Self-referential tree: sub-resources point to their parent
155
+ parentSlug String?
156
+ parent DbResource? @relation("ResourceTree", fields: [parentSlug], references: [slug])
157
+ children DbResource[] @relation("ResourceTree")
158
+ // Fields merged from former DbSubResource
159
+ tier String? // e.g. "prod", "dev"
160
+ familySlug String? // groups tier variants visually
161
+ aliases String[] @default([])
172
162
  ownerPersonSlug String?
173
- accessMaintainerGroupSlugs String[] @default([])
174
- /// [AppAccessRequest]
175
- accessRequest Json?
163
+ accessMaintainerGroupSlugs String[] @default([])
176
164
  accessComments String?
177
165
  /// [SubResourceExtra]
178
166
  extra Json?
179
- createdAt DateTime @default(now())
180
- updatedAt DateTime @updatedAt
167
+ createdAt DateTime @default(now())
168
+ updatedAt DateTime @updatedAt
181
169
 
182
- @@index([appSlug])
170
+ @@index([displayName])
171
+ @@index([slug])
172
+ @@index([tags])
173
+ @@index([parentSlug])
183
174
  }
184
175
 
185
176
  model DbAppTagDefinition {
@@ -222,15 +213,15 @@ model Source {
222
213
  }
223
214
 
224
215
  model SourceReference {
225
- id Int @id @default(autoincrement())
216
+ id Int @id @default(autoincrement())
226
217
  sourceSlug String
227
- source Source @relation(fields: [sourceSlug], references: [slug], onDelete: Cascade)
228
- appId String
229
- app DbAppForCatalog @relation(fields: [appId], references: [id], onDelete: Cascade)
218
+ source Source @relation(fields: [sourceSlug], references: [slug], onDelete: Cascade)
219
+ resourceId String
220
+ resource DbResource @relation(fields: [resourceId], references: [id], onDelete: Cascade)
230
221
  url String
231
222
  parseDate DateTime?
232
- excerpts String[] @default([])
223
+ excerpts String[] @default([])
233
224
  userPrompt String?
234
225
 
235
- @@unique([appId, url])
226
+ @@unique([resourceId, url])
236
227
  }