@igstack/app-catalog-backend-core 0.18.0 → 0.18.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.
@@ -167,7 +167,8 @@ async function syncAppCatalog(resources, tagsDefinitions, approvalMethods, scree
167
167
  extra: resource.extra ?? null,
168
168
  lastCheckedAt: resource.lastCheckedAt ? new Date(resource.lastCheckedAt) : null,
169
169
  nextCheckAfter: resource.nextCheckAfter ? new Date(resource.nextCheckAfter) : null,
170
- lastContentChangeAt: resource.lastContentChangeAt ? new Date(resource.lastContentChangeAt) : null
170
+ lastContentChangeAt: resource.lastContentChangeAt ? new Date(resource.lastContentChangeAt) : null,
171
+ createdAt: resource.catalogAddedAt ? new Date(resource.catalogAddedAt) : void 0
171
172
  };
172
173
  });
173
174
  const result = await sync.sync(dbResources);
@@ -1 +1 @@
1
- {"version":3,"file":"syncAppCatalog.mjs","names":[],"sources":["../../src/db/syncAppCatalog.ts"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\n} from '../types/common/appCatalogTypes'\nimport { getDbClient } from './client'\nimport { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'\nimport { tableSyncPrisma } from './tableSyncPrismaAdapter'\nimport { readFile, readdir, stat } from 'node:fs/promises'\nimport { group } from 'radashi'\nimport { upsertAsset } from '../modules/assets/upsertAsset'\nimport type { ApprovalMethod, Group, Person } from '../types'\nimport type { PrismaClient } from '../generated/prisma/client'\nimport { naturalSort } from '../utils/naturalSort'\nimport { parseSourceSlug } from '../utils/parseSourceSlug'\n\nexport interface SyncAppCatalogResult {\n created: number\n updated: number\n deleted: number\n total: number\n}\n\ninterface AssetSyncResult {\n screenshotIds: string[]\n iconName: string | null\n}\n\nfunction isFileNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n 'code' in error &&\n (error as NodeJS.ErrnoException).code === 'ENOENT'\n )\n}\n\nasync function processAssetDirectory(\n dirPath: string,\n appSlug: string,\n assetType: 'screenshot' | 'icon',\n prisma: PrismaClient,\n): Promise<string[]> {\n try {\n const files = await readdir(dirPath)\n const sortedFiles = naturalSort(files)\n const assetIds: string[] = []\n\n for (let i = 0; i < sortedFiles.length; i++) {\n const fileName = sortedFiles[i]\n if (!fileName) continue\n\n const assetName =\n assetType === 'screenshot'\n ? `${appSlug}-screenshot-${i + 1}`\n : `${appSlug}-icon`\n\n const id = await upsertAsset({\n prisma,\n buffer: await readFile(`${dirPath}/${fileName}`),\n originalFilename: fileName,\n name: assetName,\n assetType,\n })\n assetIds.push(id)\n\n // For icons, only process the first file\n if (assetType === 'icon') {\n break\n }\n }\n\n return assetIds\n } catch (error: unknown) {\n if (isFileNotFoundError(error)) {\n return []\n }\n throw error\n }\n}\n\nasync function syncAppAssets(\n appSlug: string,\n appPath: string,\n prisma: PrismaClient,\n): Promise<AssetSyncResult> {\n const screenshotIds = await processAssetDirectory(\n `${appPath}/screenshots`,\n appSlug,\n 'screenshot',\n prisma,\n )\n\n const iconIds = await processAssetDirectory(\n `${appPath}/icons`,\n appSlug,\n 'icon',\n prisma,\n )\n\n return {\n screenshotIds,\n iconName: iconIds.length > 0 ? `${appSlug}-icon` : null,\n }\n}\n\nasync function syncAssetsFromFileSystem(\n resources: Resource[],\n allAppsAssetsPath: string,\n) {\n const appDirectories = await readdir(allAppsAssetsPath)\n const prisma = getDbClient()\n const bySlug = group(resources, (a) => a.slug)\n\n for (const appDirName of appDirectories) {\n try {\n const stats = await stat(`${allAppsAssetsPath}/${appDirName}`)\n if (!stats.isDirectory()) {\n continue\n }\n } catch (error: unknown) {\n if (isFileNotFoundError(error)) {\n continue\n }\n throw error\n }\n\n const appSlug = appDirName\n if (!bySlug[appSlug]) {\n throw new Error(\n `App '${appSlug}' does not exist in the app catalog. Existing apps: ${Object.keys(bySlug).join(', ')}`,\n )\n }\n\n try {\n const { screenshotIds, iconName } = await syncAppAssets(\n appSlug,\n `${allAppsAssetsPath}/${appDirName}`,\n prisma,\n )\n\n const updateData: {\n screenshotIds?: string[]\n iconName?: string | null\n } = {}\n\n if (screenshotIds.length > 0) {\n updateData.screenshotIds = screenshotIds\n }\n if (iconName !== null) {\n updateData.iconName = iconName\n }\n\n if (Object.keys(updateData).length > 0) {\n await prisma.dbResource.update({\n where: { slug: appSlug },\n data: updateData,\n })\n }\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n throw new Error(\n `Error while upserting assets for app '${appSlug}': ${errorMessage}`,\n )\n }\n }\n}\n\n/**\n * Optional data to sync alongside the core app catalog.\n */\nexport interface SyncAppCatalogOptions {\n persons?: Person[]\n groups?: Group[]\n}\n\n/**\n * Syncs app catalog data to the database using table sync.\n * This will create new resources, update existing ones, and delete any that are no longer in the input.\n *\n * Note: Call connectDb() before and disconnectDb() after if running in a script.\n */\nexport async function syncAppCatalog(\n resources: Resource[],\n tagsDefinitions: GroupingTagDefinition[],\n approvalMethods: ApprovalMethod[],\n screenshotsPath?: string,\n options?: SyncAppCatalogOptions,\n): Promise<SyncAppCatalogResult> {\n try {\n const prisma = getDbClient()\n\n // Sync Persons first (groups depend on persons via memberships)\n if (options?.persons) {\n const dbPersons = options.persons.map((p) => ({\n slug: p.slug,\n firstName: p.firstName,\n lastName: p.lastName,\n email: p.email ?? null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbPerson,\n }).sync(dbPersons)\n }\n\n // Sync Groups (without memberships first)\n if (options?.groups) {\n const dbGroups = options.groups.map((g) => ({\n slug: g.slug,\n displayName: g.displayName ?? null,\n email: g.email ?? null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbGroup,\n }).sync(dbGroups)\n\n // Now sync GroupMemberships\n const allMemberships = options.groups.flatMap((g) =>\n g.memberSlugs.map((personSlug) => ({\n groupSlug: g.slug,\n personSlug,\n })),\n )\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbGroupMembership,\n }).sync(allMemberships)\n }\n\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbApprovalMethod,\n }).sync(approvalMethods)\n\n const sync = tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbResource,\n })\n\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbAppTagDefinition,\n }).sync(tagsDefinitions)\n\n // Collect all unique source slugs for sync\n const uniqueSourceSlugs = new Set<string>()\n for (const resource of resources) {\n for (const source of resource.sources ?? []) {\n const url = typeof source === 'string' ? source : source.url\n const sourceSlug = parseSourceSlug(url)\n uniqueSourceSlugs.add(sourceSlug)\n }\n }\n\n // Sync Source entries using tableSyncPrisma\n const sources = Array.from(uniqueSourceSlugs).map((slug) => ({\n slug,\n userPrompt: null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.Source,\n }).sync(sources)\n\n // Sort resources: parents first (no parentSlug), then children — for FK integrity\n const sortedResources = [...resources].sort((a, b) => {\n const aIsChild = a.parentSlug ? 1 : 0\n const bIsChild = b.parentSlug ? 1 : 0\n return aIsChild - bIsChild\n })\n\n // Transform Resource to DbResource format (scalar fields only)\n const dbResources = sortedResources.map((resource) => {\n const slug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return {\n slug,\n type: resource.type ?? 'application',\n displayName: resource.displayName,\n abbreviation: resource.abbreviation ?? null,\n nicknames: resource.nicknames ?? [],\n description: resource.description,\n teams: resource.teams ?? [],\n accessRequest: resource.accessRequest ?? null,\n notes: resource.notes ?? null,\n tags: resource.tags ?? [],\n appUrl: resource.appUrl ?? null,\n links: resource.links ?? null,\n iconName: resource.iconName ?? null,\n screenshotIds: resource.screenshotIds ?? [],\n deprecated: resource.deprecated ?? null,\n aiPrompt: resource.aiPrompt ?? null,\n // aiMemory intentionally excluded from bulk sync — it is AI-owned and\n // preserved across restarts. Static-config values are seeded below (if DB is null).\n urlIssues: resource.urlIssues ?? [],\n tiers: resource.tiers ?? null,\n // Fields from former SubResource\n parentSlug: resource.parentSlug ?? null,\n tier: resource.tier ?? null,\n familySlug: resource.familySlug ?? null,\n aliases: resource.aliases ?? [],\n ownerPersonSlug: resource.ownerPersonSlug ?? null,\n accessMaintainerGroupSlugs: resource.accessMaintainerGroupSlugs ?? [],\n accessComments: resource.accessComments ?? null,\n extra: resource.extra ?? null,\n lastCheckedAt: resource.lastCheckedAt\n ? new Date(resource.lastCheckedAt)\n : null,\n nextCheckAfter: resource.nextCheckAfter\n ? new Date(resource.nextCheckAfter)\n : null,\n lastContentChangeAt: resource.lastContentChangeAt\n ? new Date(resource.lastContentChangeAt)\n : null,\n }\n })\n\n // Sync resources\n const result = await sync.sync(dbResources)\n\n // Resolve slug -> id for synced resources so SourceReference can reference by resourceId\n const slugs = dbResources.map((a) => a.slug)\n const resourceRows = await prisma.dbResource.findMany({\n where: { slug: { in: slugs } },\n select: { slug: true, id: true },\n })\n const slugToId = Object.fromEntries(resourceRows.map((r) => [r.slug, r.id]))\n\n // Build allSourceRefs with resourceId (slug already resolved to id)\n const allSourceRefs = sortedResources.flatMap((resource) => {\n const resourceSlug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n const resourceId = slugToId[resourceSlug]\n if (!resourceId) {\n throw new Error(\n `Resource '${resourceSlug}' has no id after sync. Existing slugs: ${Object.keys(slugToId).join(', ')}`,\n )\n }\n\n return (resource.sources ?? []).map((source) => {\n const url = typeof source === 'string' ? source : source.url\n const sourceSlug = parseSourceSlug(url)\n return {\n resourceId,\n sourceSlug,\n url,\n parseDate: null,\n excerpts: [],\n userPrompt: null,\n }\n })\n })\n\n // Then sync all SourceReferences (with set semantics: old ones deleted, new ones created)\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.SourceReference,\n }).sync(allSourceRefs)\n\n // Seed aiMemory only for resources where the static config provides an initial value\n // and the DB currently has none. This preserves AI-written updates across restarts.\n const resourcesWithAiMemorySeed = sortedResources.filter(\n (r) => r.aiMemory != null && r.aiMemory.trim() !== '',\n )\n if (resourcesWithAiMemorySeed.length > 0) {\n for (const resource of resourcesWithAiMemorySeed) {\n const slug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n await prisma.dbResource.updateMany({\n where: { slug, aiMemory: null },\n data: { aiMemory: resource.aiMemory },\n })\n }\n }\n\n // Get actual synced data to calculate stats\n const actual = result.getActual()\n\n if (screenshotsPath) {\n await syncAssetsFromFileSystem(resources, screenshotsPath)\n } else {\n console.warn('Do not sync screenhots')\n }\n\n return {\n created:\n actual.length - resources.length + (resources.length - actual.length),\n updated: 0, // TableSync doesn't expose this directly\n deleted: 0, // TableSync doesn't expose this directly\n total: actual.length,\n }\n } catch (error) {\n // Wrap error with context\n const errorMessage = error instanceof Error ? error.message : String(error)\n const errorStack = error instanceof Error ? error.stack : undefined\n\n throw new Error(\n `Error syncing app catalog: ${errorMessage}\\n\\nDetails:\\n${errorStack || 'No stack trace available'}`,\n )\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"syncAppCatalog.mjs","names":[],"sources":["../../src/db/syncAppCatalog.ts"],"sourcesContent":["import type {\n GroupingTagDefinition,\n Resource,\n} from '../types/common/appCatalogTypes'\nimport { getDbClient } from './client'\nimport { TABLE_SYNC_MAGAZINE } from './tableSyncMagazine'\nimport { tableSyncPrisma } from './tableSyncPrismaAdapter'\nimport { readFile, readdir, stat } from 'node:fs/promises'\nimport { group } from 'radashi'\nimport { upsertAsset } from '../modules/assets/upsertAsset'\nimport type { ApprovalMethod, Group, Person } from '../types'\nimport type { PrismaClient } from '../generated/prisma/client'\nimport { naturalSort } from '../utils/naturalSort'\nimport { parseSourceSlug } from '../utils/parseSourceSlug'\n\nexport interface SyncAppCatalogResult {\n created: number\n updated: number\n deleted: number\n total: number\n}\n\ninterface AssetSyncResult {\n screenshotIds: string[]\n iconName: string | null\n}\n\nfunction isFileNotFoundError(error: unknown): boolean {\n return (\n error instanceof Error &&\n 'code' in error &&\n (error as NodeJS.ErrnoException).code === 'ENOENT'\n )\n}\n\nasync function processAssetDirectory(\n dirPath: string,\n appSlug: string,\n assetType: 'screenshot' | 'icon',\n prisma: PrismaClient,\n): Promise<string[]> {\n try {\n const files = await readdir(dirPath)\n const sortedFiles = naturalSort(files)\n const assetIds: string[] = []\n\n for (let i = 0; i < sortedFiles.length; i++) {\n const fileName = sortedFiles[i]\n if (!fileName) continue\n\n const assetName =\n assetType === 'screenshot'\n ? `${appSlug}-screenshot-${i + 1}`\n : `${appSlug}-icon`\n\n const id = await upsertAsset({\n prisma,\n buffer: await readFile(`${dirPath}/${fileName}`),\n originalFilename: fileName,\n name: assetName,\n assetType,\n })\n assetIds.push(id)\n\n // For icons, only process the first file\n if (assetType === 'icon') {\n break\n }\n }\n\n return assetIds\n } catch (error: unknown) {\n if (isFileNotFoundError(error)) {\n return []\n }\n throw error\n }\n}\n\nasync function syncAppAssets(\n appSlug: string,\n appPath: string,\n prisma: PrismaClient,\n): Promise<AssetSyncResult> {\n const screenshotIds = await processAssetDirectory(\n `${appPath}/screenshots`,\n appSlug,\n 'screenshot',\n prisma,\n )\n\n const iconIds = await processAssetDirectory(\n `${appPath}/icons`,\n appSlug,\n 'icon',\n prisma,\n )\n\n return {\n screenshotIds,\n iconName: iconIds.length > 0 ? `${appSlug}-icon` : null,\n }\n}\n\nasync function syncAssetsFromFileSystem(\n resources: Resource[],\n allAppsAssetsPath: string,\n) {\n const appDirectories = await readdir(allAppsAssetsPath)\n const prisma = getDbClient()\n const bySlug = group(resources, (a) => a.slug)\n\n for (const appDirName of appDirectories) {\n try {\n const stats = await stat(`${allAppsAssetsPath}/${appDirName}`)\n if (!stats.isDirectory()) {\n continue\n }\n } catch (error: unknown) {\n if (isFileNotFoundError(error)) {\n continue\n }\n throw error\n }\n\n const appSlug = appDirName\n if (!bySlug[appSlug]) {\n throw new Error(\n `App '${appSlug}' does not exist in the app catalog. Existing apps: ${Object.keys(bySlug).join(', ')}`,\n )\n }\n\n try {\n const { screenshotIds, iconName } = await syncAppAssets(\n appSlug,\n `${allAppsAssetsPath}/${appDirName}`,\n prisma,\n )\n\n const updateData: {\n screenshotIds?: string[]\n iconName?: string | null\n } = {}\n\n if (screenshotIds.length > 0) {\n updateData.screenshotIds = screenshotIds\n }\n if (iconName !== null) {\n updateData.iconName = iconName\n }\n\n if (Object.keys(updateData).length > 0) {\n await prisma.dbResource.update({\n where: { slug: appSlug },\n data: updateData,\n })\n }\n } catch (error: unknown) {\n const errorMessage =\n error instanceof Error ? error.message : String(error)\n throw new Error(\n `Error while upserting assets for app '${appSlug}': ${errorMessage}`,\n )\n }\n }\n}\n\n/**\n * Optional data to sync alongside the core app catalog.\n */\nexport interface SyncAppCatalogOptions {\n persons?: Person[]\n groups?: Group[]\n}\n\n/**\n * Syncs app catalog data to the database using table sync.\n * This will create new resources, update existing ones, and delete any that are no longer in the input.\n *\n * Note: Call connectDb() before and disconnectDb() after if running in a script.\n */\nexport async function syncAppCatalog(\n resources: Resource[],\n tagsDefinitions: GroupingTagDefinition[],\n approvalMethods: ApprovalMethod[],\n screenshotsPath?: string,\n options?: SyncAppCatalogOptions,\n): Promise<SyncAppCatalogResult> {\n try {\n const prisma = getDbClient()\n\n // Sync Persons first (groups depend on persons via memberships)\n if (options?.persons) {\n const dbPersons = options.persons.map((p) => ({\n slug: p.slug,\n firstName: p.firstName,\n lastName: p.lastName,\n email: p.email ?? null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbPerson,\n }).sync(dbPersons)\n }\n\n // Sync Groups (without memberships first)\n if (options?.groups) {\n const dbGroups = options.groups.map((g) => ({\n slug: g.slug,\n displayName: g.displayName ?? null,\n email: g.email ?? null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbGroup,\n }).sync(dbGroups)\n\n // Now sync GroupMemberships\n const allMemberships = options.groups.flatMap((g) =>\n g.memberSlugs.map((personSlug) => ({\n groupSlug: g.slug,\n personSlug,\n })),\n )\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbGroupMembership,\n }).sync(allMemberships)\n }\n\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbApprovalMethod,\n }).sync(approvalMethods)\n\n const sync = tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbResource,\n })\n\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.DbAppTagDefinition,\n }).sync(tagsDefinitions)\n\n // Collect all unique source slugs for sync\n const uniqueSourceSlugs = new Set<string>()\n for (const resource of resources) {\n for (const source of resource.sources ?? []) {\n const url = typeof source === 'string' ? source : source.url\n const sourceSlug = parseSourceSlug(url)\n uniqueSourceSlugs.add(sourceSlug)\n }\n }\n\n // Sync Source entries using tableSyncPrisma\n const sources = Array.from(uniqueSourceSlugs).map((slug) => ({\n slug,\n userPrompt: null,\n }))\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.Source,\n }).sync(sources)\n\n // Sort resources: parents first (no parentSlug), then children — for FK integrity\n const sortedResources = [...resources].sort((a, b) => {\n const aIsChild = a.parentSlug ? 1 : 0\n const bIsChild = b.parentSlug ? 1 : 0\n return aIsChild - bIsChild\n })\n\n // Transform Resource to DbResource format (scalar fields only)\n const dbResources = sortedResources.map((resource) => {\n const slug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n\n return {\n slug,\n type: resource.type ?? 'application',\n displayName: resource.displayName,\n abbreviation: resource.abbreviation ?? null,\n nicknames: resource.nicknames ?? [],\n description: resource.description,\n teams: resource.teams ?? [],\n accessRequest: resource.accessRequest ?? null,\n notes: resource.notes ?? null,\n tags: resource.tags ?? [],\n appUrl: resource.appUrl ?? null,\n links: resource.links ?? null,\n iconName: resource.iconName ?? null,\n screenshotIds: resource.screenshotIds ?? [],\n deprecated: resource.deprecated ?? null,\n aiPrompt: resource.aiPrompt ?? null,\n // aiMemory intentionally excluded from bulk sync — it is AI-owned and\n // preserved across restarts. Static-config values are seeded below (if DB is null).\n urlIssues: resource.urlIssues ?? [],\n tiers: resource.tiers ?? null,\n // Fields from former SubResource\n parentSlug: resource.parentSlug ?? null,\n tier: resource.tier ?? null,\n familySlug: resource.familySlug ?? null,\n aliases: resource.aliases ?? [],\n ownerPersonSlug: resource.ownerPersonSlug ?? null,\n accessMaintainerGroupSlugs: resource.accessMaintainerGroupSlugs ?? [],\n accessComments: resource.accessComments ?? null,\n extra: resource.extra ?? null,\n lastCheckedAt: resource.lastCheckedAt\n ? new Date(resource.lastCheckedAt)\n : null,\n nextCheckAfter: resource.nextCheckAfter\n ? new Date(resource.nextCheckAfter)\n : null,\n lastContentChangeAt: resource.lastContentChangeAt\n ? new Date(resource.lastContentChangeAt)\n : null,\n createdAt: resource.catalogAddedAt\n ? new Date(resource.catalogAddedAt)\n : undefined,\n }\n })\n\n // Sync resources\n const result = await sync.sync(dbResources)\n\n // Resolve slug -> id for synced resources so SourceReference can reference by resourceId\n const slugs = dbResources.map((a) => a.slug)\n const resourceRows = await prisma.dbResource.findMany({\n where: { slug: { in: slugs } },\n select: { slug: true, id: true },\n })\n const slugToId = Object.fromEntries(resourceRows.map((r) => [r.slug, r.id]))\n\n // Build allSourceRefs with resourceId (slug already resolved to id)\n const allSourceRefs = sortedResources.flatMap((resource) => {\n const resourceSlug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n const resourceId = slugToId[resourceSlug]\n if (!resourceId) {\n throw new Error(\n `Resource '${resourceSlug}' has no id after sync. Existing slugs: ${Object.keys(slugToId).join(', ')}`,\n )\n }\n\n return (resource.sources ?? []).map((source) => {\n const url = typeof source === 'string' ? source : source.url\n const sourceSlug = parseSourceSlug(url)\n return {\n resourceId,\n sourceSlug,\n url,\n parseDate: null,\n excerpts: [],\n userPrompt: null,\n }\n })\n })\n\n // Then sync all SourceReferences (with set semantics: old ones deleted, new ones created)\n await tableSyncPrisma({\n prisma,\n ...TABLE_SYNC_MAGAZINE.SourceReference,\n }).sync(allSourceRefs)\n\n // Seed aiMemory only for resources where the static config provides an initial value\n // and the DB currently has none. This preserves AI-written updates across restarts.\n const resourcesWithAiMemorySeed = sortedResources.filter(\n (r) => r.aiMemory != null && r.aiMemory.trim() !== '',\n )\n if (resourcesWithAiMemorySeed.length > 0) {\n for (const resource of resourcesWithAiMemorySeed) {\n const slug =\n resource.slug ||\n resource.displayName\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '')\n await prisma.dbResource.updateMany({\n where: { slug, aiMemory: null },\n data: { aiMemory: resource.aiMemory },\n })\n }\n }\n\n // Get actual synced data to calculate stats\n const actual = result.getActual()\n\n if (screenshotsPath) {\n await syncAssetsFromFileSystem(resources, screenshotsPath)\n } else {\n console.warn('Do not sync screenhots')\n }\n\n return {\n created:\n actual.length - resources.length + (resources.length - actual.length),\n updated: 0, // TableSync doesn't expose this directly\n deleted: 0, // TableSync doesn't expose this directly\n total: actual.length,\n }\n } catch (error) {\n // Wrap error with context\n const errorMessage = error instanceof Error ? error.message : String(error)\n const errorStack = error instanceof Error ? error.stack : undefined\n\n throw new Error(\n `Error syncing app catalog: ${errorMessage}\\n\\nDetails:\\n${errorStack || 'No stack trace available'}`,\n )\n }\n}\n"],"mappings":"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"}
@@ -108,6 +108,12 @@ interface Resource {
108
108
  lastCheckedAt?: string | null;
109
109
  nextCheckAfter?: string | null;
110
110
  lastContentChangeAt?: string | null;
111
+ /**
112
+ * ISO-8601 date when this app was first added to the catalog (INPUT — set in
113
+ * static config from git history). Used by the sync to seed DB `createdAt` on
114
+ * first insert and after DB rebuilds. Never changes once set.
115
+ */
116
+ catalogAddedAt?: string;
111
117
  /** ISO-8601 timestamp of when this entry was first created in the catalog DB. */
112
118
  createdAt?: string;
113
119
  }
@@ -1 +1 @@
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,SAAA;EAkBA;EAhBf,aAAA;;;;;;;EAOA,mBAAA;EAmEqB;EAjErB,OAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAQgB;EANhB,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;IAeQ,0HAbN,IAAA,iCAmBF;IAjBE,eAAA,WAqBF;IAnBE,OAAA;EAAA;EAyBF;EAtBA,QAAA;EAwBQ;EAtBR,QAAA;EA6BY;EA3BZ,SAAA;EAkCA;EAhCA,KAAA,GAAQ,WAAA;EAmCR;EA/BA,UAAA;EA+BS;EA7BT,IAAA;EAiC0B;EA/B1B,UAAA;EAgCA;EA9BA,OAAA;EAkCe;EAhCf,eAAA;;EAEA,0BAAA;EA+BA;EA7BA,cAAA;EA+BA;EA7BA,KAAA,GAAQ,MAAA;EA8BA;;;AACT;;;EAxBC,SAAA,GAAY,SAAA;EA2BL;;;;;EArBP,aAAA;EACA,cAAA;EACA,mBAAA;EAmBE;EAjBF,SAAA;AAAA;AAAA,UAIe,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;EADA;EAGA,GAAA;EAAA;EAEA,MAAA;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"}
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,SAAA;EAkBA;EAhBf,aAAA;;;;;;;EAOA,mBAAA;EAmEqB;EAjErB,OAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAQgB;EANhB,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;IAeQ,0HAbN,IAAA,iCAmBF;IAjBE,eAAA,WAqBF;IAnBE,OAAA;EAAA;EAyBF;EAtBA,QAAA;EAwBQ;EAtBR,QAAA;EA6BY;EA3BZ,SAAA;EAkCA;EAhCA,KAAA,GAAQ,WAAA;EAuCR;EAnCA,UAAA;EAqCS;EAnCT,IAAA;EAuCe;EArCf,UAAA;;EAEA,OAAA;EAqCI;EAnCJ,eAAA;EAsCoC;EApCpC,0BAAA;EAwCwB;EAtCxB,cAAA;EAoCA;EAlCA,KAAA,GAAQ,MAAA;EAoCR;;;;AACD;;EA9BC,SAAA,GAAY,SAAA;EAgCqC;;;;;EA1BjD,aAAA;EACA,cAAA;EACA,mBAAA;EAwBiD;;;;;EAlBjD,cAAA;EAsBU;EApBV,SAAA;AAAA;AAAA,UAIe,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;EAIM;EAFN,GAAA;EAK6B;EAH7B,MAAA;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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
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.18.0",
49
- "@igstack/app-catalog-table-sync": "0.18.0"
48
+ "@igstack/app-catalog-shared-core": "0.18.1",
49
+ "@igstack/app-catalog-table-sync": "0.18.1"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@tanstack/vite-config": "^0.4.3",
@@ -73,7 +73,7 @@
73
73
  "engines": {
74
74
  "node": ">=24"
75
75
  },
76
- "gitHead": "88b44c8b8ee7976bb784291646bb6c57ff7b2785",
76
+ "gitHead": "9363a32bc42ff501fb9be4622a5afadf13de13e9",
77
77
  "scripts": {
78
78
  "build": "tsdown",
79
79
  "build:lenient": "tsdown",
@@ -318,6 +318,9 @@ export async function syncAppCatalog(
318
318
  lastContentChangeAt: resource.lastContentChangeAt
319
319
  ? new Date(resource.lastContentChangeAt)
320
320
  : null,
321
+ createdAt: resource.catalogAddedAt
322
+ ? new Date(resource.catalogAddedAt)
323
+ : undefined,
321
324
  }
322
325
  })
323
326
 
@@ -128,6 +128,12 @@ export interface Resource {
128
128
  lastCheckedAt?: string | null
129
129
  nextCheckAfter?: string | null
130
130
  lastContentChangeAt?: string | null
131
+ /**
132
+ * ISO-8601 date when this app was first added to the catalog (INPUT — set in
133
+ * static config from git history). Used by the sync to seed DB `createdAt` on
134
+ * first insert and after DB rebuilds. Never changes once set.
135
+ */
136
+ catalogAddedAt?: string
131
137
  /** ISO-8601 timestamp of when this entry was first created in the catalog DB. */
132
138
  createdAt?: string
133
139
  }