Skip to content

Commit 44f18eb

Browse files
authored
Merge pull request #52 from DigitalCommons/20-link-DatasetService-to-front-end
Link ResultsPanel to API
2 parents 5aa5546 + cbe5a77 commit 44f18eb

34 files changed

+1169
-283
lines changed

apps/back-end/src/routes.ts

+13-6
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,18 @@ export function MykomapRouter(
7272

7373
searchDataset: async ({
7474
params: { datasetId },
75-
query: { filter, text },
75+
query: { filter, text, returnProps, page, pageSize },
7676
}) => {
77-
const visibleIndexes = searchDataset(datasetId, filter, text);
78-
79-
return { status: 200, body: visibleIndexes };
77+
const body = searchDataset(
78+
datasetId,
79+
filter,
80+
text,
81+
returnProps,
82+
page,
83+
pageSize,
84+
);
85+
86+
return { status: 200, body };
8087
},
8188

8289
getDatasetItem: async ({ params: { datasetId, datasetItemIdOrIx } }) => {
@@ -90,8 +97,8 @@ export function MykomapRouter(
9097
});
9198
}
9299

93-
const itemId = Number(datasetItemIdOrIx.substring(1));
94-
const item = getDatasetItem(datasetId, itemId);
100+
const itemIx = Number(datasetItemIdOrIx.substring(1));
101+
const item = getDatasetItem(datasetId, itemIx);
95102

96103
return { status: 200, body: item };
97104
},

apps/back-end/src/services/Dataset.ts

+50-7
Original file line numberDiff line numberDiff line change
@@ -81,19 +81,19 @@ export class Dataset {
8181
}
8282
}
8383

84-
getItem = (itemId: number) => {
85-
if (!fs.existsSync(path.join(this.folderPath, "items", `${itemId}.json`))) {
84+
getItem = (itemIx: number) => {
85+
if (!fs.existsSync(path.join(this.folderPath, "items", `${itemIx}.json`))) {
8686
throw new TsRestResponseError(contract.getDatasetItem, {
8787
status: 404,
8888
body: {
89-
message: `can't retrieve data for dataset ${this.id} item ${itemId}`,
89+
message: `can't retrieve data for dataset ${this.id} item @${itemIx}`,
9090
},
9191
});
9292
}
9393

9494
return JSON.parse(
9595
fs.readFileSync(
96-
path.join(this.folderPath, "items", `${itemId}.json`),
96+
path.join(this.folderPath, "items", `${itemIx}.json`),
9797
"utf8",
9898
),
9999
);
@@ -104,7 +104,17 @@ export class Dataset {
104104
getLocations = (): fs.ReadStream =>
105105
fs.createReadStream(path.join(this.folderPath, "locations.json"), "utf8");
106106

107-
search = (filters?: string[], text?: string): number[] => {
107+
/**
108+
* Returns an array of item indexes that match the given criteria, or an array of objects if
109+
* returnProps is specified. Also supports pagination.
110+
*/
111+
search = (
112+
filters?: string[],
113+
text?: string,
114+
returnProps?: string[],
115+
page?: number,
116+
pageSize?: number,
117+
): (string | { [prop: string]: unknown })[] => {
108118
const propMatchers: {
109119
propIndex: number;
110120
propMatcher: (value: string | string[]) => boolean;
@@ -174,7 +184,7 @@ export class Dataset {
174184
});
175185
}
176186

177-
return this.searchablePropValues
187+
const visibleIndexes = this.searchablePropValues
178188
.map((itemValues, itemIx) =>
179189
// item must match all given propMatchers
180190
propMatchers.every(({ propIndex, propMatcher }) =>
@@ -183,7 +193,40 @@ export class Dataset {
183193
? itemIx
184194
: "",
185195
)
186-
.filter((v) => typeof v === "number");
196+
.filter((v) => v !== "");
197+
198+
return visibleIndexes
199+
.slice(
200+
(page ?? 0) * (pageSize ?? 0),
201+
((page ?? visibleIndexes.length) + 1) *
202+
(pageSize ?? visibleIndexes.length),
203+
)
204+
.map((itemIx) => {
205+
if (returnProps) {
206+
const item = this.getItem(itemIx);
207+
// Return only the requested properties
208+
const strippedItem: { [prop: string]: unknown } = {};
209+
210+
for (const prop of returnProps) {
211+
if (item[prop] === undefined) {
212+
throw new TsRestResponseError(contract.searchDataset, {
213+
status: 400,
214+
body: {
215+
message: `Unknown propery name '${prop}'`,
216+
},
217+
});
218+
}
219+
strippedItem[prop] = item[prop];
220+
}
221+
222+
return {
223+
index: `@${itemIx}`,
224+
...strippedItem,
225+
};
226+
} else {
227+
return `@${itemIx}`;
228+
}
229+
});
187230
};
188231
}
189232

apps/back-end/src/services/datasetService.ts

+12-6
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,9 @@ const getDatasetOrThrow404 = (
4949
return dataset;
5050
};
5151

52-
export const getDatasetItem = (datasetId: string, datasetItemId: number) => {
52+
export const getDatasetItem = (datasetId: string, datasetItemIx: number) => {
5353
const dataset = getDatasetOrThrow404(contract.getDatasetItem, datasetId);
54-
return dataset.getItem(datasetItemId);
54+
return dataset.getItem(datasetItemIx);
5555
};
5656

5757
export const getDatasetConfig = (datasetId: string): GetConfigBody => {
@@ -68,10 +68,16 @@ export const searchDataset = (
6868
datasetId: string,
6969
filter?: string[],
7070
text?: string,
71+
returnProps?: string[],
72+
page?: number,
73+
pageSize?: number,
7174
): SearchDatasetBody => {
7275
const dataset = getDatasetOrThrow404(contract.searchDataset, datasetId);
73-
const visibleIndexes = dataset.search(filter, text);
74-
// Add '@' before index numbers.
75-
// TODO: Maybe skip this step and just return numbers?
76-
return visibleIndexes.map((index) => `@${index}`);
76+
return dataset.search(
77+
filter,
78+
text,
79+
returnProps,
80+
page,
81+
pageSize,
82+
) as SearchDatasetBody;
7783
};

apps/back-end/test/data/dataset-cli/expected/dummy/items/0.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"name": "Apple Co-op",
44
"manlat": 0,
55
"manlng": 0,
6-
"lat": 51.608437,
7-
"lng": -3.654778,
6+
"lat": 51.60844,
7+
"lng": -3.65478,
88
"address": "1 Apple Way, Appleton",
99
"activity": "AM130",
1010
"otherActivities": [],

apps/back-end/test/data/dataset-cli/expected/dummy/items/1.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"name": "Banana Co",
44
"manlat": 0,
55
"manlng": 0,
6-
"lat": 55.964698,
7-
"lng": -3.173305,
6+
"lat": 55.9647,
7+
"lng": -3.17331,
88
"address": "1 Banana Boulevard, Skinningdale",
99
"activity": "AM60",
1010
"otherActivities": [

apps/back-end/test/data/dataset-cli/expected/dummy/items/2.json

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@
33
"name": "The Cabbage Collective",
44
"manlat": null,
55
"manlng": null,
6-
"lat": 54.974469,
7-
"lng": -1.610894,
6+
"lat": 54.97447,
7+
"lng": -1.61089,
88
"address": "2 Cabbage Close, Caulfield",
99
"activity": "AM60",
1010
"otherActivities": [
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[[-3.654778,51.608437],[-3.173305,55.964698],[-1.610894,54.974469],null]
1+
[[-3.65478,51.60844],[-3.17331,55.9647],[-1.61089,54.97447],null]

apps/back-end/test/data/datasets/dataset-A/items/1.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "test/cuk/R000002",
2+
"id": "test/dc/R000002",
33
"name": "Pears United",
44
"description": "We sell pears",
55
"website": "https://pears.coop",
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"id": "test/cuk/R000003",
3+
"name": "Invisible Collab",
4+
"description": "Can't find us anywhere",
5+
"website": null,
6+
"dc_domains": [],
7+
"country_id": "GB",
8+
"primary_activity": "ICA220",
9+
"organisational_structure": "OS60",
10+
"typology": "BMT10",
11+
"latitude": null,
12+
"longitude": null,
13+
"geocontainer_lat": null,
14+
"geocontainer_lon": null,
15+
"geocoded_addr": null,
16+
"data_sources": ["CUK"]
17+
}
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
[
22
[-0.12783, 51.50748],
3-
[3.92473, 46.85045]
3+
[3.92473, 46.85045],
4+
null
45
]

apps/back-end/test/data/datasets/dataset-A/searchable.json

+7
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,13 @@
2020
"BMT20",
2121
["DC"],
2222
"pears united 79 rue de la mare aux carats 34090 montpellier france pears coop"
23+
],
24+
[
25+
"GB",
26+
"ICA220",
27+
"BMT10",
28+
["CUK"],
29+
"invisible collab"
2330
]
2431
]
2532
}

apps/back-end/test/plugin.test.ts

+69-4
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
import { expect, test, describe } from "vitest";
44
import Fastify from "fastify";
5-
import fastifyPlugin from "../src/pluginApi";
5+
import qs from "qs";
66
import * as path from "node:path";
7+
import fastifyPlugin from "../src/pluginApi";
78
import { MykomapRouterConfig } from "../src/routes";
89

910
const opts: MykomapRouterConfig = {
@@ -12,7 +13,7 @@ const opts: MykomapRouterConfig = {
1213
},
1314
};
1415

15-
const fastify = Fastify();
16+
const fastify = Fastify({ querystringParser: (str) => qs.parse(str) });
1617
fastify.register(fastifyPlugin, opts);
1718

1819
// Note: see src/api/contract.ts in the @mykomap/common module for definitions
@@ -30,6 +31,7 @@ describe("getDatasetLocations", () => {
3031
expect(res.json()).toStrictEqual([
3132
[-0.12783, 51.50748],
3233
[3.92473, 46.85045],
34+
null,
3335
]);
3436
});
3537
});
@@ -89,8 +91,6 @@ describe("searchDataset", () => {
8991
});
9092

9193
test.each([
92-
"filter=country_id:GB",
93-
"filter=country_id:GB&filter=typology:BMT20",
9494
"filter=country_id:GB&filter=typology:BMT20",
9595
"text=1+West+Street",
9696
"filter=country_id:GB&text=1+West+Street",
@@ -127,6 +127,71 @@ describe("searchDataset", () => {
127127
expect(res.json()).toStrictEqual(["@0", "@1"]);
128128
},
129129
);
130+
131+
test.each(["returnProps[]=name&page=0&pageSize=2"])(
132+
"Paginated search query '%s' returns the first 2 items",
133+
async (query) => {
134+
const res = await fastify.inject({
135+
method: "GET",
136+
url: `/dataset/dataset-A/search?${query}`,
137+
});
138+
expect(res.statusCode).toBe(200);
139+
expect(res.json()).toStrictEqual([
140+
{ index: "@0", name: "Apples Co-op" },
141+
{ index: "@1", name: "Pears United" },
142+
]);
143+
},
144+
);
145+
146+
test.each(["returnProps[]=name&page=1&pageSize=2"])(
147+
"Paginated search query '%s' returns the final item",
148+
async (query) => {
149+
const res = await fastify.inject({
150+
method: "GET",
151+
url: `/dataset/dataset-A/search?${query}`,
152+
});
153+
expect(res.statusCode).toBe(200);
154+
expect(res.json()).toStrictEqual([
155+
{ index: "@2", name: "Invisible Collab" },
156+
]);
157+
},
158+
);
159+
160+
test.each([
161+
"returnProps[]=name",
162+
"returnProps[]=name&pageSize=2",
163+
"returnProps[]=name&page=0",
164+
])(
165+
"Search query '%s' with unspecified pagination params returns all items",
166+
async (query) => {
167+
const res = await fastify.inject({
168+
method: "GET",
169+
url: `/dataset/dataset-A/search?${query}`,
170+
});
171+
expect(res.statusCode).toBe(200);
172+
expect(res.json()).toStrictEqual([
173+
{ index: "@0", name: "Apples Co-op" },
174+
{ index: "@1", name: "Pears United" },
175+
{ index: "@2", name: "Invisible Collab" },
176+
]);
177+
},
178+
);
179+
180+
test.each([
181+
"page=-1&pageSize=2",
182+
"page=1&pageSize=0",
183+
"page=0.5&pageSize=2",
184+
"page=1&pageSize=1.5",
185+
])(
186+
"Paginated search query '%s' with bad params returns status code 400",
187+
async (query) => {
188+
const res = await fastify.inject({
189+
method: "GET",
190+
url: `/dataset/dataset-A/search?${query}`,
191+
});
192+
expect(res.statusCode).toBe(400);
193+
},
194+
);
130195
});
131196

132197
describe("dataset does not exist", () => {

apps/front-end/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
"eslint-plugin-storybook": "^0.8.0",
7373
"fastify": "^4.28.1",
7474
"jsdom": "^23.2.0",
75+
"msw": "^2.6.5",
7576
"postcss": "^8.4.41",
7677
"postcss-import": "^16.1.0",
7778
"prettier": "^3.3.3",

apps/front-end/src/App.test.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { screen, fireEvent } from "@testing-library/react";
1+
import { screen, fireEvent, waitFor } from "@testing-library/react";
22
import App from "./App";
33
import { renderWithProviders } from "./utils/test-utils";
44

@@ -15,7 +15,9 @@ import { renderWithProviders } from "./utils/test-utils";
1515
test("App should have correct initial render on mobile", () => {
1616
renderWithProviders(<App />);
1717

18-
expect(screen.getByText("Search")).toBeInTheDocument();
18+
waitFor(() => {
19+
expect(screen.getByText("Search")).toBeInTheDocument();
20+
});
1921
});
2022

2123
// test("Increment value and Decrement value should work as expected", async () => {

apps/front-end/src/App.tsx

+5-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { useEffect } from "react";
22
import MapWrapper from "./components/map/MapWrapper";
33
import Panel from "./components/panel/Panel";
4-
import logo from "./logo.svg";
54
import { fetchConfig, setLanguage } from "./app/configSlice";
65
import { useAppDispatch } from "./app/hooks";
76
import { getUrlSearchParam } from "./utils/window-utils";
@@ -23,7 +22,11 @@ const App = () => {
2322
console.log("API version info", versionInfo);
2423
})
2524
.catch((error) => {
26-
console.error("Error fetching API version info", error);
25+
console.error(
26+
"Error fetching API version info",
27+
error.message,
28+
import.meta.env.VITE_API_URL,
29+
);
2730
});
2831
}, []);
2932

0 commit comments

Comments
 (0)