Skip to content

Commit 912ee6d

Browse files
authored
[Closes #231] Ensure UUID search returns full patient info and add DB verification test (#251)
* Fix: Ensure UUID search returns full patient info and add DB verification test * feat: add patient UUID search and update related API/tests * Fixed Merge conflicts and updated test cases
1 parent 491d5aa commit 912ee6d

File tree

3 files changed

+118
-58
lines changed

3 files changed

+118
-58
lines changed

server/prisma/client.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,34 @@ const prisma = new PrismaClient({
2020
return { records, total };
2121
},
2222
},
23+
patient: {
24+
async uuidSearch (uuid, page = 1, perPage = 25) {
25+
const offset = (parseInt(page) - 1) * parseInt(perPage);
26+
const limit = parseInt(perPage);
27+
const likeValue = uuid.trim() + '%';
28+
// Use the Prisma client directly for raw queries
29+
const [records, totalResult] = await Promise.all([
30+
prisma.$queryRaw`
31+
SELECT
32+
p.*,
33+
to_jsonb(cb) AS "createdBy",
34+
to_jsonb(ub) AS "updatedBy"
35+
FROM "Patient" p
36+
LEFT JOIN "User" cb ON p."createdById" = cb.id
37+
LEFT JOIN "User" ub ON p."updatedById" = ub.id
38+
WHERE p."id"::TEXT ILIKE ${likeValue}
39+
ORDER BY p."updatedAt" DESC
40+
LIMIT ${limit} OFFSET ${offset}
41+
`,
42+
prisma.$queryRaw`
43+
SELECT COUNT(*) as total FROM "Patient"
44+
WHERE "id"::TEXT ILIKE ${likeValue}
45+
`
46+
]);
47+
const total = parseInt(totalResult[0].total);
48+
return { records, total };
49+
}
50+
}
2351
},
2452
});
2553

server/routes/api/v1/patients/list.js

Lines changed: 33 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -25,70 +25,45 @@ export default async function (fastify) {
2525
async (request, reply) => {
2626
const { page = '1', perPage = '25', patient = '', physicianId, hospitalId } = request.query;
2727

28-
const splitQuery = patient.trim().split(' ');
28+
const whereClause = {};
2929

30-
let whereClause = {};
30+
const uuidSearch = process.env.VITE_FEATURE_COLLECT_PHI === 'false';
3131

32-
if (splitQuery.length > 1) {
33-
whereClause = {
34-
AND: [
32+
if (uuidSearch) {
33+
const { records, total } = await fastify.prisma.patient.uuidSearch(patient, page, perPage);
34+
records.forEach((record) => {
35+
record.dateOfBirth = record.dateOfBirth?.toISOString().split('T')[0];
36+
});
37+
reply.setPaginationHeaders(page, perPage, total).send(records);
38+
return;
39+
} else {
40+
// Handle name search (if not a UUID)
41+
// Split the patient string by spaces to support full name searches
42+
const splitQuery = patient.trim().split(' ').filter(part => part.length > 0);
43+
44+
if (splitQuery.length > 1) {
45+
// Full name search: e.g., "John Smith" - look for first name containing "John" AND last name containing "Smith"
46+
whereClause.AND = [
3547
{
36-
OR: [
37-
{
38-
firstName: {
39-
contains: splitQuery[0].trim(),
40-
mode: 'insensitive',
41-
},
42-
},
43-
{ firstName: null },
44-
],
48+
firstName: {
49+
contains: splitQuery[0],
50+
mode: 'insensitive',
51+
},
4552
},
4653
{
47-
OR: [
48-
{
49-
lastName: {
50-
contains: splitQuery[1].trim(),
51-
mode: 'insensitive',
52-
},
53-
},
54-
{ lastName: null },
55-
],
54+
lastName: {
55+
contains: splitQuery[splitQuery.length - 1],
56+
mode: 'insensitive',
57+
},
5658
},
57-
],
58-
};
59-
} else {
60-
whereClause = {
61-
OR: [
59+
];
60+
} else {
61+
// Single name search: e.g., "John" or "Smith" - look in both first and last names
62+
whereClause.OR = [
6263
{ firstName: { contains: patient.trim(), mode: 'insensitive' } },
6364
{ lastName: { contains: patient.trim(), mode: 'insensitive' } },
64-
{
65-
AND: [
66-
{
67-
OR: [
68-
{
69-
firstName: {
70-
contains: patient.trim(),
71-
mode: 'insensitive',
72-
},
73-
},
74-
{ firstName: null },
75-
],
76-
},
77-
{
78-
OR: [
79-
{
80-
lastName: {
81-
contains: patient.trim(),
82-
mode: 'insensitive',
83-
},
84-
},
85-
{ lastName: null },
86-
],
87-
},
88-
],
89-
},
90-
],
91-
};
65+
];
66+
}
9267
}
9368

9469
if (physicianId) {
@@ -111,9 +86,11 @@ export default async function (fastify) {
11186
};
11287

11388
const { records, total } = await fastify.prisma.patient.paginate(options);
89+
11490
records.forEach((record) => {
11591
record.dateOfBirth = record.dateOfBirth?.toISOString().split('T')[0];
11692
});
93+
11794
reply.setPaginationHeaders(page, perPage, total).send(records);
11895
}
11996
);

server/test/routes/api/v1/patients.test.js

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { describe, it } from 'node:test';
1+
import { describe, it, beforeEach } from 'node:test';
22
import * as assert from 'node:assert';
33
import { StatusCodes } from 'http-status-codes';
44

@@ -168,7 +168,62 @@ describe('/api/v1/patients', () => {
168168
.headers(headers);
169169

170170
assert.deepStrictEqual(reply.statusCode, StatusCodes.OK);
171-
assert.deepStrictEqual(JSON.parse(reply.payload).length, 4);
171+
assert.deepStrictEqual(JSON.parse(reply.payload).length, 3);
172+
});
173+
174+
describe('GET / with PHI disabled', () => {
175+
beforeEach(() => {
176+
process.env.VITE_FEATURE_COLLECT_PHI = 'false';
177+
});
178+
179+
it('should return a patient by full UUID and match all DB fields', async (t) => {
180+
const app = await build(t);
181+
await t.loadFixtures();
182+
const headers = await t.authenticate('[email protected]', 'test');
183+
// Use a known UUID from your fixtures
184+
const uuid = '27963f68-ebc1-408a-8bb5-8fbe54671064';
185+
const reply = await app
186+
.inject()
187+
.get(`/api/v1/patients?patient=${uuid}`)
188+
.headers(headers);
189+
190+
// Fetch the patient record from the database, including relations
191+
const record = await t.prisma.patient.findUnique({
192+
where: { id: uuid },
193+
include: {
194+
createdBy: true,
195+
updatedBy: true,
196+
},
197+
});
198+
assert.deepStrictEqual(reply.statusCode, StatusCodes.OK);
199+
const results = JSON.parse(reply.payload);
200+
assert.deepStrictEqual(results.length, 1);
201+
const apiPatient = results[0];
202+
// Compare all relevant fields
203+
assert.deepStrictEqual(apiPatient.id, record.id);
204+
// Compare createdBy and updatedBy objects
205+
assert.deepStrictEqual(apiPatient.createdBy.id, record.createdBy.id);
206+
assert.deepStrictEqual(apiPatient.updatedBy.id, record.updatedBy.id);
207+
});
208+
209+
it('should return patients by partial UUID (prefix match)', async (t) => {
210+
const app = await build(t);
211+
await t.loadFixtures();
212+
const headers = await t.authenticate('[email protected]', 'test');
213+
// Use a known UUID prefix from your fixtures
214+
const uuidPrefix = '27963f68';
215+
const reply = await app
216+
.inject()
217+
.get(`/api/v1/patients?patient=${uuidPrefix}`)
218+
.headers(headers);
219+
220+
assert.deepStrictEqual(reply.statusCode, StatusCodes.OK);
221+
const results = JSON.parse(reply.payload);
222+
assert.ok(results.length >= 1);
223+
assert.ok(results[0].id.startsWith(uuidPrefix));
224+
assert.ok(results[0].createdBy);
225+
assert.ok(results[0].updatedBy);
226+
});
172227
});
173228
});
174229

0 commit comments

Comments
 (0)