Skip to content

Commit d89daa7

Browse files
authored
Merge pull request #27 from cph-cachet/24-Update-client-ts-with-endpoints-for-data-visualization
2 parents fdbaf71 + 6c7baa1 commit d89daa7

File tree

9 files changed

+849
-1184
lines changed

9 files changed

+849
-1184
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
### Added
66

77
- Added `getRedirectURIs` endpoint
8+
- Added `getDataStreamSummary` endpoint
89

910
### Fixed
1011

pnpm-lock.yaml

Lines changed: 658 additions & 1171 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/endpoints/dataStreams.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
DataStreamId,
44
DataStreamsConfiguration,
55
DataStreamServiceRequest,
6+
DataStreamSummary,
7+
DataStreamSummaryRequest,
68
Measurement,
79
MutableDataStreamSequence,
810
NamespacedId,
@@ -15,6 +17,7 @@ import {
1517
} from "@/shared";
1618
import Endpoint from "./endpoint";
1719
import CarpDataStreamBatch from "@/shared/models/carpDataStreamBatch";
20+
import { objectKeysFromSnakeToCamel } from "@/shared/utils";
1821

1922
class DataStreams extends Endpoint {
2023
endpoint: string = "/api/data-stream-service";
@@ -212,6 +215,24 @@ class DataStreams extends Endpoint {
212215

213216
await this.actions.post(this.endpoint, serializedRequest);
214217
}
218+
219+
/**
220+
* Get data stream summary
221+
* @returns The data stream summary
222+
* @param request The data stream summary request
223+
*/
224+
async getDataStreamSummary(
225+
request: DataStreamSummaryRequest,
226+
): Promise<DataStreamSummary> {
227+
const response = await this.actions.get<DataStreamSummary>(
228+
`${this.endpoint}/summary`,
229+
{
230+
params: objectKeysFromSnakeToCamel(request),
231+
},
232+
);
233+
234+
return response.data;
235+
}
215236
}
216237

217238
export default DataStreams;

src/shared/coreTypes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ import DataStreamPoint = datadk.cachet.carp.data.application.DataStreamPoint;
7676
import SyncPoint = datadk.cachet.carp.data.application.SyncPoint;
7777
import Measurement = datadk.cachet.carp.data.application.Measurement;
7878
import Geolocation = cdk.cachet.carp.common.application.data.Geolocation;
79+
import CompletedTask = cdk.cachet.carp.common.application.data.CompletedTask;
7980

8081
const { Roles } = cdk.cachet.carp.common.application.users.AssignedTo;
8182
const { EmailAddress } = cdk.cachet.carp.common.application;
@@ -200,4 +201,5 @@ export {
200201
DeviceConfiguration,
201202
ExpectedParticipantDataCore,
202203
SelectOne,
204+
CompletedTask,
203205
};

src/shared/models/dataStream.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { CompletedTask, Data } from "../coreTypes";
2+
3+
export type DataStreamType =
4+
| "survey"
5+
| "health"
6+
| "cognition"
7+
| "image"
8+
| "audio"
9+
| "video"
10+
| "informed_consent"
11+
| "sensing";
12+
export type DataStreamScope = "study" | "deployment" | "participant";
13+
14+
export interface DataStreamSummaryRequest {
15+
study_id: string;
16+
deployment_id?: string;
17+
participant_id?: string;
18+
scope: DataStreamScope;
19+
type: DataStreamType;
20+
from: string; // ISO8601String;
21+
to: string; // ISO8601String;
22+
}
23+
24+
export interface DateTaskQuantityTriple {
25+
date: string; // ISO8601String;
26+
task: string;
27+
quantity: number;
28+
}
29+
30+
export interface DataStreamSummary {
31+
data: DateTaskQuantityTriple[];
32+
studyId: string;
33+
deploymentId: string;
34+
participantId: string;
35+
scope: DataStreamScope;
36+
type: DataStreamType;
37+
from: string; // ISO8601String;
38+
to: string; // ISO8601String;
39+
}
40+
41+
export class CompletedAppTask extends CompletedTask {
42+
public static dataType = "dk.cachet.carp.completedapptask";
43+
44+
public completedAt: Date;
45+
46+
constructor(
47+
taskName: string,
48+
public taskType: DataStreamType,
49+
private taskDataType: string,
50+
taskData?: Data | null,
51+
) {
52+
super(taskName, taskData as any);
53+
this.completedAt = new Date();
54+
}
55+
56+
public toJSON = () => {
57+
const modifiedTaskData = {
58+
...Object.fromEntries(
59+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
60+
Object.entries(this.taskData).filter(([_, value]) => value),
61+
),
62+
__type: this.taskDataType,
63+
};
64+
65+
return {
66+
__type: CompletedAppTask.dataType,
67+
taskName: this.taskName,
68+
taskType: this.taskType,
69+
taskData: modifiedTaskData,
70+
completedAt: this.completedAt.toISOString(),
71+
};
72+
};
73+
}

src/shared/models/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ export * from "./protocol";
66
export * from "./studies";
77
export * from "./carpFile";
88
export * from "./participantInfo";
9+
export * from "./dataStream";

src/shared/utils.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,3 +68,19 @@ export const parseUser = (accessToken: string): User => {
6868
role: [role],
6969
};
7070
};
71+
72+
export const objectKeysFromSnakeToCamel = (
73+
obj: Record<string, any>,
74+
): Record<string, any> =>
75+
Object.fromEntries(
76+
Object.entries(obj).map(([key, value]) => {
77+
const parts = key.split("_");
78+
const camelKey =
79+
parts[0] +
80+
parts
81+
.slice(1)
82+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
83+
.join("");
84+
return [camelKey, value];
85+
}),
86+
);

src/test/consts.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@ export const STUDY_PROTOCOL: StudyProtocol = {
1010
tasks: [
1111
{
1212
name: "Monitor movement",
13-
__type: "dk.cachet.carp.common.application.tasks.BackgroundTask",
13+
__type: "dk.cachet.carp.common.application.tasks.Task",
1414
duration: "PT168H",
15+
title: "Monitor movement",
1516
measures: [
1617
{
1718
type: "dk.cachet.carp.geolocation",
@@ -39,6 +40,10 @@ export const STUDY_PROTOCOL: StudyProtocol = {
3940
"dk.cachet.carp.common.application.sampling.NoOptionsSamplingConfiguration",
4041
},
4142
},
43+
{
44+
type: "dk.cachet.carp.completedapptask",
45+
__type: "dk.cachet.carp.common.application.tasks.Measure.DataStream",
46+
},
4247
],
4348
description: "Track step count and geolocation for one week.",
4449
},

src/test/endpoints/dataStreams.test.ts

Lines changed: 71 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,6 @@ import { afterAll, beforeAll, expect, test } from "vitest";
33
import {
44
StudyStatus,
55
ParticipantGroupStatus,
6-
getSerializer,
7-
StudyProtocolSnapshot,
8-
DefaultSerializer,
96
DataStreamsConfiguration,
107
NamespacedId,
118
MutableDataStreamSequence,
@@ -15,6 +12,10 @@ import {
1512
Measurement,
1613
Geolocation,
1714
toList,
15+
CompletedAppTask,
16+
DefaultSerializer,
17+
getSerializer,
18+
StudyProtocolSnapshot,
1819
} from "@/shared";
1920
import { CarpTestClient } from "@/client";
2021
import { STUDY_PROTOCOL } from "../consts";
@@ -90,13 +91,7 @@ describe("DataStreams", () => {
9091

9192
await testClient.authentication.refresh();
9293

93-
namespaceId = new NamespacedId(
94-
STUDY_PROTOCOL.tasks[0].measures[0].type.replace(
95-
`.${STUDY_PROTOCOL.tasks[0].measures[0].type.split(".").pop()}`,
96-
"",
97-
),
98-
STUDY_PROTOCOL.tasks[0].measures[0].type.split(".").pop(),
99-
);
94+
namespaceId = new NamespacedId("dk.cachet.carp", "completedapptask");
10095
await expect(
10196
testClient.dataStreams.openDataStreams({
10297
studyDeploymentId: participantGroupStatus.id.stringRepresentation,
@@ -132,7 +127,12 @@ describe("DataStreams", () => {
132127
toLong(1),
133128
null,
134129
namespaceId,
135-
new Geolocation(57, 45, null) as any,
130+
new CompletedAppTask(
131+
"Monitor movement",
132+
"sensing",
133+
"dk.cachet.carp.geolocation",
134+
new Geolocation(57, 45, null) as any,
135+
) as any,
136136
),
137137
]),
138138
);
@@ -201,7 +201,12 @@ describe("DataStreams", () => {
201201
toLong(1),
202202
null,
203203
namespaceId,
204-
new Geolocation(57, 45, null) as any,
204+
new CompletedAppTask(
205+
"Monitor movement",
206+
"sensing",
207+
"dk.cachet.carp.geolocation",
208+
new Geolocation(57, 45, null) as any,
209+
) as any,
205210
),
206211
]),
207212
);
@@ -293,6 +298,60 @@ describe("DataStreams", () => {
293298
});
294299
});
295300

301+
test("should be able to get summary for datastreams", async () => {
302+
const batch = new CarpDataStreamBatch();
303+
const sequence = new MutableDataStreamSequence(
304+
new DataStreamId(
305+
participantGroupStatus.id,
306+
STUDY_PROTOCOL.primaryDevices[0].roleName,
307+
namespaceId,
308+
),
309+
toLong(1),
310+
toList([1]),
311+
SyncPoint.Companion.UnixEpoch,
312+
);
313+
sequence.appendMeasurementsList(
314+
toList([
315+
new Measurement(
316+
toLong(1),
317+
null,
318+
namespaceId,
319+
new CompletedAppTask(
320+
"Monitor movement",
321+
"sensing",
322+
"dk.cachet.carp.geolocation",
323+
new Geolocation(57, 45, null) as any,
324+
) as any,
325+
),
326+
]),
327+
);
328+
329+
batch.sequences = [sequence];
330+
331+
await expect(
332+
testClient.dataStreams.appendToDataStreams({
333+
studyDeploymentId: participantGroupStatus.id.stringRepresentation,
334+
batch,
335+
}),
336+
).resolves.not.toThrow();
337+
338+
const response = await testClient.dataStreams.getDataStreamSummary({
339+
study_id: study.studyId.stringRepresentation,
340+
scope: "study",
341+
type: "sensing",
342+
from: new Date(Date.now() - 1000 * 60 * 60 * 24 * 28).toISOString(),
343+
to: new Date().toISOString(),
344+
});
345+
346+
expect(response.studyId).toBe(study.studyId.stringRepresentation);
347+
expect(response.scope).toBe("study");
348+
expect(response.type).toBe("sensing");
349+
response.data.forEach((data) => {
350+
expect(data.quantity).to.be.at.least(1);
351+
expect(data.task).toBe("Monitor movement");
352+
});
353+
});
354+
296355
afterAll(async () => {
297356
if (study) {
298357
if (participantGroupStatus) {

0 commit comments

Comments
 (0)