Skip to content

Commit b9aca98

Browse files
Streamline Recorded FHIR Metadata (#109)
* use package json version * update path * log process version as OSVersion * specify firebase * update customCodes * update subject reference * change system to spezi.stanford.edu * lint:fix * fix import 2 * NicotineScoreCalculator.calculate function sync to iOS, null as default * fix for smoking status codes * add version to function packages * add packageVersion helper ES5 cant handle import assertions :( * withdrawl -> withdrawl thanks lukas Co-Authored-By: Lukas Kollmer <contact@lukaskollmer.de> * fix tests... * lint:fix * manual linter fixes * package lock sync * add tests for uncovered functions * lint:fix * manual linter fix * manual linter fix 2 --------- Co-authored-by: Lukas Kollmer <contact@lukaskollmer.de>
1 parent a311e3d commit b9aca98

18 files changed

+291
-86
lines changed

functions/package-lock.json

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

functions/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
{
22
"name": "functions",
3+
"version": "0.1.1",
34
"description": "Cloud Functions for Firebase",
45
"type": "module",
56
"scripts": {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// This source file is part of the Stanford Biodesign Digital Health MyHeart Counts open-source project based on the Stanford Spezi Template Application project
3+
//
4+
// SPDX-FileCopyrightText: 2025 Stanford University
5+
//
6+
// SPDX-License-Identifier: MIT
7+
//
8+
9+
import { expect } from "chai";
10+
import type { https } from "firebase-functions/v2";
11+
import { it } from "mocha";
12+
import { markAccountForStudyReenrollment } from "./markAccountForStudyReenrollment.js";
13+
import { describeWithEmulators } from "../tests/functions/testEnvironment.js";
14+
import { expectError } from "../tests/helpers.js";
15+
16+
describeWithEmulators("function: markAccountForStudyReenrollment", (env) => {
17+
it("re-enrolls user in study successfully", async () => {
18+
const userId = await env.createUser({});
19+
20+
// First withdraw from study...
21+
await env.factory.user().markAccountForStudyWithdrawal(userId, new Date());
22+
23+
// ... then re-enroll!
24+
const result = await env.call(
25+
markAccountForStudyReenrollment,
26+
{},
27+
{ uid: userId },
28+
);
29+
30+
expect(result.success).to.equal(true);
31+
expect(result.reenrolledAt).to.be.a("string");
32+
const reenrolledTime = new Date(result.reenrolledAt).getTime();
33+
expect(reenrolledTime).to.be.lessThanOrEqual(Date.now());
34+
});
35+
36+
it("throws error when user account not found", async () => {
37+
const authUser = await env.auth.createUser({});
38+
39+
await expectError(
40+
() =>
41+
env.call(markAccountForStudyReenrollment, {}, { uid: authUser.uid }),
42+
(error) => {
43+
const httpsError = error as https.HttpsError;
44+
expect(httpsError.code).to.equal("not-found");
45+
},
46+
);
47+
});
48+
49+
it("throws error when user account is disabled", async () => {
50+
const userId = await env.createUser({ disabled: true });
51+
52+
await expectError(
53+
() => env.call(markAccountForStudyReenrollment, {}, { uid: userId }),
54+
(error) => {
55+
const httpsError = error as https.HttpsError;
56+
expect(httpsError.code).to.equal("failed-precondition");
57+
},
58+
);
59+
});
60+
});
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// This source file is part of the Stanford Biodesign Digital Health MyHeart Counts open-source project based on the Stanford Spezi Template Application project
3+
//
4+
// SPDX-FileCopyrightText: 2025 Stanford University
5+
//
6+
// SPDX-License-Identifier: MIT
7+
//
8+
9+
import { expect } from "chai";
10+
import type { https } from "firebase-functions/v2";
11+
import { it } from "mocha";
12+
import { markAccountForStudyWithdrawal } from "./markAccountForStudyWithdrawl.js";
13+
import { describeWithEmulators } from "../tests/functions/testEnvironment.js";
14+
import { expectError } from "../tests/helpers.js";
15+
16+
describeWithEmulators("function: markAccountForStudyWithdrawal", (env) => {
17+
it("marks user for study withdrawal successfully", async () => {
18+
const userId = await env.createUser({});
19+
20+
const result = await env.call(
21+
markAccountForStudyWithdrawal,
22+
{},
23+
{ uid: userId },
24+
);
25+
26+
expect(result.success).to.equal(true);
27+
expect(result.withdrawnAt).to.be.a("string");
28+
const withdrawnTime = new Date(result.withdrawnAt).getTime();
29+
expect(withdrawnTime).to.be.lessThanOrEqual(Date.now());
30+
});
31+
32+
it("throws error when user account not found", async () => {
33+
const authUser = await env.auth.createUser({});
34+
35+
await expectError(
36+
() => env.call(markAccountForStudyWithdrawal, {}, { uid: authUser.uid }),
37+
(error) => {
38+
const httpsError = error as https.HttpsError;
39+
expect(httpsError.code).to.equal("not-found");
40+
},
41+
);
42+
});
43+
44+
it("throws error when user account is disabled", async () => {
45+
const userId = await env.createUser({ disabled: true });
46+
47+
await expectError(
48+
() => env.call(markAccountForStudyWithdrawal, {}, { uid: userId }),
49+
(error) => {
50+
const httpsError = error as https.HttpsError;
51+
expect(httpsError.code).to.equal("failed-precondition");
52+
},
53+
);
54+
});
55+
});

functions/src/functions/markAccountForStudyWithdrawl.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ import { z } from "zod";
1111
import { validatedOnCall, defaultServiceAccount } from "./helpers.js";
1212
import { getServiceFactory } from "../services/factory/getServiceFactory.js";
1313

14-
const markAccountForStudyWithdrawlInputSchema = z.object({});
14+
const markAccountForStudyWithdrawalInputSchema = z.object({});
1515

16-
interface MarkAccountForStudyWithdrawlOutput {
16+
interface MarkAccountForStudyWithdrawalOutput {
1717
success: boolean;
1818
withdrawnAt: string;
1919
}
2020

21-
export const markAccountForStudyWithdrawl = validatedOnCall(
22-
"markAccountForStudyWithdrawl",
23-
markAccountForStudyWithdrawlInputSchema,
24-
async (request): Promise<MarkAccountForStudyWithdrawlOutput> => {
21+
export const markAccountForStudyWithdrawal = validatedOnCall(
22+
"markAccountForStudyWithdrawal",
23+
markAccountForStudyWithdrawalInputSchema,
24+
async (request): Promise<MarkAccountForStudyWithdrawalOutput> => {
2525
const factory = getServiceFactory();
2626
const credential = factory.credential(request.auth);
2727
const userService = factory.user();
@@ -49,7 +49,7 @@ export const markAccountForStudyWithdrawl = validatedOnCall(
4949
}
5050

5151
const withdrawnAt = new Date();
52-
await userService.markAccountForStudyWithdrawl(userId, withdrawnAt);
52+
await userService.markAccountForStudyWithdrawal(userId, withdrawnAt);
5353

5454
logger.info(
5555
`User ${userId} successfully marked their account for study withdrawal`,
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// This source file is part of the MyHeartCounts-Firebase project
3+
//
4+
// SPDX-FileCopyrightText: 2023 Stanford University
5+
//
6+
// SPDX-License-Identifier: MIT
7+
//
8+
9+
// Small helper function to inject the current functions version to the backend generated FHIR Observations
10+
11+
import { readFileSync } from "fs";
12+
import { dirname, join } from "path";
13+
import { fileURLToPath } from "url";
14+
15+
const __filename = fileURLToPath(import.meta.url);
16+
const __dirname = dirname(__filename);
17+
18+
interface PackageJson {
19+
version: string;
20+
}
21+
22+
let cachedVersion: string | undefined;
23+
24+
export const getPackageVersion = (): string => {
25+
if (cachedVersion) {
26+
return cachedVersion;
27+
}
28+
29+
try {
30+
const packageJsonPath = join(__dirname, "../../package.json");
31+
const packageJsonContent = readFileSync(packageJsonPath, "utf-8");
32+
const packageJson = JSON.parse(packageJsonContent) as PackageJson;
33+
cachedVersion = packageJson.version;
34+
return cachedVersion;
35+
} catch (error) {
36+
console.error("Failed to read package.json version:", error);
37+
return "unknown";
38+
}
39+
};

functions/src/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,5 @@ export * from "./functions/onUserQuestionnaireResponseWritten.js";
5252
export * from "./functions/deleteHealthSamples.js";
5353
export * from "./functions/onArchivedLiveHealthSampleUploaded.js";
5454
export * from "./functions/markAccountForDeletion.js";
55-
export * from "./functions/markAccountForStudyWithdrawl.js";
5655
export * from "./functions/markAccountForStudyReenrollment.js";
5756
export * from "./functions/processUserDeletions.js";

functions/src/services/questionnaireResponse/fhirObservationConverter.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type Score,
1313
type FHIRCodeableConcept,
1414
} from "@stanfordbdhg/myheartcounts-models";
15+
import { getPackageVersion } from "../../helpers/packageVersion.js";
1516

1617
export interface QuestionnaireObservationConfig {
1718
customCode: string;
@@ -33,8 +34,7 @@ export const scoreToObservation = (
3334
{
3435
code: config.customCode,
3536
display: config.display,
36-
system:
37-
"https://myheartcounts.stanford.edu/fhir/CodeSystem/observation-codes",
37+
system: "https://spezi.stanford.edu",
3838
},
3939
],
4040
text: config.display,
@@ -44,7 +44,7 @@ export const scoreToObservation = (
4444
id: observationId,
4545
status: FHIRObservationStatus.final,
4646
subject: {
47-
reference: `Patient/${userId}`,
47+
reference: `user/${userId}`,
4848
},
4949
code: codeableConcept,
5050
valueQuantity: {
@@ -67,26 +67,26 @@ export const scoreToObservation = (
6767
},
6868
{
6969
url: "https://bdh.stanford.edu/fhir/defs/sourceRevision/source/name",
70-
valueString: "My Heart Counts",
70+
valueString: "My Heart Counts Firebase",
7171
},
7272
{
7373
url: "https://bdh.stanford.edu/fhir/defs/sourceRevision/source/bundleIdentifier",
7474
valueString: "edu.stanford.MyHeartCounts",
7575
},
7676
{
7777
url: "https://bdh.stanford.edu/fhir/defs/sourceRevision/version",
78-
valueString: "3.0.0 (955)",
78+
valueString: getPackageVersion(),
7979
},
8080
{
8181
url: "https://bdh.stanford.edu/fhir/defs/sourceRevision/OSVersion",
82-
valueString: "18.5.0",
82+
valueString: process.version,
8383
},
8484
],
8585
});
8686
};
8787

8888
export const getDietObservationConfig = (): QuestionnaireObservationConfig => ({
89-
customCode: "diet-mepa-score",
89+
customCode: "MHCCustomSampleTypeDietMEPAScore",
9090
display: "Diet MEPA Score",
9191
unit: "count",
9292
unitSystem: "http://unitsofmeasure.org",
@@ -95,15 +95,15 @@ export const getDietObservationConfig = (): QuestionnaireObservationConfig => ({
9595

9696
export const getNicotineObservationConfig =
9797
(): QuestionnaireObservationConfig => ({
98-
customCode: "nicotine-exposure-score",
98+
customCode: "MHCCustomSampleTypeNicotineExposure",
9999
display: "Nicotine Exposure Score",
100100
unit: "count",
101101
unitSystem: "http://unitsofmeasure.org",
102102
ucumCode: "{count}",
103103
});
104104

105105
export const getWho5ObservationConfig = (): QuestionnaireObservationConfig => ({
106-
customCode: "who5-wellbeing-score",
106+
customCode: "MHCCustomSampleTypeWHO5Score",
107107
display: "WHO-5 Well-Being Score",
108108
unit: "count",
109109
unitSystem: "http://unitsofmeasure.org",

functions/src/services/questionnaireResponse/heartRiskLdlParsingService.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@ export class HeartRiskLdlParsingQuestionnaireResponseService extends Questionnai
119119
id: observationId,
120120
status: FHIRObservationStatus.final,
121121
subject: {
122-
reference: `Patient/${userId}`,
122+
reference: `user/${userId}`,
123123
},
124124
code: {
125125
coding: [
@@ -130,8 +130,7 @@ export class HeartRiskLdlParsingQuestionnaireResponseService extends Questionnai
130130
{
131131
code: "MHCCustomSampleTypeBloodLipidMeasurement",
132132
display: "LDL Cholesterol",
133-
system:
134-
"https://myheartcounts.stanford.edu/fhir/CodeSystem/observation-codes",
133+
system: "https://spezi.stanford.edu",
135134
},
136135
],
137136
},

functions/src/services/questionnaireResponse/heartRiskNicotineScoringService.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,11 +95,11 @@ export class HeartRiskNicotineScoringQuestionnaireResponseService extends Questi
9595
}
9696

9797
const answer = responseItem.answer[0];
98-
if (answer.valueCoding?.display) {
99-
return answer.valueCoding.display;
98+
if (answer.valueCoding?.code) {
99+
return answer.valueCoding.code;
100100
}
101101

102-
logger.warn(`No valueCoding.display found for linkId '${linkId}'`);
102+
logger.warn(`No valueCoding.code found for linkId '${linkId}'`);
103103
return null;
104104
} catch (error) {
105105
logger.warn(

0 commit comments

Comments
 (0)