diff --git a/lib/edge/resolve.test.js b/lib/edge/resolve.test.js new file mode 100644 index 0000000..8a311f3 --- /dev/null +++ b/lib/edge/resolve.test.js @@ -0,0 +1,88 @@ +import { getConfig } from "../config"; +import { parseResolveResponse, Resolve } from "./resolve"; + +describe("resolve", () => { + test("forwards identifier when present", () => { + const config = getConfig({ host: "host", site: "site" }); + + const fetchSpy = jest.spyOn(window, "fetch"); + + Resolve(config, "id"); + expect(fetchSpy).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET", + url: `https://host/site/v1/resolve?id=id&osdk=web-0.0.0-experimental&cookies=yes`, + }) + ); + + Resolve(config); + expect.objectContaining({ + method: "GET", + url: `https://host/site/v1/resolve?osdk=web-0.0.0-experimental&cookies=yes`, + }); + }); +}); + +describe("parseResolveResponse", () => { + test("parses expected responses", () => { + const empty = { clusters: [], lmpid: "" }; + + const cases = [ + // Unexpected response types return empty response + { input: {}, output: empty }, + { input: null, output: empty }, + { input: undefined, output: empty }, + { input: 1, output: empty }, + { input: true, output: empty }, + { input: [], output: empty }, + + { input: { clusters: [null] }, output: empty }, + { input: { clusters: [undefined] }, output: empty }, + { input: { clusters: [1] }, output: empty }, + { input: { clusters: [true] }, output: empty }, + { input: { clusters: [[]] }, output: empty }, + + { input: { clusters: [{ ids: {}, traits: {} }] }, output: empty }, + { input: { clusters: [{ ids: null, traits: null }] }, output: empty }, + { input: { clusters: [{ ids: undefined, traits: undefined }] }, output: empty }, + { input: { clusters: [{ ids: 1, traits: 1 }] }, output: empty }, + { input: { clusters: [{ ids: true, traits: true }] }, output: empty }, + { input: { clusters: [{ ids: [], traits: [] }] }, output: empty }, + + { input: { clusters: [{ ids: [null], traits: [null] }] }, output: empty }, + { input: { clusters: [{ ids: [undefined], traits: [undefined] }] }, output: empty }, + { input: { clusters: [{ ids: [1], traits: [1] }] }, output: empty }, + { input: { clusters: [{ ids: [true], traits: [true] }] }, output: empty }, + + // Additional properties are skipped + { + input: { + clusters: [ + { ids: ["i4:", "e:"], traits: [{ key: "", value: "" }], additional: "property" }, + ], + }, + output: { + clusters: [{ ids: ["i4:", "e:"], traits: [{ key: "", value: "" }] }], + lmpid: "", + }, + }, + { + input: { clusters: [{ ids: ["i4:", "e:"], traits: [{ key: "", value: "" }] }] }, + output: { + clusters: [{ ids: ["i4:", "e:"], traits: [{ key: "", value: "" }] }], + lmpid: "", + }, + }, + + // Lmpid is returned when matching expected type + { input: { lmpid: 1 }, output: { clusters: [], lmpid: "" } }, + { input: { lmpid: null }, output: { clusters: [], lmpid: "" } }, + { input: { lmpid: undefined }, output: { clusters: [], lmpid: "" } }, + { input: { lmpid: "lmpid" }, output: { clusters: [], lmpid: "lmpid" } }, + ]; + + for (const c of cases) { + expect(parseResolveResponse(c.input)).toEqual(c.output); + } + }); +}); diff --git a/lib/edge/resolve.ts b/lib/edge/resolve.ts new file mode 100644 index 0000000..5f1e1f2 --- /dev/null +++ b/lib/edge/resolve.ts @@ -0,0 +1,76 @@ +import type { ResolvedConfig } from "../config"; +import { fetch } from "../core/network"; + +type ResolveTrait = { + key: string; + value: string; +}; + +type ResolveCluster = { + ids: string[]; + traits: ResolveTrait[]; +}; + +type ResolveResponse = { + clusters: ResolveCluster[]; + lmpid?: string; +}; + +async function Resolve(config: ResolvedConfig, id?: string): Promise { + const searchParams = new URLSearchParams(); + if (typeof id === "string") { + searchParams.append("id", id); + } + const path = "/v1/resolve?" + searchParams.toString(); + + const response = await fetch(path, config, { + method: "GET", + headers: { Accept: "application/json" }, + }); + + return parseResolveResponse(response); +} + +function parseResolveResponse(resolveResponse: unknown): ResolveResponse { + const response: ResolveResponse = { clusters: [], lmpid: "" }; + + if (typeof resolveResponse !== "object" || resolveResponse === null) { + return response; + } + + if ("lmpid" in resolveResponse && typeof resolveResponse?.lmpid === "string") { + response.lmpid = resolveResponse.lmpid; + } + + if (!("clusters" in resolveResponse) || !Array.isArray(resolveResponse?.clusters)) { + return response; + } + + for (const c of resolveResponse.clusters) { + const cluster: ResolveCluster = { ids: [], traits: [] }; + + if (Array.isArray(c?.ids)) { + for (const id of c.ids) { + if (typeof id === "string") { + cluster.ids.push(id); + } + } + } + + if (Array.isArray(c?.traits)) { + for (const trait of c.traits) { + if (typeof trait?.key === "string" && typeof trait?.value === "string") { + cluster.traits.push({ key: trait.key, value: trait.value }); + } + } + } + + if (cluster.ids.length > 0 || cluster.traits.length > 0) { + response.clusters.push(cluster); + } + } + + return response; +} + +export { Resolve, ResolveResponse, parseResolveResponse }; diff --git a/lib/sdk.ts b/lib/sdk.ts index 9da89b3..e5fc3e0 100644 --- a/lib/sdk.ts +++ b/lib/sdk.ts @@ -5,6 +5,7 @@ import type { WitnessProperties } from "./edge/witness"; import type { ProfileTraits } from "./edge/profile"; import { Identify } from "./edge/identify"; import { Uid2Token } from "./edge/uid2_token"; +import { Resolve, ResolveResponse } from "./edge/resolve"; import { Site, SiteResponse, SiteFromCache } from "./edge/site"; import { TargetingKeyValues, @@ -99,6 +100,11 @@ class OptableSDK { return Tokenize(this.dcn, id); } + async resolve(id?: string): Promise { + await this.init; + return Resolve(this.dcn, id); + } + static eid(email: string): string { return email ? "e:" + sha256.hex(email.toLowerCase().trim()) : ""; }