Implement JSON search through fuse.js + web worker + react hook

This commit is contained in:
Eric Allam
2022-03-25 14:42:03 +00:00
parent 8caa11ba1c
commit e368db81c8
12 changed files with 738 additions and 40 deletions
+2 -1
View File
@@ -10,4 +10,5 @@ node_modules
/dist /dist
.mf .mf
/meta.json /meta.json
/stats.html /stats.html
public/entry.worker.js
+28
View File
@@ -0,0 +1,28 @@
import { useJsonSearchApi, useJsonSearchState } from "~/hooks/useJsonSearch";
export function SearchPalette() {
const searchState = useJsonSearchState();
const searchApi = useJsonSearchApi();
return (
<div>
<label>Search json</label>
<div>
<input
type="text"
value={searchState.query ?? ""}
onChange={(e) => searchApi.search(e.currentTarget.value)}
/>
</div>
<ul>
{searchState.results?.map((result) => (
<li key={result.item.path}>
[{result.item.path}] {result.item.formattedValue}
{" - "}
{result.item.rawValue}
</li>
))}
</ul>
</div>
);
}
+60
View File
@@ -0,0 +1,60 @@
/// <reference lib="WebWorker" />
import Fuse from "fuse.js";
import { createSearchIndex, JsonSearchEntry } from "./utilities/search";
type SearchWorker = {
entries?: Array<JsonSearchEntry>;
index?: Fuse.FuseIndex<JsonSearchEntry>;
fuse?: Fuse<JsonSearchEntry>;
};
export type {};
declare let self: DedicatedWorkerGlobalScope & SearchWorker;
type InitializeIndexEvent = {
type: "initialize-index";
payload: { json: unknown; fuseOptions: Fuse.IFuseOptions<JsonSearchEntry> };
};
type SearchEvent = {
type: "search";
payload: { query: string };
};
type SearchWorkerEvent = InitializeIndexEvent | SearchEvent;
self.onmessage = (e: MessageEvent<SearchWorkerEvent>) => {
const { type, payload } = e.data;
console.group(`SearchWorker: ${type}`);
console.log(payload);
console.groupEnd();
switch (type) {
case "initialize-index": {
const { json, fuseOptions } = payload;
const [index, entries] = createSearchIndex(json);
self.entries = entries;
self.index = index;
self.fuse = new Fuse(entries, fuseOptions, index);
self.postMessage({ type: "index-initialized" });
break;
}
case "search": {
const { query } = payload;
if (!self.fuse) {
throw new Error("Search index not initialized");
}
const results = self.fuse.search(query);
self.postMessage({ type: "search-results", payload: { results } });
}
}
};
+170
View File
@@ -0,0 +1,170 @@
import { useJson } from "./useJson";
import Fuse from "fuse.js";
import { JsonSearchEntry } from "~/utilities/search";
import {
createContext,
useCallback,
useContext,
useEffect,
useReducer,
useRef,
} from "react";
export type InitializeIndexEvent = {
type: "initialize-index";
payload: { json: unknown; fuseOptions: Fuse.IFuseOptions<JsonSearchEntry> };
};
export type SearchEvent = {
type: "search";
payload: { query: string };
};
export type SearchSendWorkerEvent = InitializeIndexEvent | SearchEvent;
export type IndexInitializedEvent = {
type: "index-initialized";
};
export type SearchResultsEvent = {
type: "search-results";
payload: { results: Fuse.FuseResult<JsonSearchEntry>[] };
};
export type SearchReceiveWorkerEvent =
| IndexInitializedEvent
| SearchResultsEvent;
export type JsonSearchApi = {
search: (query: string) => void;
};
const JsonSearchStateContext = createContext<JsonSearchState>(
{} as JsonSearchState
);
const JsonSearchApiContext = createContext<JsonSearchApi>({} as JsonSearchApi);
export type JsonSearchState = {
status: "initializing" | "idle" | "searching";
query?: string;
results?: Fuse.FuseResult<JsonSearchEntry>[];
};
type SearchAction = {
type: "search";
payload: { query: string };
};
type JsonSearchAction = SearchReceiveWorkerEvent | SearchAction;
function reducer(
state: JsonSearchState,
action: JsonSearchAction
): JsonSearchState {
switch (action.type) {
case "index-initialized":
return {
...state,
status: "idle",
results: undefined,
};
case "search-results":
return {
...state,
status: "idle",
results: action.payload.results,
};
case "search":
return {
...state,
status: "searching",
query: action.payload.query,
};
default:
return state;
}
}
export function JsonSearchProvider({
children,
}: {
children: React.ReactNode;
}) {
const [json] = useJson();
const [state, dispatch] = useReducer<
React.Reducer<JsonSearchState, JsonSearchAction>
>(reducer, { status: "initializing" });
const search = useCallback(
(query: string) => {
dispatch({ type: "search", payload: { query } });
},
[dispatch]
);
const handleWorkerMessage = useCallback(
(e: MessageEvent<SearchReceiveWorkerEvent>) => dispatch(e.data),
[dispatch]
);
const workerRef = useRef<Worker | null>();
useEffect(() => {
if (typeof window === "undefined" || typeof window.Worker === "undefined") {
return;
}
if (workerRef.current) {
return;
}
const worker = new Worker("/entry.worker.js");
worker.onmessage = handleWorkerMessage;
workerRef.current = worker;
workerRef.current.postMessage({
type: "initialize-index",
payload: {
json,
fuseOptions: {
includeScore: true,
includeMatches: true,
minMatchCharLength: 1,
isCaseSensitive: false,
threshold: 0.6,
distance: 200,
},
},
});
}, [json, workerRef.current]);
useEffect(() => {
if (state.status !== "searching") {
return;
}
workerRef.current?.postMessage({
type: "search",
payload: { query: state.query },
});
}, [state.status, workerRef.current]);
return (
<JsonSearchStateContext.Provider value={state}>
<JsonSearchApiContext.Provider value={{ search }}>
{children}
</JsonSearchApiContext.Provider>
</JsonSearchStateContext.Provider>
);
}
export function useJsonSearchState(): JsonSearchState {
return useContext(JsonSearchStateContext);
}
export function useJsonSearchApi(): JsonSearchApi {
return useContext(JsonSearchApiContext);
}
+27 -24
View File
@@ -21,6 +21,7 @@ import { JsonSchemaProvider } from "~/hooks/useJsonSchema";
import { JsonView } from "~/components/JsonView"; import { JsonView } from "~/components/JsonView";
import safeFetch from "~/utilities/safeFetch"; import safeFetch from "~/utilities/safeFetch";
import { JsonTreeViewProvider } from "~/hooks/useJsonTree"; import { JsonTreeViewProvider } from "~/hooks/useJsonTree";
import { JsonSearchProvider } from "~/hooks/useJsonSearch";
export const loader: LoaderFunction = async ({ params, request }) => { export const loader: LoaderFunction = async ({ params, request }) => {
invariant(params.id, "expected params.id"); invariant(params.id, "expected params.id");
@@ -114,34 +115,36 @@ export default function JsonDocumentRoute() {
<JsonProvider initialJson={loaderData.json}> <JsonProvider initialJson={loaderData.json}>
<JsonSchemaProvider> <JsonSchemaProvider>
<JsonColumnViewProvider> <JsonColumnViewProvider>
<JsonTreeViewProvider overscan={25}> <JsonSearchProvider>
<div> <JsonTreeViewProvider overscan={25}>
<div className="h-screen flex flex-col"> <div>
<Header /> <div className="h-screen flex flex-col">
<div className="bg-slate-50 flex-grow transition dark:bg-slate-900"> <Header />
<div className="main-container flex justify-items-stretch h-full"> <div className="bg-slate-50 flex-grow transition dark:bg-slate-900">
<SideBar /> <div className="main-container flex justify-items-stretch h-full">
<JsonView> <SideBar />
<Outlet /> <JsonView>
</JsonView> <Outlet />
</JsonView>
<Resizable <Resizable
isHorizontal={true} isHorizontal={true}
initialSize={500} initialSize={500}
minimumSize={280} minimumSize={280}
maximumSize={900} maximumSize={900}
> >
<div className="info-panel flex-grow h-full"> <div className="info-panel flex-grow h-full">
<InfoPanel /> <InfoPanel />
</div> </div>
</Resizable> </Resizable>
</div>
</div> </div>
</div>
<Footer></Footer> <Footer></Footer>
</div>
</div> </div>
</div> </JsonTreeViewProvider>
</JsonTreeViewProvider> </JsonSearchProvider>
</JsonColumnViewProvider> </JsonColumnViewProvider>
</JsonSchemaProvider> </JsonSchemaProvider>
</JsonProvider> </JsonProvider>
+1 -1
View File
@@ -220,7 +220,7 @@ export function useColumnView({
selectedNodes, selectedNodes,
highlightedNodeId, highlightedNodeId,
highlightedPath, highlightedPath,
columns, columns: columns ?? [],
getColumnViewProps, getColumnViewProps,
canGoBack, canGoBack,
canGoForward, canGoForward,
+16 -1
View File
@@ -26,9 +26,20 @@ export function formatRawValue(type: JSONValueType): string {
} }
} }
export function formatValue(type: JSONValueType): string | undefined { export type FormatValueOptions = {
leafNodesOnly?: boolean;
};
export function formatValue(
type: JSONValueType,
options?: FormatValueOptions
): string | undefined {
switch (type.name) { switch (type.name) {
case "array": { case "array": {
if (options?.leafNodesOnly) {
return;
}
if (type.value.length == 0) { if (type.value.length == 0) {
return formatRawValue(type); return formatRawValue(type);
} else if (type.value.length === 1) { } else if (type.value.length === 1) {
@@ -38,6 +49,10 @@ export function formatValue(type: JSONValueType): string | undefined {
} }
} }
case "object": { case "object": {
if (options?.leafNodesOnly) {
return;
}
if (Object.keys(type.value).length == 0) { if (Object.keys(type.value).length == 0) {
return formatRawValue(type); return formatRawValue(type);
} else if (Object.keys(type.value).length === 1) { } else if (Object.keys(type.value).length === 1) {
-2
View File
@@ -26,8 +26,6 @@ import {
} from "@heroicons/react/outline"; } from "@heroicons/react/outline";
import { inferType, JSONValueType } from "@jsonhero/json-infer-types"; import { inferType, JSONValueType } from "@jsonhero/json-infer-types";
import { JSONHeroPath, PathComponent } from "@jsonhero/path"; import { JSONHeroPath, PathComponent } from "@jsonhero/path";
import { ArrayIcon } from "~/components/Icons/ArrayIcon";
import { ObjectIcon } from "~/components/Icons/ObjectIcon";
import { StringIcon } from "~/components/Icons/StringIcon"; import { StringIcon } from "~/components/Icons/StringIcon";
import { ColumnViewNode, IconComponent } from "~/useColumnView"; import { ColumnViewNode, IconComponent } from "~/useColumnView";
import { formatValue } from "./formatter"; import { formatValue } from "./formatter";
+85
View File
@@ -0,0 +1,85 @@
import { JSONValueType, inferType } from "@jsonhero/json-infer-types";
import { JSONHeroPath } from "@jsonhero/path";
import Fuse from "fuse.js";
import { formatValue } from "./formatter";
export interface JsonSearchEntry {
path: string;
rawValue?: string;
formattedValue?: string;
}
export function createSearchIndex(
json: unknown
): [Fuse.FuseIndex<JsonSearchEntry>, Array<JsonSearchEntry>] {
const entries = createSearchEntries(json);
const index = Fuse.createIndex<JsonSearchEntry>(
["path", "rawValue", "formattedValue"],
entries
);
return [index, entries];
}
export function createSearchEntries(json: unknown): Array<JsonSearchEntry> {
return createSearchEntryChildren(inferType(json), new JSONHeroPath("$"));
}
function createSearchEntryChildren(
info: JSONValueType,
path: JSONHeroPath
): Array<JsonSearchEntry> {
if (info.name === "array" && info.value) {
return info.value.flatMap((value, index) => {
const childPath = path.child(index.toString());
const childInfo = inferType(value);
const children = createSearchEntryChildren(childInfo, childPath);
return [
{
path: childPath.toString(),
rawValue: getRawValue(childInfo),
formattedValue: formatValue(childInfo, { leafNodesOnly: true }),
},
...children,
];
});
}
if (info.name === "object" && info.value) {
return Object.entries(info.value).flatMap(([key, value]) => {
const childPath = path.child(key);
const childInfo = inferType(value);
const children = createSearchEntryChildren(childInfo, childPath);
return [
{
path: childPath.toString(),
rawValue: getRawValue(childInfo),
formattedValue: formatValue(childInfo, { leafNodesOnly: true }),
},
...children,
];
});
}
return [];
}
export function getRawValue(type: JSONValueType): string | undefined {
switch (type.name) {
case "string":
return type.value;
case "int":
return type.value.toString();
case "float":
return type.value.toString();
case "bool":
return type.value ? "true" : "false";
case "null":
return "null";
}
}
+21 -7
View File
@@ -27,6 +27,7 @@
"color": "^4.2.1", "color": "^4.2.1",
"fathom-client": "^3.4.1", "fathom-client": "^3.4.1",
"framer-motion": "^6.2.4", "framer-motion": "^6.2.4",
"fuse.js": "^6.5.3",
"json-source-map": "^0.6.1", "json-source-map": "^0.6.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -58,7 +59,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"tailwindcss": "^3.0.22", "tailwindcss": "^3.0.22",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"typescript": "^4.1.2" "typescript": "^4.6.x"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@@ -5925,6 +5926,14 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"node_modules/fuse.js": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg==",
"engines": {
"node": ">=10"
}
},
"node_modules/gensync": { "node_modules/gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -12791,9 +12800,9 @@
} }
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "4.5.5", "version": "4.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"dev": true, "dev": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
@@ -18048,6 +18057,11 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
}, },
"fuse.js": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-6.5.3.tgz",
"integrity": "sha512-sA5etGE7yD/pOqivZRBvUBd/NaL2sjAu6QuSaFoe1H2BrJSkH/T/UXAJ8CdXdw7DvY3Hs8CXKYkDWX7RiP5KOg=="
},
"gensync": { "gensync": {
"version": "1.0.0-beta.2", "version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -23005,9 +23019,9 @@
} }
}, },
"typescript": { "typescript": {
"version": "4.5.5", "version": "4.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.6.3.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "integrity": "sha512-yNIatDa5iaofVozS/uQJEl3JRWLKKGJKh6Yaiv0GLGSuhpFJe7P3SbHZ8/yjAHRQwKRoA6YZqlfjXWmVzoVSMw==",
"dev": true "dev": true
}, },
"unbox-primitive": { "unbox-primitive": {
+7 -4
View File
@@ -5,14 +5,16 @@
"license": "apache-2.0", "license": "apache-2.0",
"scripts": { "scripts": {
"clean": "rimraf dist", "clean": "rimraf dist",
"build": "npm run build:css && remix build", "build": "npm run build:css && npm run build:search && remix build",
"build:css": "tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --minify", "build:css": "tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --minify",
"build:worker": "esbuild --define:process.env.NODE_ENV='\"production\"' --minify --bundle --sourcemap --outdir=dist ./worker", "build:worker": "esbuild --define:process.env.NODE_ENV='\"production\"' --minify --bundle --sourcemap --outdir=dist ./worker",
"build:worker:analyze": "esbuild --define:process.env.NODE_ENV='\"production\"' --minify --bundle --sourcemap --metafile=meta.json --outdir=dist ./worker", "build:worker:analyze": "esbuild --define:process.env.NODE_ENV='\"production\"' --minify --bundle --sourcemap --metafile=meta.json --outdir=dist ./worker",
"build:visualize": "npm run clean && npm run build && npm run build:worker:analyze && esbuild-visualizer --metadata ./meta.json --exclude *.png", "build:visualize": "npm run clean && npm run build && npm run build:worker:analyze && esbuild-visualizer --metadata ./meta.json --exclude *.png",
"build:search": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\"production\"'",
"dev:search": "esbuild ./app/entry.worker.ts --outfile=./public/entry.worker.js --bundle --format=esm --define:process.env.NODE_ENV='\"development\"' --watch",
"dev:worker": "esbuild --define:process.env.NODE_ENV='\"development\"' --bundle --sourcemap --outdir=dist ./worker", "dev:worker": "esbuild --define:process.env.NODE_ENV='\"development\"' --bundle --sourcemap --outdir=dist ./worker",
"start:worker": "miniflare --env .env --build-command \"npm run dev:worker\" --watch", "start:worker": "miniflare --env .env --build-command \"npm run dev:worker\" --watch",
"dev": "concurrently \"npm run dev:css\" \"remix watch\"", "dev": "concurrently \"npm run dev:css\" \"npm run dev:search\" \"remix watch\"",
"dev:css": "tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --watch", "dev:css": "tailwindcss -i ./styles/tailwind.css -o ./app/tailwind.css --watch",
"postinstall": "remix setup cloudflare-workers", "postinstall": "remix setup cloudflare-workers",
"start": "concurrently \"npm run dev\" \"npm run start:worker\"", "start": "concurrently \"npm run dev\" \"npm run start:worker\"",
@@ -44,6 +46,7 @@
"color": "^4.2.1", "color": "^4.2.1",
"fathom-client": "^3.4.1", "fathom-client": "^3.4.1",
"framer-motion": "^6.2.4", "framer-motion": "^6.2.4",
"fuse.js": "^6.5.3",
"json-source-map": "^0.6.1", "json-source-map": "^0.6.1",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"lodash-es": "^4.17.21", "lodash-es": "^4.17.21",
@@ -75,7 +78,7 @@
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"tailwindcss": "^3.0.22", "tailwindcss": "^3.0.22",
"ts-jest": "^27.1.3", "ts-jest": "^27.1.3",
"typescript": "^4.1.2" "typescript": "^4.6.x"
}, },
"engines": { "engines": {
"node": ">=14" "node": ">=14"
@@ -100,4 +103,4 @@
}, },
"sideEffects": false, "sideEffects": false,
"main": "dist/worker.js" "main": "dist/worker.js"
} }
+321
View File
@@ -0,0 +1,321 @@
import Fuse from "fuse.js";
import {
createSearchEntries,
createSearchIndex,
} from "../app/utilities/search";
const json = {
records: [
{
id: "1",
createdAt: "2020-01-01T00:00:00.000Z",
updatedAt: "2020-12-02T11:34:00.000Z",
name: "John Doe",
email: "john@doe.com",
website: "https://john.doe.com",
orders: [
{
id: "1",
productName: "Product 1",
quantity: 1,
price: 10.0,
currency: "USD",
},
{
id: "2",
productName: "Product 2",
quantity: 100,
price: 999.0,
currency: "USD",
},
],
},
{
id: "2",
createdAt: "2020-01-02T00:00:00.000Z",
updatedAt: "2020-12-03T07:55:32.000Z",
name: "Jane Doe",
email: "jane@icloud.com",
website: "https://jane.doe.co.uk",
orders: [
{
id: "3",
productName: "Product 3",
quantity: 1,
price: 10.0,
currency: "GBP",
},
{
id: "4",
productName: "Product 1",
quantity: 2,
price: 76.45,
currency: "GBP",
},
],
},
],
};
describe("createSearchIndex", () => {
it("creates a search index that can search keys, raw values, and formatted values", () => {
const [index, entries] = createSearchIndex(json);
const fuse = new Fuse(
entries,
{
includeScore: true,
includeMatches: true,
minMatchCharLength: 1,
isCaseSensitive: false,
threshold: 0.6,
distance: 200,
},
index
);
const searchResults = fuse.search("john doe");
expect(searchResults[0].item.path).toBe("$.records.0.name");
expect(searchResults[1].item.path).toBe("$.records.0.email");
expect(searchResults[2].item.path).toBe("$.records.0.website");
expect(searchResults[3].item.path).toBe("$.records.1.name");
const searchResultsPaths = fuse.search("currency");
expect(searchResultsPaths[0].item.path).toBe(
"$.records.0.orders.0.currency"
);
expect(searchResultsPaths[1].item.path).toBe(
"$.records.0.orders.1.currency"
);
expect(searchResultsPaths[2].item.path).toBe(
"$.records.1.orders.0.currency"
);
expect(searchResultsPaths[3].item.path).toBe(
"$.records.1.orders.1.currency"
);
const searchResultsFormattedValues = fuse.search("dec 2");
expect(searchResultsFormattedValues[0].item.path).toBe(
"$.records.0.updatedAt"
);
});
});
describe("createSearchEntries", () => {
it("creates searchable entries for the passed in json", () => {
expect(createSearchEntries(json)).toMatchInlineSnapshot(`
Array [
Object {
"formattedValue": undefined,
"path": "$.records",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.0",
"rawValue": undefined,
},
Object {
"formattedValue": "1",
"path": "$.records.0.id",
"rawValue": "1",
},
Object {
"formattedValue": "Jan 1, 2020, 12:00:00 AM GMT",
"path": "$.records.0.createdAt",
"rawValue": "2020-01-01T00:00:00.000Z",
},
Object {
"formattedValue": "Dec 2, 2020, 11:34:00 AM GMT",
"path": "$.records.0.updatedAt",
"rawValue": "2020-12-02T11:34:00.000Z",
},
Object {
"formattedValue": "John Doe",
"path": "$.records.0.name",
"rawValue": "John Doe",
},
Object {
"formattedValue": "john@doe.com",
"path": "$.records.0.email",
"rawValue": "john@doe.com",
},
Object {
"formattedValue": "https://john.doe.com",
"path": "$.records.0.website",
"rawValue": "https://john.doe.com",
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders.0",
"rawValue": undefined,
},
Object {
"formattedValue": "1",
"path": "$.records.0.orders.0.id",
"rawValue": "1",
},
Object {
"formattedValue": "Product 1",
"path": "$.records.0.orders.0.productName",
"rawValue": "Product 1",
},
Object {
"formattedValue": "1",
"path": "$.records.0.orders.0.quantity",
"rawValue": "1",
},
Object {
"formattedValue": "10",
"path": "$.records.0.orders.0.price",
"rawValue": "10",
},
Object {
"formattedValue": "USD",
"path": "$.records.0.orders.0.currency",
"rawValue": "USD",
},
Object {
"formattedValue": undefined,
"path": "$.records.0.orders.1",
"rawValue": undefined,
},
Object {
"formattedValue": "2",
"path": "$.records.0.orders.1.id",
"rawValue": "2",
},
Object {
"formattedValue": "Product 2",
"path": "$.records.0.orders.1.productName",
"rawValue": "Product 2",
},
Object {
"formattedValue": "100",
"path": "$.records.0.orders.1.quantity",
"rawValue": "100",
},
Object {
"formattedValue": "999",
"path": "$.records.0.orders.1.price",
"rawValue": "999",
},
Object {
"formattedValue": "USD",
"path": "$.records.0.orders.1.currency",
"rawValue": "USD",
},
Object {
"formattedValue": undefined,
"path": "$.records.1",
"rawValue": undefined,
},
Object {
"formattedValue": "2",
"path": "$.records.1.id",
"rawValue": "2",
},
Object {
"formattedValue": "Jan 2, 2020, 12:00:00 AM GMT",
"path": "$.records.1.createdAt",
"rawValue": "2020-01-02T00:00:00.000Z",
},
Object {
"formattedValue": "Dec 3, 2020, 7:55:32 AM GMT",
"path": "$.records.1.updatedAt",
"rawValue": "2020-12-03T07:55:32.000Z",
},
Object {
"formattedValue": "Jane Doe",
"path": "$.records.1.name",
"rawValue": "Jane Doe",
},
Object {
"formattedValue": "jane@icloud.com",
"path": "$.records.1.email",
"rawValue": "jane@icloud.com",
},
Object {
"formattedValue": "https://jane.doe.co.uk",
"path": "$.records.1.website",
"rawValue": "https://jane.doe.co.uk",
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders",
"rawValue": undefined,
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders.0",
"rawValue": undefined,
},
Object {
"formattedValue": "3",
"path": "$.records.1.orders.0.id",
"rawValue": "3",
},
Object {
"formattedValue": "Product 3",
"path": "$.records.1.orders.0.productName",
"rawValue": "Product 3",
},
Object {
"formattedValue": "1",
"path": "$.records.1.orders.0.quantity",
"rawValue": "1",
},
Object {
"formattedValue": "10",
"path": "$.records.1.orders.0.price",
"rawValue": "10",
},
Object {
"formattedValue": "GBP",
"path": "$.records.1.orders.0.currency",
"rawValue": "GBP",
},
Object {
"formattedValue": undefined,
"path": "$.records.1.orders.1",
"rawValue": undefined,
},
Object {
"formattedValue": "4",
"path": "$.records.1.orders.1.id",
"rawValue": "4",
},
Object {
"formattedValue": "Product 1",
"path": "$.records.1.orders.1.productName",
"rawValue": "Product 1",
},
Object {
"formattedValue": "2",
"path": "$.records.1.orders.1.quantity",
"rawValue": "2",
},
Object {
"formattedValue": "76.45",
"path": "$.records.1.orders.1.price",
"rawValue": "76.45",
},
Object {
"formattedValue": "GBP",
"path": "$.records.1.orders.1.currency",
"rawValue": "GBP",
},
]
`);
});
});