@igstack/app-catalog-backend-core 6.12.4 → 6.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.mts +2 -2
- package/dist/modules/appCatalog/service.d.mts.map +1 -1
- package/dist/modules/appCatalog/service.mjs +21 -1
- package/dist/modules/appCatalog/service.mjs.map +1 -1
- package/dist/server/controller.d.mts +16 -0
- package/dist/server/controller.d.mts.map +1 -1
- package/dist/server/controller.mjs +2 -1
- package/dist/server/controller.mjs.map +1 -1
- package/dist/types/common/approvalMethodTypes.d.mts +54 -1
- package/dist/types/common/approvalMethodTypes.d.mts.map +1 -1
- package/package.json +4 -4
- package/src/__tests__/serviceDesks.test.ts +76 -0
- package/src/modules/appCatalog/service.ts +24 -0
- package/src/server/controller.ts +2 -0
- package/src/types/common/approvalMethodTypes.ts +50 -0
package/dist/index.d.mts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AcMetaDictionary, Tag } from "./types/common/sharedTypes.mjs";
|
|
2
2
|
import { AcResourceIndexed } from "./types/common/resourceTypes.mjs";
|
|
3
|
-
import { AccessRequest, AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, Role, ServiceConfig } from "./types/common/approvalMethodTypes.mjs";
|
|
3
|
+
import { AccessRequest, AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, Role, ServiceConfig, ServicePortal, ServiceRequestField, ServiceRequestType } from "./types/common/approvalMethodTypes.mjs";
|
|
4
4
|
import { Group, Person, PersonOrGroup } from "./types/common/personGroupTypes.mjs";
|
|
5
5
|
import { AppApprovalMethod, AppCatalogData, AppCategory, AppVersionInfo, Claim, Freshness, GroupingTagDefinition, GroupingTagValue, QuickJump, Resource, SourceCadence, SourceCheck, SourcePulse, SourcePulseState, SourceReference, SourceSchedule, TierVariant, VersionInfo } from "./types/common/appCatalogTypes.mjs";
|
|
6
6
|
import { AcEnvIndexed } from "./types/common/env/envTypes.mjs";
|
|
@@ -43,4 +43,4 @@ import { injectCustomScripts } from "./middleware/htmlInjection.mjs";
|
|
|
43
43
|
import { runLighthouseKeeperDemo } from "./modules/lighthouseKeeper/demo.mjs";
|
|
44
44
|
import { APP_CATALOG_AI_SYSTEM_PROMPT, createAppCatalogAITools } from "./modules/lighthouseKeeper/tools.mjs";
|
|
45
45
|
import { getBuildPipelineId, getFrontendPackageVersion, getVersionInfo } from "./utils/versionUtils.mjs";
|
|
46
|
-
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AccessRequest, type Actor, type ActorKind, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, AppRole, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth, type CaptureResult, Claim, type CommunityToolsOptions, CustomConfig, EDIT_WINDOW_MS, type FeedbackAttachmentControllerConfig, type FeedbackList, type FeedbackView, Freshness, Group, GroupingTagDefinition, GroupingTagValue, type IconRestControllerConfig, MAX_ATTACHMENTS, MAX_BODY_LENGTH, MAX_SUBJECT_LENGTH, type MakeTFromPrismaModel, type MiddlewareContext, type ObjectKeys, Person, PersonOrGroup, PersonTeamConfig, QuickJump, type Resource, Role, type ScalarFilter, type ScalarKeys, type ScreenshotCapturer, type ScreenshotRestControllerConfig, ServiceConfig, SourceCadence, SourceCheck, SourcePulse, SourcePulseState, SourceReference, SourceSchedule, type SyncAppCatalogResult, type SyncAssetsConfig, TABLE_SYNC_MAGAZINE, type TRPCRouter, type TableSyncMagazine, type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma, Tag, TierVariant, type ToolInput, type ToolPreview, type UpsertIconInput, VISITOR_COOKIE, VersionInfo, type Visitor, type Writer, buildPgSchemaOptions, buildPgSslConfig, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createCommunityToolsRouter, createCorePrismaClient, createFeedbackRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getResourcesFromPrisma, getVersionInfo, injectCustomScripts, printLinkCheckReport, publicIdFromHash, registerAssetRestController, registerAuthRoutes, registerFeedbackAttachmentController, registerIconRestController, registerScreenshotRestController, resolveActor, resolveDbSchema, resolveVisitor, runLighthouseKeeperDemo, setDbClient, staticControllerContract, stripHiddenText, syncAppCatalog, syncAssets, tableSyncPrisma, toolInputSchema, upsertIcon, upsertIcons, verifyDbSchema, visitorFromSeed };
|
|
46
|
+
export { APP_CATALOG_AI_SYSTEM_PROMPT, AcAppIndexed, AcAppPageIndexed, AcAppUiIndexed, AcAppsMeta, type AcAuthConfig, AcBackendAppDto, AcBackendAppInput, AcBackendAppUIBaseInput, AcBackendAppUIInput, AcBackendCredentialInput, AcBackendDataFreshness, AcBackendDataSourceInput, AcBackendDataSourceInputCommon, AcBackendDataSourceInputDb, AcBackendDataSourceInputKafka, AcBackendDataVersion, AcBackendDeployableInput, AcBackendDeployment, AcBackendDeploymentInput, AcBackendEnvironmentInput, AcBackendPageInput, type AcBackendProvider, AcBackendTagDescriptionDataIndexed, AcBackendTagFixedTagValue, AcBackendTagsDescriptionDataIndexed, AcBackendUiDefaultsInput, AcBackendVersionsRequestParams, AcBackendVersionsReturn, AcContextIndexed, type AcDatabaseConfig, AcDatabaseManager, AcEnvIndexed, type AcFeatureToggles, type AcLifecycleHooks, type AcLighthouseKeeperConfig, type AcMcpServerConfig, AcMetaDictionary, type AcMiddlewareOptions, type AcMiddlewareResult, AcResourceIndexed, type AcStaticControllerContract, type AcTrpcContext, type AcTrpcContextOptions, AccessRequest, type Actor, type ActorKind, AppAccessRequest, AppApprovalMethod, AppCatalogCompanySpecificBackend, AppCatalogData, AppCategory, AppRole, AppVersionInfo, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, type AssetRestControllerConfig, type AuthRouter, type BetterAuth, type CaptureResult, Claim, type CommunityToolsOptions, CustomConfig, EDIT_WINDOW_MS, type FeedbackAttachmentControllerConfig, type FeedbackList, type FeedbackView, Freshness, Group, GroupingTagDefinition, GroupingTagValue, type IconRestControllerConfig, MAX_ATTACHMENTS, MAX_BODY_LENGTH, MAX_SUBJECT_LENGTH, type MakeTFromPrismaModel, type MiddlewareContext, type ObjectKeys, Person, PersonOrGroup, PersonTeamConfig, QuickJump, type Resource, Role, type ScalarFilter, type ScalarKeys, type ScreenshotCapturer, type ScreenshotRestControllerConfig, ServiceConfig, ServicePortal, ServiceRequestField, ServiceRequestType, SourceCadence, SourceCheck, SourcePulse, SourcePulseState, SourceReference, SourceSchedule, type SyncAppCatalogResult, type SyncAssetsConfig, TABLE_SYNC_MAGAZINE, type TRPCRouter, type TableSyncMagazine, type TableSyncMagazineModelNameKey, type TableSyncParamsPrisma, Tag, TierVariant, type ToolInput, type ToolPreview, type UpsertIconInput, VISITOR_COOKIE, VersionInfo, type Visitor, type Writer, buildPgSchemaOptions, buildPgSslConfig, checkAllLinks, connectDb, createAcMiddleware, createAcTrpcContext, createAppCatalogAITools, createAuth, createAuthRouter, createCommunityToolsRouter, createCorePrismaClient, createFeedbackRouter, createTrpcRouter, disconnectDb, getAssetByName, getBuildPipelineId, getDbClient, getFrontendPackageVersion, getResourcesFromPrisma, getVersionInfo, injectCustomScripts, printLinkCheckReport, publicIdFromHash, registerAssetRestController, registerAuthRoutes, registerFeedbackAttachmentController, registerIconRestController, registerScreenshotRestController, resolveActor, resolveDbSchema, resolveVisitor, runLighthouseKeeperDemo, setDbClient, staticControllerContract, stripHiddenText, syncAppCatalog, syncAssets, tableSyncPrisma, toolInputSchema, upsertIcon, upsertIcons, verifyDbSchema, visitorFromSeed };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service.d.mts","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"service.d.mts","names":[],"sources":["../../../src/modules/appCatalog/service.ts"],"mappings":";;iBA2PsB,sBAAA,CAAA,GAA0B,OAAA,CAAQ,QAAA"}
|
|
@@ -38,7 +38,27 @@ async function getGroupingTagDefinitionsFromPrisma() {
|
|
|
38
38
|
};
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* Every approval method for the main catalog payload. A service desk's `portal`
|
|
43
|
+
* (its forms and their fields) is dropped: across a hundred desks it outweighs
|
|
44
|
+
* the rest of the payload, and only the Service Desks view reads it, through
|
|
45
|
+
* `getServiceDesksFromPrisma`.
|
|
46
|
+
*/
|
|
41
47
|
async function getApprovalMethodsFromPrisma() {
|
|
48
|
+
return (await getAllApprovalMethodsFromPrisma()).map((method) => {
|
|
49
|
+
if (method.type !== "service" || !method.config.portal) return method;
|
|
50
|
+
const { portal: _portal, ...config } = method.config;
|
|
51
|
+
return {
|
|
52
|
+
...method,
|
|
53
|
+
config
|
|
54
|
+
};
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
/** The `service` approval methods with their portals, for the Service Desks view. */
|
|
58
|
+
async function getServiceDesksFromPrisma() {
|
|
59
|
+
return (await getAllApprovalMethodsFromPrisma()).filter((m) => m.type === "service");
|
|
60
|
+
}
|
|
61
|
+
async function getAllApprovalMethodsFromPrisma() {
|
|
42
62
|
return (await getDbClient().dbApprovalMethod.findMany()).map((row) => {
|
|
43
63
|
const baseFields = {
|
|
44
64
|
slug: row.slug,
|
|
@@ -225,5 +245,5 @@ async function getAppCatalogData(getResourcesOptional) {
|
|
|
225
245
|
}
|
|
226
246
|
|
|
227
247
|
//#endregion
|
|
228
|
-
export { getAppCatalogData, getResourcesFromPrisma, updateAiMemory, updateApp };
|
|
248
|
+
export { getAppCatalogData, getResourcesFromPrisma, getServiceDesksFromPrisma, updateAiMemory, updateApp };
|
|
229
249
|
//# sourceMappingURL=service.mjs.map
|
|
@@ -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 { 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\nexport function 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 background = row.background == null ? undefined : row.background\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 claims =\n row.claims == null\n ? undefined\n : (row.claims as unknown as Resource['claims'])\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 background,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n aiPrompt,\n aiMemory,\n urlIssues,\n tiers,\n quickJumps,\n claims,\n childrenLabel: row.childrenLabel ?? undefined,\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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"}
|
|
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\n/**\n * Every approval method for the main catalog payload. A service desk's `portal`\n * (its forms and their fields) is dropped: across a hundred desks it outweighs\n * the rest of the payload, and only the Service Desks view reads it, through\n * `getServiceDesksFromPrisma`.\n */\nexport async function getApprovalMethodsFromPrisma(): Promise<\n AppApprovalMethod[]\n> {\n return (await getAllApprovalMethodsFromPrisma()).map((method) => {\n if (method.type !== 'service' || !method.config.portal) return method\n const { portal: _portal, ...config } = method.config\n return { ...method, config }\n })\n}\n\n/** The `service` approval methods with their portals, for the Service Desks view. */\nexport async function getServiceDesksFromPrisma(): Promise<\n Extract<AppApprovalMethod, { type: 'service' }>[]\n> {\n return (await getAllApprovalMethodsFromPrisma()).filter(\n (m): m is Extract<AppApprovalMethod, { type: 'service' }> =>\n m.type === 'service',\n )\n}\n\nasync function getAllApprovalMethodsFromPrisma(): Promise<AppApprovalMethod[]> {\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\nexport function 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 background = row.background == null ? undefined : row.background\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 claims =\n row.claims == null\n ? undefined\n : (row.claims as unknown as Resource['claims'])\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 background,\n tags,\n appUrl,\n iconName,\n screenshotIds,\n sources,\n deprecated,\n aiPrompt,\n aiMemory,\n urlIssues,\n tiers,\n quickJumps,\n claims,\n childrenLabel: row.childrenLabel ?? undefined,\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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"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ServiceConfig } from "../types/common/approvalMethodTypes.mjs";
|
|
1
2
|
import { AppCatalogData, Resource } from "../types/common/appCatalogTypes.mjs";
|
|
2
3
|
import { AcTrpcContext } from "./acTrpcContext.mjs";
|
|
3
4
|
import { BetterAuth } from "../modules/auth/auth.mjs";
|
|
@@ -33,6 +34,21 @@ declare function createTrpcRouter(auth?: BetterAuth, options?: {
|
|
|
33
34
|
output: AppCatalogData;
|
|
34
35
|
meta: object;
|
|
35
36
|
}>;
|
|
37
|
+
getServiceDesks: _trpc_server0.TRPCQueryProcedure<{
|
|
38
|
+
input: void;
|
|
39
|
+
output: Omit<{
|
|
40
|
+
slug: string;
|
|
41
|
+
displayName: string;
|
|
42
|
+
shortName?: string;
|
|
43
|
+
deprecatedAliases?: string[];
|
|
44
|
+
createdAt?: Date;
|
|
45
|
+
updatedAt?: Date;
|
|
46
|
+
} & {
|
|
47
|
+
type: "service";
|
|
48
|
+
config: ServiceConfig;
|
|
49
|
+
}, "createdAt" | "updatedAt">[];
|
|
50
|
+
meta: object;
|
|
51
|
+
}>;
|
|
36
52
|
updateApp: _trpc_server0.TRPCMutationProcedure<{
|
|
37
53
|
input: {
|
|
38
54
|
id: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.mts","names":[],"sources":["../../src/server/controller.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"controller.d.mts","names":[],"sources":["../../src/server/controller.ts"],"mappings":";;;;;;;;;;;;;;;;iBAsCgB,gBAAA,CACd,IAAA,GAAO,UAAA,EACP,OAAA;EACE,eAAA;EACA,cAAA,GAAiB,qBAAA;AAAA,kBAClB,eAAA;OADuC,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KA2C9B,UAAA,GAAa,UAAA,QAAkB,gBAAA"}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getAppCatalogData, updateAiMemory, updateApp } from "../modules/appCatalog/service.mjs";
|
|
1
|
+
import { getAppCatalogData, getServiceDesksFromPrisma, updateAiMemory, updateApp } from "../modules/appCatalog/service.mjs";
|
|
2
2
|
import { createAuthRouter } from "../modules/auth/authRouter.mjs";
|
|
3
3
|
import { publicProcedure, router, t } from "./trpcSetup.mjs";
|
|
4
4
|
import { createFeedbackRouter } from "../modules/feedback/feedbackRouter.mjs";
|
|
@@ -33,6 +33,7 @@ function createTrpcRouter(auth, options) {
|
|
|
33
33
|
...versions && { versions }
|
|
34
34
|
};
|
|
35
35
|
}),
|
|
36
|
+
getServiceDesks: publicProcedure.query(() => getServiceDesksFromPrisma()),
|
|
36
37
|
updateApp: publicProcedure.input(updateAppInputSchema).mutation(async ({ input }) => {
|
|
37
38
|
return updateApp(input);
|
|
38
39
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.mjs","names":["updateAppService","updateAiMemoryService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n updateAiMemory as updateAiMemoryService,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { Resource } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createFeedbackRouter } from '../modules/feedback/feedbackRouter'\nimport { createCommunityToolsRouter } from '../modules/communityTools/router'\nimport type { CommunityToolsOptions } from '../middleware/types'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: {\n devLoginEnabled?: boolean\n communityTools?: CommunityToolsOptions\n },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAppService(input)\n }),\n updateAiMemory: publicProcedure\n .input(\n z.object({\n id: z.string(),\n aiMemory: z.string().nullable(),\n }),\n )\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAiMemoryService(input)\n }),\n }),\n\n feedback: createFeedbackRouter(),\n\n communityTools: createCommunityToolsRouter(options?.communityTools),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;;;
|
|
1
|
+
{"version":3,"file":"controller.mjs","names":["updateAppService","updateAiMemoryService"],"sources":["../../src/server/controller.ts"],"sourcesContent":["import {\n getAppCatalogData,\n getServiceDesksFromPrisma,\n updateAiMemory as updateAiMemoryService,\n updateApp as updateAppService,\n} from '../modules/appCatalog/service'\nimport type { AppCatalogData } from '../types'\nimport type { Resource } from '../types/common/appCatalogTypes'\n\nimport type { BetterAuth } from '../modules/auth/auth'\nimport { createAuthRouter } from '../modules/auth/authRouter.js'\nimport { createFeedbackRouter } from '../modules/feedback/feedbackRouter'\nimport { createCommunityToolsRouter } from '../modules/communityTools/router'\nimport type { CommunityToolsOptions } from '../middleware/types'\nimport { publicProcedure, router, t } from './trpcSetup'\nimport { z } from 'zod'\n\nconst updateAppInputSchema = z.object({\n id: z.string(),\n data: z\n .object({\n displayName: z.string().optional(),\n abbreviation: z.string().max(20).nullable().optional(),\n slug: z.string().optional(),\n appUrl: z.string().optional(),\n description: z.string().optional(),\n sources: z.array(z.string()).optional(),\n aiPrompt: z.string().nullable().optional(),\n })\n .refine((d) => Object.keys(d).length > 0, {\n message: 'At least one field required',\n }),\n})\n\n/**\n * Create the main tRPC router with optional auth instance\n * @param auth - Optional Better Auth instance for auth-related queries\n */\nexport function createTrpcRouter(\n auth?: BetterAuth,\n options?: {\n devLoginEnabled?: boolean\n communityTools?: CommunityToolsOptions\n },\n) {\n return router({\n appCatalog: router({\n getData: publicProcedure.query(\n async ({ ctx }): Promise<AppCatalogData> => {\n const baseData = await getAppCatalogData()\n const versions = await ctx.companySpecificBackend.getVersionInfo?.()\n\n return {\n ...baseData,\n ...(versions && { versions }),\n }\n },\n ),\n getServiceDesks: publicProcedure.query(() => getServiceDesksFromPrisma()),\n updateApp: publicProcedure\n .input(updateAppInputSchema)\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAppService(input)\n }),\n updateAiMemory: publicProcedure\n .input(\n z.object({\n id: z.string(),\n aiMemory: z.string().nullable(),\n }),\n )\n .mutation(async ({ input }): Promise<Resource> => {\n return updateAiMemoryService(input)\n }),\n }),\n\n feedback: createFeedbackRouter(),\n\n communityTools: createCommunityToolsRouter(options?.communityTools),\n\n // Auth routes (requires auth instance)\n auth: createAuthRouter(t, auth, options),\n })\n}\n\nexport type TRPCRouter = ReturnType<typeof createTrpcRouter>\n"],"mappings":";;;;;;;;AAiBA,MAAM,uBAAuB,EAAE,OAAO;CACpC,IAAI,EAAE,QAAQ;CACd,MAAM,EACH,OAAO;EACN,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,cAAc,EAAE,QAAQ,CAAC,IAAI,GAAG,CAAC,UAAU,CAAC,UAAU;EACtD,MAAM,EAAE,QAAQ,CAAC,UAAU;EAC3B,QAAQ,EAAE,QAAQ,CAAC,UAAU;EAC7B,aAAa,EAAE,QAAQ,CAAC,UAAU;EAClC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAC,CAAC,UAAU;EACvC,UAAU,EAAE,QAAQ,CAAC,UAAU,CAAC,UAAU;EAC3C,CAAC,CACD,QAAQ,MAAM,OAAO,KAAK,EAAE,CAAC,SAAS,GAAG,EACxC,SAAS,+BACV,CAAC;CACL,CAAC;;;;;AAMF,SAAgB,iBACd,MACA,SAIA;AACA,QAAO,OAAO;EACZ,YAAY,OAAO;GACjB,SAAS,gBAAgB,MACvB,OAAO,EAAE,UAAmC;IAC1C,MAAM,WAAW,MAAM,mBAAmB;IAC1C,MAAM,WAAW,MAAM,IAAI,uBAAuB,kBAAkB;AAEpE,WAAO;KACL,GAAG;KACH,GAAI,YAAY,EAAE,UAAU;KAC7B;KAEJ;GACD,iBAAiB,gBAAgB,YAAY,2BAA2B,CAAC;GACzE,WAAW,gBACR,MAAM,qBAAqB,CAC3B,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOA,UAAiB,MAAM;KAC9B;GACJ,gBAAgB,gBACb,MACC,EAAE,OAAO;IACP,IAAI,EAAE,QAAQ;IACd,UAAU,EAAE,QAAQ,CAAC,UAAU;IAChC,CAAC,CACH,CACA,SAAS,OAAO,EAAE,YAA+B;AAChD,WAAOC,eAAsB,MAAM;KACnC;GACL,CAAC;EAEF,UAAU,sBAAsB;EAEhC,gBAAgB,2BAA2B,SAAS,eAAe;EAGnE,MAAM,iBAAiB,GAAG,MAAM,QAAQ;EACzC,CAAC"}
|
|
@@ -13,6 +13,59 @@ interface ServiceConfig {
|
|
|
13
13
|
url?: string;
|
|
14
14
|
icon?: string;
|
|
15
15
|
description?: string;
|
|
16
|
+
/**
|
|
17
|
+
* What the desk's portal offers: its request forms and the fields each one
|
|
18
|
+
* asks for. Set for ticketing portals the deployment reads from its service
|
|
19
|
+
* desk tool; drives search and the details card in the Service Desks view.
|
|
20
|
+
* Too heavy for the main catalog payload, so `appCatalog.getData` leaves it
|
|
21
|
+
* out and `appCatalog.getServiceDesks` returns it.
|
|
22
|
+
*/
|
|
23
|
+
portal?: ServicePortal;
|
|
24
|
+
}
|
|
25
|
+
/** A ticketing portal behind a `service` approval method. */
|
|
26
|
+
interface ServicePortal {
|
|
27
|
+
/** Short code people quote and search by ("HELP"). */
|
|
28
|
+
key: string;
|
|
29
|
+
/** Business area the desk serves, used to group and tag it. */
|
|
30
|
+
area?: string;
|
|
31
|
+
requestTypes: ServiceRequestType[];
|
|
32
|
+
/** Markdown: what happens after filing, when to chase, where to ask. */
|
|
33
|
+
followUp?: string;
|
|
34
|
+
keyPeople?: {
|
|
35
|
+
name: string;
|
|
36
|
+
role?: string;
|
|
37
|
+
}[];
|
|
38
|
+
/** A thread or page with good example requests. */
|
|
39
|
+
examples?: {
|
|
40
|
+
label: string;
|
|
41
|
+
url: string;
|
|
42
|
+
};
|
|
43
|
+
/** URLs the portal entry is built from. */
|
|
44
|
+
sources?: string[];
|
|
45
|
+
/** Guidance for whoever maintains this entry. */
|
|
46
|
+
aiPrompt?: string;
|
|
47
|
+
/** Facts the maintainer has learned about this desk. */
|
|
48
|
+
aiMemory?: string;
|
|
49
|
+
/** When the request types were last read from the source, ISO 8601. */
|
|
50
|
+
syncedAt?: string;
|
|
51
|
+
}
|
|
52
|
+
/** One form on a portal. */
|
|
53
|
+
interface ServiceRequestType {
|
|
54
|
+
id: string;
|
|
55
|
+
name: string;
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Opens this exact form. */
|
|
58
|
+
url: string;
|
|
59
|
+
fields: ServiceRequestField[];
|
|
60
|
+
}
|
|
61
|
+
/** One field on a form, as the source describes it. */
|
|
62
|
+
interface ServiceRequestField {
|
|
63
|
+
name: string;
|
|
64
|
+
required: boolean;
|
|
65
|
+
/** Source field type: string, user, date, array, option… */
|
|
66
|
+
type?: string;
|
|
67
|
+
/** Choices for a dropdown field. */
|
|
68
|
+
options?: string[];
|
|
16
69
|
}
|
|
17
70
|
/**
|
|
18
71
|
* PersonTeam type config - for person/group-based approval
|
|
@@ -139,5 +192,5 @@ type AccessRequest = AppAccessRequest;
|
|
|
139
192
|
/** @deprecated Use AppRole instead (same type, new canonical name) */
|
|
140
193
|
type Role = AppRole;
|
|
141
194
|
//#endregion
|
|
142
|
-
export { AccessRequest, AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, Role, ServiceConfig };
|
|
195
|
+
export { AccessRequest, AppAccessRequest, AppRole, ApprovalMethod, ApprovalMethodConfig, ApprovalMethodType, ApprovalUrl, CustomConfig, PersonTeamConfig, Role, ServiceConfig, ServicePortal, ServiceRequestField, ServiceRequestType };
|
|
143
196
|
//# sourceMappingURL=approvalMethodTypes.d.mts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;EACA,WAAA;AAAA;
|
|
1
|
+
{"version":3,"file":"approvalMethodTypes.d.mts","names":[],"sources":["../../../src/types/common/approvalMethodTypes.ts"],"mappings":";;AAWA;;;;;KAAY,kBAAA;;;;UAUK,aAAA;EACf,GAAA;EACA,IAAA;EACA,WAAA;EAQS;;;AAIX;;;;EAJE,MAAA,GAAS,aAAA;AAAA;;UAIM,aAAA;EAOf;EALA,GAAA;EAMc;EAJd,IAAA;EACA,YAAA,EAAc,kBAAA;EAKD;EAHb,QAAA;EACA,SAAA;IAAc,IAAA;IAAc,IAAA;EAAA;EAUpB;EARR,QAAA;IAAa,KAAA;IAAe,GAAA;EAAA;EAkBD;EAhB3B,OAAA;EAYA;EAVA,QAAA;EAaA;EAXA,QAAA;EAYQ;EAVR,QAAA;AAAA;AAcF;AAAA,UAViB,kBAAA;EACf,EAAA;EACA,IAAA;EACA,WAAA;EASA;EAPA,GAAA;EACA,MAAA,EAAQ,mBAAA;AAAA;;UAIO,mBAAA;EACf,IAAA;EACA,QAAA;EAWA;EATA,IAAA;EAgBe;EAdf,OAAA;AAAA;;;AAqBF;UAfiB,gBAAA;EACf,WAAA;EACA,UAAA;AAAA;;;;UAMe,YAAA;;;;KAOL,oBAAA,GACR,aAAA,GACA,gBAAA,GACA,YAAA;;;;KAKQ,cAAA;EACV,IAAA;EA8BY;;;EA1BZ,WAAA;EAsCwB;;;;;;;;;;;EA1BxB,SAAA;EAUY;;;EANZ,iBAAA;EACA,SAAA,GAAY,IAAA;EACZ,SAAA,GAAY,IAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,aAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,gBAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;EAGR,IAAA;EACA,MAAA,EAAQ,YAAA;AAAA;;;AAiCd;UAtBiB,OAAA;;;;EAIf,WAAA;EA0B+B;;;EAtB/B,WAAA;EA+BA;;;;;;;EAvBA,UAAA;AAAA;;AAkDF;;UA5CiB,WAAA;EACf,KAAA;EACA,GAAA;AAAA;;;;UAMe,gBAAA;;;;EAIf,kBAAA;;;;EAKA,QAAA;;;;EAIA,aAAA;;;;EAIA,wBAAA;;;;EAKA,KAAA,GAAQ,OAAA;;;;;EAMR,aAAA;;;;EAIA,IAAA,GAAO,WAAA;AAAA;;KAIG,aAAA,GAAgB,gBAAA;;KAGhB,IAAA,GAAO,OAAA"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@igstack/app-catalog-backend-core",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.13.0",
|
|
4
4
|
"description": "Backend core library for App Catalog",
|
|
5
5
|
"homepage": "https://github.com/lislon/app-catalog",
|
|
6
6
|
"repository": {
|
|
@@ -44,8 +44,8 @@
|
|
|
44
44
|
"tsyringe": "^4.10.0",
|
|
45
45
|
"yaml": "^2.8.0",
|
|
46
46
|
"zod": "^4.3.5",
|
|
47
|
-
"@igstack/app-catalog-
|
|
48
|
-
"@igstack/app-catalog-
|
|
47
|
+
"@igstack/app-catalog-shared-core": "6.13.0",
|
|
48
|
+
"@igstack/app-catalog-table-sync": "6.13.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@tanstack/vite-config": "^0.4.3",
|
|
@@ -79,7 +79,7 @@
|
|
|
79
79
|
"engines": {
|
|
80
80
|
"node": ">=24"
|
|
81
81
|
},
|
|
82
|
-
"gitHead": "
|
|
82
|
+
"gitHead": "41778e087976cc63f03b3e341a4433e16c69433b",
|
|
83
83
|
"scripts": {
|
|
84
84
|
"build": "tsdown",
|
|
85
85
|
"build:lenient": "tsdown",
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// @vitest-environment node
|
|
2
|
+
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import * as dbClient from '../db/client'
|
|
4
|
+
import type { PrismaClient } from '../generated/prisma/client'
|
|
5
|
+
import {
|
|
6
|
+
getApprovalMethodsFromPrisma,
|
|
7
|
+
getServiceDesksFromPrisma,
|
|
8
|
+
} from '../modules/appCatalog/service'
|
|
9
|
+
|
|
10
|
+
const portal = {
|
|
11
|
+
key: 'HELP',
|
|
12
|
+
area: 'IT',
|
|
13
|
+
requestTypes: [
|
|
14
|
+
{
|
|
15
|
+
id: '7',
|
|
16
|
+
name: 'Hardware request',
|
|
17
|
+
url: 'https://desk.example.com/portal/1/create/7',
|
|
18
|
+
fields: [{ name: 'Item', required: true, options: ['Laptop', 'Dock'] }],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function given(rows: object[]) {
|
|
24
|
+
vi.spyOn(dbClient, 'getDbClient').mockReturnValue({
|
|
25
|
+
dbApprovalMethod: { findMany: vi.fn().mockResolvedValue(rows) },
|
|
26
|
+
} as unknown as PrismaClient)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const deskRow = {
|
|
30
|
+
slug: 'help-desk',
|
|
31
|
+
type: 'service',
|
|
32
|
+
displayName: 'Help Desk',
|
|
33
|
+
config: { url: 'https://desk.example.com/portal/1', portal },
|
|
34
|
+
}
|
|
35
|
+
const customRow = {
|
|
36
|
+
slug: 'ask-owner',
|
|
37
|
+
type: 'custom',
|
|
38
|
+
displayName: 'Ask the owner',
|
|
39
|
+
config: {},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
describe('service desk portals', () => {
|
|
43
|
+
afterEach(() => vi.restoreAllMocks())
|
|
44
|
+
|
|
45
|
+
it('leaves the portal out of the main catalog payload', async () => {
|
|
46
|
+
given([deskRow, customRow])
|
|
47
|
+
|
|
48
|
+
expect(await getApprovalMethodsFromPrisma()).toEqual([
|
|
49
|
+
{
|
|
50
|
+
slug: 'help-desk',
|
|
51
|
+
displayName: 'Help Desk',
|
|
52
|
+
type: 'service',
|
|
53
|
+
config: { url: 'https://desk.example.com/portal/1' },
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
slug: 'ask-owner',
|
|
57
|
+
displayName: 'Ask the owner',
|
|
58
|
+
type: 'custom',
|
|
59
|
+
config: {},
|
|
60
|
+
},
|
|
61
|
+
])
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('returns only service desks, portal included, for the Service Desks view', async () => {
|
|
65
|
+
given([deskRow, customRow])
|
|
66
|
+
|
|
67
|
+
expect(await getServiceDesksFromPrisma()).toEqual([
|
|
68
|
+
{
|
|
69
|
+
slug: 'help-desk',
|
|
70
|
+
displayName: 'Help Desk',
|
|
71
|
+
type: 'service',
|
|
72
|
+
config: { url: 'https://desk.example.com/portal/1', portal },
|
|
73
|
+
},
|
|
74
|
+
])
|
|
75
|
+
})
|
|
76
|
+
})
|
|
@@ -74,9 +74,33 @@ export async function getGroupingTagDefinitionsFromPrisma(): Promise<
|
|
|
74
74
|
})
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Every approval method for the main catalog payload. A service desk's `portal`
|
|
79
|
+
* (its forms and their fields) is dropped: across a hundred desks it outweighs
|
|
80
|
+
* the rest of the payload, and only the Service Desks view reads it, through
|
|
81
|
+
* `getServiceDesksFromPrisma`.
|
|
82
|
+
*/
|
|
77
83
|
export async function getApprovalMethodsFromPrisma(): Promise<
|
|
78
84
|
AppApprovalMethod[]
|
|
79
85
|
> {
|
|
86
|
+
return (await getAllApprovalMethodsFromPrisma()).map((method) => {
|
|
87
|
+
if (method.type !== 'service' || !method.config.portal) return method
|
|
88
|
+
const { portal: _portal, ...config } = method.config
|
|
89
|
+
return { ...method, config }
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** The `service` approval methods with their portals, for the Service Desks view. */
|
|
94
|
+
export async function getServiceDesksFromPrisma(): Promise<
|
|
95
|
+
Extract<AppApprovalMethod, { type: 'service' }>[]
|
|
96
|
+
> {
|
|
97
|
+
return (await getAllApprovalMethodsFromPrisma()).filter(
|
|
98
|
+
(m): m is Extract<AppApprovalMethod, { type: 'service' }> =>
|
|
99
|
+
m.type === 'service',
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function getAllApprovalMethodsFromPrisma(): Promise<AppApprovalMethod[]> {
|
|
80
104
|
const prisma = getDbClient()
|
|
81
105
|
|
|
82
106
|
// Fetch all approval methods
|
package/src/server/controller.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getAppCatalogData,
|
|
3
|
+
getServiceDesksFromPrisma,
|
|
3
4
|
updateAiMemory as updateAiMemoryService,
|
|
4
5
|
updateApp as updateAppService,
|
|
5
6
|
} from '../modules/appCatalog/service'
|
|
@@ -55,6 +56,7 @@ export function createTrpcRouter(
|
|
|
55
56
|
}
|
|
56
57
|
},
|
|
57
58
|
),
|
|
59
|
+
getServiceDesks: publicProcedure.query(() => getServiceDesksFromPrisma()),
|
|
58
60
|
updateApp: publicProcedure
|
|
59
61
|
.input(updateAppInputSchema)
|
|
60
62
|
.mutation(async ({ input }): Promise<Resource> => {
|
|
@@ -23,6 +23,56 @@ export interface ServiceConfig {
|
|
|
23
23
|
url?: string // Service URL (clickable in UI)
|
|
24
24
|
icon?: string // Icon identifier
|
|
25
25
|
description?: string // Optional human description of the service desk (shown as subtext in the Service Desks view)
|
|
26
|
+
/**
|
|
27
|
+
* What the desk's portal offers: its request forms and the fields each one
|
|
28
|
+
* asks for. Set for ticketing portals the deployment reads from its service
|
|
29
|
+
* desk tool; drives search and the details card in the Service Desks view.
|
|
30
|
+
* Too heavy for the main catalog payload, so `appCatalog.getData` leaves it
|
|
31
|
+
* out and `appCatalog.getServiceDesks` returns it.
|
|
32
|
+
*/
|
|
33
|
+
portal?: ServicePortal
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A ticketing portal behind a `service` approval method. */
|
|
37
|
+
export interface ServicePortal {
|
|
38
|
+
/** Short code people quote and search by ("HELP"). */
|
|
39
|
+
key: string
|
|
40
|
+
/** Business area the desk serves, used to group and tag it. */
|
|
41
|
+
area?: string
|
|
42
|
+
requestTypes: ServiceRequestType[]
|
|
43
|
+
/** Markdown: what happens after filing, when to chase, where to ask. */
|
|
44
|
+
followUp?: string
|
|
45
|
+
keyPeople?: { name: string; role?: string }[]
|
|
46
|
+
/** A thread or page with good example requests. */
|
|
47
|
+
examples?: { label: string; url: string }
|
|
48
|
+
/** URLs the portal entry is built from. */
|
|
49
|
+
sources?: string[]
|
|
50
|
+
/** Guidance for whoever maintains this entry. */
|
|
51
|
+
aiPrompt?: string
|
|
52
|
+
/** Facts the maintainer has learned about this desk. */
|
|
53
|
+
aiMemory?: string
|
|
54
|
+
/** When the request types were last read from the source, ISO 8601. */
|
|
55
|
+
syncedAt?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One form on a portal. */
|
|
59
|
+
export interface ServiceRequestType {
|
|
60
|
+
id: string
|
|
61
|
+
name: string
|
|
62
|
+
description?: string
|
|
63
|
+
/** Opens this exact form. */
|
|
64
|
+
url: string
|
|
65
|
+
fields: ServiceRequestField[]
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One field on a form, as the source describes it. */
|
|
69
|
+
export interface ServiceRequestField {
|
|
70
|
+
name: string
|
|
71
|
+
required: boolean
|
|
72
|
+
/** Source field type: string, user, date, array, option… */
|
|
73
|
+
type?: string
|
|
74
|
+
/** Choices for a dropdown field. */
|
|
75
|
+
options?: string[]
|
|
26
76
|
}
|
|
27
77
|
|
|
28
78
|
/**
|