Skip to main content
Glama
projectclone.ts5.81 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import { created, forbidden, getResourceTypes, isResourceType, Operator } from '@medplum/core'; import type { FhirRequest, FhirResponse } from '@medplum/fhir-router'; import type { Binary, Project, Resource, ResourceType } from '@medplum/fhirtypes'; import { getAuthenticatedContext } from '../../context'; import { getBinaryStorage } from '../../storage/loader'; import type { Repository } from '../repo'; import { buildBinaryIds } from './utils/binary'; /** * Handles a Project clone request. * * Endpoint: [fhir base]/Project/[id]/$clone * @param req - The FHIR request. * @returns The FHIR response. */ export async function projectCloneHandler(req: FhirRequest): Promise<FhirResponse> { const ctx = getAuthenticatedContext(); if (!ctx.project.superAdmin) { return [forbidden]; } const { id } = req.params; const { name, resourceTypes, includeIds, excludeIds } = req.body; const cloner = new ProjectCloner(ctx.repo, id, name, resourceTypes, includeIds, excludeIds); const result = await cloner.cloneProject(); return [created, result]; } class ProjectCloner { readonly repo: Repository; readonly projectId: string; readonly projectName: string; readonly allowedResourceTypes: string[]; readonly includeIds: string[]; readonly excludeIds: string[]; readonly idMap: Map<string, string>; constructor( repo: Repository, projectId: string, projectName: string = '', allowedResourceTypes: string[] = [], includeIds: string[] = [], excludeIds: string[] = [], idMap = new Map<string, string>() ) { this.repo = repo; this.projectId = projectId; this.projectName = projectName; this.allowedResourceTypes = allowedResourceTypes; this.includeIds = includeIds; this.excludeIds = excludeIds; this.idMap = idMap; } async cloneProject(): Promise<Project> { const repo = this.repo; const project = await repo.readResource<Project>('Project', this.projectId); const resourceTypes = getResourceTypes(); const allResources: Resource[] = []; const binaryIds = new Set<string>(); const maxResourcesPerResourceType = 1000; for (const resourceType of resourceTypes) { if (!this.isAllowedResourceType(resourceType) || resourceType === 'Binary') { continue; } const bundle = await repo.search({ resourceType, count: maxResourcesPerResourceType, filters: [{ code: '_project', operator: Operator.EQUALS, value: project.id }], }); if (!bundle.entry) { continue; } for (const entry of bundle.entry) { if (!entry.resource || !this.isAllowedResourceId(entry.resource.id)) { continue; } this.idMap.set(entry.resource.id, repo.generateId()); buildBinaryIds(entry.resource, binaryIds); if (entry.resource.resourceType !== 'Project') { allResources.push(entry.resource); } } } // Get all binary resources if (this.isAllowedResourceType('Binary')) { for (const binaryId of binaryIds) { const binary = await repo.readResource<Binary>('Binary', binaryId); this.idMap.set(binary.id, repo.generateId()); allResources.push(binary); } } // Create the project first - otherwise project references will fail const newProject = await repo.updateResource<Project>(this.rewriteIds(project)); // Then create all other resources for (const resource of allResources) { // Use updateResource to create with specified ID // That feature is only available to super admins const result = await repo.updateResource(this.rewriteIds(resource)); if (resource.resourceType === 'Binary') { await getBinaryStorage().copyBinary(resource, result as Binary); } } return newProject; } isAllowedResourceId(resourceId: string): boolean { if (this.includeIds.length > 0 && !this.includeIds.includes(resourceId)) { return false; } return !this.excludeIds.includes(resourceId); } isAllowedResourceType(resourceType: ResourceType): boolean { if (resourceType === 'Project') { return true; } if (this.allowedResourceTypes.length > 0) { return this.allowedResourceTypes.includes(resourceType); } return true; } rewriteIds<T extends Resource>(resource: T): T { const resourceObj = JSON.parse(JSON.stringify(resource, (k, v) => this.rewriteKeyReplacer(k, v))); if (this.projectName) { switch (resource.resourceType) { case 'Project': resourceObj.name = this.projectName; break; case 'ProjectMembership': resourceObj.project.display = this.projectName; break; case 'ClientApplication': if (resource.name?.endsWith(' Default Client')) { resourceObj.name = `${this.projectName} Default Client`; resourceObj.description = `Default client for ${this.projectName}`; } break; default: return resourceObj; } } return resourceObj; } rewriteKeyReplacer(key: string, value: unknown): unknown { if ((key === 'id' || key === 'project') && typeof value === 'string' && this.idMap.has(value)) { return this.idMap.get(value); } if ( (key === 'reference' && typeof value === 'string' && value.includes('/')) || (key === 'url' && typeof value === 'string' && value.startsWith('Binary/')) ) { const [resourceType, id] = value.split('/'); if (isResourceType(resourceType) && this.idMap.has(id)) { return resourceType + '/' + this.idMap.get(id); } } return value; } }

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