Skip to content

Commit f29adf1

Browse files
committed
feat: add routes to update and delete instrument records
1 parent 6927b3a commit f29adf1

3 files changed

Lines changed: 84 additions & 14 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { DataTransferObject } from '@douglasneuroinformatics/libnest';
2+
import { z } from 'zod';
3+
4+
export class UpdateInstrumentRecordDto extends DataTransferObject({
5+
data: z.union([z.record(z.string(), z.any()), z.array(z.any())])
6+
}) {}

apps/api/src/instrument-records/instrument-records.controller.ts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
/* eslint-disable perfectionist/sort-classes */
22

3-
import { CurrentUser, ParseSchemaPipe, RouteAccess } from '@douglasneuroinformatics/libnest';
3+
import { CurrentUser, ParseSchemaPipe, RouteAccess, ValidObjectIdPipe } from '@douglasneuroinformatics/libnest';
44
import type { AppAbility } from '@douglasneuroinformatics/libnest';
5-
import { Body, Controller, Get, Post, Query } from '@nestjs/common';
5+
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from '@nestjs/common';
66
import { ApiOperation, ApiTags } from '@nestjs/swagger';
77
import type { InstrumentKind } from '@opendatacapture/runtime-core';
88
import { z } from 'zod';
99

1010
import { CreateInstrumentRecordDto } from './dto/create-instrument-record.dto';
11+
import { UpdateInstrumentRecordDto } from './dto/update-instrument-record.dto';
1112
import { UploadInstrumentRecordsDto } from './dto/upload-instrument-record.dto';
1213
import { InstrumentRecordsService } from './instrument-records.service';
1314

@@ -51,6 +52,14 @@ export class InstrumentRecordsController {
5152
return this.instrumentRecordsService.find({ groupId, instrumentId, kind, minDate, subjectId }, { ability });
5253
}
5354

55+
@ApiOperation({ summary: 'Delete Record' })
56+
@Delete(':id')
57+
@HttpCode(HttpStatus.NO_CONTENT)
58+
@RouteAccess({ action: 'delete', subject: 'InstrumentRecord' })
59+
async deleteById(@Param('id', ValidObjectIdPipe) id: string, @CurrentUser('ability') ability: AppAbility) {
60+
await this.instrumentRecordsService.deleteById(id, { ability });
61+
}
62+
5463
@ApiOperation({ summary: 'Export Records' })
5564
@Get('export')
5665
@RouteAccess({ action: 'read', subject: 'InstrumentRecord' })
@@ -68,4 +77,15 @@ export class InstrumentRecordsController {
6877
): Promise<{ [key: string]: { intercept: number; slope: number; stdErr: number } }> {
6978
return this.instrumentRecordsService.linearModel({ groupId, instrumentId }, { ability });
7079
}
80+
81+
@ApiOperation({ summary: 'Update Instrument Record' })
82+
@Patch(':id')
83+
@RouteAccess({ action: 'delete', subject: 'InstrumentRecord' })
84+
updateById(
85+
@Param('id', ValidObjectIdPipe) id: string,
86+
@Body() { data }: UpdateInstrumentRecordDto,
87+
@CurrentUser('ability') ability: AppAbility
88+
) {
89+
return this.instrumentRecordsService.updateById(id, data, { ability });
90+
}
7191
}

apps/api/src/instrument-records/instrument-records.service.ts

Lines changed: 56 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import { replacer, reviver, yearsPassed } from '@douglasneuroinformatics/libjs';
22
import { accessibleQuery, InjectModel } from '@douglasneuroinformatics/libnest';
33
import type { Model } from '@douglasneuroinformatics/libnest';
44
import { linearRegression } from '@douglasneuroinformatics/libstats';
5-
import { Injectable, UnprocessableEntityException } from '@nestjs/common';
6-
import type { ScalarInstrument } from '@opendatacapture/runtime-core';
5+
import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common';
6+
import type { Json, ScalarInstrument } from '@opendatacapture/runtime-core';
77
import { DEFAULT_GROUP_NAME } from '@opendatacapture/schemas/core';
88
import type {
99
CreateInstrumentRecordData,
@@ -15,7 +15,7 @@ import type {
1515
} from '@opendatacapture/schemas/instrument-records';
1616
import { Prisma } from '@prisma/client';
1717
import type { Session } from '@prisma/client';
18-
import { isNumber, pickBy } from 'lodash-es';
18+
import { isNumber, mergeWith, pickBy } from 'lodash-es';
1919

2020
import type { EntityOperationOptions } from '@/core/types';
2121
import { GroupsService } from '@/groups/groups.service';
@@ -105,6 +105,10 @@ export class InstrumentRecordsService {
105105
}
106106

107107
async deleteById(id: string, { ability }: EntityOperationOptions = {}) {
108+
const isExisting = await this.instrumentRecordModel.exists({ id });
109+
if (!isExisting) {
110+
throw new NotFoundException(`Could not find record with ID '${id}'`);
111+
}
108112
return this.instrumentRecordModel.delete({
109113
where: { AND: [accessibleQuery(ability, 'delete', 'InstrumentRecord')], id }
110114
});
@@ -189,12 +193,7 @@ export class InstrumentRecordsService {
189193

190194
const records = await this.instrumentRecordModel.findMany({
191195
include: {
192-
instrument: {
193-
select: {
194-
bundle: true,
195-
id: true
196-
}
197-
}
196+
instrument: false
198197
},
199198
where: {
200199
AND: [
@@ -218,9 +217,7 @@ export class InstrumentRecordsService {
218217
if (groupId) {
219218
await this.groupsService.findById(groupId);
220219
}
221-
const instrument = await this.instrumentsService
222-
.findById(instrumentId)
223-
.then((instrument) => this.instrumentsService.getInstrumentInstance(instrument));
220+
const instrument = await this.getInstrumentById(instrumentId);
224221

225222
if (instrument.kind === 'SERIES') {
226223
throw new UnprocessableEntityException(`Cannot create linear model for series instrument '${instrument.id}'`);
@@ -261,6 +258,47 @@ export class InstrumentRecordsService {
261258
return results;
262259
}
263260

261+
async updateById(id: string, data: unknown[] | { [key: string]: unknown }, { ability }: EntityOperationOptions = {}) {
262+
const instrumentRecord = await this.instrumentRecordModel.findFirst({
263+
where: { id }
264+
});
265+
if (!instrumentRecord) {
266+
throw new NotFoundException(`Could not find record with ID '${id}'`);
267+
}
268+
269+
if (Array.isArray(instrumentRecord.data) && !Array.isArray(data)) {
270+
throw new BadRequestException('Data must be an array when the instrument record data is an array');
271+
}
272+
273+
// all records must be attached to scalar instruments
274+
const instrument = (await this.getInstrumentById(instrumentRecord.instrumentId)) as ScalarInstrument;
275+
276+
const updatedData = mergeWith(instrumentRecord.data, data, (updatedValue: unknown, sourceValue: unknown) => {
277+
if (Array.isArray(sourceValue)) {
278+
return updatedValue;
279+
}
280+
return undefined;
281+
});
282+
283+
const parseResult = await instrument.validationSchema.safeParseAsync(updatedData);
284+
if (!parseResult.success) {
285+
throw new BadRequestException({
286+
issues: parseResult.error.issues,
287+
message: 'Merged data does not match validation schema'
288+
});
289+
}
290+
291+
return this.instrumentRecordModel.update({
292+
data: {
293+
computedMeasures: instrument.measures
294+
? this.instrumentMeasuresService.computeMeasures(instrument.measures, parseResult.data as Json)
295+
: null,
296+
data: parseResult.data
297+
},
298+
where: { AND: [accessibleQuery(ability, 'delete', 'InstrumentRecord')], id }
299+
});
300+
}
301+
264302
async upload(
265303
{ groupId, instrumentId, records }: UploadInstrumentRecordsData,
266304
options?: EntityOperationOptions
@@ -340,6 +378,12 @@ export class InstrumentRecordsService {
340378
}
341379
}
342380

381+
private getInstrumentById(instrumentId: string) {
382+
return this.instrumentsService
383+
.findById(instrumentId)
384+
.then((instrument) => this.instrumentsService.getInstrumentInstance(instrument));
385+
}
386+
343387
private parseJson(data: unknown) {
344388
return JSON.parse(JSON.stringify(data), reviver) as unknown;
345389
}

0 commit comments

Comments
 (0)