Skip to content

Commit fa515aa

Browse files
Merge pull request #17 from Fullscript/pii-data-token
Tokenize patient data if provided
2 parents 992b6db + 6244784 commit fa515aa

File tree

7 files changed

+139
-40
lines changed

7 files changed

+139
-40
lines changed

src/feature/feature.spec.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -72,30 +72,30 @@ describe("feature", () => {
7272

7373
describe("mount", () => {
7474
it("calls the createIframe function when mount is called", () => {
75-
return import("./feature").then(({ getFeature }) => {
75+
return import("./feature").then(async ({ getFeature }) => {
7676
// hoisting issue with jest.mocks()
7777
const feature = getFeature(
7878
mockFeatureType,
7979
mockFeatureOptions,
8080
mockFullscriptOptions,
8181
dispatcher
8282
);
83-
feature.mount("someid");
83+
await feature.mount("someid");
8484

8585
expect(mockCreateIframe).toBeCalled();
8686
});
8787
});
8888

8989
it("appends child to the mountPoint when mount is called", () => {
90-
return import("./feature").then(({ getFeature }) => {
90+
return import("./feature").then(async ({ getFeature }) => {
9191
mockMountPoint.appendChild = jest.fn();
9292
const feature = getFeature(
9393
mockFeatureType,
9494
mockFeatureOptions,
9595
mockFullscriptOptions,
9696
dispatcher
9797
);
98-
feature.mount("someid");
98+
await feature.mount("someid");
9999

100100
expect(mockMountPoint.appendChild).toBeCalled();
101101
});
@@ -105,7 +105,7 @@ describe("feature", () => {
105105
it("does not return an iframe and throws an error if the provided elementId is undefined", () => {
106106
document.getElementById = jest.fn(() => null);
107107

108-
return import("./feature").then(({ getFeature }) => {
108+
return import("./feature").then(async ({ getFeature }) => {
109109
mockMountPoint.appendChild = jest.fn();
110110
const feature = getFeature(
111111
mockFeatureType,
@@ -116,9 +116,7 @@ describe("feature", () => {
116116

117117
expect(mockMountPoint.appendChild).not.toBeCalled();
118118

119-
expect(() => {
120-
feature.mount("blah");
121-
}).toThrow(
119+
await expect(feature.mount("blah")).rejects.toThrow(
122120
"Could not find the mount point for the iframe. Please check that the elementId provided in .mount() matches the one that's used in the DOM"
123121
);
124122
});

src/feature/feature.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ const getFeature = <F extends FeatureType>(
1818
): Feature => {
1919
let mountPoint: HTMLElement;
2020
const frameId = uuidv4();
21-
const url = getFeatureURL(featureType, featureOptions, fullscriptOptions, frameId);
2221

2322
// TODO: If we can only mount a feature once, throw an error if attempting to mount a second time
24-
const mount = (elementId: string) => {
23+
const mount = async (elementId: string) => {
24+
const url = await getFeatureURL(featureType, featureOptions, fullscriptOptions, frameId);
2525
mountPoint = document.getElementById(elementId);
2626
validateMountPoint(mountPoint);
2727
const iframe = createIframe(url);

src/feature/featureType.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ type FeatureOptions<F extends FeatureType> = F extends "treatmentPlan"
2424
: Record<any, never>;
2525

2626
interface Feature {
27-
mount: (elementId: string) => void;
27+
mount: (elementId: string) => Promise<void>;
2828
unmount: () => void;
2929
on: EventListenerFunction;
3030
off: EventListenerFunction;

src/feature/featureUtil.spec.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ describe("getFeatureUrl", () => {
77
let mockFeatureOptions: FeatureOptions<"treatmentPlan">;
88
let mockFullscriptOptions: FullscriptOptions;
99
let mockFrameId: string;
10+
const mockDataToken = "random+data_token";
11+
12+
window.fetch = jest.fn(() =>
13+
Promise.resolve({
14+
json: () => Promise.resolve({ data_token: mockDataToken }),
15+
})
16+
) as jest.Mock;
1017

1118
beforeEach(() => {
1219
mockFeatureOptions = {
@@ -24,35 +31,39 @@ describe("getFeatureUrl", () => {
2431
mockFrameId = "uuid";
2532
});
2633

27-
it("returns the proper url", () => {
28-
const url = getFeatureURL(
34+
it("returns the proper url", async () => {
35+
const url = await getFeatureURL(
2936
"treatmentPlan",
3037
mockFeatureOptions,
3138
mockFullscriptOptions,
3239
mockFrameId
3340
);
3441

3542
expect(url).toEqual(
36-
"https://us-snd.fullscript.io/api/embeddable/session/treatment_plans/new?patient[id]=patientId&secret_token=secretToken&public_key=publicKey&frame_id=uuid&target_origin=http://localhost"
43+
`https://us-snd.fullscript.io/api/embeddable/session/treatment_plans/new?data_token=${encodeURIComponent(
44+
mockDataToken
45+
)}&secret_token=secretToken&public_key=publicKey&frame_id=uuid&target_origin=http://localhost`
3746
);
3847
});
3948

40-
it("returns proper custom url if domain is present", () => {
49+
it("returns proper custom url if domain is present", async () => {
4150
const customDomain = "https://staging.r.fullscript.io";
4251
mockFullscriptOptions = {
4352
...mockFullscriptOptions,
4453
domain: customDomain,
4554
};
4655

47-
const url = getFeatureURL(
56+
const url = await getFeatureURL(
4857
"treatmentPlan",
4958
mockFeatureOptions,
5059
mockFullscriptOptions,
5160
mockFrameId
5261
);
5362

5463
expect(url).toEqual(
55-
`${customDomain}/api/embeddable/session/treatment_plans/new?patient[id]=patientId&secret_token=secretToken&public_key=publicKey&frame_id=uuid&target_origin=http://localhost`
64+
`${customDomain}/api/embeddable/session/treatment_plans/new?data_token=${encodeURIComponent(
65+
mockDataToken
66+
)}&secret_token=secretToken&public_key=publicKey&frame_id=uuid&target_origin=http://localhost`
5667
);
5768
});
5869
});

src/feature/featureUtil.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,19 @@ import { buildQueryString } from "../utils";
55
import { FEATURE_PATHS } from "./featurePath";
66
import { FeatureType, FeatureOptions } from "./featureType";
77

8-
const getFeatureURL = <F extends FeatureType>(
8+
const getFeatureURL = async <F extends FeatureType>(
99
featureType: F,
1010
featureOptions: FeatureOptions<F>,
1111
fullscriptOptions: FullscriptOptions,
1212
frameId: string
13-
): string => {
13+
): Promise<string> => {
1414
const { publicKey, env, domain } = fullscriptOptions;
15-
const queryString = buildQueryString({ ...featureOptions, publicKey, frameId });
15+
const queryString = await buildQueryString({
16+
...featureOptions,
17+
fullscriptOptions,
18+
publicKey,
19+
frameId,
20+
});
1621
validateFeatureType(featureType);
1722
const fsDomain = domain ?? FULLSCRIPT_DOMAINS[env];
1823

src/utils/utils.spec.ts

Lines changed: 55 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ describe("utils", () => {
55
let mockParams: Params;
66
const mockKey = "pupper";
77
const mockValue = "nessa";
8+
const mockDataToken = "random+data_token";
9+
const mockFullscriptOptions = {
10+
publicKey: "mockPublicKey",
11+
env: "us",
12+
};
813

914
beforeEach(() => {
1015
elem = document.createElement("div");
@@ -32,28 +37,66 @@ describe("utils", () => {
3237
});
3338

3439
describe("buildQueryString", () => {
35-
it("returns an empty string if params have no keys", () => {
40+
it("returns an empty string if params have no keys", async () => {
3641
mockParams = {};
37-
const builtQueryString = buildQueryString(mockParams);
38-
expect(builtQueryString).toHaveLength(0);
42+
const builtQueryString = await buildQueryString(mockParams);
43+
await expect(builtQueryString).toHaveLength(0);
3944
});
4045

41-
it("properly builds a query string with proper key and param values", () => {
42-
const builtQueryString = buildQueryString(mockParams);
46+
it("properly builds a query string with proper key and param values", async () => {
47+
const builtQueryString = await buildQueryString(mockParams);
4348

44-
expect(builtQueryString).toEqual(`?key=${mockKey}&value=${mockValue}`);
49+
await expect(builtQueryString).toEqual(`?key=${mockKey}&value=${mockValue}`);
4550
});
4651

47-
it("converts keys into snakecase", () => {
52+
it("converts keys into snakecase", async () => {
4853
mockParams = { fooBar: "foobar" };
49-
const builtQueryString = buildQueryString(mockParams);
50-
expect(builtQueryString).toEqual(`?foo_bar=${mockParams.fooBar}`);
54+
const builtQueryString = await buildQueryString(mockParams);
55+
await expect(builtQueryString).toEqual(`?foo_bar=${mockParams.fooBar}`);
5156
});
5257

53-
it("accept object params", () => {
58+
it("accept object params", async () => {
5459
mockParams = { something: "else", foo: { bar: "foobar" } };
55-
const queryString = buildQueryString(mockParams);
56-
expect(queryString).toEqual(`?something=else&foo[bar]=${mockParams.foo.bar}`);
60+
const queryString = await buildQueryString(mockParams);
61+
await expect(queryString).toEqual(`?something=else&foo[bar]=${mockParams.foo.bar}`);
62+
});
63+
64+
it("properly converts patient info to token", async () => {
65+
mockParams = {
66+
fooBar: "foobar",
67+
patient: { id: "patientId" },
68+
fullscriptOptions: mockFullscriptOptions,
69+
};
70+
71+
window.fetch = jest.fn(() =>
72+
Promise.resolve({
73+
json: () => Promise.resolve({ data_token: mockDataToken }),
74+
})
75+
) as jest.Mock;
76+
77+
const builtQueryString = await buildQueryString(mockParams);
78+
79+
await expect(builtQueryString).toEqual(
80+
`?data_token=${encodeURIComponent(mockDataToken)}&foo_bar=${mockParams.fooBar}`
81+
);
82+
});
83+
84+
it("passes null data token if tokenization fails", async () => {
85+
mockParams = {
86+
fooBar: "foobar",
87+
patient: { id: "patientId" },
88+
fullscriptOptions: mockFullscriptOptions,
89+
};
90+
91+
window.fetch = jest.fn(() =>
92+
Promise.reject({
93+
json: () => Promise.resolve({ error: "some error" }),
94+
})
95+
) as jest.Mock;
96+
97+
const builtQueryString = await buildQueryString(mockParams);
98+
99+
await expect(builtQueryString).toEqual(`?data_token=null&foo_bar=${mockParams.fooBar}`);
57100
});
58101
});
59102
});

src/utils/utils.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { FULLSCRIPT_DOMAINS } from "../fullscript";
2+
13
export type Params = { key: string; value: any } | any;
24

35
const removeChildren = (element: HTMLElement): void => {
@@ -13,16 +15,56 @@ const snakeCase = (word: string): string => {
1315
.join("_");
1416
};
1517

16-
const buildQueryString = (params: Params): string => {
18+
const snakeCaseObjectKeys = (obj: any) => {
19+
return Object.keys(obj).reduce(
20+
(result, key) => ({
21+
...result,
22+
[snakeCase(key)]: obj[key],
23+
}),
24+
{}
25+
);
26+
};
27+
28+
const tokenizeData = async (patientInfo, fullscriptOptions) => {
29+
try {
30+
const { env, domain } = fullscriptOptions;
31+
const fsDomain = domain ?? FULLSCRIPT_DOMAINS[env];
32+
33+
const tokenizedInfo = await fetch(`${fsDomain}/api/embeddable/tokenize`, {
34+
method: "POST",
35+
body: JSON.stringify({ data: patientInfo }),
36+
headers: { "Content-Type": "application/json" },
37+
}).then(res => res.json());
38+
39+
if (typeof tokenizedInfo.data_token !== "string") throw new Error("Invalid response");
40+
41+
return tokenizedInfo.data_token;
42+
} catch (error) {
43+
return null;
44+
}
45+
};
46+
47+
const buildQueryString = async (params: Params): Promise<string> => {
1748
if (!Object.keys(params) || Object.keys(params).length === 0) return "";
18-
return Object.keys(params).reduce((queryString, key) => {
19-
let newParam = `${snakeCase(key)}=${params[key]}`;
2049

21-
if (typeof params[key] !== "string") {
22-
newParam = Object.keys(params[key]).reduce(
50+
const { patient: patientInfo, fullscriptOptions, ...exposedParams } = params;
51+
let startingQueryString = "?";
52+
53+
if (patientInfo) {
54+
const snakeCasePatient = snakeCaseObjectKeys(patientInfo);
55+
const dataToken = await tokenizeData(snakeCasePatient, fullscriptOptions);
56+
57+
startingQueryString += `data_token=${encodeURIComponent(dataToken)}&`;
58+
}
59+
60+
return Object.keys(exposedParams).reduce((queryString, key) => {
61+
let newParam = `${snakeCase(key)}=${exposedParams[key]}`;
62+
63+
if (typeof exposedParams[key] !== "string") {
64+
newParam = Object.keys(exposedParams[key]).reduce(
2365
(objectParams, attribute, currentIndex): string => {
2466
const objectParam = `${snakeCase(key)}[${snakeCase(attribute)}]=${encodeURIComponent(
25-
params[key][attribute]
67+
exposedParams[key][attribute]
2668
)}`;
2769

2870
if (currentIndex === 0) {
@@ -34,12 +76,12 @@ const buildQueryString = (params: Params): string => {
3476
);
3577
}
3678

37-
if (queryString !== "?") {
79+
if (queryString !== startingQueryString) {
3880
newParam = `&${newParam}`;
3981
}
4082

4183
return `${queryString}${newParam}`;
42-
}, "?");
84+
}, startingQueryString);
4385
};
4486

4587
export { removeChildren, buildQueryString };

0 commit comments

Comments
 (0)