Skip to main content
Glama
set-accounts.ts7.38 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { SearchRequest } from '@medplum/core'; import { accepted, AccessPolicyInteraction, allOk, append, badRequest, concatUrls, forbidden, isResourceType, notFound, OperationOutcomeError, parseSearchRequest, } from '@medplum/core'; import type { FhirRequest, FhirResponse } from '@medplum/fhir-router'; import type { OperationDefinition, Parameters, Reference, Resource, ResourceType } from '@medplum/fhirtypes'; import { getConfig } from '../../config/loader'; import { getAuthenticatedContext } from '../../context'; import { addSetAccountsJobData } from '../../workers/set-accounts'; import type { Repository } from '../repo'; import { getSystemRepo } from '../repo'; import { searchPatientCompartment } from './patienteverything'; import { AsyncJobExecutor } from './utils/asyncjobexecutor'; import { buildOutputParameters, parseInputParameters } from './utils/parameters'; const operation: OperationDefinition = { resourceType: 'OperationDefinition', id: 'set-accounts', name: 'SetAccounts', status: 'active', kind: 'operation', code: 'set-accounts', description: `Updates account references for the target resource, and optionally any resources in the target's FHIR compartment`, resource: ['Resource' as ResourceType], system: false, type: false, instance: true, parameter: [ { use: 'in', name: 'accounts', documentation: 'List of account references to set', type: 'Reference', min: 0, max: '*', }, { use: 'in', name: 'propagate', documentation: 'If set, also push changes to other resources in the compartment of the target resource', type: 'boolean', min: 0, max: '1', }, { use: 'out', name: 'resourcesUpdated', documentation: 'Number of resources that were updated', type: 'integer', min: 1, max: '1', }, ], }; export interface SetAccountsParameters { accounts: Reference[]; propagate?: boolean; } /** * Handles the $set-accounts operation. * This operation updates the account reference for a resource, and optionally * all resources in the target compartment as well. * * NOTE: the operation is currently capped at 1000 resources in the patient compartment. * @param req - The FHIR request. * @returns The FHIR response. */ export async function setAccountsHandler(req: FhirRequest): Promise<FhirResponse> { const { id, resourceType } = req.params; if (!id || !resourceType) { return [badRequest('Must specify resource type and ID')]; } if (!isResourceType(resourceType)) { return [badRequest('Invalid resource type')]; } const params = parseInputParameters<SetAccountsParameters>(operation, req); const { repo } = getAuthenticatedContext(); if (req.headers?.['prefer'] === 'respond-async' && params.propagate) { const { baseUrl } = getConfig(); const exec = new AsyncJobExecutor(repo); const asyncJob = await exec.init(concatUrls(baseUrl, `${resourceType}/${id}/$set-accounts`)); await exec.run(async () => { await addSetAccountsJobData({ asyncJob, resourceType, id, accounts: params.accounts, authState: getAuthenticatedContext().authState, }); }); return [accepted(exec.getContentLocation(baseUrl))]; } else { const result = await setResourceAccounts(repo, resourceType, id, params); return [allOk, result]; } } /** * Sets the `meta.accounts` array for the given resource, and optionally all resources in its compartment. * @param repo - The FHIR repository of the user. * @param resourceType - The type of the target resource. * @param id - The ID of the target resource. * @param params - Operation parameters. * @returns The number of resources updated. */ export async function setResourceAccounts( repo: Repository, resourceType: ResourceType, id: string, params: SetAccountsParameters ): Promise<Parameters> { const isSuperAdmin = repo.isSuperAdmin(); if (!repo.isProjectAdmin() && !isSuperAdmin) { throw new OperationOutcomeError(forbidden); } // Use system repo to read the resource, ensuring we get access to the full `meta.accounts` const systemRepo = getSystemRepo(); const target = await systemRepo.readResource(resourceType, id); // Ensure user's repo can read this resource as well if (!repo.canPerformInteraction(AccessPolicyInteraction.READ, target)) { throw new OperationOutcomeError(notFound); } const accounts = params.accounts; const oldAccounts = target.meta?.accounts; // Update the target resource with the new accounts target.meta = { ...target.meta, accounts: accounts, account: accounts?.[0], }; if (!repo.canPerformInteraction(AccessPolicyInteraction.UPDATE, target)) { throw new OperationOutcomeError(forbidden); } await getAuthenticatedContext().fhirRateLimiter?.recordWrite(); await systemRepo.updateResource(target); let count = 1; // Target resource is updated already if (params.propagate && target.resourceType === 'Patient') { // Calculate the difference between the previous accounts array and new one, in order to // propagate only those changes to compartment resources const additions = accounts.filter((a) => !oldAccounts?.find((o) => o.reference === a.reference)); const removals = oldAccounts?.filter((o) => !accounts.some((a) => a.reference === o.reference)) ?? []; // Update the resources in the target compartment to trigger meta.accounts refresh const search: Partial<SearchRequest> = { offset: 0, count: 1000 }; const maxSearchOffset = getConfig().maxSearchOffset ?? Number.POSITIVE_INFINITY; while ((search.offset ?? 0) <= maxSearchOffset) { const bundle = await searchPatientCompartment(repo, target, search); for (const entry of bundle.entry ?? []) { const resource = entry.resource; if (resource && resource.resourceType !== 'Patient') { await updateCompartmentResource(systemRepo, resource, additions, removals); count++; } } const nextLink = bundle.link?.find((l) => l.relation === 'next'); if (nextLink?.url) { const nextSearch = parseSearchRequest(nextLink.url); search.offset = nextSearch.offset; } else { break; } } } return buildOutputParameters(operation, { resourcesUpdated: count }); } async function updateCompartmentResource<T extends Resource>( systemRepo: Repository, resource: T, additions: Reference[], removals: Reference[] ): Promise<T> { let accountList = resource.meta?.accounts; for (const added of additions) { if (!accountList?.find((a) => a.reference === added.reference)) { accountList = append(accountList, added); } } for (const dropped of removals) { const index = accountList?.findIndex((a) => a.reference === dropped.reference) ?? -1; if (index > -1) { accountList?.splice(index, 1); } } resource.meta = { ...resource.meta, accounts: accountList, account: accountList?.[0], }; // Use system repo to force update meta.accounts await getAuthenticatedContext().fhirRateLimiter?.recordWrite(); return systemRepo.updateResource(resource); }

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