@igstack/app-catalog-backend-core 0.13.3 → 0.14.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/dist/modules/appCatalog/service.mjs +2 -1
- package/dist/modules/appCatalog/service.mjs.map +1 -1
- package/dist/types/common/appCatalogTypes.d.mts +2 -0
- package/dist/types/common/appCatalogTypes.d.mts.map +1 -1
- package/package.json +4 -4
- package/src/modules/appCatalog/service.ts +1 -0
- package/src/types/common/appCatalogTypes.ts +2 -0
|
@@ -121,7 +121,8 @@ function rowToResource(row) {
|
|
|
121
121
|
accessMaintainerGroupSlugs: row.accessMaintainerGroupSlugs.length ? row.accessMaintainerGroupSlugs : void 0,
|
|
122
122
|
accessComments: row.accessComments ?? void 0,
|
|
123
123
|
extra: row.extra ? row.extra : void 0,
|
|
124
|
-
freshness: freshness.lastCheckedAt ? freshness : void 0
|
|
124
|
+
freshness: freshness.lastCheckedAt ? freshness : void 0,
|
|
125
|
+
createdAt: row.createdAt.toISOString()
|
|
125
126
|
};
|
|
126
127
|
}
|
|
127
128
|
async function getResourcesFromPrisma() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.mjs","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"sourcesContent":["import { getDbClient } from '../../db/client'\nimport type { Prisma } from '../../generated/prisma/client'\nimport type {\n AppApprovalMethod,\n AppCatalogData,\n AppCategory,\n GroupingTagDefinition,\n Resource,\n} from '../../types/common/appCatalogTypes'\nimport type {\n CustomConfig,\n ServiceConfig,\n} from '../../types/common/approvalMethodTypes'\nimport type { Group, Person } from '../../types/common/personGroupTypes'\nimport { omit } from 'radashi'\nimport { parseSourceSlug } from '../../utils/parseSourceSlug'\nimport { computeFreshness } from './freshness'\n\n/** Prisma query result for DbResource with sourceRefs included (used by rowToResource) */\ntype ResourceRowWithSourceRefs = Prisma.DbResourceGetPayload<{\n include: { sourceRefs: true }\n}>\n\nfunction capitalize(word: string): string {\n if (!word) return word\n return word.charAt(0).toUpperCase() + word.slice(1)\n}\n\nexport async function getGroupingTagDefinitionsFromPrisma(): Promise<\n GroupingTagDefinition[]\n> {\n const prisma = getDbClient()\n\n // Fetch all tag definitions\n const rows = await prisma.dbAppTagDefinition.findMany()\n\n // Fetch all resources to count tag usage\n const resources = await prisma.dbResource.findMany({\n select: { tags: true },\n })\n\n // Count tag values across all resources\n const tagCounts = new Map<string, Map<string, number>>()\n\n for (const resource of resources) {\n const tags = (resource.tags as unknown as string[] | null) ?? []\n for (const tag of tags) {\n const [prefix, value] = tag.split(':')\n if (prefix && value) {\n if (!tagCounts.has(prefix)) {\n tagCounts.set(prefix, new Map())\n }\n const prefixCounts = tagCounts.get(prefix)!\n prefixCounts.set(value, (prefixCounts.get(value) ?? 0) + 1)\n }\n }\n }\n\n // Sort values by count and return definitions\n return rows.map((row) => {\n const definition = omit(row, ['id', 'updatedAt', 'createdAt'])\n const counts = tagCounts.get(definition.prefix) ?? new Map()\n\n // Sort values by count (descending)\n const sortedValues = [...definition.values].sort((a, b) => {\n const countA = counts.get(a.value) ?? 0\n const countB = counts.get(b.value) ?? 0\n return countB - countA\n })\n\n return {\n ...definition,\n values: sortedValues,\n }\n })\n}\n\nexport async function getApprovalMethodsFromPrisma(): Promise<\n AppApprovalMethod[]\n> {\n const prisma = getDbClient()\n\n // Fetch all approval methods\n const rows = await prisma.dbApprovalMethod.findMany()\n\n return rows.map((row) => {\n // Handle discriminated union by explicitly narrowing based on type\n const baseFields = {\n slug: row.slug,\n displayName: row.displayName,\n }\n\n // Provide default empty config if null, as AppApprovalMethod discriminated union requires config\n const config = row.config ?? {}\n\n switch (row.type) {\n case 'service':\n return {\n ...baseFields,\n type: 'service' as const,\n config: config as ServiceConfig,\n }\n case 'custom':\n return {\n ...baseFields,\n type: 'custom' as const,\n config: config as CustomConfig,\n }\n case 'noAccessRequired':\n return {\n ...baseFields,\n type: 'noAccessRequired' as const,\n config: config as CustomConfig,\n }\n case 'unknown':\n return {\n ...baseFields,\n type: 'unknown' as const,\n config: config as CustomConfig,\n }\n case 'personTeam':\n // Legacy: map personTeam to custom\n return {\n ...baseFields,\n type: 'custom' as const,\n config: config as CustomConfig,\n }\n }\n })\n}\n\nfunction rowToResource(row: ResourceRowWithSourceRefs): Resource {\n const accessRequest =\n row.accessRequest as unknown as Resource['accessRequest']\n const teams = (row.teams as unknown as string[] | null) ?? []\n const tags = (row.tags as unknown as Resource['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as Resource['screenshotIds']) ?? []\n const sources = row.sourceRefs.map((ref) => ({\n sourceSlug: ref.sourceSlug,\n url: ref.url,\n parseDate: ref.parseDate ? ref.parseDate.toISOString() : null,\n }))\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n const abbreviation = row.abbreviation == null ? undefined : row.abbreviation\n const nicknames = (row.nicknames as unknown as string[] | null)?.length\n ? (row.nicknames as unknown as string[])\n : undefined\n const deprecated =\n row.deprecated == null\n ? undefined\n : (row.deprecated as unknown as Resource['deprecated'])\n const aiPrompt = row.aiPrompt == null ? undefined : row.aiPrompt\n const urlIssues = (row.urlIssues as unknown as string[] | null)?.length\n ? (row.urlIssues as unknown as string[])\n : undefined\n const tiers =\n row.tiers == null ? undefined : (row.tiers as unknown as Resource['tiers'])\n const freshness = computeFreshness(\n row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,\n row.nextCheckAfter ? row.nextCheckAfter.toISOString() : null,\n )\n\n return {\n id: row.id,\n slug: row.slug,\n type: row.type,\n displayName: row.displayName,\n abbreviation,\n nicknames,\n description: row.description,\n accessRequest,\n teams,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n aiPrompt,\n urlIssues,\n tiers,\n // Fields from former SubResource\n parentSlug: row.parentSlug ?? undefined,\n tier: row.tier ?? undefined,\n familySlug: row.familySlug ?? undefined,\n aliases: row.aliases.length ? row.aliases : undefined,\n ownerPersonSlug: row.ownerPersonSlug ?? undefined,\n accessMaintainerGroupSlugs: row.accessMaintainerGroupSlugs.length\n ? row.accessMaintainerGroupSlugs\n : undefined,\n accessComments: row.accessComments ?? undefined,\n extra: row.extra\n ? (row.extra as unknown as Record<string, unknown>)\n : undefined,\n // Only surface freshness once the app has actually been scanned.\n freshness: freshness.lastCheckedAt ? freshness : undefined,\n }\n}\n\nexport async function getResourcesFromPrisma(): Promise<Resource[]> {\n const prisma = getDbClient()\n\n const rows = await prisma.dbResource.findMany({\n include: {\n sourceRefs: true,\n },\n })\n\n return rows.map(rowToResource)\n}\n\n/** @deprecated Use getResourcesFromPrisma instead */\nexport const getAppsFromPrisma = getResourcesFromPrisma\n\nexport interface UpdateAppInput {\n id: string\n data: {\n displayName?: string\n abbreviation?: string | null\n slug?: string\n appUrl?: string\n description?: string\n sources?: string[]\n aiPrompt?: string | null\n }\n}\n\nexport async function updateApp(input: UpdateAppInput): Promise<Resource> {\n const prisma = getDbClient()\n const { id, data } = input\n\n const updatePayload: {\n displayName?: string\n abbreviation?: string | null\n slug?: string\n appUrl?: string\n description?: string\n aiPrompt?: string | null\n } = {}\n if (data.displayName !== undefined)\n updatePayload.displayName = data.displayName\n if (data.abbreviation !== undefined)\n updatePayload.abbreviation = data.abbreviation\n if (data.slug !== undefined) updatePayload.slug = data.slug\n if (data.appUrl !== undefined) updatePayload.appUrl = data.appUrl\n if (data.description !== undefined)\n updatePayload.description = data.description\n if (data.aiPrompt !== undefined) updatePayload.aiPrompt = data.aiPrompt\n\n const updated = await prisma.dbResource.update({\n where: { id },\n data: updatePayload,\n include: { sourceRefs: true },\n })\n\n if (data.sources !== undefined) {\n const urls = [...new Set(data.sources.map((u) => u.trim()).filter(Boolean))]\n const uniqueSourceSlugs = [...new Set(urls.map(parseSourceSlug))]\n\n for (const sourceSlug of uniqueSourceSlugs) {\n await prisma.source.upsert({\n where: { slug: sourceSlug },\n create: { slug: sourceSlug },\n update: {},\n })\n }\n\n await prisma.sourceReference.deleteMany({\n where: { resourceId: updated.id },\n })\n\n if (urls.length > 0) {\n await prisma.sourceReference.createMany({\n data: urls.map((url) => ({\n resourceId: updated.id,\n sourceSlug: parseSourceSlug(url),\n url: url.trim(),\n })),\n })\n }\n\n const refetched = await prisma.dbResource.findUnique({\n where: { id },\n include: { sourceRefs: true },\n })\n if (!refetched) throw new Error('Resource not found after update')\n return rowToResource(refetched)\n }\n\n return rowToResource(updated)\n}\n\nexport function deriveCategories(resources: Resource[]): AppCategory[] {\n const tagSet = new Set<string>()\n for (const resource of resources) {\n for (const tag of resource.tags ?? []) {\n const normalized = tag.trim().toLowerCase()\n if (normalized) tagSet.add(normalized)\n }\n }\n const categories: AppCategory[] = [{ id: 'all', name: 'All' }]\n for (const tag of Array.from(tagSet).sort()) {\n categories.push({ id: tag, name: capitalize(tag) })\n }\n return categories\n}\n\nexport async function getPersonsFromPrisma(): Promise<Person[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbPerson.findMany()\n return rows.map((row) => ({\n slug: row.slug,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email ?? undefined,\n }))\n}\n\nexport async function getGroupsFromPrisma(): Promise<Group[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbGroup.findMany({\n include: { memberships: true },\n })\n return rows.map((row) => ({\n slug: row.slug,\n displayName: row.displayName ?? undefined,\n email: row.email ?? undefined,\n memberSlugs: row.memberships.map((m) => m.personSlug),\n }))\n}\n\nexport async function getAppCatalogData(\n getResourcesOptional?: () => Promise<Resource[]>,\n): Promise<AppCatalogData> {\n const resources = getResourcesOptional\n ? await getResourcesOptional()\n : await getResourcesFromPrisma()\n\n return {\n resources,\n tagsDefinitions: await getGroupingTagDefinitionsFromPrisma(),\n approvalMethods: await getApprovalMethodsFromPrisma(),\n persons: await getPersonsFromPrisma(),\n groups: await getGroupsFromPrisma(),\n }\n}\n"],"mappings":";;;;;;AA4BA,eAAsB,sCAEpB;CACA,MAAM,SAAS,aAAa;CAG5B,MAAM,OAAO,MAAM,OAAO,mBAAmB,UAAU;CAGvD,MAAM,YAAY,MAAM,OAAO,WAAW,SAAS,EACjD,QAAQ,EAAE,MAAM,MAAM,EACvB,CAAC;CAGF,MAAM,4BAAY,IAAI,KAAkC;AAExD,MAAK,MAAM,YAAY,WAAW;EAChC,MAAM,OAAQ,SAAS,QAAuC,EAAE;AAChE,OAAK,MAAM,OAAO,MAAM;GACtB,MAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,IAAI;AACtC,OAAI,UAAU,OAAO;AACnB,QAAI,CAAC,UAAU,IAAI,OAAO,CACxB,WAAU,IAAI,wBAAQ,IAAI,KAAK,CAAC;IAElC,MAAM,eAAe,UAAU,IAAI,OAAO;AAC1C,iBAAa,IAAI,QAAQ,aAAa,IAAI,MAAM,IAAI,KAAK,EAAE;;;;AAMjE,QAAO,KAAK,KAAK,QAAQ;EACvB,MAAM,aAAa,KAAK,KAAK;GAAC;GAAM;GAAa;GAAY,CAAC;EAC9D,MAAM,SAAS,UAAU,IAAI,WAAW,OAAO,oBAAI,IAAI,KAAK;EAG5D,MAAM,eAAe,CAAC,GAAG,WAAW,OAAO,CAAC,MAAM,GAAG,MAAM;GACzD,MAAM,SAAS,OAAO,IAAI,EAAE,MAAM,IAAI;AAEtC,WADe,OAAO,IAAI,EAAE,MAAM,IAAI,KACtB;IAChB;AAEF,SAAO;GACL,GAAG;GACH,QAAQ;GACT;GACD;;AAGJ,eAAsB,+BAEpB;AAMA,SAFa,MAHE,aAAa,CAGF,iBAAiB,UAAU,EAEzC,KAAK,QAAQ;EAEvB,MAAM,aAAa;GACjB,MAAM,IAAI;GACV,aAAa,IAAI;GAClB;EAGD,MAAM,SAAS,IAAI,UAAU,EAAE;AAE/B,UAAQ,IAAI,MAAZ;GACE,KAAK,UACH,QAAO;IACL,GAAG;IACH,MAAM;IACE;IACT;GACH,KAAK,SACH,QAAO;IACL,GAAG;IACH,MAAM;IACE;IACT;GACH,KAAK,mBACH,QAAO;IACL,GAAG;IACH,MAAM;IACE;IACT;GACH,KAAK,UACH,QAAO;IACL,GAAG;IACH,MAAM;IACE;IACT;GACH,KAAK,aAEH,QAAO;IACL,GAAG;IACH,MAAM;IACE;IACT;;GAEL;;AAGJ,SAAS,cAAc,KAA0C;CAC/D,MAAM,gBACJ,IAAI;CACN,MAAM,QAAS,IAAI,SAAwC,EAAE;CAC7D,MAAM,OAAQ,IAAI,QAAwC,EAAE;CAC5D,MAAM,gBACH,IAAI,iBAA0D,EAAE;CACnE,MAAM,UAAU,IAAI,WAAW,KAAK,SAAS;EAC3C,YAAY,IAAI;EAChB,KAAK,IAAI;EACT,WAAW,IAAI,YAAY,IAAI,UAAU,aAAa,GAAG;EAC1D,EAAE;CACH,MAAM,QAAQ,IAAI,SAAS,OAAO,SAAY,IAAI;CAClD,MAAM,SAAS,IAAI,UAAU,OAAO,SAAY,IAAI;CACpD,MAAM,WAAW,IAAI,YAAY,OAAO,SAAY,IAAI;CACxD,MAAM,eAAe,IAAI,gBAAgB,OAAO,SAAY,IAAI;CAChE,MAAM,YAAa,IAAI,WAA0C,SAC5D,IAAI,YACL;CACJ,MAAM,aACJ,IAAI,cAAc,OACd,SACC,IAAI;CACX,MAAM,WAAW,IAAI,YAAY,OAAO,SAAY,IAAI;CACxD,MAAM,YAAa,IAAI,WAA0C,SAC5D,IAAI,YACL;CACJ,MAAM,QACJ,IAAI,SAAS,OAAO,SAAa,IAAI;CACvC,MAAM,YAAY,iBAChB,IAAI,gBAAgB,IAAI,cAAc,aAAa,GAAG,MACtD,IAAI,iBAAiB,IAAI,eAAe,aAAa,GAAG,KACzD;AAED,QAAO;EACL,IAAI,IAAI;EACR,MAAM,IAAI;EACV,MAAM,IAAI;EACV,aAAa,IAAI;EACjB;EACA;EACA,aAAa,IAAI;EACjB;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAEA,YAAY,IAAI,cAAc;EAC9B,MAAM,IAAI,QAAQ;EAClB,YAAY,IAAI,cAAc;EAC9B,SAAS,IAAI,QAAQ,SAAS,IAAI,UAAU;EAC5C,iBAAiB,IAAI,mBAAmB;EACxC,4BAA4B,IAAI,2BAA2B,SACvD,IAAI,6BACJ;EACJ,gBAAgB,IAAI,kBAAkB;EACtC,OAAO,IAAI,QACN,IAAI,QACL;EAEJ,WAAW,UAAU,gBAAgB,YAAY;EAClD;;AAGH,eAAsB,yBAA8C;AASlE,SANa,MAFE,aAAa,CAEF,WAAW,SAAS,EAC5C,SAAS,EACP,YAAY,MACb,EACF,CAAC,EAEU,IAAI,cAAc;;AAmBhC,eAAsB,UAAU,OAA0C;CACxE,MAAM,SAAS,aAAa;CAC5B,MAAM,EAAE,IAAI,SAAS;CAErB,MAAM,gBAOF,EAAE;AACN,KAAI,KAAK,gBAAgB,OACvB,eAAc,cAAc,KAAK;AACnC,KAAI,KAAK,iBAAiB,OACxB,eAAc,eAAe,KAAK;AACpC,KAAI,KAAK,SAAS,OAAW,eAAc,OAAO,KAAK;AACvD,KAAI,KAAK,WAAW,OAAW,eAAc,SAAS,KAAK;AAC3D,KAAI,KAAK,gBAAgB,OACvB,eAAc,cAAc,KAAK;AACnC,KAAI,KAAK,aAAa,OAAW,eAAc,WAAW,KAAK;CAE/D,MAAM,UAAU,MAAM,OAAO,WAAW,OAAO;EAC7C,OAAO,EAAE,IAAI;EACb,MAAM;EACN,SAAS,EAAE,YAAY,MAAM;EAC9B,CAAC;AAEF,KAAI,KAAK,YAAY,QAAW;EAC9B,MAAM,OAAO,CAAC,GAAG,IAAI,IAAI,KAAK,QAAQ,KAAK,MAAM,EAAE,MAAM,CAAC,CAAC,OAAO,QAAQ,CAAC,CAAC;EAC5E,MAAM,oBAAoB,CAAC,GAAG,IAAI,IAAI,KAAK,IAAI,gBAAgB,CAAC,CAAC;AAEjE,OAAK,MAAM,cAAc,kBACvB,OAAM,OAAO,OAAO,OAAO;GACzB,OAAO,EAAE,MAAM,YAAY;GAC3B,QAAQ,EAAE,MAAM,YAAY;GAC5B,QAAQ,EAAE;GACX,CAAC;AAGJ,QAAM,OAAO,gBAAgB,WAAW,EACtC,OAAO,EAAE,YAAY,QAAQ,IAAI,EAClC,CAAC;AAEF,MAAI,KAAK,SAAS,EAChB,OAAM,OAAO,gBAAgB,WAAW,EACtC,MAAM,KAAK,KAAK,SAAS;GACvB,YAAY,QAAQ;GACpB,YAAY,gBAAgB,IAAI;GAChC,KAAK,IAAI,MAAM;GAChB,EAAE,EACJ,CAAC;EAGJ,MAAM,YAAY,MAAM,OAAO,WAAW,WAAW;GACnD,OAAO,EAAE,IAAI;GACb,SAAS,EAAE,YAAY,MAAM;GAC9B,CAAC;AACF,MAAI,CAAC,UAAW,OAAM,IAAI,MAAM,kCAAkC;AAClE,SAAO,cAAc,UAAU;;AAGjC,QAAO,cAAc,QAAQ;;AAkB/B,eAAsB,uBAA0C;AAG9D,SADa,MADE,aAAa,CACF,SAAS,UAAU,EACjC,KAAK,SAAS;EACxB,MAAM,IAAI;EACV,WAAW,IAAI;EACf,UAAU,IAAI;EACd,OAAO,IAAI,SAAS;EACrB,EAAE;;AAGL,eAAsB,sBAAwC;AAK5D,SAHa,MADE,aAAa,CACF,QAAQ,SAAS,EACzC,SAAS,EAAE,aAAa,MAAM,EAC/B,CAAC,EACU,KAAK,SAAS;EACxB,MAAM,IAAI;EACV,aAAa,IAAI,eAAe;EAChC,OAAO,IAAI,SAAS;EACpB,aAAa,IAAI,YAAY,KAAK,MAAM,EAAE,WAAW;EACtD,EAAE;;AAGL,eAAsB,kBACpB,sBACyB;AAKzB,QAAO;EACL,WALgB,uBACd,MAAM,sBAAsB,GAC5B,MAAM,wBAAwB;EAIhC,iBAAiB,MAAM,qCAAqC;EAC5D,iBAAiB,MAAM,8BAA8B;EACrD,SAAS,MAAM,sBAAsB;EACrC,QAAQ,MAAM,qBAAqB;EACpC"}
|
|
1
|
+
{"version":3,"file":"service.mjs","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"sourcesContent":["import { getDbClient } from '../../db/client'\nimport type { Prisma } from '../../generated/prisma/client'\nimport type {\n AppApprovalMethod,\n AppCatalogData,\n AppCategory,\n GroupingTagDefinition,\n Resource,\n} from '../../types/common/appCatalogTypes'\nimport type {\n CustomConfig,\n ServiceConfig,\n} from '../../types/common/approvalMethodTypes'\nimport type { Group, Person } from '../../types/common/personGroupTypes'\nimport { omit } from 'radashi'\nimport { parseSourceSlug } from '../../utils/parseSourceSlug'\nimport { computeFreshness } from './freshness'\n\n/** Prisma query result for DbResource with sourceRefs included (used by rowToResource) */\ntype ResourceRowWithSourceRefs = Prisma.DbResourceGetPayload<{\n include: { sourceRefs: true }\n}>\n\nfunction capitalize(word: string): string {\n if (!word) return word\n return word.charAt(0).toUpperCase() + word.slice(1)\n}\n\nexport async function getGroupingTagDefinitionsFromPrisma(): Promise<\n GroupingTagDefinition[]\n> {\n const prisma = getDbClient()\n\n // Fetch all tag definitions\n const rows = await prisma.dbAppTagDefinition.findMany()\n\n // Fetch all resources to count tag usage\n const resources = await prisma.dbResource.findMany({\n select: { tags: true },\n })\n\n // Count tag values across all resources\n const tagCounts = new Map<string, Map<string, number>>()\n\n for (const resource of resources) {\n const tags = (resource.tags as unknown as string[] | null) ?? []\n for (const tag of tags) {\n const [prefix, value] = tag.split(':')\n if (prefix && value) {\n if (!tagCounts.has(prefix)) {\n tagCounts.set(prefix, new Map())\n }\n const prefixCounts = tagCounts.get(prefix)!\n prefixCounts.set(value, (prefixCounts.get(value) ?? 0) + 1)\n }\n }\n }\n\n // Sort values by count and return definitions\n return rows.map((row) => {\n const definition = omit(row, ['id', 'updatedAt', 'createdAt'])\n const counts = tagCounts.get(definition.prefix) ?? new Map()\n\n // Sort values by count (descending)\n const sortedValues = [...definition.values].sort((a, b) => {\n const countA = counts.get(a.value) ?? 0\n const countB = counts.get(b.value) ?? 0\n return countB - countA\n })\n\n return {\n ...definition,\n values: sortedValues,\n }\n })\n}\n\nexport async function getApprovalMethodsFromPrisma(): Promise<\n AppApprovalMethod[]\n> {\n const prisma = getDbClient()\n\n // Fetch all approval methods\n const rows = await prisma.dbApprovalMethod.findMany()\n\n return rows.map((row) => {\n // Handle discriminated union by explicitly narrowing based on type\n const baseFields = {\n slug: row.slug,\n displayName: row.displayName,\n }\n\n // Provide default empty config if null, as AppApprovalMethod discriminated union requires config\n const config = row.config ?? {}\n\n switch (row.type) {\n case 'service':\n return {\n ...baseFields,\n type: 'service' as const,\n config: config as ServiceConfig,\n }\n case 'custom':\n return {\n ...baseFields,\n type: 'custom' as const,\n config: config as CustomConfig,\n }\n case 'noAccessRequired':\n return {\n ...baseFields,\n type: 'noAccessRequired' as const,\n config: config as CustomConfig,\n }\n case 'unknown':\n return {\n ...baseFields,\n type: 'unknown' as const,\n config: config as CustomConfig,\n }\n case 'personTeam':\n // Legacy: map personTeam to custom\n return {\n ...baseFields,\n type: 'custom' as const,\n config: config as CustomConfig,\n }\n }\n })\n}\n\nfunction rowToResource(row: ResourceRowWithSourceRefs): Resource {\n const accessRequest =\n row.accessRequest as unknown as Resource['accessRequest']\n const teams = (row.teams as unknown as string[] | null) ?? []\n const tags = (row.tags as unknown as Resource['tags']) ?? []\n const screenshotIds =\n (row.screenshotIds as unknown as Resource['screenshotIds']) ?? []\n const sources = row.sourceRefs.map((ref) => ({\n sourceSlug: ref.sourceSlug,\n url: ref.url,\n parseDate: ref.parseDate ? ref.parseDate.toISOString() : null,\n }))\n const notes = row.notes == null ? undefined : row.notes\n const appUrl = row.appUrl == null ? undefined : row.appUrl\n const iconName = row.iconName == null ? undefined : row.iconName\n const abbreviation = row.abbreviation == null ? undefined : row.abbreviation\n const nicknames = (row.nicknames as unknown as string[] | null)?.length\n ? (row.nicknames as unknown as string[])\n : undefined\n const deprecated =\n row.deprecated == null\n ? undefined\n : (row.deprecated as unknown as Resource['deprecated'])\n const aiPrompt = row.aiPrompt == null ? undefined : row.aiPrompt\n const urlIssues = (row.urlIssues as unknown as string[] | null)?.length\n ? (row.urlIssues as unknown as string[])\n : undefined\n const tiers =\n row.tiers == null ? undefined : (row.tiers as unknown as Resource['tiers'])\n const freshness = computeFreshness(\n row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,\n row.nextCheckAfter ? row.nextCheckAfter.toISOString() : null,\n )\n\n return {\n id: row.id,\n slug: row.slug,\n type: row.type,\n displayName: row.displayName,\n abbreviation,\n nicknames,\n description: row.description,\n accessRequest,\n teams,\n notes,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n aiPrompt,\n urlIssues,\n tiers,\n // Fields from former SubResource\n parentSlug: row.parentSlug ?? undefined,\n tier: row.tier ?? undefined,\n familySlug: row.familySlug ?? undefined,\n aliases: row.aliases.length ? row.aliases : undefined,\n ownerPersonSlug: row.ownerPersonSlug ?? undefined,\n accessMaintainerGroupSlugs: row.accessMaintainerGroupSlugs.length\n ? row.accessMaintainerGroupSlugs\n : undefined,\n accessComments: row.accessComments ?? undefined,\n extra: row.extra\n ? (row.extra as unknown as Record<string, unknown>)\n : undefined,\n // Only surface freshness once the app has actually been scanned.\n freshness: freshness.lastCheckedAt ? freshness : undefined,\n createdAt: row.createdAt.toISOString(),\n }\n}\n\nexport async function getResourcesFromPrisma(): Promise<Resource[]> {\n const prisma = getDbClient()\n\n const rows = await prisma.dbResource.findMany({\n include: {\n sourceRefs: true,\n },\n })\n\n return rows.map(rowToResource)\n}\n\n/** @deprecated Use getResourcesFromPrisma instead */\nexport const getAppsFromPrisma = getResourcesFromPrisma\n\nexport interface UpdateAppInput {\n id: string\n data: {\n displayName?: string\n abbreviation?: string | null\n slug?: string\n appUrl?: string\n description?: string\n sources?: string[]\n aiPrompt?: string | null\n }\n}\n\nexport async function updateApp(input: UpdateAppInput): Promise<Resource> {\n const prisma = getDbClient()\n const { id, data } = input\n\n const updatePayload: {\n displayName?: string\n abbreviation?: string | null\n slug?: string\n appUrl?: string\n description?: string\n aiPrompt?: string | null\n } = {}\n if (data.displayName !== undefined)\n updatePayload.displayName = data.displayName\n if (data.abbreviation !== undefined)\n updatePayload.abbreviation = data.abbreviation\n if (data.slug !== undefined) updatePayload.slug = data.slug\n if (data.appUrl !== undefined) updatePayload.appUrl = data.appUrl\n if (data.description !== undefined)\n updatePayload.description = data.description\n if (data.aiPrompt !== undefined) updatePayload.aiPrompt = data.aiPrompt\n\n const updated = await prisma.dbResource.update({\n where: { id },\n data: updatePayload,\n include: { sourceRefs: true },\n })\n\n if (data.sources !== undefined) {\n const urls = [...new Set(data.sources.map((u) => u.trim()).filter(Boolean))]\n const uniqueSourceSlugs = [...new Set(urls.map(parseSourceSlug))]\n\n for (const sourceSlug of uniqueSourceSlugs) {\n await prisma.source.upsert({\n where: { slug: sourceSlug },\n create: { slug: sourceSlug },\n update: {},\n })\n }\n\n await prisma.sourceReference.deleteMany({\n where: { resourceId: updated.id },\n })\n\n if (urls.length > 0) {\n await prisma.sourceReference.createMany({\n data: urls.map((url) => ({\n resourceId: updated.id,\n sourceSlug: parseSourceSlug(url),\n url: url.trim(),\n })),\n })\n }\n\n const refetched = await prisma.dbResource.findUnique({\n where: { id },\n include: { sourceRefs: true },\n })\n if (!refetched) throw new Error('Resource not found after update')\n return rowToResource(refetched)\n }\n\n return rowToResource(updated)\n}\n\nexport function deriveCategories(resources: Resource[]): AppCategory[] {\n const tagSet = new Set<string>()\n for (const resource of resources) {\n for (const tag of resource.tags ?? []) {\n const normalized = tag.trim().toLowerCase()\n if (normalized) tagSet.add(normalized)\n }\n }\n const categories: AppCategory[] = [{ id: 'all', name: 'All' }]\n for (const tag of Array.from(tagSet).sort()) {\n categories.push({ id: tag, name: capitalize(tag) })\n }\n return categories\n}\n\nexport async function getPersonsFromPrisma(): Promise<Person[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbPerson.findMany()\n return rows.map((row) => ({\n slug: row.slug,\n firstName: row.firstName,\n lastName: row.lastName,\n email: row.email ?? undefined,\n }))\n}\n\nexport async function getGroupsFromPrisma(): Promise<Group[]> {\n const prisma = getDbClient()\n const rows = await prisma.dbGroup.findMany({\n include: { memberships: true },\n })\n return rows.map((row) => ({\n slug: row.slug,\n displayName: row.displayName ?? undefined,\n email: row.email ?? undefined,\n memberSlugs: row.memberships.map((m) => m.personSlug),\n }))\n}\n\nexport async function getAppCatalogData(\n getResourcesOptional?: () => Promise<Resource[]>,\n): Promise<AppCatalogData> {\n const resources = getResourcesOptional\n ? await getResourcesOptional()\n : await getResourcesFromPrisma()\n\n return {\n resources,\n tagsDefinitions: await getGroupingTagDefinitionsFromPrisma(),\n approvalMethods: await getApprovalMethodsFromPrisma(),\n persons: await getPersonsFromPrisma(),\n groups: await getGroupsFromPrisma(),\n }\n}\n"],"mappings":"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"}
|
|
@@ -98,6 +98,8 @@ interface Resource {
|
|
|
98
98
|
*/
|
|
99
99
|
lastCheckedAt?: string | null;
|
|
100
100
|
nextCheckAfter?: string | null;
|
|
101
|
+
/** ISO-8601 timestamp of when this entry was first created in the catalog DB. */
|
|
102
|
+
createdAt?: string;
|
|
101
103
|
}
|
|
102
104
|
interface AppCategory {
|
|
103
105
|
id: string;
|
|
@@ -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;;AAkBF;;;UAXiB,SAAA;EA4BM;EA1BrB,aAAA;EA0DQ;EAxDR,OAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAMA;EAJA,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;IAWA,0HATE,IAAA,iCAWM;IATN,eAAA,WAeF;IAbE,OAAA;EAAA;EAmBF;EAhBA,QAAA;EAoBA;EAlBA,SAAA;EAoBQ;EAlBR,KAAA,GAAQ,WAAA;EAyBI;EArBZ,UAAA;EA4BA;EA1BA,IAAA;
|
|
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;;AAkBF;;;UAXiB,SAAA;EA4BM;EA1BrB,aAAA;EA0DQ;EAxDR,OAAA;AAAA;;;;;UAOe,QAAA;EACf,EAAA;EACA,IAAA;EAMA;EAJA,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;IAWA,0HATE,IAAA,iCAWM;IATN,eAAA,WAeF;IAbE,OAAA;EAAA;EAmBF;EAhBA,QAAA;EAoBA;EAlBA,SAAA;EAoBQ;EAlBR,KAAA,GAAQ,WAAA;EAyBI;EArBZ,UAAA;EA4BA;EA1BA,IAAA;EA4BS;EA1BT,UAAA;EA8Be;EA5Bf,OAAA;;EAEA,eAAA;EA4BI;EA1BJ,0BAAA;EA6BoC;EA3BpC,cAAA;EA+BwB;EA7BxB,KAAA,GAAQ,MAAA;EA2BR;;;;;;EApBA,SAAA,GAAY,SAAA;EAyBO;;;;;EAnBnB,aAAA;EACA,cAAA;EAmBM;EAjBN,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;EAFe;EAIf,GAAA;;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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-backend-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.14.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.
|
|
49
|
-
"@igstack/app-catalog-table-sync": "0.
|
|
48
|
+
"@igstack/app-catalog-shared-core": "0.14.1",
|
|
49
|
+
"@igstack/app-catalog-table-sync": "0.14.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": "
|
|
76
|
+
"gitHead": "30c7add877ea524033c3f4e96b4e4c56a1a933e8",
|
|
77
77
|
"scripts": {
|
|
78
78
|
"build": "tsdown",
|
|
79
79
|
"build:lenient": "tsdown",
|
|
@@ -198,6 +198,7 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
|
|
|
198
198
|
: undefined,
|
|
199
199
|
// Only surface freshness once the app has actually been scanned.
|
|
200
200
|
freshness: freshness.lastCheckedAt ? freshness : undefined,
|
|
201
|
+
createdAt: row.createdAt.toISOString(),
|
|
201
202
|
}
|
|
202
203
|
}
|
|
203
204
|
|
|
@@ -118,6 +118,8 @@ export interface Resource {
|
|
|
118
118
|
*/
|
|
119
119
|
lastCheckedAt?: string | null
|
|
120
120
|
nextCheckAfter?: string | null
|
|
121
|
+
/** ISO-8601 timestamp of when this entry was first created in the catalog DB. */
|
|
122
|
+
createdAt?: string
|
|
121
123
|
}
|
|
122
124
|
|
|
123
125
|
// Derived catalog data returned by backend
|