Skip to main content
Glama
expunge.ts3.61 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { accepted, allOk, concatUrls, forbidden, getResourceTypes, Operator } from '@medplum/core'; import type { FhirRequest, FhirResponse } from '@medplum/fhir-router'; import type { ResourceType } from '@medplum/fhirtypes'; import { getConfig } from '../../config/loader'; import { getAuthenticatedContext } from '../../context'; import type { Repository } from '../repo'; import { AsyncJobExecutor } from './utils/asyncjobexecutor'; import { buildBinaryIds } from './utils/binary'; /** * Handles an expunge request. * * Endpoint: [fhir base]/[resourceType]/[id]/$expunge * @param req - The FHIR request. * @returns The FHIR response. */ export async function expungeHandler(req: FhirRequest): Promise<FhirResponse> { const ctx = getAuthenticatedContext(); if (!ctx.project.superAdmin && !ctx.membership.admin) { return [forbidden]; } const { resourceType, id } = req.params; const { everything } = req.query; if (resourceType === 'Project' || everything === 'true') { const { baseUrl } = getConfig(); const exec = new AsyncJobExecutor(ctx.repo); await exec.init(concatUrls(baseUrl, 'fhir/R4' + req.pathname)); exec.start(async () => { ctx.logger.info('Expunge started', { resourceType, id }); await new Expunger(ctx.repo, id).expunge(); }); return [accepted(exec.getContentLocation(baseUrl))]; } else { await ctx.repo.expungeResource(resourceType, id); return [allOk]; } } export class Expunger { readonly repo: Repository; readonly compartment: string; readonly maxResultsPerPage: number; constructor(repo: Repository, compartment: string, maxResultsPerPage = 10000) { this.repo = repo; this.compartment = compartment; this.maxResultsPerPage = maxResultsPerPage; } async expunge(): Promise<void> { const resourceTypes = getResourceTypes(); for (const resourceType of resourceTypes) { await this.expungeByResourceType(resourceType); } } async expungeByResourceType(resourceType: ResourceType): Promise<void> { if (resourceType === 'Binary') { return; } const repo = this.repo; // NOTE(ThatOneBro 23/01/2025): Attempted to convert this to using `repo.processAllResources`, // But it doesn't quite fit the pattern since the "next" links are not usable // As the expunge process is destructive and the "cursor" essentially is always at the beginning of the table // We delete the next N resources over and over and use the "next" link as a signal if there are more resources to delete let hasNext = true; while (hasNext) { const bundle = await repo.search({ resourceType, count: this.maxResultsPerPage, filters: [{ code: '_compartment', operator: Operator.EQUALS, value: this.compartment }], }); if (!bundle.entry || bundle.entry.length === 0) { break; } const resourcesToExpunge: string[] = []; const binaryIds = new Set<string>(); for (const entry of bundle.entry) { if (entry.resource?.id) { resourcesToExpunge.push(entry.resource.id); buildBinaryIds(entry.resource, binaryIds); } } await repo.expungeResources(resourceType, resourcesToExpunge); if (binaryIds.size > 0) { await repo.expungeResources('Binary', Array.from(binaryIds)); } const linkNext = bundle.link?.find((b) => b.relation === 'next'); if (!linkNext?.url) { hasNext = false; } } } }

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