Skip to content

Commit

Permalink
🚧 progress: Record patient changes on insert/update.
Browse files Browse the repository at this point in the history
  • Loading branch information
make-github-pseudonymous-again committed Jan 15, 2025
1 parent 9b7a812 commit 9e147ba
Show file tree
Hide file tree
Showing 21 changed files with 558 additions and 132 deletions.
2 changes: 1 addition & 1 deletion imports/_test/fixtures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export const dropId = ({_id, ...rest}) => {
return rest;
};

export const dropIds = (x) => x.map(dropId);
export const dropIds = (x) => x.map((x) => x === null ? null : dropId(x));

export const dropOwner = ({owner, ...rest}) => {
assert(typeof owner === 'string');
Expand Down
6 changes: 6 additions & 0 deletions imports/api/Changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export type Changes<T> = {
$set?: Partial<T>;
$unset?: {
[K in keyof T]?: boolean;
};
};
30 changes: 26 additions & 4 deletions imports/api/Document.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,31 @@
import {type Document as MongoDocument} from 'mongodb';

import schema from '../lib/schema';

type Document = MongoDocument;
const literalSchema = schema.union([
schema.string(),
schema.number(),
schema.boolean(),
schema.undefined(), // TODO: Keep in document, remove from updates.
schema.null(), // TODO: Remove from document, add to update.
schema.date(),
]);

type Literal = schema.infer<typeof literalSchema>;

type DocumentValue = Literal | {[key: string]: DocumentValue} | DocumentValue[];

const documentValue: schema.ZodType<DocumentValue> = schema.lazy(() =>
schema.union([
literalSchema,
schema.record(schema.string(), documentValue),
schema.array(documentValue),
]),
);

const documentKey = schema.string();

export const document = schema.record(schema.string(), schema.any());
export const document = schema.record(documentKey, documentValue);
// TODO: The following does not work:
// type Document = schema.infer<typeof document>;
type Document = {[key: string]: DocumentValue | any};

export default Document;
59 changes: 59 additions & 0 deletions imports/api/collection/changes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import schema from '../../lib/schema';
import { document } from '../Document';
import define from './define';

const changeDocument = schema.object({
_id: schema.string(),
owner: schema.string(),
when: schema.date(),
who: schema.object({
type: schema.literal('user'),
_id: schema.string(),
}),
why: schema.object({
method: schema.string(),
source: schema.union([
schema.object({
type: schema.literal('manual')
}),
schema.object({
type: schema.literal('entity'),
collection: schema.string(),
_id: schema.string(),
}),
])
}),
what: schema.object({
type: schema.union([
schema.literal('patient'),
schema.never(),
]),
_id: schema.string(),
}),
operation: schema.union([
schema.object({
type: schema.literal('update'),
$set: document.optional(),
$unset: schema.record(
schema.string(),
schema.boolean(),
).optional(),
}),
schema.object({
type: schema.literal('create'),
$set: document.optional(),
}),
schema.object({
type: schema.literal('read'),
}),
schema.object({
type: schema.literal('delete'),
}),
])
});

export type ChangeDocument = schema.infer<typeof changeDocument>;


const collection = 'changes';
export const Changes = define<ChangeDocument>(collection);
2 changes: 1 addition & 1 deletion imports/api/collection/define.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import Collection from '../Collection';

import {hasCollection, addCollection} from './registry';

const define = <T extends Document, U = T>(name: string) => {
const define = <T extends Document, U = T>(name: string): Collection<T, U> => {
assert(!hasCollection(name));

const collection = new Collection<T, U>(name, {
Expand Down
16 changes: 11 additions & 5 deletions imports/api/endpoint/appointments/schedule.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import assert from 'assert';

import {Appointments} from '../../collection/appointments';
import {AppointmentDocument, Appointments} from '../../collection/appointments';
import {Patients} from '../../collection/patients';
import {sanitizeAppointmentUpdate, appointmentUpdate} from '../../appointments';
import {availability} from '../../availability';
Expand Down Expand Up @@ -33,14 +33,17 @@ export default define({

const owner = this.userId;

let patientId: string;

if (createPatient) {
$set.patientId = await compose(db, createPatientForAppointment, this, [
patientId = await compose(db, createPatientForAppointment, this, [
createPatient,
]);
} else {
assert(typeof $set.patientId === 'string');
patientId = $set.patientId;
const patient = await db.findOne(Patients, {
_id: $set.patientId,
_id: patientId,
owner,
});
if (patient === null) {
Expand All @@ -53,12 +56,15 @@ export default define({
const createdAt = new Date();
const lastModifiedAt = createdAt;

const {insertedId: appointmentId} = await db.insertOne(Appointments, {
const document = {
...$set,
patientId,
createdAt,
lastModifiedAt,
owner,
});
} as Omit<AppointmentDocument, '_id'>;

const {insertedId: appointmentId} = await db.insertOne(Appointments, document);

return {
_id: appointmentId,
Expand Down
13 changes: 4 additions & 9 deletions imports/api/endpoint/consultations/insert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import assert from 'assert';
import {
Consultations,
consultationFields,
ConsultationDocument,
} from '../../collection/consultations';

import {computeUpdate, consultations} from '../../consultations';
Expand All @@ -15,14 +16,13 @@ import type TransactionDriver from '../../transaction/TransactionDriver';
import {Patients} from '../../collection/patients';
import {AuthenticationLoggedIn} from '../../Authentication';
import schema from '../../../lib/schema';
import {documentUpdate} from '../../DocumentUpdate';

const {sanitize} = consultations;

export default define({
name: 'consultations.insert',
authentication: AuthenticationLoggedIn,
schema: schema.tuple([documentUpdate(consultationFields)]),
schema: schema.tuple([consultationFields]),
async transaction(db: TransactionDriver, consultation) {
const owner = this.userId;
const changes = sanitize(consultation);
Expand All @@ -33,12 +33,7 @@ export default define({
changes,
);

assert(
$unset === undefined ||
!Object.keys($unset).some((key) =>
Object.prototype.hasOwnProperty.call(newState, key),
),
);
assert($unset === undefined || Object.keys($unset).length === 0);

const patient = await db.findOne(Patients, {_id: $set.patientId, owner});

Expand All @@ -58,7 +53,7 @@ export default define({
createdAt,
lastModifiedAt,
owner,
};
} as Omit<ConsultationDocument, '_id'>;

const {begin, end} = document;

Expand Down
66 changes: 51 additions & 15 deletions imports/api/endpoint/patients/insert.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,74 @@
import {AuthenticationLoggedIn} from '../../Authentication';

import schema from '../../../lib/schema';
import {patientFields, Patients} from '../../collection/patients';
import {PatientFields, patientFields, Patients} from '../../collection/patients';
import { Changes } from '../../collection/changes';
import {computeUpdate, patients} from '../../patients';
import type TransactionDriver from '../../transaction/TransactionDriver';

import define from '../define';

const {sanitize, updateIndex, updateTags} = patients;

export const _insert = async (
db: TransactionDriver,
owner: string,
fields: PatientFields,
) => {
const changes = sanitize(fields);
const {newState} = await computeUpdate(
db,
owner,
undefined,
changes,
);

const patient = {
...newState,
createdAt: new Date(),
owner,
};
await updateTags(db, owner, patient);
const {insertedId: patientId} = await db.insertOne(Patients, patient);
await updateIndex(db, owner, patientId, patient);
return {
...patient,
_id: patientId,
};
};

export default define({
name: '/api/patients/insert',
authentication: AuthenticationLoggedIn,
schema: schema.tuple([patientFields.strict()]),
async transaction(db: TransactionDriver, patient) {
async transaction(db: TransactionDriver, fields) {
const owner = this.userId;
const changes = sanitize(patient);
const {newState: fields} = await computeUpdate(
db,
owner,
undefined,
changes,
);

await updateTags(db, owner, fields);
const {_id: patientId, ...$set} = await _insert(db, owner, fields);

const {insertedId: patientId} = await db.insertOne(Patients, {
...fields,
createdAt: new Date(),
await db.insertOne(Changes, {
owner,
when: $set.createdAt,
who: {
type: 'user',
_id: owner,
},
why: {
method: 'insert',
source: {
type: 'manual'
},
},
what: {
type: 'patient',
_id: patientId,
},
operation: {
type: 'create',
$set,
},
});

await updateIndex(db, owner, patientId, fields);

return patientId;
},
simulate(_patient) {
Expand Down
19 changes: 19 additions & 0 deletions imports/api/endpoint/patients/insertFromEid.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,4 +96,23 @@ server(__filename, () => {
),
);
});

it('does not create duplicate eid entries', async () => {
const userId = randomUserId();

const eid = newEidData();

await invoke(insertFromEid, {userId}, [eid]);
const oldEntry = await Eids.findOneAsync({owner: userId});
assert.isDefined(oldEntry);

await invoke(insertFromEid, {userId}, [eid]);
const entries = await Eids.find({owner: userId}).fetchAsync();

assert.strictEqual(entries.length, 1);

const {createdAt, lastUsedAt} = entries[0]!;
assert.strictEqual(createdAt.valueOf(), oldEntry.createdAt.valueOf());
assert.isAbove(lastUsedAt.valueOf(), oldEntry.lastUsedAt.valueOf());
});
});
43 changes: 37 additions & 6 deletions imports/api/endpoint/patients/insertFromEid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {patientFieldsFromEid} from '../../patients';
import type TransactionDriver from '../../transaction/TransactionDriver';

import define from '../define';
import compose from '../compose';

import insert from './insert';
import {_insert} from './insert';
import { Changes } from '../../collection/changes';

export default define({
name: '/api/patients/insertFromEid',
Expand All @@ -19,7 +19,7 @@ export default define({

const lastUsedAt = new Date();

await db.updateOne(
const {_id: eidId} = (await db.findOneAndUpdate(
Eids,
{
owner,
Expand All @@ -35,12 +35,43 @@ export default define({
},
{
upsert: true,
returnDocument: 'after',
projection: {
_id: 1
}
},
);
))!;

const patient = patientFieldsFromEid(eid);
const fields = patientFieldsFromEid(eid);

return compose(db, insert, this, [patient]);
const {_id: patientId, ...$set} = await _insert(db, owner, fields);

await db.insertOne(Changes, {
owner,
when: lastUsedAt,
who: {
type: 'user',
_id: owner,
},
why: {
method: 'insert',
source: {
type: 'entity',
collection: 'eid',
_id: eidId,

Check warning on line 61 in imports/api/endpoint/patients/insertFromEid.ts

View check run for this annotation

Codecov / codecov/patch

imports/api/endpoint/patients/insertFromEid.ts#L61

Added line #L61 was not covered by tests
},
},
what: {
type: 'patient',
_id: patientId,
},
operation: {
type: 'create',
$set,
},
});

return patientId;
},
simulate(_patient) {
return undefined;
Expand Down
Loading

0 comments on commit 9e147ba

Please sign in to comment.