Skip to main content
Glama
repo.test.tsโ€ข64.4 kB
// SPDX-FileCopyrightText: Copyright Orangebot, Inc. and Medplum contributors // SPDX-License-Identifier: Apache-2.0 import type { WithId } from '@medplum/core'; import { allOk, badRequest, ContentType, created, createReference, encodeBase64, getReferenceString, isOk, normalizeErrorString, notFound, OperationOutcomeError, Operator, parseSearchRequest, preconditionFailed, toTypedValue, } from '@medplum/core'; import type { Binary, BundleEntry, ElementDefinition, Login, Observation, OperationOutcome, Organization, Patient, Practitioner, Project, ProjectMembership, Questionnaire, ResearchDefinition, ResourceType, ServiceRequest, StructureDefinition, User, UserConfiguration, ValueSet, } from '@medplum/fhirtypes'; import { randomBytes, randomUUID } from 'crypto'; import { readFileSync } from 'fs'; import assert from 'node:assert'; import { resolve } from 'path'; import { initAppServices, shutdownApp } from '../app'; import type { RegisterRequest } from '../auth/register'; import { registerNew } from '../auth/register'; import { getConfig, loadTestConfig } from '../config/loader'; import { r4ProjectId, systemResourceProjectId } from '../constants'; import { DatabaseMode, getDatabasePool } from '../database'; import { getLogger } from '../logger'; import { bundleContains, createTestProject, withTestContext } from '../test.setup'; import { getRepoForLogin } from './accesspolicy'; import { getSystemRepo, Repository, setTypedPropertyValue } from './repo'; import { SelectQuery } from './sql'; jest.mock('hibp'); describe('FHIR Repo', () => { let testProject: WithId<Project>; let testProjectRepo: Repository; let systemRepo: Repository; const usCorePatientProfile = JSON.parse( readFileSync(resolve(__dirname, '__test__/us-core-patient.json'), 'utf8') ) as StructureDefinition; beforeAll(async () => { const config = await loadTestConfig(); await initAppServices(config); testProject = await getSystemRepo().createResource({ resourceType: 'Project', id: randomUUID(), }); testProjectRepo = new Repository({ projects: [testProject], extendedMode: true, author: { reference: 'Practitioner/' + randomUUID(), }, }); }); afterAll(async () => { await shutdownApp(); }); beforeEach(() => { systemRepo = getSystemRepo(); }); test('getRepoForLogin', async () => { await expect(() => getRepoForLogin({ login: { resourceType: 'Login' } as Login, membership: { resourceType: 'ProjectMembership', project: createReference(testProject), } as WithId<ProjectMembership>, project: testProject, userConfig: {} as UserConfiguration, }) ).rejects.toThrow('Invalid author reference'); }); test('Read resource with undefined id', async () => { try { await systemRepo.readResource('Patient', undefined as unknown as string); fail('Should have thrown'); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(isOk(outcome)).toBe(false); } }); test('Read resource with blank id', async () => { try { await systemRepo.readResource('Patient', ''); fail('Should have thrown'); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(isOk(outcome)).toBe(false); } }); test('Read resource with invalid id', async () => { try { await systemRepo.readResource('Patient', 'x'); fail('Should have thrown'); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(isOk(outcome)).toBe(false); } }); test('Read invalid resource with `checkCacheOnly` set', async () => { await expect(systemRepo.readResource('Subscription', randomUUID(), { checkCacheOnly: true })).rejects.toThrow( new OperationOutcomeError(notFound) ); }); test('Repo read malformed reference', async () => { try { await systemRepo.readReference({ reference: undefined }); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcome).id).not.toBe('ok'); } try { await systemRepo.readReference({ reference: '' }); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcome).id).not.toBe('ok'); } try { await systemRepo.readReference({ reference: '////' }); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcome).id).not.toBe('ok'); } try { await systemRepo.readReference({ reference: 'Patient/123/foo' }); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcome).id).not.toBe('ok'); } }); test('Read references with various reference shapes', async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['ReadReferences'], family: 'ReadReferences' }], }); const references = [ { reference: undefined }, { reference: '' }, { reference: 'Patient/' + patient.id }, { display: 'test' }, { reference: 'Patient/' + patient.id }, { resource: patient }, ]; const results = await systemRepo.readReferences(references); if (!Array.isArray(results)) { throw new Error('Should have returned an array'); } expect(results).toHaveLength(6); expect((results[0] as OperationOutcomeError).outcome.id).toBe('not-found'); expect((results[1] as OperationOutcomeError).outcome.id).toBe('not-found'); expect((results[2] as WithId<Patient>).id).toBe(patient.id); expect((results[3] as OperationOutcomeError).outcome.id).toBe('not-found'); expect((results[4] as WithId<Patient>).id).toBe(patient.id); expect((results[5] as OperationOutcomeError).outcome.id).toBe('not-found'); }); describe('Read history', () => { const versions: Record<string, WithId<Patient>> = {}; beforeAll(async () => withTestContext(async () => { systemRepo ??= getSystemRepo(); versions.v1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { lastUpdated: new Date(Date.now() - 1000 * 60).toISOString(), }, }); expect(versions.v1.id).toBeDefined(); versions.v2 = await systemRepo.updateResource<Patient>({ resourceType: 'Patient', id: versions.v1.id, active: true, meta: { lastUpdated: new Date().toISOString(), }, }); expect(versions.v2.id).toStrictEqual(versions.v1.id); expect(versions.v2.meta?.versionId).not.toStrictEqual(versions.v1.meta?.versionId); }) ); test.each([ ['no options', {}, ['v2', 'v1']], ['limit', { limit: 1 }, ['v2']], ['offset', { offset: 1 }, ['v1']], ['limit and offset', { limit: 1, offset: 1 }, ['v1']], ['negative offset', { offset: -1 }, ['v2', 'v1']], ['large offset', { offset: 10000 }, []], ['negative limit', { limit: -1 }, ['v2', 'v1']], ['large limit', { limit: 100000 }, ['v2', 'v1']], ])('options: %s', async (_, options, expected) => { const history = await systemRepo.readHistory('Patient', versions.v1.id, options); if (expected.length === 0) { expect(history).toBeDefined(); expect(history.entry?.length).toBe(0); } else { expect(history).toBeDefined(); expect(history.entry?.length).toBe(expected.length); for (let i = 0; i < expected.length; i++) { expect(history.entry?.[i]?.resource?.id).toBe(versions[expected[i]].id); } } }); test('with config.maxSearchOffset', async () => { const prevMax = getConfig().maxSearchOffset; getConfig().maxSearchOffset = 200; try { await systemRepo.readHistory('Patient', versions.v1.id, { offset: 300 }); throw new Error('Expected to throw'); } catch (err) { expect(normalizeErrorString(err)).toStrictEqual('Search offset exceeds maximum (got 300, max 200)'); } finally { getConfig().maxSearchOffset = prevMax; } }); }); test('Update patient', () => withTestContext(async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Update1'], family: 'Update1' }], }); const patient2 = await systemRepo.updateResource<Patient>({ ...(patient1 as Patient), active: true, }); expect(patient2.id).toStrictEqual(patient1.id); expect(patient2.meta?.versionId).not.toStrictEqual(patient1.meta?.versionId); })); test('Update patient remove meta.profile', () => withTestContext(async () => { const profileUrl = 'http://example.com/patient-profile'; const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { profile: [profileUrl] }, name: [{ given: ['Update1'], family: 'Update1' }], }); expect(patient1.meta?.profile).toStrictEqual(expect.arrayContaining([profileUrl])); expect(patient1.meta?.profile?.length).toStrictEqual(1); const patientWithoutProfile = { ...patient1 }; delete (patientWithoutProfile.meta as any).profile; const patient2 = await systemRepo.updateResource<Patient>(patientWithoutProfile); expect('profile' in (patient2.meta as any)).toBe(false); })); test('meta.project preserved after attempting to remove it', () => withTestContext(async () => { const { project, repo } = await createTestProject({ withClient: true, withRepo: true }); const patient1 = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Update1'], family: 'Update1' }], }); expect(patient1.meta?.project).toBeDefined(); expect(patient1.meta?.project).toStrictEqual(project.id); const patientWithoutProject = { ...patient1 }; delete (patientWithoutProject.meta as any).project; const patient2 = await systemRepo.updateResource<Patient>(patientWithoutProject); expect(patient2.meta?.project).toBeDefined(); expect(patient2.meta?.project).toStrictEqual(project.id); })); test('Update patient no changes', () => withTestContext(async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Update1'], family: 'Update1' }], }); const patient2 = await systemRepo.updateResource<Patient>({ ...(patient1 as Patient), }); expect(patient2.id).toStrictEqual(patient1.id); expect(patient2.meta?.versionId).toStrictEqual(patient1.meta?.versionId); })); test('Update patient multiple names', () => withTestContext(async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Suzy'], family: 'Smith' }], }); const patient2 = await systemRepo.updateResource<Patient>({ ...(patient1 as Patient), name: [ { given: ['Suzy'], family: 'Smith' }, { given: ['Suzy'], family: 'Jones' }, ], }); expect(patient2.id).toStrictEqual(patient1.id); expect(patient2.meta?.versionId).not.toStrictEqual(patient1.meta?.versionId); expect(patient2.name?.length).toStrictEqual(2); expect(patient2.name?.[0]?.family).toStrictEqual('Smith'); expect(patient2.name?.[1]?.family).toStrictEqual('Jones'); })); test('Create Patient with custom ID', async () => { const { repo } = await createTestProject({ withRepo: true }); await withTestContext(async () => { // Try to "update" a resource, which does not exist. // Some FHIR systems allow users to set ID's. // We do not. try { await repo.updateResource<Patient>({ resourceType: 'Patient', id: randomUUID(), name: [{ given: ['Alice'], family: 'Smith' }], }); } catch (err) { const outcome = (err as OperationOutcomeError).outcome; expect(outcome.id).toStrictEqual('not-found'); } }); }); test('Create Patient with no author', () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); expect(patient.meta?.author?.reference).toStrictEqual('system'); })); test('Create Patient as system on behalf of author', () => withTestContext(async () => { const author = 'Practitioner/' + randomUUID(); const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], meta: { author: { reference: author, }, }, }); expect(patient.meta?.author?.reference).toStrictEqual(author); })); test('Create Patient as ClientApplication with no author', () => withTestContext(async () => { const { client, repo } = await createTestProject({ withClient: true, withRepo: true }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], identifier: [], }); expect(patient.meta?.author?.reference).toStrictEqual(getReferenceString(client)); // empty identifier array should removed when read from cache const readPatient = await repo.readResource<Patient>('Patient', patient.id, { checkCacheOnly: true }); expect(readPatient.identifier).toBeUndefined(); })); test('Create Patient as Practitioner with no author', () => withTestContext(async () => { const author = 'Practitioner/' + randomUUID(); const repo = new Repository({ extendedMode: true, author: { reference: author, }, }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); expect(patient.meta?.author?.reference).toStrictEqual(author); })); test('Create Patient as Practitioner on behalf of author', () => withTestContext(async () => { const author = 'Practitioner/' + randomUUID(); const fakeAuthor = 'Practitioner/' + randomUUID(); const repo = new Repository({ extendedMode: true, author: { reference: author, }, }); // We are acting as a Practitioner // Practitioner does *not* have the right to set the author // So even though we pass in an author, // We expect the Practitioner to be in the result. const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], meta: { author: { reference: fakeAuthor, }, }, }); expect(patient.meta?.author?.reference).toStrictEqual(author); })); test('Create resource with lastUpdated', () => withTestContext(async () => { const lastUpdated = '2020-01-01T12:00:00Z'; // System systemRepo has the ability to write custom timestamps const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], meta: { lastUpdated, }, }); expect(patient.meta?.lastUpdated).toStrictEqual(lastUpdated); })); const fourByteChars = '๐“ƒ’๐“ƒ”๐“ƒ•๐“ƒ–๐“ƒ—๐“ƒ˜๐“ƒ™๐“ƒš๐“ƒ›๐“ƒœ๐“ƒ๐“ƒž๐“ƒŸ๐“ƒ ๐“ƒก๐“ƒข๐“ƒฅ๐“ƒฆ๐“ƒง๐“ƒฉ๐“ƒช๐“ƒญ๐“ƒฎ๐“ƒฏ๐“ƒฐ๐“ƒฑ๐“ƒฒ๐“„๐“…‚๐“…ƒ๐“… ๐“…š'; test.each([ ['2736 chars, 2736 random bytes', randomBytes(2050).toString('base64')], ['6668 chars, 6668 random bytes', randomBytes(5000).toString('base64')], ['6400 chars, 12800 bytes', shuffleString(fourByteChars.repeat(100))], ])('Create ResearchDefinition with long description (%s)', (_testTitle, description) => withTestContext(async () => { const author = 'Practitioner/' + randomUUID(); const repo = new Repository({ extendedMode: true, author: { reference: author, }, }); await repo.createResource<ResearchDefinition>({ resourceType: 'ResearchDefinition', status: 'active', population: { reference: '123' }, description, }); }) ); test('Update resource with lastUpdated', () => withTestContext(async () => { const lastUpdated = '2020-01-01T12:00:00Z'; // System systemRepo has the ability to write custom timestamps const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], meta: { lastUpdated, }, }); expect(patient1.meta?.lastUpdated).toStrictEqual(lastUpdated); // But system cannot update the timestamp const patient2 = await systemRepo.updateResource<Patient>({ ...(patient1 as Patient), active: true, meta: { lastUpdated, }, }); expect(patient2.meta?.lastUpdated).not.toStrictEqual(lastUpdated); })); test('Update resource with missing id', () => withTestContext(async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: 'Test' }], }); const { id, ...rest } = patient1; expect(id).toBeDefined(); expect((rest as Patient).id).toBeUndefined(); try { await systemRepo.updateResource<Patient>(rest); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcomeError).outcome).toMatchObject(badRequest('Missing id')); } })); test('Update resource with matching versionId', () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: 'Test' }], }); patient.name = [{ family: 'TestUpdated' }]; await systemRepo.updateResource<Patient>(patient, { ifMatch: patient.meta?.versionId }); expect(patient.name?.at(0)?.family).toStrictEqual('TestUpdated'); })); test('Update resource with different versionId', () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: 'Test' }], }); await expect(systemRepo.updateResource(patient, { ifMatch: 'bad-id' })).rejects.toThrow( new OperationOutcomeError(preconditionFailed) ); })); test('Patch resource with matching versionId', () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: 'Test' }], }); const patched = await systemRepo.patchResource<Patient>( patient.resourceType, patient.id, [{ op: 'replace', path: '/name/0/family', value: 'TestUpdated' }], { ifMatch: patient.meta?.versionId, } ); expect(patched.name?.at(0)?.family).toStrictEqual('TestUpdated'); })); test('Patch resource with different versionId', () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ family: 'Test' }], }); await expect( systemRepo.patchResource<Patient>( patient.resourceType, patient.id, [{ op: 'add', path: '/birthDate', value: '1993-09-14' }], { ifMatch: 'bad-id' } ) ).rejects.toThrow(new OperationOutcomeError(preconditionFailed)); })); test('Compartment permissions', () => withTestContext(async () => { const registration1: RegisterRequest = { firstName: randomUUID(), lastName: randomUUID(), projectName: randomUUID(), email: randomUUID() + '@example.com', password: randomUUID(), }; const result1 = await registerNew(registration1); expect(result1.profile).toBeDefined(); const repo1 = await getRepoForLogin({ project: result1.project, membership: result1.membership, login: result1.login, userConfig: {} as UserConfiguration, }); const patient1 = await repo1.createResource<Patient>({ resourceType: 'Patient', }); expect(patient1).toBeDefined(); expect(patient1.id).toBeDefined(); const patient2 = await repo1.readResource('Patient', patient1.id); expect(patient2).toBeDefined(); expect(patient2.id).toStrictEqual(patient1.id); const registration2: RegisterRequest = { firstName: randomUUID(), lastName: randomUUID(), projectName: randomUUID(), email: randomUUID() + '@example.com', password: randomUUID(), }; const result2 = await registerNew(registration2); expect(result2.profile).toBeDefined(); const repo2 = await getRepoForLogin({ project: result2.project, membership: result2.membership, login: result2.login, userConfig: {} as UserConfiguration, }); try { await repo2.readResource('Patient', patient1.id); fail('Should have thrown'); } catch (err) { expect((err as OperationOutcomeError).outcome).toMatchObject(notFound); } })); test('Read history after delete', () => withTestContext(async () => { // Create the patient const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); const history1 = await systemRepo.readHistory('Patient', patient.id); expect(history1.entry?.length).toBe(1); // Delete the patient await systemRepo.deleteResource('Patient', patient.id); const history2 = await systemRepo.readHistory('Patient', patient.id); expect(history2.entry?.length).toBe(2); // Restore the patient await systemRepo.updateResource({ ...patient, meta: undefined }); const history3 = await systemRepo.readHistory('Patient', patient.id); expect(history3.entry?.length).toBe(3); const entries = history3.entry as BundleEntry[]; expect(entries[0].response?.status).toStrictEqual('200'); expect(entries[0].resource).toBeDefined(); expect(entries[1].response?.status).toStrictEqual('410'); expect((entries[1].response?.outcome as OperationOutcome).issue?.[0]?.details?.text).toMatch(/Deleted on /); expect(entries[1].resource).toBeUndefined(); expect(entries[2].response?.status).toStrictEqual('200'); expect(entries[2].resource).toBeDefined(); })); test('Delete Binary', () => withTestContext(async () => { // Create the resource const binary = await systemRepo.createResource<Binary>({ resourceType: 'Binary', contentType: 'text/plain', }); // Delete the resource await systemRepo.deleteResource('Binary', binary.id); const history2 = await systemRepo.readHistory('Binary', binary.id); expect(history2.entry?.length).toBe(2); })); test('Reindex resource as non-admin', async () => { const { repo } = await createTestProject({ withRepo: true }); try { await repo.reindexResource('Practitioner', randomUUID()); fail('Expected error'); } catch (err) { expect(isOk(err as OperationOutcome)).toBe(false); } const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); try { await repo.withTransaction(async (conn) => { await repo.reindexResources(conn, [patient]); }); fail('Expected error'); } catch (err) { expect(isOk(err as OperationOutcome)).toBe(false); } }); test('Reindex resource not found', async () => { try { await systemRepo.reindexResource('Practitioner', randomUUID()); fail('Expected error'); } catch (err) { expect(isOk(err as OperationOutcome)).toBe(false); } }); test('Reindex resource errors logged', async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Identifier'], family: 'Test' }], identifier: [{ system: 'https://example.com/', value: 'some-value' }], }); const buildColumnSpy = jest.spyOn(Repository.prototype as any, 'buildColumn').mockImplementation(() => { throw new Error('test error'); }); const logger = getLogger(); const errorSpy = jest.spyOn(logger, 'error').mockImplementation(() => {}); await expect( systemRepo.withTransaction(async (conn) => { await systemRepo.reindexResources(conn, [patient1]); }) ).rejects.toThrow('test error'); expect(errorSpy).toHaveBeenCalledWith('Error building row for resource', { resource: 'Patient/' + patient1.id, err: expect.any(Error), }); buildColumnSpy.mockRestore(); errorSpy.mockRestore(); }); test('Remove property', () => withTestContext(async () => { const value = randomUUID(); // Create a patient with an identifier const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Identifier'], family: 'Test' }], identifier: [{ system: 'https://example.com/', value }], }); // Search for patient by identifier // This should succeed const bundle1 = await systemRepo.search<Patient>({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value, }, ], }); expect(bundle1.entry?.length).toStrictEqual(1); const { identifier, ...rest } = patient1; expect(identifier).toBeDefined(); expect((rest as Patient).identifier).toBeUndefined(); const patient2 = await systemRepo.updateResource<Patient>(rest); expect(patient2.identifier).toBeUndefined(); // Try to search for the identifier // This should return empty result const bundle2 = await systemRepo.search<Patient>({ resourceType: 'Patient', filters: [ { code: 'identifier', operator: Operator.EQUALS, value, }, ], }); expect(bundle2.entry?.length).toStrictEqual(0); })); test('Delete Questionnaire.subjectType', () => withTestContext(async () => { const nonce = randomUUID(); const resource1 = await systemRepo.createResource<Questionnaire>({ resourceType: 'Questionnaire', status: 'active', subjectType: [nonce as ResourceType], }); const resource2 = await systemRepo.search({ resourceType: 'Questionnaire', filters: [ { code: 'subject-type', operator: Operator.EQUALS, value: nonce, }, ], }); expect(resource2.entry?.length).toStrictEqual(1); delete resource1.subjectType; await systemRepo.updateResource<Questionnaire>(resource1); const resource4 = await systemRepo.search({ resourceType: 'Questionnaire', filters: [ { code: 'subject-type', operator: Operator.EQUALS, value: nonce, }, ], }); expect(resource4.entry?.length).toStrictEqual(0); })); test('Empty objects', () => withTestContext(async () => { const patient1 = await systemRepo.createResource<Patient>({ resourceType: 'Patient', contact: [ { name: { given: ['Test'], }, }, ], }); const patient2 = await systemRepo.updateResource<Patient>({ resourceType: 'Patient', id: patient1.id, contact: [ { name: { given: ['Test'], }, }, ], }); expect(patient2.id).toStrictEqual(patient1.id); })); test('expungeResource forbidden', async () => { // Try to expunge as a regular user await expect(testProjectRepo.expungeResource('Patient', new Date().toISOString())).rejects.toThrow('Forbidden'); }); test('expungeResources forbidden', async () => { // Try to expunge as a regular user await expect(testProjectRepo.expungeResources('Patient', [new Date().toISOString()])).rejects.toThrow('Forbidden'); }); test('Purge forbidden', async () => { // Try to purge as a regular user await expect(testProjectRepo.purgeResources('Patient', new Date().toISOString())).rejects.toThrow('Forbidden'); }); test('Purge Login', () => withTestContext(async () => { const oldDate = '2000-01-01T00:00:00.000Z'; // Create a login using super admin with a date in the distant past // This takes advantage of the fact that super admins can set meta.lastUpdated const login = await systemRepo.createResource<Login>({ resourceType: 'Login', meta: { lastUpdated: oldDate, }, user: { reference: 'system' }, authMethod: 'password', authTime: oldDate, }); const bundle1 = await systemRepo.search({ resourceType: 'Login', filters: [{ code: '_lastUpdated', operator: Operator.LESS_THAN_OR_EQUALS, value: oldDate }], }); expect(bundleContains(bundle1, login)).toBeTruthy(); // Purge logins before the cutoff date await systemRepo.purgeResources('Login', oldDate); // Make sure the login is truly gone const bundle = await systemRepo.search({ resourceType: 'Login', filters: [{ code: '_lastUpdated', operator: Operator.ENDS_BEFORE, value: oldDate }], total: 'accurate', count: 0, }); expect(bundle.total).toStrictEqual(0); })); test('Malformed client assigned ID', async () => { await expect(systemRepo.updateResource({ resourceType: 'Patient', id: '123' })).rejects.toThrow('Invalid id'); }); test('Profile validation', async () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); const profile = { ...usCorePatientProfile, url: 'urn:uuid:' + randomUUID() }; const patient: Patient = { resourceType: 'Patient', meta: { profile: [profile.url], }, identifier: [ { system: 'http://example.com/patient-id', value: 'foo', }, ], name: [ { given: ['Alex'], family: 'Baker', }, ], // Missing gender property is required by profile }; await expect(repo.createResource(patient)).resolves.toBeTruthy(); await repo.createResource(profile); await expect(repo.createResource(patient)).rejects.toThrow( new Error('Missing required property (Patient.gender)') ); })); test('Profile update', async () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); const profile = await repo.createResource<StructureDefinition>({ ...usCorePatientProfile, url: 'urn:uuid:' + randomUUID(), }); const patient: Patient = { resourceType: 'Patient', meta: { profile: [profile.url] }, identifier: [{ system: 'http://example.com/patient-id', value: 'foo' }], name: [{ given: ['Alex'], family: 'Baker' }], gender: 'male', }; // Create the patient // This should succeed await expect(repo.createResource(patient)).resolves.toBeTruthy(); // Now update the profile to make "address" a required field await repo.updateResource<StructureDefinition>({ ...profile, snapshot: { ...profile.snapshot, element: profile.snapshot?.element?.map((e) => { if (e.path === 'Patient.address') { return { ...e, min: 1, }; } return e; }) as ElementDefinition[], }, }); // Now try to create another patient without an address // This should fail await expect(repo.createResource(patient)).rejects.toThrow( new Error('Missing required property (Patient.address)') ); })); describe('Update resource with terminology validation', () => { const patient: Patient = { resourceType: 'Patient', identifier: [{ use: 'usual', system: 'urn:oid:1.2.36.146.595.217.0.1', value: '12345' }], active: true, name: [ { use: 'official', family: 'Chalmers', given: ['Peter', 'James'] }, { use: 'usual', given: ['Jim'] }, { use: 'maiden', family: 'Windsor', given: ['Peter', 'James'] }, ], telecom: [ { use: 'home', system: 'url', value: 'http://example.com' }, { system: 'phone', value: '(03) 5555 6473', use: 'work', rank: 1 }, { system: 'phone', value: '(03) 3410 5613', use: 'mobile', rank: 2 }, { system: 'phone', value: '(03) 5555 8834', use: 'old' }, ], gender: 'male', birthDate: '1974-12-25', address: [{ use: 'home', type: 'both', text: '534 Erewhon St PeasantVille, Rainbow, Vic 3999' }], contact: [ { name: { use: 'usual', family: 'du Marchรฉ', given: ['Bรฉnรฉdicte'] }, telecom: [{ system: 'phone', value: '+33 (237) 998327', use: 'home' }], address: { use: 'home', type: 'both', line: ['534 Erewhon St'], city: 'PleasantVille', postalCode: '3999' }, gender: 'female', }, ], communication: [{ language: { coding: [{ system: 'urn:ietf:bcp:47', code: 'en' }] } }], }; let repo: Repository; let profile: StructureDefinition; beforeAll(async () => { const result = await createTestProject({ withRepo: { validateTerminology: true } }); repo = result.repo; // Create modified US Core Patient profile to have 'required' binding for communication.language const modifiedPatientProfile = JSON.parse( readFileSync(resolve(__dirname, '__test__/us-core-patient.json'), 'utf8') ) as StructureDefinition; const commLang = modifiedPatientProfile.snapshot?.element.find((e) => e.id === 'Patient.communication.language'); assert(commLang?.binding?.valueSet === 'http://hl7.org/fhir/us/core/ValueSet/simple-language'); assert(commLang.binding.strength === 'extensible'); commLang.binding.strength = 'required'; profile = await repo.createResource<StructureDefinition>({ ...modifiedPatientProfile, url: 'urn:uuid:' + randomUUID(), }); // Create a ValueSet for the US Core Patient profile that includes only 'en' as a valid language await repo.createResource<ValueSet>({ resourceType: 'ValueSet', url: 'http://hl7.org/fhir/us/core/ValueSet/simple-language', expansion: { timestamp: new Date().toISOString(), contains: [ { system: 'urn:ietf:bcp:47', code: 'en', }, ], }, status: 'active', }); }); test('Valid patient without any profiles', async () => withTestContext(async () => { await expect(repo.createResource(patient)).resolves.toBeDefined(); })); test('Invalid gender', async () => withTestContext(async () => { await expect( repo.createResource({ ...patient, gender: 'enby' as unknown as Patient['gender'] }) ).rejects.toThrow( `Value "enby" did not satisfy terminology binding http://hl7.org/fhir/ValueSet/administrative-gender|4.0.1 (Patient.gender)` ); })); test('Valid patient with US Core Patient profile', async () => withTestContext(async () => { await expect(repo.createResource({ ...patient, meta: { profile: [profile.url] } })).resolves.toBeDefined(); })); test.failing( '[FAILING] Invalid patient with US Core Patient profile (communication.language not in ValueSet)', async () => withTestContext(async () => { await expect( repo.createResource({ ...patient, meta: { profile: [profile.url] }, communication: [{ language: { coding: [{ system: 'urn:ietf:bcp:47', code: 'fr' }] } }], }) ).rejects.toThrow( `Value "fr" did not satisfy terminology binding http://hl7.org/fhir/us/core/ValueSet/simple-language` ); }) ); }); test('Conditional update', () => withTestContext(async () => { const mrn = randomUUID(); const patient: Patient = { resourceType: 'Patient', identifier: [{ system: 'http://example.com/mrn', value: mrn }], }; // Invalid search resource type mismatch await expect( systemRepo.conditionalUpdate(patient, { resourceType: 'Observation', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], }) ).rejects.toThrow('Search type must match resource type for conditional update'); // Invalid create with preassigned ID await expect( systemRepo.conditionalUpdate( { ...patient, id: randomUUID() }, { resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], } ) ).rejects.toThrow('Cannot perform create as update with client-assigned ID (Patient.id)'); // Create new resource const create = await systemRepo.conditionalUpdate(patient, { resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], }); expect(create.resource.id).toBeDefined(); const existing = create.resource; expect(create.outcome.id).toStrictEqual(created.id); // Update existing resource patient.gender = 'unknown'; const update = await systemRepo.conditionalUpdate(patient, { resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], }); expect(update.resource.id).toStrictEqual(existing.id); expect(update.resource.gender).toStrictEqual('unknown'); expect(update.outcome.id).toStrictEqual(allOk.id); // Update with incorrect ID patient.id = randomUUID(); await expect( systemRepo.conditionalUpdate(patient, { resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], }) ).rejects.toThrow('Resource ID did not match resolved ID (Patient.id)'); // Create duplicate resource const duplicate = await systemRepo.createResource(patient); expect(duplicate.id).not.toStrictEqual(existing.id); // Invalid update with ambiguous target await expect( systemRepo.conditionalUpdate(patient, { resourceType: 'Patient', filters: [{ code: 'identifier', operator: Operator.EQUALS, value: 'http://example.com/mrn|' + mrn }], }) ).rejects.toThrow('Multiple resources found matching condition'); })); test('Double DELETE', async () => withTestContext(async () => { const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient' }); await systemRepo.deleteResource(patient.resourceType, patient.id); await expect(systemRepo.deleteResource(patient.resourceType, patient.id)).resolves.toBeUndefined(); })); describe('Array column padding', () => { let prevConfig: string | undefined; beforeEach(() => { const config = getConfig(); prevConfig = config.arrayColumnPadding && JSON.stringify(config.arrayColumnPadding); }); afterEach(() => { if (prevConfig) { const config = getConfig(); config.arrayColumnPadding = JSON.parse(prevConfig); } }); test.each([ ['no config', undefined, false], // off by default [ 'no resourceType array', { config: { // ensure a padding element is chosen m: 1, lambda: 300, statisticsTarget: 1, }, }, true, ], [ 'resourceType in the resourceType array', { resourceType: ['Patient', 'Observation'], config: { m: 1, lambda: 300, statisticsTarget: 1, }, }, true, ], [ 'resourceType NOT in the resourceType array', { resourceType: ['Patient'], config: { m: 1, lambda: 300, statisticsTarget: 1, }, }, false, ], ])('with %s', async (desc, identifierArrayColumnPadding, shouldPad) => withTestContext(async () => { const config = getConfig(); if (identifierArrayColumnPadding) { config.arrayColumnPadding = { identifier: identifierArrayColumnPadding, }; } const res = await systemRepo.createResource<Observation>({ resourceType: 'Observation', status: 'unknown', code: { coding: [{ system: 'http://loinc.org', code: '72166-2', display: 'Test Observation' }] }, }); const db = getDatabasePool(DatabaseMode.READER); const results = await db.query('SELECT "__identifier" FROM "Observation" WHERE "id" = $1', [res.id]); if (shouldPad) { expect(results.rows).toStrictEqual([{ __identifier: ['00000000-0000-0000-0000-000000000000'] }]); } else { expect(results.rows).toStrictEqual([{ __identifier: [] }]); } // deleted rows also get padded await systemRepo.deleteResource(res.resourceType, res.id); if (shouldPad) { expect(results.rows).toStrictEqual([{ __identifier: ['00000000-0000-0000-0000-000000000000'] }]); } else { expect(results.rows).toStrictEqual([{ __identifier: [] }]); } }) ); }); test('Conditional reference resolution', async () => withTestContext(async () => { const practitionerIdentifier = randomUUID(); const practitioner = await systemRepo.createResource<Practitioner>({ resourceType: 'Practitioner', identifier: [{ system: 'http://hl7.org.fhir/sid/us-npi', value: practitionerIdentifier }], }); const conditionalReference = { reference: 'Practitioner?identifier=http://hl7.org.fhir/sid/us-npi|' + practitionerIdentifier, }; const patient = await systemRepo.createResource<Patient>({ resourceType: 'Patient', meta: { account: conditionalReference }, generalPractitioner: [conditionalReference], }); const expectedPractitioner = getReferenceString(practitioner); expect(patient.generalPractitioner?.[0]?.reference).toStrictEqual(expectedPractitioner); expect(patient.meta?.account?.reference).toStrictEqual(expectedPractitioner); expect(patient.meta?.accounts).toHaveLength(1); expect(patient.meta?.accounts).toContainEqual({ reference: expectedPractitioner }); })); test('Conditional reference resolution failure', async () => withTestContext(async () => { const practitionerIdentifier = randomUUID(); const patient: Patient = { resourceType: 'Patient', generalPractitioner: [ { reference: 'Practitioner?identifier=http://hl7.org.fhir/sid/us-npi|' + practitionerIdentifier }, ], }; await expect(systemRepo.createResource<Patient>(patient)).rejects.toThrow(/did not match any resources/); })); test('Conditional reference resolution multiple matches', async () => withTestContext(async () => { const practitionerIdentifier = randomUUID(); await systemRepo.createResource<Practitioner>({ resourceType: 'Practitioner', identifier: [{ system: 'http://hl7.org.fhir/sid/us-npi', value: practitionerIdentifier }], }); await systemRepo.createResource<Practitioner>({ resourceType: 'Practitioner', identifier: [{ system: 'http://hl7.org.fhir/sid/us-npi', value: practitionerIdentifier }], }); const patient: Patient = { resourceType: 'Patient', generalPractitioner: [ { reference: 'Practitioner?identifier=http://hl7.org.fhir/sid/us-npi|' + practitionerIdentifier }, ], }; await expect(systemRepo.createResource<Patient>(patient)).rejects.toThrow(); })); test('Conditional reference replaced before validation', async () => withTestContext(async () => { const mrn = randomUUID(); const patient: Patient = { resourceType: 'Patient', identifier: [{ value: mrn }], }; await systemRepo.createResource<Patient>(patient); const serviceRequest = { resourceType: 'ServiceRequest', status: 'active', intent: 'order', code: { coding: [ { system: 'http://snomed.info/sct', code: '308471005', display: 'Referral to cardiologist', }, ], }, // Reference should be replaced and NOT cause a validation error subject: { reference: 'Patient?identifier=' + mrn, }, // The performerType field should be a CodeableConcept, not an array performerType: [ { coding: [ { system: 'http://snomed.info/sct', code: '17561000', display: 'Cardiologist', }, ], }, ], } as unknown as ServiceRequest; await expect(systemRepo.createResource(serviceRequest)).rejects.toThrow(/^Expected single .*?performerType\)$/); })); test('Project default profiles', async () => withTestContext(async () => { const { repo } = await createTestProject({ withClient: true, withRepo: true, project: { defaultProfile: [ { resourceType: 'Observation', profile: ['http://hl7.org/fhir/StructureDefinition/vitalsigns'] }, ], }, }); const observation: Observation = { resourceType: 'Observation', status: 'final', category: [ { coding: [{ system: 'http://terminology.hl7.org/CodeSystem/observation-category', code: 'vital-signs' }] }, ], code: { text: 'Strep test' }, effectiveDateTime: '2024-02-13T14:34:56Z', valueBoolean: true, }; await expect(systemRepo.createResource(observation)).resolves.toBeDefined(); await expect(repo.createResource(observation)).rejects.toThrow('Missing required property (Observation.subject)'); observation.subject = { identifier: { value: randomUUID() } }; await expect(repo.createResource(observation)).resolves.toMatchObject<Partial<Observation>>({ meta: expect.objectContaining({ profile: ['http://hl7.org/fhir/StructureDefinition/vitalsigns'], }), }); })); test('Prevents setting Project compartments', async () => withTestContext(async () => { const { repo, project } = await createTestProject({ withRepo: true }); const { project: otherProject, repo: otherRepo } = await createTestProject({ withRepo: true }); const projectReference = createReference(otherProject); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', meta: { compartment: [projectReference], account: projectReference }, }); expect(patient.meta?.compartment).toContainEqual({ reference: getReferenceString(project) }); expect(patient.meta?.compartment).toContainEqual({ reference: getReferenceString(patient) }); expect(patient.meta?.compartment).not.toContainEqual({ reference: getReferenceString(otherProject) }); const results = await otherRepo.searchResources(parseSearchRequest('Patient')); expect(results).toHaveLength(0); })); test('setTypedValue', () => { const patient: Patient = { resourceType: 'Patient', photo: [ { contentType: 'image/png', url: 'https://example.com/photo.png', }, { contentType: 'image/png', data: 'base64data', }, ], }; setTypedPropertyValue(toTypedValue(patient), 'photo[1].contentType', { type: 'string', value: 'image/jpeg' }); expect(patient.photo?.[1].contentType).toStrictEqual('image/jpeg'); }); async function getProjectIdColumn(id: string): Promise<string | null> { const projectIdQuery = new SelectQuery('User').column('projectId').where('id', '=', id); const client = getSystemRepo().getDatabaseClient(DatabaseMode.WRITER); return (await projectIdQuery.execute(client))[0].projectId; } test('Super admin can edit User.meta.project', async () => withTestContext(async () => { const { project, repo } = await createTestProject({ withRepo: true }); // Create a user in the project const user1 = await repo.createResource<User>({ resourceType: 'User', email: randomUUID() + '@example.com', firstName: randomUUID(), lastName: randomUUID(), }); expect(user1.meta?.project).toStrictEqual(project.id); expect(await getProjectIdColumn(user1.id)).toStrictEqual(project.id); // Try to change the project as the normal user // Should silently fail, and preserve the meta.project const user2 = await repo.updateResource<User>({ ...user1, meta: { project: undefined }, }); expect(user2.meta?.project).toStrictEqual(project.id); expect(await getProjectIdColumn(user2.id)).toStrictEqual(project.id); // Now try to change the project as the super admin // Should succeed const user3 = await systemRepo.updateResource<User>({ ...user2, meta: { project: undefined }, }); expect(user3.meta?.project).toBeUndefined(); expect(await getProjectIdColumn(user3.id)).toStrictEqual(systemResourceProjectId); })); test('Handles caching of profile from linked project', async () => withTestContext(async () => { const systemRepo = getSystemRepo(); const { membership, project } = await registerNew({ firstName: randomUUID(), lastName: randomUUID(), projectName: randomUUID(), email: randomUUID() + '@example.com', password: randomUUID(), }); const { membership: membership2, project: project2 } = await registerNew({ firstName: randomUUID(), lastName: randomUUID(), projectName: randomUUID(), email: randomUUID() + '@example.com', password: randomUUID(), }); const updatedProject = await systemRepo.updateResource({ ...project, link: [{ project: createReference(project2) }], }); const repo2 = await getRepoForLogin({ login: {} as Login, membership: membership2, project: project2, userConfig: {} as UserConfiguration, }); const profile = await repo2.createResource({ ...usCorePatientProfile, url: 'urn:uuid:' + randomUUID() }); const patientJson: Patient = { resourceType: 'Patient', meta: { profile: [profile.url], }, }; // Resource upload should fail with profile linked let repo = await getRepoForLogin({ login: {} as Login, membership, project: updatedProject, userConfig: {} as UserConfiguration, }); await expect(repo.createResource(patientJson)).rejects.toThrow(/Missing required property/); // Unlink Project and verify that profile is not cached; resource upload should succeed without access to profile const unlinkedProject = await systemRepo.updateResource({ ...updatedProject, link: undefined, }); repo = await getRepoForLogin({ login: {} as Login, membership, project: unlinkedProject, userConfig: {} as UserConfiguration, }); await expect(repo.createResource(patientJson)).resolves.toBeDefined(); })); test('Patch post-commit stores full resource in cache', async () => withTestContext(async () => { const { project, repo, login, membership } = await createTestProject({ withRepo: { extendedMode: false }, withAccessToken: true, withClient: true, }); const extendedRepo = await getRepoForLogin( { login, project, membership, userConfig: {} as UserConfiguration }, true ); const patient = await repo.createResource<Patient>({ resourceType: 'Patient' }); expect(patient.meta?.project).toBeUndefined(); expect(patient.gender).toBeUndefined(); const updatedPatient = await repo.patchResource<Patient>('Patient', patient.id, [ { op: 'add', path: '/gender', value: 'unknown' }, ]); expect(updatedPatient.meta?.project).toBeUndefined(); expect(updatedPatient.gender).toStrictEqual('unknown'); const cachedPatient = await extendedRepo.readResource<Patient>('Patient', patient.id); expect(cachedPatient.meta?.project).toStrictEqual(project.id); expect(cachedPatient.gender).toStrictEqual('unknown'); })); test.each(['commit', 'rollback'])('Post-commit handling on %s', async (mode) => { const repo = getSystemRepo(); const loggerErrorSpy = jest.spyOn(getLogger(), 'error').mockImplementation(() => {}); const finalPostCommit = jest.fn(); const error = new Error('Post-commit hook failed'); const promise = repo.withTransaction(async () => { await repo.postCommit(async () => { throw new Error('Post-commit hook failed'); }); await repo.postCommit(async () => { // eslint-disable-next-line no-throw-literal throw 'Post-commit hook failed with string'; }); await repo.postCommit(finalPostCommit); if (mode === 'rollback') { throw new Error('Transaction failed'); } }); if (mode === 'commit') { await promise; expect(finalPostCommit).toHaveBeenCalled(); expect(loggerErrorSpy).toHaveBeenCalledTimes(2); expect(loggerErrorSpy).toHaveBeenCalledWith(expect.any(String), error); expect(loggerErrorSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ err: 'Post-commit hook failed with string', }) ); } else { await expect(promise).rejects.toThrow('Transaction failed'); expect(finalPostCommit).not.toHaveBeenCalled(); expect(loggerErrorSpy).toHaveBeenCalledTimes(1); expect(loggerErrorSpy).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ error: 'Transaction failed', }) ); } loggerErrorSpy.mockRestore(); }); test('Handles resources with many entries stored in lookup table', async () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); const patient: Patient = { resourceType: 'Patient', link: [], }; // Postgres uses a 16-bit counter for placeholder formats internally, // so (2^16 + 1) / 3 = (64k + 1) / 3 will definitely overflow it if not sent in smaller batches // the division by three since there are 3 column placeholders per inserted row for (let i = 0; i < Math.ceil((64 * 1024 + 1) / 3); i++) { patient.link?.push({ type: 'seealso', other: { reference: 'Patient/' + randomUUID() } }); } await repo.withTransaction(async (client) => { const querySpy = jest.spyOn(client, 'query'); await repo.createResource<Patient>(patient); const calls = querySpy.mock.calls; expect(calls.filter((c) => c[0].includes('INSERT INTO "Patient"'))).toHaveLength(1); expect(calls.filter((c) => c[0].includes('INSERT INTO "Patient_History"'))).toHaveLength(1); expect(calls.filter((c) => c[0].includes('INSERT INTO "Patient_References"')).length).toBeGreaterThanOrEqual(2); querySpy.mockRestore(); }); })); test('__version column', async () => { const { repo } = await createTestProject({ withRepo: true, superAdmin: true }); await withTestContext(async () => { const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }); const versionQuery = new SelectQuery('Patient').column('__version').where('id', '=', patient.id); const client = repo.getDatabaseClient(DatabaseMode.WRITER); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(Repository.VERSION); // Simulate the resource being at an older version const OLDER_VERSION = Repository.VERSION - 1; await client.query('UPDATE "Patient" SET __version = $1 WHERE id = $2', [OLDER_VERSION, patient.id]); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(OLDER_VERSION); // noop update should not change the version await repo.updateResource<Patient>(patient); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(OLDER_VERSION); // meaningful update should change the version await repo.updateResource<Patient>({ ...patient, name: [{ given: ['Bob'], family: 'Smith' }], }); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(Repository.VERSION); // Simulate the resource being at an older version await client.query('UPDATE "Patient" SET __version = $1 WHERE id = $2', [OLDER_VERSION, patient.id]); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(OLDER_VERSION); // reindex SHOULD change the version await repo.reindexResource('Patient', patient.id); expect((await versionQuery.execute(client))[0].__version).toStrictEqual(Repository.VERSION); }); }); test('Legacy UUID support -- non-conformant IDs that match UUID form are accepted', () => withTestContext(async () => { const { repo } = await createTestProject({ withRepo: true }); // Random invalid UUID that is invalid based on RFC9562 for the following reasons: // 1. The version field (the first digit in the third group) should be 4 to indicate UUID version 4, but here it's e // 2. The variant field (the first digit in the fourth group) should be 8, 9, a, or b, but here it's c // This is invalid in a similar way to some of the legacy UUIDs imported from other systems which we must continue to support // This test fails using the version of the validator.js isUUID (13.15.0) that caused the regression this PR fixed: https://github.com/medplum/medplum/pull/6289 const nonconformantUuid = '03a8d57b-91c2-e45f-c312-a7fe09c2d8e4'; // cleanup if it exists so the test can run again successfully await systemRepo.expungeResource('Patient', nonconformantUuid); const patient = await repo.createResource<Patient>( { id: nonconformantUuid, resourceType: 'Patient', name: [{ given: ['Alice'], family: 'Smith' }], }, { assignedId: true } ); expect(patient.id).toStrictEqual(nonconformantUuid); })); test('Project.exportedResourceType', () => withTestContext(async () => { const { project: linkedProject, repo: linkedRepo } = await createTestProject({ project: { exportedResourceType: ['Organization'] }, withRepo: true, }); const regRequest: RegisterRequest = { firstName: randomUUID(), lastName: randomUUID(), projectName: randomUUID(), email: randomUUID() + '@example.com', password: randomUUID(), }; const regResult = await registerNew(regRequest); let project = regResult.project; // add linkedProject to `Project.link` project = await getSystemRepo().updateResource({ ...project, link: [{ project: createReference(linkedProject) }], }); const repo = await getRepoForLogin({ project, membership: regResult.membership, login: regResult.login, userConfig: {} as UserConfiguration, }); const linkedOrg = await linkedRepo.createResource<Organization>({ resourceType: 'Organization', name: 'Linked Organization', }); const linkedPatient = await linkedRepo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Linked'], family: 'Patient' }], managingOrganization: createReference(linkedOrg), }); const org = await repo.createResource<Organization>({ resourceType: 'Organization', name: 'Non-linked Organization', }); const patient = await repo.createResource<Patient>({ resourceType: 'Patient', name: [{ given: ['Non-linked'], family: 'Patient' }], managingOrganization: createReference(org), }); const projects = await repo.searchResources({ resourceType: 'Project' }); expect(projects.length).toStrictEqual(3); expect(projects.map((p) => p.id)).toContain(project.id); expect(projects.map((p) => p.id)).toContain(linkedProject.id); expect(projects.map((p) => p.id)).toContain(r4ProjectId); const patients = await repo.searchResources({ resourceType: 'Patient' }); expect(patients.length).toStrictEqual(1); expect(patients.map((p) => p.id)).toContain(patient.id); expect(patients.map((p) => p.id)).not.toContain(linkedPatient.id); const orgs = await repo.searchResources({ resourceType: 'Organization' }); expect(orgs.length).toStrictEqual(2); expect(orgs.map((p) => p.id)).toContain(org.id); expect(orgs.map((p) => p.id)).toContain(linkedOrg.id); })); test('Binary writes no search parameter columns', () => withTestContext(async () => { const { project, repo } = await createTestProject({ withClient: true, withRepo: true }); const buildResourceRowSpy = jest.spyOn(Repository.prototype as any, 'buildResourceRow'); const binary = await repo.createResource<Binary>({ resourceType: 'Binary', contentType: ContentType.TEXT, data: encodeBase64('this is some test data'), meta: { tag: [{ system: 'https://example.com', code: 'tag' }], security: [{ system: 'https://example.com', code: 'security' }], }, }); expect(binary).toBeDefined(); expect(binary.id).toBeDefined(); expect(buildResourceRowSpy).toHaveBeenCalledTimes(1); const binaryRow = buildResourceRowSpy.mock.results[0].value as Record<string, any>; expect(binaryRow).toStrictEqual({ id: binary.id, lastUpdated: expect.any(String), deleted: false, projectId: project.id, content: expect.any(String), __version: Repository.VERSION, }); buildResourceRowSpy.mockRestore(); })); }); function shuffleString(s: string): string { const arr = Array.from(s); const len = arr.length; for (let i = 0; i < len - 1; ++i) { const j = Math.floor(Math.random() * len); const temp = arr[i]; arr[i] = arr[j]; arr[j] = temp; } return arr.join(''); }

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