Skip to content

Commit e374196

Browse files
authored
Merge pull request #1156 from joshunrau/cli-done
feat: add new features to cli
2 parents ee475c1 + 769c239 commit e374196

File tree

7 files changed

+588
-255
lines changed

7 files changed

+588
-255
lines changed
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
}

apps/api/src/instruments/instruments.controller.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,11 @@ export class InstrumentsController {
4040
): Promise<InstrumentInfo[]> {
4141
return this.instrumentsService.findInfo({ kind, subjectId }, { ability });
4242
}
43+
44+
@ApiOperation({ summary: 'List Instruments' })
45+
@Get('list')
46+
@RouteAccess({ action: 'read', subject: 'Instrument' })
47+
async list(@CurrentUser('ability') ability: AppAbility, @Query('kind') kind?: InstrumentKind) {
48+
return this.instrumentsService.list({ kind }, ability);
49+
}
4350
}

apps/api/src/instruments/instruments.service.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
LoggingService,
66
VirtualizationService
77
} from '@douglasneuroinformatics/libnest';
8-
import type { Model } from '@douglasneuroinformatics/libnest';
8+
import type { AppAbility, Model } from '@douglasneuroinformatics/libnest';
99
import { Injectable } from '@nestjs/common';
1010
import {
1111
ConflictException,
@@ -234,6 +234,16 @@ export class InstrumentsService {
234234
return instance;
235235
}
236236

237+
async list<TKind extends InstrumentKind>(query: InstrumentQuery<TKind> = {}, ability: AppAbility) {
238+
return this.find(query, { ability }).then((arr) => {
239+
return arr.map((instrument) => ({
240+
id: instrument.id,
241+
internal: instrument.internal,
242+
title: instrument.details.title
243+
}));
244+
});
245+
}
246+
237247
private async instantiate(instruments: Pick<InstrumentBundleContainer, 'bundle' | 'id'>[]) {
238248
return Promise.all(
239249
instruments.map((instrument) => {

0 commit comments

Comments
 (0)