@igstack/app-catalog-backend-core 6.1.3 → 6.1.4-alpha-20260926184125

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.
@@ -1 +1 @@
1
- {"version":3,"file":"tableSyncPrismaAdapter.mjs","names":[],"sources":["../../src/db/tableSyncPrismaAdapter.ts"],"sourcesContent":["import { tableSync } from '@igstack/app-catalog-table-sync'\nimport type { Prisma, PrismaClient } from '../generated/prisma/client'\nimport { mapValues, omit, pick } from 'radashi'\n\nexport type ScalarKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\nexport type ObjectKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['objects']\n\nexport type ScalarFilter<TPrismaModelName extends Prisma.ModelName> = Partial<\n Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\n>\n\nexport type GetOperationFns<TModel extends Prisma.ModelName> = {\n [TOperation in keyof Prisma.TypeMap['model']['DbResource']['operations']]: (\n args: Prisma.TypeMap['model'][TModel]['operations'][TOperation]['args'],\n ) => Promise<\n Prisma.TypeMap['model'][TModel]['operations'][TOperation]['result']\n >\n}\n\nexport interface TableSyncParamsPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends readonly ScalarKeys<TPrismaModelName>[],\n TRelationColumns extends readonly ObjectKeys<TPrismaModelName>[],\n> {\n id?: ScalarKeys<TPrismaModelName>\n prisma: TPrismaClient\n prismaModelName: TPrismaModelName\n uniqColumns: TUniqColumns\n relationColumns?: TRelationColumns\n where?: ScalarFilter<TPrismaModelName>\n upsertOnly?: boolean\n}\n\nfunction getPrismaModelOperations<\n TPrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n>(prisma: TPrismaClient, prismaModelName: TPrismaModelName) {\n const key = (prismaModelName.slice(0, 1).toLowerCase() +\n prismaModelName.slice(1)) as keyof TPrismaClient\n return prisma[key] as GetOperationFns<TPrismaModelName>\n}\n\nexport type MakeTFromPrismaModel<TPrismaModelName extends Prisma.ModelName> =\n NonNullable<\n Prisma.TypeMap['model'][TPrismaModelName]['operations']['findUnique']['result']\n >\n\nexport function tableSyncPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends readonly ScalarKeys<TPrismaModelName>[],\n TRelationColumns extends readonly ObjectKeys<TPrismaModelName>[],\n TId extends ScalarKeys<TPrismaModelName> = ScalarKeys<TPrismaModelName>,\n>(\n params: TableSyncParamsPrisma<\n TPrismaClient,\n TPrismaModelName,\n TUniqColumns,\n TRelationColumns\n >,\n) {\n const {\n prisma,\n prismaModelName,\n uniqColumns,\n where: whereGlobal,\n upsertOnly,\n } = params\n const prismOperations = getPrismaModelOperations(prisma, prismaModelName)\n\n const idColumn = (params.id ?? 'id') as TId\n // @ts-ignore maybe someday later (never)\n return tableSync<MakeTFromPrismaModel<TPrismaModelName>, TUniqColumns, TId>({\n id: idColumn,\n uniqColumns,\n readAll: async () => {\n const findManyArgs = whereGlobal\n ? {\n where: whereGlobal,\n }\n : {}\n return (await prismOperations.findMany(\n findManyArgs,\n )) as MakeTFromPrismaModel<TPrismaModelName>[]\n },\n writeAll: async (createData, update, deleteIds) => {\n const prismaUniqKey = params.uniqColumns.join('_')\n const relationColumnList =\n params.relationColumns ?? ([] as ObjectKeys<TPrismaModelName>[])\n\n return prisma.$transaction(async (tx) => {\n const txOps = getPrismaModelOperations(tx, prismaModelName)\n for (const { data, where } of update) {\n const uniqKeyWhere =\n Object.keys(where).length > 1\n ? {\n [prismaUniqKey]: where,\n }\n : where\n\n const dataScalar = omit(data, relationColumnList)\n const dataRelations = mapValues(\n pick(data, relationColumnList),\n (value) => {\n return {\n set: value,\n }\n },\n )\n\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.update({\n data: { ...dataScalar, ...dataRelations },\n where: { ...uniqKeyWhere },\n })\n }\n\n if (upsertOnly !== true) {\n await txOps.deleteMany({\n where: {\n [idColumn]: {\n in: deleteIds,\n },\n },\n })\n }\n\n // Validate uniqueness of uniqColumns before creating records\n const createDataMapped = createData.map((data) => {\n // @ts-expect-error This is too difficult for me to come up with right types\n const dataScalar = omit(data, relationColumnList)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n const onlyRelationColumns = pick(data, relationColumnList)\n const dataRelations = mapValues(onlyRelationColumns, (value) => {\n return {\n connect: value,\n }\n })\n\n return { ...dataScalar, ...dataRelations }\n })\n\n // Check for duplicates in the data to be created\n if (createDataMapped.length > 0) {\n const uniqKeysInCreate = new Set<string>()\n const duplicateKeys: string[] = []\n\n for (const data of createDataMapped) {\n const keyParts = params.uniqColumns.map((col) => {\n const value = data[col as keyof typeof data]\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive: unique columns may be nullable in schemas\n return value === null || value === undefined\n ? 'null'\n : String(value)\n })\n const key = keyParts.join(':')\n\n if (uniqKeysInCreate.has(key)) {\n duplicateKeys.push(key)\n } else {\n uniqKeysInCreate.add(key)\n }\n }\n\n if (duplicateKeys.length > 0) {\n const uniqColumnsStr = params.uniqColumns.join(', ')\n throw new Error(\n `Duplicate unique key values found in data to be created. ` +\n `Model: ${prismaModelName}, Unique columns: [${uniqColumnsStr}], ` +\n `Duplicate keys: [${duplicateKeys.join(', ')}]`,\n )\n }\n }\n\n const results: MakeTFromPrismaModel<TPrismaModelName>[] = []\n\n if (relationColumnList.length === 0) {\n // @ts-expect-error This is too difficult for me to come up with right types\n const batchResult = await txOps.createManyAndReturn({\n data: createDataMapped,\n })\n\n results.push(...batchResult)\n } else {\n for (const dataMappedElement of createDataMapped) {\n // @ts-expect-error too difficult for me\n const newVar = await txOps.create({\n data: dataMappedElement,\n })\n results.push(newVar as MakeTFromPrismaModel<TPrismaModelName>)\n }\n }\n\n return results\n })\n },\n })\n}\n"],"mappings":";;;;AAoCA,SAAS,yBAGP,QAAuB,iBAAmC;AAG1D,QAAO,OAFM,gBAAgB,MAAM,GAAG,EAAE,CAAC,aAAa,GACpD,gBAAgB,MAAM,EAAE;;AAS5B,SAAgB,gBAOd,QAMA;CACA,MAAM,EACJ,QACA,iBACA,aACA,OAAO,aACP,eACE;CACJ,MAAM,kBAAkB,yBAAyB,QAAQ,gBAAgB;CAEzE,MAAM,WAAY,OAAO,MAAM;AAE/B,QAAO,UAAqE;EAC1E,IAAI;EACJ;EACA,SAAS,YAAY;GACnB,MAAM,eAAe,cACjB,EACE,OAAO,aACR,GACD,EAAE;AACN,UAAQ,MAAM,gBAAgB,SAC5B,aACD;;EAEH,UAAU,OAAO,YAAY,QAAQ,cAAc;GACjD,MAAM,gBAAgB,OAAO,YAAY,KAAK,IAAI;GAClD,MAAM,qBACJ,OAAO,mBAAoB,EAAE;AAE/B,UAAO,OAAO,aAAa,OAAO,OAAO;IACvC,MAAM,QAAQ,yBAAyB,IAAI,gBAAgB;AAC3D,SAAK,MAAM,EAAE,MAAM,WAAW,QAAQ;KACpC,MAAM,eACJ,OAAO,KAAK,MAAM,CAAC,SAAS,IACxB,GACG,gBAAgB,OAClB,GACD;KAEN,MAAM,aAAa,KAAK,MAAM,mBAAmB;KACjD,MAAM,gBAAgB,UACpB,KAAK,MAAM,mBAAmB,GAC7B,UAAU;AACT,aAAO,EACL,KAAK,OACN;OAEJ;AAGD,WAAM,MAAM,OAAO;MACjB,MAAM;OAAE,GAAG;OAAY,GAAG;OAAe;MACzC,OAAO,EAAE,GAAG,cAAc;MAC3B,CAAC;;AAGJ,QAAI,eAAe,KACjB,OAAM,MAAM,WAAW,EACrB,OAAO,GACJ,WAAW,EACV,IAAI,WACL,EACF,EACF,CAAC;IAIJ,MAAM,mBAAmB,WAAW,KAAK,SAAS;KAEhD,MAAM,aAAa,KAAK,MAAM,mBAAmB;KAIjD,MAAM,gBAAgB,UADM,KAAK,MAAM,mBAAmB,GACJ,UAAU;AAC9D,aAAO,EACL,SAAS,OACV;OACD;AAEF,YAAO;MAAE,GAAG;MAAY,GAAG;MAAe;MAC1C;AAGF,QAAI,iBAAiB,SAAS,GAAG;KAC/B,MAAM,mCAAmB,IAAI,KAAa;KAC1C,MAAM,gBAA0B,EAAE;AAElC,UAAK,MAAM,QAAQ,kBAAkB;MAQnC,MAAM,MAPW,OAAO,YAAY,KAAK,QAAQ;OAC/C,MAAM,QAAQ,KAAK;AAEnB,cAAO,UAAU,QAAQ,UAAU,SAC/B,SACA,OAAO,MAAM;QACjB,CACmB,KAAK,IAAI;AAE9B,UAAI,iBAAiB,IAAI,IAAI,CAC3B,eAAc,KAAK,IAAI;UAEvB,kBAAiB,IAAI,IAAI;;AAI7B,SAAI,cAAc,SAAS,GAAG;MAC5B,MAAM,iBAAiB,OAAO,YAAY,KAAK,KAAK;AACpD,YAAM,IAAI,MACR,mEACY,gBAAgB,qBAAqB,eAAe,sBAC1C,cAAc,KAAK,KAAK,CAAC,GAChD;;;IAIL,MAAM,UAAoD,EAAE;AAE5D,QAAI,mBAAmB,WAAW,GAAG;KAEnC,MAAM,cAAc,MAAM,MAAM,oBAAoB,EAClD,MAAM,kBACP,CAAC;AAEF,aAAQ,KAAK,GAAG,YAAY;UAE5B,MAAK,MAAM,qBAAqB,kBAAkB;KAEhD,MAAM,SAAS,MAAM,MAAM,OAAO,EAChC,MAAM,mBACP,CAAC;AACF,aAAQ,KAAK,OAAiD;;AAIlE,WAAO;KACP;;EAEL,CAAC"}
1
+ {"version":3,"file":"tableSyncPrismaAdapter.mjs","names":[],"sources":["../../src/db/tableSyncPrismaAdapter.ts"],"sourcesContent":["import { tableSync } from '@igstack/app-catalog-table-sync'\nimport type { Prisma, PrismaClient } from '../generated/prisma/client'\nimport { mapValues, omit, pick } from 'radashi'\n\nexport type ScalarKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\nexport type ObjectKeys<TPrismaModelName extends Prisma.ModelName> =\n keyof Prisma.TypeMap['model'][TPrismaModelName]['payload']['objects']\n\nexport type ScalarFilter<TPrismaModelName extends Prisma.ModelName> = Partial<\n Prisma.TypeMap['model'][TPrismaModelName]['payload']['scalars']\n>\n\nexport type GetOperationFns<TModel extends Prisma.ModelName> = {\n [TOperation in keyof Prisma.TypeMap['model']['DbResource']['operations']]: (\n args: Prisma.TypeMap['model'][TModel]['operations'][TOperation]['args'],\n ) => Promise<\n Prisma.TypeMap['model'][TModel]['operations'][TOperation]['result']\n >\n}\n\nexport interface TableSyncParamsPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends readonly ScalarKeys<TPrismaModelName>[],\n TRelationColumns extends readonly ObjectKeys<TPrismaModelName>[],\n> {\n id?: ScalarKeys<TPrismaModelName>\n prisma: TPrismaClient\n prismaModelName: TPrismaModelName\n uniqColumns: TUniqColumns\n relationColumns?: TRelationColumns\n where?: ScalarFilter<TPrismaModelName>\n upsertOnly?: boolean\n}\n\nfunction getPrismaModelOperations<\n TPrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n>(prisma: TPrismaClient, prismaModelName: TPrismaModelName) {\n const key = (prismaModelName.slice(0, 1).toLowerCase() +\n prismaModelName.slice(1)) as keyof TPrismaClient\n return prisma[key] as GetOperationFns<TPrismaModelName>\n}\n\nexport type MakeTFromPrismaModel<TPrismaModelName extends Prisma.ModelName> =\n NonNullable<\n Prisma.TypeMap['model'][TPrismaModelName]['operations']['findUnique']['result']\n >\n\nexport function tableSyncPrisma<\n TPrismaClient extends PrismaClient,\n TPrismaModelName extends Prisma.ModelName,\n TUniqColumns extends readonly ScalarKeys<TPrismaModelName>[],\n TRelationColumns extends readonly ObjectKeys<TPrismaModelName>[],\n TId extends ScalarKeys<TPrismaModelName> = ScalarKeys<TPrismaModelName>,\n>(\n params: TableSyncParamsPrisma<\n TPrismaClient,\n TPrismaModelName,\n TUniqColumns,\n TRelationColumns\n >,\n) {\n const {\n prisma,\n prismaModelName,\n uniqColumns,\n where: whereGlobal,\n upsertOnly,\n } = params\n const prismOperations = getPrismaModelOperations(prisma, prismaModelName)\n\n const idColumn = (params.id ?? 'id') as TId\n // @ts-ignore maybe someday later (never)\n return tableSync<MakeTFromPrismaModel<TPrismaModelName>, TUniqColumns, TId>({\n id: idColumn,\n uniqColumns,\n readAll: async () => {\n const findManyArgs = whereGlobal\n ? {\n where: whereGlobal,\n }\n : {}\n return await prismOperations.findMany(findManyArgs)\n },\n writeAll: async (createData, update, deleteIds) => {\n const prismaUniqKey = params.uniqColumns.join('_')\n const relationColumnList =\n params.relationColumns ?? ([] as ObjectKeys<TPrismaModelName>[])\n\n return prisma.$transaction(async (tx) => {\n const txOps = getPrismaModelOperations(tx, prismaModelName)\n for (const { data, where } of update) {\n const uniqKeyWhere =\n Object.keys(where).length > 1\n ? {\n [prismaUniqKey]: where,\n }\n : where\n\n const dataScalar = omit(data, relationColumnList)\n const dataRelations = mapValues(\n pick(data, relationColumnList),\n (value) => {\n return {\n set: value,\n }\n },\n )\n\n // @ts-expect-error This is too difficult for me to come up with right types\n await txOps.update({\n data: { ...dataScalar, ...dataRelations },\n where: { ...uniqKeyWhere },\n })\n }\n\n if (upsertOnly !== true) {\n await txOps.deleteMany({\n where: {\n [idColumn]: {\n in: deleteIds,\n },\n },\n })\n }\n\n // Validate uniqueness of uniqColumns before creating records\n const createDataMapped = createData.map((data) => {\n // @ts-expect-error This is too difficult for me to come up with right types\n const dataScalar = omit(data, relationColumnList)\n\n // @ts-expect-error This is too difficult for me to come up with right types\n const onlyRelationColumns = pick(data, relationColumnList)\n const dataRelations = mapValues(onlyRelationColumns, (value) => {\n return {\n connect: value,\n }\n })\n\n return { ...dataScalar, ...dataRelations }\n })\n\n // Check for duplicates in the data to be created\n if (createDataMapped.length > 0) {\n const uniqKeysInCreate = new Set<string>()\n const duplicateKeys: string[] = []\n\n for (const data of createDataMapped) {\n const keyParts = params.uniqColumns.map((col) => {\n const value = data[col as keyof typeof data]\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- defensive: unique columns may be nullable in schemas\n return value === null || value === undefined\n ? 'null'\n : String(value)\n })\n const key = keyParts.join(':')\n\n if (uniqKeysInCreate.has(key)) {\n duplicateKeys.push(key)\n } else {\n uniqKeysInCreate.add(key)\n }\n }\n\n if (duplicateKeys.length > 0) {\n const uniqColumnsStr = params.uniqColumns.join(', ')\n throw new Error(\n `Duplicate unique key values found in data to be created. ` +\n `Model: ${prismaModelName}, Unique columns: [${uniqColumnsStr}], ` +\n `Duplicate keys: [${duplicateKeys.join(', ')}]`,\n )\n }\n }\n\n const results: MakeTFromPrismaModel<TPrismaModelName>[] = []\n\n if (relationColumnList.length === 0) {\n // @ts-expect-error This is too difficult for me to come up with right types\n const batchResult = await txOps.createManyAndReturn({\n data: createDataMapped,\n })\n\n results.push(...batchResult)\n } else {\n for (const dataMappedElement of createDataMapped) {\n // @ts-expect-error too difficult for me\n const newVar = await txOps.create({\n data: dataMappedElement,\n })\n results.push(newVar)\n }\n }\n\n return results\n })\n },\n })\n}\n"],"mappings":";;;;AAoCA,SAAS,yBAGP,QAAuB,iBAAmC;AAG1D,QAAO,OAFM,gBAAgB,MAAM,GAAG,EAAE,CAAC,aAAa,GACpD,gBAAgB,MAAM,EAAE;;AAS5B,SAAgB,gBAOd,QAMA;CACA,MAAM,EACJ,QACA,iBACA,aACA,OAAO,aACP,eACE;CACJ,MAAM,kBAAkB,yBAAyB,QAAQ,gBAAgB;CAEzE,MAAM,WAAY,OAAO,MAAM;AAE/B,QAAO,UAAqE;EAC1E,IAAI;EACJ;EACA,SAAS,YAAY;GACnB,MAAM,eAAe,cACjB,EACE,OAAO,aACR,GACD,EAAE;AACN,UAAO,MAAM,gBAAgB,SAAS,aAAa;;EAErD,UAAU,OAAO,YAAY,QAAQ,cAAc;GACjD,MAAM,gBAAgB,OAAO,YAAY,KAAK,IAAI;GAClD,MAAM,qBACJ,OAAO,mBAAoB,EAAE;AAE/B,UAAO,OAAO,aAAa,OAAO,OAAO;IACvC,MAAM,QAAQ,yBAAyB,IAAI,gBAAgB;AAC3D,SAAK,MAAM,EAAE,MAAM,WAAW,QAAQ;KACpC,MAAM,eACJ,OAAO,KAAK,MAAM,CAAC,SAAS,IACxB,GACG,gBAAgB,OAClB,GACD;KAEN,MAAM,aAAa,KAAK,MAAM,mBAAmB;KACjD,MAAM,gBAAgB,UACpB,KAAK,MAAM,mBAAmB,GAC7B,UAAU;AACT,aAAO,EACL,KAAK,OACN;OAEJ;AAGD,WAAM,MAAM,OAAO;MACjB,MAAM;OAAE,GAAG;OAAY,GAAG;OAAe;MACzC,OAAO,EAAE,GAAG,cAAc;MAC3B,CAAC;;AAGJ,QAAI,eAAe,KACjB,OAAM,MAAM,WAAW,EACrB,OAAO,GACJ,WAAW,EACV,IAAI,WACL,EACF,EACF,CAAC;IAIJ,MAAM,mBAAmB,WAAW,KAAK,SAAS;KAEhD,MAAM,aAAa,KAAK,MAAM,mBAAmB;KAIjD,MAAM,gBAAgB,UADM,KAAK,MAAM,mBAAmB,GACJ,UAAU;AAC9D,aAAO,EACL,SAAS,OACV;OACD;AAEF,YAAO;MAAE,GAAG;MAAY,GAAG;MAAe;MAC1C;AAGF,QAAI,iBAAiB,SAAS,GAAG;KAC/B,MAAM,mCAAmB,IAAI,KAAa;KAC1C,MAAM,gBAA0B,EAAE;AAElC,UAAK,MAAM,QAAQ,kBAAkB;MAQnC,MAAM,MAPW,OAAO,YAAY,KAAK,QAAQ;OAC/C,MAAM,QAAQ,KAAK;AAEnB,cAAO,UAAU,QAAQ,UAAU,SAC/B,SACA,OAAO,MAAM;QACjB,CACmB,KAAK,IAAI;AAE9B,UAAI,iBAAiB,IAAI,IAAI,CAC3B,eAAc,KAAK,IAAI;UAEvB,kBAAiB,IAAI,IAAI;;AAI7B,SAAI,cAAc,SAAS,GAAG;MAC5B,MAAM,iBAAiB,OAAO,YAAY,KAAK,KAAK;AACpD,YAAM,IAAI,MACR,mEACY,gBAAgB,qBAAqB,eAAe,sBAC1C,cAAc,KAAK,KAAK,CAAC,GAChD;;;IAIL,MAAM,UAAoD,EAAE;AAE5D,QAAI,mBAAmB,WAAW,GAAG;KAEnC,MAAM,cAAc,MAAM,MAAM,oBAAoB,EAClD,MAAM,kBACP,CAAC;AAEF,aAAQ,KAAK,GAAG,YAAY;UAE5B,MAAK,MAAM,qBAAqB,kBAAkB;KAEhD,MAAM,SAAS,MAAM,MAAM,OAAO,EAChC,MAAM,mBACP,CAAC;AACF,aAAQ,KAAK,OAAO;;AAIxB,WAAO;KACP;;EAEL,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"service.d.mts","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"mappings":";;;iBAgOsB,sBAAA,CAAA,GAA0B,OAAA,CAAQ,QAAA"}
1
+ {"version":3,"file":"service.d.mts","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"mappings":";;;iBA2NsB,sBAAA,CAAA,GAA0B,OAAA,CAAQ,QAAA"}
@@ -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 SourceCheck,\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'\nimport { computeSourcePulse } from './sourcePulse'\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 // Derived here rather than shipped raw so the pulse mark can stay a dumb\n // renderer, the same way `freshness` is computed for the entry below.\n pulse: computeSourcePulse({\n lastCheckedAt: ref.lastCheckedAt?.toISOString() ?? null,\n nextCheckAfter: ref.nextCheckAfter?.toISOString() ?? null,\n lastContentChangeAt: ref.lastContentChangeAt?.toISOString() ?? null,\n checkIntervalHours: ref.checkIntervalHours,\n changeHistory: ref.changeHistory as SourceCheck[] | null,\n }),\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 aiMemory = row.aiMemory == null ? undefined : row.aiMemory\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 quickJumps =\n row.quickJumps == null\n ? undefined\n : (row.quickJumps as unknown as Resource['quickJumps'])\n const freshness = computeFreshness({\n lastCheckedAt: row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,\n nextCheckAfter: row.nextCheckAfter\n ? row.nextCheckAfter.toISOString()\n : null,\n lastContentChangeAt: row.lastContentChangeAt\n ? row.lastContentChangeAt.toISOString()\n : 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 aiMemory,\n urlIssues,\n tiers,\n quickJumps,\n // Fields from former SubResource\n parentSlug: row.parentSlug ?? undefined,\n tier: row.tier ?? undefined,\n aliases: row.aliases.length ? row.aliases : undefined,\n ownerPersonSlug: row.ownerPersonSlug ?? undefined,\n approverSlugs: row.approverSlugs.length ? row.approverSlugs : 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 interface UpdateAiMemoryInput {\n /** Resource id (DB id, not slug) */\n id: string\n /** New value for aiMemory. Pass null to clear. */\n aiMemory: string | null\n}\n\n/**\n * Updates the AI-owned scratchpad for a resource.\n * This mutation is intentionally separate from updateApp so AI agents can\n * write knowledge without a full MR cycle. It does NOT touch aiPrompt.\n */\nexport async function updateAiMemory(\n input: UpdateAiMemoryInput,\n): Promise<Resource> {\n const prisma = getDbClient()\n const updated = await prisma.dbResource.update({\n where: { id: input.id },\n data: { aiMemory: input.aiMemory },\n include: { sourceRefs: true },\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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"}
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 SourceCheck,\n} from '../../types/common/appCatalogTypes'\nimport type { ServiceConfig } 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'\nimport { computeSourcePulse } from './sourcePulse'\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,\n }\n case 'noAccessRequired':\n return {\n ...baseFields,\n type: 'noAccessRequired' as const,\n config: config,\n }\n case 'unknown':\n return {\n ...baseFields,\n type: 'unknown' as const,\n config: config,\n }\n case 'personTeam':\n // Legacy: map personTeam to custom\n return {\n ...baseFields,\n type: 'custom' as const,\n config: config,\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 // Derived here rather than shipped raw so the pulse mark can stay a dumb\n // renderer, the same way `freshness` is computed for the entry below.\n pulse: computeSourcePulse({\n lastCheckedAt: ref.lastCheckedAt?.toISOString() ?? null,\n nextCheckAfter: ref.nextCheckAfter?.toISOString() ?? null,\n lastContentChangeAt: ref.lastContentChangeAt?.toISOString() ?? null,\n checkIntervalHours: ref.checkIntervalHours,\n changeHistory: ref.changeHistory as SourceCheck[] | null,\n }),\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\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 aiMemory = row.aiMemory == null ? undefined : row.aiMemory\n const urlIssues = (row.urlIssues as unknown as string[] | null)?.length\n ? row.urlIssues\n : undefined\n const tiers =\n row.tiers == null ? undefined : (row.tiers as unknown as Resource['tiers'])\n const quickJumps =\n row.quickJumps == null\n ? undefined\n : (row.quickJumps as unknown as Resource['quickJumps'])\n const freshness = computeFreshness({\n lastCheckedAt: row.lastCheckedAt ? row.lastCheckedAt.toISOString() : null,\n nextCheckAfter: row.nextCheckAfter\n ? row.nextCheckAfter.toISOString()\n : null,\n lastContentChangeAt: row.lastContentChangeAt\n ? row.lastContentChangeAt.toISOString()\n : 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 aiMemory,\n urlIssues,\n tiers,\n quickJumps,\n // Fields from former SubResource\n parentSlug: row.parentSlug ?? undefined,\n tier: row.tier ?? undefined,\n aliases: row.aliases.length ? row.aliases : undefined,\n ownerPersonSlug: row.ownerPersonSlug ?? undefined,\n approverSlugs: row.approverSlugs.length ? row.approverSlugs : undefined,\n accessComments: row.accessComments ?? undefined,\n extra: row.extra ? row.extra : 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 interface UpdateAiMemoryInput {\n /** Resource id (DB id, not slug) */\n id: string\n /** New value for aiMemory. Pass null to clear. */\n aiMemory: string | null\n}\n\n/**\n * Updates the AI-owned scratchpad for a resource.\n * This mutation is intentionally separate from updateApp so AI agents can\n * write knowledge without a full MR cycle. It does NOT touch aiPrompt.\n */\nexport async function updateAiMemory(\n input: UpdateAiMemoryInput,\n): Promise<Resource> {\n const prisma = getDbClient()\n const updated = await prisma.dbResource.update({\n where: { id: input.id },\n data: { aiMemory: input.aiMemory },\n include: { sourceRefs: true },\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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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@igstack/app-catalog-backend-core",
3
- "version": "6.1.3",
3
+ "version": "6.1.4-alpha-20260926184125",
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": "6.1.3",
49
- "@igstack/app-catalog-table-sync": "6.1.3"
48
+ "@igstack/app-catalog-shared-core": "6.1.4-alpha-20260926184125",
49
+ "@igstack/app-catalog-table-sync": "6.1.4-alpha-20260926184125"
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": "1aaba32ee75a50b049cfcbb724f459eb137cc3cb",
76
+ "gitHead": "b872da7af4525d2fa487f7e5e543aa2084be72ad",
77
77
  "scripts": {
78
78
  "build": "tsdown",
79
79
  "build:lenient": "tsdown",
@@ -19,7 +19,7 @@ describe('buildPgSslConfig', () => {
19
19
  afterEach(() => rmSync(dir, { recursive: true, force: true }))
20
20
 
21
21
  const env = (over: Record<string, string | undefined>): NodeJS.ProcessEnv =>
22
- over as NodeJS.ProcessEnv
22
+ over
23
23
 
24
24
  it('returns undefined when PGSSLMODE is unset (leave pg defaults)', () => {
25
25
  expect(buildPgSslConfig(env({}))).toBeUndefined()
@@ -82,9 +82,7 @@ export function tableSyncPrisma<
82
82
  where: whereGlobal,
83
83
  }
84
84
  : {}
85
- return (await prismOperations.findMany(
86
- findManyArgs,
87
- )) as MakeTFromPrismaModel<TPrismaModelName>[]
85
+ return await prismOperations.findMany(findManyArgs)
88
86
  },
89
87
  writeAll: async (createData, update, deleteIds) => {
90
88
  const prismaUniqKey = params.uniqColumns.join('_')
@@ -191,7 +189,7 @@ export function tableSyncPrisma<
191
189
  const newVar = await txOps.create({
192
190
  data: dataMappedElement,
193
191
  })
194
- results.push(newVar as MakeTFromPrismaModel<TPrismaModelName>)
192
+ results.push(newVar)
195
193
  }
196
194
  }
197
195
 
@@ -8,10 +8,7 @@ import type {
8
8
  Resource,
9
9
  SourceCheck,
10
10
  } from '../../types/common/appCatalogTypes'
11
- import type {
12
- CustomConfig,
13
- ServiceConfig,
14
- } from '../../types/common/approvalMethodTypes'
11
+ import type { ServiceConfig } from '../../types/common/approvalMethodTypes'
15
12
  import type { Group, Person } from '../../types/common/personGroupTypes'
16
13
  import { omit } from 'radashi'
17
14
  import { parseSourceSlug } from '../../utils/parseSourceSlug'
@@ -106,26 +103,26 @@ export async function getApprovalMethodsFromPrisma(): Promise<
106
103
  return {
107
104
  ...baseFields,
108
105
  type: 'custom' as const,
109
- config: config as CustomConfig,
106
+ config: config,
110
107
  }
111
108
  case 'noAccessRequired':
112
109
  return {
113
110
  ...baseFields,
114
111
  type: 'noAccessRequired' as const,
115
- config: config as CustomConfig,
112
+ config: config,
116
113
  }
117
114
  case 'unknown':
118
115
  return {
119
116
  ...baseFields,
120
117
  type: 'unknown' as const,
121
- config: config as CustomConfig,
118
+ config: config,
122
119
  }
123
120
  case 'personTeam':
124
121
  // Legacy: map personTeam to custom
125
122
  return {
126
123
  ...baseFields,
127
124
  type: 'custom' as const,
128
- config: config as CustomConfig,
125
+ config: config,
129
126
  }
130
127
  }
131
128
  })
@@ -157,7 +154,7 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
157
154
  const iconName = row.iconName == null ? undefined : row.iconName
158
155
  const abbreviation = row.abbreviation == null ? undefined : row.abbreviation
159
156
  const nicknames = (row.nicknames as unknown as string[] | null)?.length
160
- ? (row.nicknames as unknown as string[])
157
+ ? row.nicknames
161
158
  : undefined
162
159
  const deprecated =
163
160
  row.deprecated == null
@@ -166,7 +163,7 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
166
163
  const aiPrompt = row.aiPrompt == null ? undefined : row.aiPrompt
167
164
  const aiMemory = row.aiMemory == null ? undefined : row.aiMemory
168
165
  const urlIssues = (row.urlIssues as unknown as string[] | null)?.length
169
- ? (row.urlIssues as unknown as string[])
166
+ ? row.urlIssues
170
167
  : undefined
171
168
  const tiers =
172
169
  row.tiers == null ? undefined : (row.tiers as unknown as Resource['tiers'])
@@ -213,9 +210,7 @@ function rowToResource(row: ResourceRowWithSourceRefs): Resource {
213
210
  ownerPersonSlug: row.ownerPersonSlug ?? undefined,
214
211
  approverSlugs: row.approverSlugs.length ? row.approverSlugs : undefined,
215
212
  accessComments: row.accessComments ?? undefined,
216
- extra: row.extra
217
- ? (row.extra as unknown as Record<string, unknown>)
218
- : undefined,
213
+ extra: row.extra ? row.extra : undefined,
219
214
  // Only surface freshness once the app has actually been scanned.
220
215
  freshness: freshness.lastCheckedAt ? freshness : undefined,
221
216
  createdAt: row.createdAt.toISOString(),