Skip to main content
Glama
resource-usage.ts8.01 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { BotEvent, MedplumClient } from '@medplum/core'; import type { Bundle, BundleEntry, Project } from '@medplum/fhirtypes'; // Email address to receive the resource usage report const REPORT_EMAIL = 'admin@example.com'; /** * Bot handler to generate and email a resource usage report for the current project. * This bot counts all resources by type in the project and sends a CSV report via email. * * @param medplum - The Medplum client instance * @param event - The bot event trigger * @returns A success message with the project ID processed */ export async function handler(medplum: MedplumClient, event: BotEvent): Promise<{ success: boolean }> { // Get the current project from the bot's metadata const bot = await medplum.readReference(event.bot); const projectId = bot.meta?.project as string; // Validate that we can determine the project ID if (!projectId) { console.warn('Could not determine project id'); } console.log('Generating resource usage report for project:', projectId); // Fetch the project resource to get its name const project = projectId ? await medplum.readResource('Project', projectId) : undefined; if (project) { console.log('Project name:', project.name); } // Get counts for all resource types in this project const counts = await getCountsByResourceType(medplum, projectId); // Generate CSV content const csvContent = generateCsvReport(project, counts); // Send the report via email await medplum.sendEmail({ to: REPORT_EMAIL, subject: `Resource Usage Report - ${project?.name || projectId || 'Unknown'}`, text: `Please find attached the resource usage report for project "${project?.name || projectId || 'Unknown'}".\n\nGenerated: ${new Date().toISOString()}`, attachments: [ { filename: `resource-usage-${projectId}-${new Date().toISOString().split('T')[0]}.csv`, content: csvContent, }, ], }); console.log(`Resource usage report emailed to ${REPORT_EMAIL}`); return { success: true }; } /** * Generates a CSV report from the resource counts. * * @param project - The project being reported on * @param counts - A map of resource type to count * @returns CSV string content with BOM for Excel compatibility */ function generateCsvReport(project: Project | undefined, counts: Record<string, number>): string { // Build CSV rows const output: string[][] = [ ['Project', project?.name || project?.id || 'Unknown'], ['Generated', new Date().toISOString()], [], // Empty row for spacing ['Resource Type', 'Count'], ]; // Create array of [resourceType, count] pairs and sort by count descending const resourceTypesWithCounts = RESOURCE_TYPES.map((resourceType) => ({ resourceType, count: counts[resourceType] || 0, })).sort((a, b) => b.count - a.count); // Add sorted resource types to output for (const { resourceType, count } of resourceTypesWithCounts) { output.push([resourceType, count.toString()]); } return output.map((row) => row.join(',')).join('\n'); } /** * Fetches resource counts for all FHIR resource types in a project. * Uses a batch request to efficiently query all resource types at once. * * @param medplum - The Medplum client instance * @param projectId - The project ID to get counts for * @returns A map of resource type to count */ async function getCountsByResourceType( medplum: MedplumClient, projectId: string | undefined ): Promise<Record<string, number>> { // Build a batch bundle with count queries for each resource type const request: Bundle = { resourceType: 'Bundle', type: 'batch', entry: RESOURCE_TYPES.map((resourceType: string) => ({ request: { method: 'GET', url: `${resourceType}?_summary=count${projectId ? `&_project=${projectId}` : ''}`, }, })), }; console.log(`Fetching counts for ${RESOURCE_TYPES.length} resource types...`); // Execute the batch request const response = await medplum.executeBatch(request); // Extract counts from the response const counts = (response.entry as BundleEntry[]).map((entry: BundleEntry) => { return ((entry.resource as Bundle)?.total as number) ?? NaN; }); console.log('Successfully retrieved resource counts'); // Convert to a map of resource type -> count return Object.fromEntries(counts.map((c, index) => [RESOURCE_TYPES[index], c])); } /** * List of all FHIR resource types to count in the report. * This includes both standard FHIR resources and Medplum-specific resources. */ export const RESOURCE_TYPES = [ 'Account', 'ActivityDefinition', 'AdverseEvent', 'Agent', 'AllergyIntolerance', 'Appointment', 'AppointmentResponse', 'AsyncJob', 'AuditEvent', 'Basic', 'BiologicallyDerivedProduct', 'BodyStructure', 'Bot', 'BulkDataExport', 'Bundle', 'CapabilityStatement', 'CarePlan', 'CareTeam', 'CatalogEntry', 'ChargeItem', 'ChargeItemDefinition', 'Claim', 'ClaimResponse', 'ClientApplication', 'ClinicalImpression', 'CodeSystem', 'Communication', 'CommunicationRequest', 'CompartmentDefinition', 'Composition', 'ConceptMap', 'Condition', 'Consent', 'Contract', 'Coverage', 'CoverageEligibilityRequest', 'CoverageEligibilityResponse', 'DetectedIssue', 'Device', 'DeviceDefinition', 'DeviceMetric', 'DeviceRequest', 'DeviceUseStatement', 'DiagnosticReport', 'DocumentManifest', 'DocumentReference', 'DomainConfiguration', 'EffectEvidenceSynthesis', 'Encounter', 'Endpoint', 'EnrollmentRequest', 'EnrollmentResponse', 'EpisodeOfCare', 'EventDefinition', 'Evidence', 'EvidenceVariable', 'ExampleScenario', 'ExplanationOfBenefit', 'FamilyMemberHistory', 'Flag', 'Goal', 'GraphDefinition', 'Group', 'GuidanceResponse', 'HealthcareService', 'ImagingStudy', 'Immunization', 'ImmunizationEvaluation', 'ImmunizationRecommendation', 'ImplementationGuide', 'InsurancePlan', 'Invoice', 'JsonWebKey', 'Library', 'Linkage', 'List', 'Location', 'Login', 'Measure', 'MeasureReport', 'Media', 'Medication', 'MedicationAdministration', 'MedicationDispense', 'MedicationKnowledge', 'MedicationRequest', 'MedicationStatement', 'MedicinalProduct', 'MedicinalProductAuthorization', 'MedicinalProductContraindication', 'MedicinalProductIndication', 'MedicinalProductIngredient', 'MedicinalProductInteraction', 'MedicinalProductManufactured', 'MedicinalProductPackaged', 'MedicinalProductPharmaceutical', 'MedicinalProductUndesirableEffect', 'MessageDefinition', 'MessageHeader', 'MolecularSequence', 'NamingSystem', 'NutritionOrder', 'Observation', 'ObservationDefinition', 'OperationDefinition', 'OperationOutcome', 'Organization', 'OrganizationAffiliation', 'Parameters', 'PasswordChangeRequest', 'Patient', 'PaymentNotice', 'PaymentReconciliation', 'Person', 'PlanDefinition', 'Practitioner', 'PractitionerRole', 'Procedure', 'Project', 'ProjectMembership', 'Provenance', 'Questionnaire', 'QuestionnaireResponse', 'RelatedPerson', 'RequestGroup', 'ResearchDefinition', 'ResearchElementDefinition', 'ResearchStudy', 'ResearchSubject', 'RiskAssessment', 'RiskEvidenceSynthesis', 'Schedule', 'SearchParameter', 'ServiceRequest', 'Slot', 'SmartAppLaunch', 'Specimen', 'SpecimenDefinition', 'StructureDefinition', 'StructureMap', 'Subscription', 'SubscriptionStatus', 'Substance', 'SubstanceNucleicAcid', 'SubstancePolymer', 'SubstanceProtein', 'SubstanceReferenceInformation', 'SubstanceSourceMaterial', 'SubstanceSpecification', 'SupplyDelivery', 'SupplyRequest', 'Task', 'TerminologyCapabilities', 'TestReport', 'TestScript', 'User', 'UserConfiguration', 'ValueSet', 'VerificationResult', 'VisionPrescription', ];

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/medplum/medplum'

If you have feedback or need assistance with the MCP directory API, please join our Discord server