From c491933f4972c65cb19990bdb7f6f08e5bfa32c4 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 17 Dec 2024 15:56:00 +0000 Subject: [PATCH 1/5] add metadata endpoint --- src/controllers/metadataController.ts | 9 +++++++++ src/routes.ts | 2 ++ src/server.ts | 12 ++++++++---- src/server/buildMetadata.ts | 15 +++++++++++++++ src/{ => server}/configReader.ts | 0 src/{ => server}/discover.ts | 6 +++--- src/types/app.ts | 13 +++++++++++++ tests/unit/configReader.spec.ts | 2 +- tests/unit/discover.spec.ts | 2 +- 9 files changed, 52 insertions(+), 9 deletions(-) create mode 100644 src/controllers/metadataController.ts create mode 100644 src/server/buildMetadata.ts rename src/{ => server}/configReader.ts (100%) rename src/{ => server}/discover.ts (92%) diff --git a/src/controllers/metadataController.ts b/src/controllers/metadataController.ts new file mode 100644 index 0000000..3c68d65 --- /dev/null +++ b/src/controllers/metadataController.ts @@ -0,0 +1,9 @@ +import {Request, Response} from "express"; +import {jsonResponseSuccess} from "../jsonResponse"; + +export class MetadataController { + static getMetadata = (req: Request, res: Response) => { + const metadata = req.app.locals.metadata; + jsonResponseSuccess(metadata, res); + } +} \ No newline at end of file diff --git a/src/routes.ts b/src/routes.ts index b23e6cb..22f2b49 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,10 +2,12 @@ import { Router } from "express"; import { IndexController } from "./controllers/indexController"; import { TileController } from "./controllers/tileController"; import notFound from "./errors/notFound"; +import {MetadataController} from "./controllers/metadataController"; export const registerRoutes = () => { const router = Router(); router.get("/", IndexController.getIndex); + router.get("/metadata", MetadataController.getMetadata); router.get("/tile/:dataset/:level/:z/:x/:y", TileController.getTile); // provide an endpoint we can use to test 500 response behaviour by throwing an "unexpected error" - but only if we diff --git a/src/server.ts b/src/server.ts index f2892bf..ad5346a 100644 --- a/src/server.ts +++ b/src/server.ts @@ -2,12 +2,13 @@ import compression from "compression"; import cors from "cors"; import express from "express"; import * as path from "node:path"; -import { ConfigReader } from "./configReader"; -import { GroutConfig } from "./types/app"; +import { ConfigReader } from "./server/configReader"; +import {GroutConfig, GroutMetadata} from "./types/app"; import { registerRoutes } from "./routes"; import { initialiseLogging } from "./logging"; -import { discoverTileDatasets } from "./discover"; +import { discoverTileDatasets } from "./server/discover"; import { handleError } from "./errors/handleError"; +import { buildMetadata }from "./server/buildMetadata"; // Wrap the main server set-up functionality in a non-top-level method so we can use async - we can revert this in // https://mrc-ide.myjetbrains.com/youtrack/issue/mrc-6134/Add-Vite-build-and-related-tidy-up @@ -27,8 +28,11 @@ const main = async () => { path.resolve(path.join(rootDir, "data")) ); + const metadata = buildMetadata(tileDatasets); + Object.assign(app.locals, { - tileDatasets + tileDatasets, + metadata }); Object.freeze(app.locals); // We don't expect anything else to modify app.locals diff --git a/src/server/buildMetadata.ts b/src/server/buildMetadata.ts new file mode 100644 index 0000000..350c97d --- /dev/null +++ b/src/server/buildMetadata.ts @@ -0,0 +1,15 @@ +import {GroutMetadata, TileDataset} from "../types/app"; +import {Dict} from "../types/utils"; + +// Build metadata response on start-up as it will not change while the app is running +export const buildMetadata = (tileDatasets: Dict): GroutMetadata => { + const tileDatasetMetadata = {}; + for (const datasetName of Object.keys(tileDatasets)) { + tileDatasetMetadata[datasetName] = { levels: Object.keys(tileDatasets[datasetName]) } + } + return { + datasets: { + tile: tileDatasetMetadata + } + }; +} \ No newline at end of file diff --git a/src/configReader.ts b/src/server/configReader.ts similarity index 100% rename from src/configReader.ts rename to src/server/configReader.ts diff --git a/src/discover.ts b/src/server/discover.ts similarity index 92% rename from src/discover.ts rename to src/server/discover.ts index 6555211..82146b3 100644 --- a/src/discover.ts +++ b/src/server/discover.ts @@ -1,8 +1,8 @@ import * as fs from "fs"; import * as path from "node:path"; -import { TileDatabase } from "./db/tileDatabase"; -import { TileDataset } from "./types/app"; -import { Dict } from "./types/utils"; +import { TileDatabase } from "../db/tileDatabase"; +import { TileDataset } from "../types/app"; +import { Dict } from "../types/utils"; export const discoverTileDatasets = async ( root: string diff --git a/src/types/app.ts b/src/types/app.ts index 9c87139..a8df7cb 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -8,6 +8,19 @@ export interface GroutConfig { // We represent this as a dict, where the keys are the level names export type TileDataset = Dict; +export type GroutDatasetMetadata = { + levels: string[] +} + +// We only support tile datasets at the moment +export type datasetTypes = "tile"; + +// Data type of metadata response - currently provides only the dataset names and levels for tile data, but will +// eventually include other types of metadata +export interface GroutMetadata { + datasets: Record> +} + export interface AppLocals { tileDatasets: Dict; } diff --git a/tests/unit/configReader.spec.ts b/tests/unit/configReader.spec.ts index 843dd13..281d882 100644 --- a/tests/unit/configReader.spec.ts +++ b/tests/unit/configReader.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, vi } from "vitest"; import { fs, vol } from "memfs"; -import { ConfigReader } from "../../src/configReader"; +import { ConfigReader } from "../../src/server/configReader"; // tell vitest to use fs mock from __mocks__ folder vi.mock("fs"); diff --git a/tests/unit/discover.spec.ts b/tests/unit/discover.spec.ts index 51cf400..45b946e 100644 --- a/tests/unit/discover.spec.ts +++ b/tests/unit/discover.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi, beforeEach } from "vitest"; import { fs, vol } from "memfs"; -import { discoverTileDatasets } from "../../src/discover"; +import { discoverTileDatasets } from "../../src/server/discover"; // tell vitest to use fs mock from __mocks__ folder vi.mock("fs"); From e49b8cd725f90b5121de2921c64336df28282ad3 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 17 Dec 2024 16:33:42 +0000 Subject: [PATCH 2/5] tests --- tests/integration/index.spec.ts | 9 ++--- tests/integration/integrationTest.ts | 10 ++++++ tests/integration/metadata.spec.ts | 17 +++++++++ .../controllers/metadataController.spec.ts | 25 +++++++++++++ tests/unit/routes.spec.ts | 6 ++++ tests/unit/server/buildMetadata.spec.ts | 35 +++++++++++++++++++ tests/unit/{ => server}/configReader.spec.ts | 2 +- tests/unit/{ => server}/discover.spec.ts | 4 +-- 8 files changed, 98 insertions(+), 10 deletions(-) create mode 100644 tests/integration/metadata.spec.ts create mode 100644 tests/unit/controllers/metadataController.spec.ts create mode 100644 tests/unit/server/buildMetadata.spec.ts rename tests/unit/{ => server}/configReader.spec.ts (94%) rename tests/unit/{ => server}/discover.spec.ts (96%) diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index 3babcfa..2dfe829 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -1,14 +1,9 @@ import { describe, expect, test } from "vitest"; -import { grout } from "./integrationTest"; +import { getData} from "./integrationTest"; describe("index endpoint", () => { test("returns package version", async () => { - const response = await grout.get("/"); - expect(response.status).toBe(200); - expect(response.body.status).toBe("success"); - expect(response.body.errors).toBe(null); - const data = response.body.data; - + const data = await getData("/"); const expectedVersion = process.env.npm_package_version; expect(data.version).toBe(expectedVersion); }); diff --git a/tests/integration/integrationTest.ts b/tests/integration/integrationTest.ts index 6e491d9..d91380d 100644 --- a/tests/integration/integrationTest.ts +++ b/tests/integration/integrationTest.ts @@ -1,3 +1,13 @@ import request from "supertest"; +import {expect} from "vitest"; export const grout = request("http://localhost:5000"); + +// Get data from server, expect successful result and return body.data +export const getData = async (url: string) => { + const response = await grout.get(url); + expect(response.status).toBe(200); + expect(response.body.status).toBe("success"); + expect(response.body.errors).toBe(null); + return response.body.data; +} diff --git a/tests/integration/metadata.spec.ts b/tests/integration/metadata.spec.ts new file mode 100644 index 0000000..3d0ce3f --- /dev/null +++ b/tests/integration/metadata.spec.ts @@ -0,0 +1,17 @@ +import { describe, expect, test } from "vitest"; +import { getData} from "./integrationTest"; + +describe("metadata endpoint", () => { + test("returns expected dataset metadata", async () => { + const data = await getData("/metadata"); + expect(data).toStrictEqual({ + datasets: { + tile: { + gadm41: { + levels: ["admin0", "admin1"] + } + } + } + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/controllers/metadataController.spec.ts b/tests/unit/controllers/metadataController.spec.ts new file mode 100644 index 0000000..523583a --- /dev/null +++ b/tests/unit/controllers/metadataController.spec.ts @@ -0,0 +1,25 @@ +import { describe, expect, test, vi } from "vitest"; +import {MetadataController} from "../../../src/controllers/metadataController"; + +const mockJsonResponseSuccess = vi.hoisted(() => vi.fn()); +vi.mock("../../../src/jsonResponse", () => ({ + jsonResponseSuccess: mockJsonResponseSuccess +})); + +describe("MetadataController", () => { + test("returns metadata from app locals", () => { + const mockMetadata = { + "datasets": {} + }; + const mockReq = { + app: { + locals: { + metadata: mockMetadata + } + } + } as any; + const mockRes = {} as any; + MetadataController.getMetadata(mockReq, mockRes); + expect(mockJsonResponseSuccess).toHaveBeenCalledWith(mockMetadata, mockRes); + }); +}); \ No newline at end of file diff --git a/tests/unit/routes.spec.ts b/tests/unit/routes.spec.ts index cade70a..43dc985 100644 --- a/tests/unit/routes.spec.ts +++ b/tests/unit/routes.spec.ts @@ -3,6 +3,7 @@ import { registerRoutes } from "../../src/routes"; import { IndexController } from "../../src/controllers/indexController"; import { TileController } from "../../src/controllers/tileController"; import notFound from "../../src/errors/notFound"; +import {MetadataController} from "../../src/controllers/metadataController"; const { mockRouterConstructor, mockRouter } = vi.hoisted(() => { const mockRouter = { @@ -29,6 +30,11 @@ describe("registerRoutes", () => { ); expect(mockRouter.get).toHaveBeenNthCalledWith( 2, + "/metadata", + MetadataController.getMetadata + ); + expect(mockRouter.get).toHaveBeenNthCalledWith( + 3, "/tile/:dataset/:level/:z/:x/:y", TileController.getTile ); diff --git a/tests/unit/server/buildMetadata.spec.ts b/tests/unit/server/buildMetadata.spec.ts new file mode 100644 index 0000000..7016afd --- /dev/null +++ b/tests/unit/server/buildMetadata.spec.ts @@ -0,0 +1,35 @@ +import { describe, expect, test, vi, beforeEach } from "vitest"; +import {buildMetadata} from "../../../src/server/buildMetadata"; + +describe("buildMetadata", () => { + test("builds expected metadata from datasets", () => { + const mockTileDatasets = { + ds1: { + level0: { + db: {} + }, + level1: { + db: {} + } + }, + ds2: { + level2: { + db: {} + } + } + } as any; + const result = buildMetadata(mockTileDatasets); + expect(result).toStrictEqual({ + datasets: { + tile: { + ds1: { + levels: ["level0", "level1"] + }, + ds2: { + levels: ["level2"] + } + } + } + }); + }); +}); \ No newline at end of file diff --git a/tests/unit/configReader.spec.ts b/tests/unit/server/configReader.spec.ts similarity index 94% rename from tests/unit/configReader.spec.ts rename to tests/unit/server/configReader.spec.ts index 281d882..7316827 100644 --- a/tests/unit/configReader.spec.ts +++ b/tests/unit/server/configReader.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test, beforeEach, vi } from "vitest"; import { fs, vol } from "memfs"; -import { ConfigReader } from "../../src/server/configReader"; +import { ConfigReader } from "../../../src/server/configReader"; // tell vitest to use fs mock from __mocks__ folder vi.mock("fs"); diff --git a/tests/unit/discover.spec.ts b/tests/unit/server/discover.spec.ts similarity index 96% rename from tests/unit/discover.spec.ts rename to tests/unit/server/discover.spec.ts index 45b946e..3567e13 100644 --- a/tests/unit/discover.spec.ts +++ b/tests/unit/server/discover.spec.ts @@ -1,6 +1,6 @@ import { describe, expect, test, vi, beforeEach } from "vitest"; import { fs, vol } from "memfs"; -import { discoverTileDatasets } from "../../src/server/discover"; +import { discoverTileDatasets } from "../../../src/server/discover"; // tell vitest to use fs mock from __mocks__ folder vi.mock("fs"); @@ -17,7 +17,7 @@ const mockDatabaseConstructor = vi.hoisted(() => { .mockImplementation((path: string) => ({ path, open: vi.fn() })); }); -vi.mock("../../src/db/tileDatabase", () => ({ +vi.mock("../../../src/db/tileDatabase", () => ({ TileDatabase: mockDatabaseConstructor })); From 3176c7bc6eff877651737f0f0650640c8f765e97 Mon Sep 17 00:00:00 2001 From: Emma Date: Tue, 17 Dec 2024 16:57:22 +0000 Subject: [PATCH 3/5] lint --- src/controllers/metadataController.ts | 8 ++++---- src/routes.ts | 2 +- src/server.ts | 4 ++-- src/server/buildMetadata.ts | 14 +++++++++----- src/types/app.ts | 6 +++--- tests/integration/index.spec.ts | 2 +- tests/integration/integrationTest.ts | 4 ++-- tests/integration/metadata.spec.ts | 4 ++-- tests/unit/controllers/metadataController.spec.ts | 11 +++++++---- tests/unit/routes.spec.ts | 2 +- tests/unit/server/buildMetadata.spec.ts | 4 ++-- 11 files changed, 34 insertions(+), 27 deletions(-) diff --git a/src/controllers/metadataController.ts b/src/controllers/metadataController.ts index 3c68d65..13af3e4 100644 --- a/src/controllers/metadataController.ts +++ b/src/controllers/metadataController.ts @@ -1,9 +1,9 @@ -import {Request, Response} from "express"; -import {jsonResponseSuccess} from "../jsonResponse"; +import { Request, Response } from "express"; +import { jsonResponseSuccess } from "../jsonResponse"; export class MetadataController { static getMetadata = (req: Request, res: Response) => { const metadata = req.app.locals.metadata; jsonResponseSuccess(metadata, res); - } -} \ No newline at end of file + }; +} diff --git a/src/routes.ts b/src/routes.ts index 22f2b49..875166a 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -2,7 +2,7 @@ import { Router } from "express"; import { IndexController } from "./controllers/indexController"; import { TileController } from "./controllers/tileController"; import notFound from "./errors/notFound"; -import {MetadataController} from "./controllers/metadataController"; +import { MetadataController } from "./controllers/metadataController"; export const registerRoutes = () => { const router = Router(); diff --git a/src/server.ts b/src/server.ts index ad5346a..477a984 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,12 +3,12 @@ import cors from "cors"; import express from "express"; import * as path from "node:path"; import { ConfigReader } from "./server/configReader"; -import {GroutConfig, GroutMetadata} from "./types/app"; +import { GroutConfig } from "./types/app"; import { registerRoutes } from "./routes"; import { initialiseLogging } from "./logging"; import { discoverTileDatasets } from "./server/discover"; import { handleError } from "./errors/handleError"; -import { buildMetadata }from "./server/buildMetadata"; +import { buildMetadata } from "./server/buildMetadata"; // Wrap the main server set-up functionality in a non-top-level method so we can use async - we can revert this in // https://mrc-ide.myjetbrains.com/youtrack/issue/mrc-6134/Add-Vite-build-and-related-tidy-up diff --git a/src/server/buildMetadata.ts b/src/server/buildMetadata.ts index 350c97d..6a7aa0d 100644 --- a/src/server/buildMetadata.ts +++ b/src/server/buildMetadata.ts @@ -1,15 +1,19 @@ -import {GroutMetadata, TileDataset} from "../types/app"; -import {Dict} from "../types/utils"; +import { GroutMetadata, TileDataset } from "../types/app"; +import { Dict } from "../types/utils"; // Build metadata response on start-up as it will not change while the app is running -export const buildMetadata = (tileDatasets: Dict): GroutMetadata => { +export const buildMetadata = ( + tileDatasets: Dict +): GroutMetadata => { const tileDatasetMetadata = {}; for (const datasetName of Object.keys(tileDatasets)) { - tileDatasetMetadata[datasetName] = { levels: Object.keys(tileDatasets[datasetName]) } + tileDatasetMetadata[datasetName] = { + levels: Object.keys(tileDatasets[datasetName]) + }; } return { datasets: { tile: tileDatasetMetadata } }; -} \ No newline at end of file +}; diff --git a/src/types/app.ts b/src/types/app.ts index a8df7cb..4ffc869 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -9,8 +9,8 @@ export interface GroutConfig { export type TileDataset = Dict; export type GroutDatasetMetadata = { - levels: string[] -} + levels: string[]; +}; // We only support tile datasets at the moment export type datasetTypes = "tile"; @@ -18,7 +18,7 @@ export type datasetTypes = "tile"; // Data type of metadata response - currently provides only the dataset names and levels for tile data, but will // eventually include other types of metadata export interface GroutMetadata { - datasets: Record> + datasets: Record>; } export interface AppLocals { diff --git a/tests/integration/index.spec.ts b/tests/integration/index.spec.ts index 2dfe829..10329f0 100644 --- a/tests/integration/index.spec.ts +++ b/tests/integration/index.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getData} from "./integrationTest"; +import { getData } from "./integrationTest"; describe("index endpoint", () => { test("returns package version", async () => { diff --git a/tests/integration/integrationTest.ts b/tests/integration/integrationTest.ts index d91380d..8bfe0eb 100644 --- a/tests/integration/integrationTest.ts +++ b/tests/integration/integrationTest.ts @@ -1,5 +1,5 @@ import request from "supertest"; -import {expect} from "vitest"; +import { expect } from "vitest"; export const grout = request("http://localhost:5000"); @@ -10,4 +10,4 @@ export const getData = async (url: string) => { expect(response.body.status).toBe("success"); expect(response.body.errors).toBe(null); return response.body.data; -} +}; diff --git a/tests/integration/metadata.spec.ts b/tests/integration/metadata.spec.ts index 3d0ce3f..4022d36 100644 --- a/tests/integration/metadata.spec.ts +++ b/tests/integration/metadata.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vitest"; -import { getData} from "./integrationTest"; +import { getData } from "./integrationTest"; describe("metadata endpoint", () => { test("returns expected dataset metadata", async () => { @@ -14,4 +14,4 @@ describe("metadata endpoint", () => { } }); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/controllers/metadataController.spec.ts b/tests/unit/controllers/metadataController.spec.ts index 523583a..d6c9f55 100644 --- a/tests/unit/controllers/metadataController.spec.ts +++ b/tests/unit/controllers/metadataController.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi } from "vitest"; -import {MetadataController} from "../../../src/controllers/metadataController"; +import { MetadataController } from "../../../src/controllers/metadataController"; const mockJsonResponseSuccess = vi.hoisted(() => vi.fn()); vi.mock("../../../src/jsonResponse", () => ({ @@ -9,7 +9,7 @@ vi.mock("../../../src/jsonResponse", () => ({ describe("MetadataController", () => { test("returns metadata from app locals", () => { const mockMetadata = { - "datasets": {} + datasets: {} }; const mockReq = { app: { @@ -20,6 +20,9 @@ describe("MetadataController", () => { } as any; const mockRes = {} as any; MetadataController.getMetadata(mockReq, mockRes); - expect(mockJsonResponseSuccess).toHaveBeenCalledWith(mockMetadata, mockRes); + expect(mockJsonResponseSuccess).toHaveBeenCalledWith( + mockMetadata, + mockRes + ); }); -}); \ No newline at end of file +}); diff --git a/tests/unit/routes.spec.ts b/tests/unit/routes.spec.ts index 43dc985..8ac4733 100644 --- a/tests/unit/routes.spec.ts +++ b/tests/unit/routes.spec.ts @@ -3,7 +3,7 @@ import { registerRoutes } from "../../src/routes"; import { IndexController } from "../../src/controllers/indexController"; import { TileController } from "../../src/controllers/tileController"; import notFound from "../../src/errors/notFound"; -import {MetadataController} from "../../src/controllers/metadataController"; +import { MetadataController } from "../../src/controllers/metadataController"; const { mockRouterConstructor, mockRouter } = vi.hoisted(() => { const mockRouter = { diff --git a/tests/unit/server/buildMetadata.spec.ts b/tests/unit/server/buildMetadata.spec.ts index 7016afd..2c74891 100644 --- a/tests/unit/server/buildMetadata.spec.ts +++ b/tests/unit/server/buildMetadata.spec.ts @@ -1,5 +1,5 @@ import { describe, expect, test, vi, beforeEach } from "vitest"; -import {buildMetadata} from "../../../src/server/buildMetadata"; +import { buildMetadata } from "../../../src/server/buildMetadata"; describe("buildMetadata", () => { test("builds expected metadata from datasets", () => { @@ -32,4 +32,4 @@ describe("buildMetadata", () => { } }); }); -}); \ No newline at end of file +}); From 07dbee9a0dc72d6d7883ddf766a91685bf03b715 Mon Sep 17 00:00:00 2001 From: Emma Russell <44669576+EmmaLRussell@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:31:20 +0000 Subject: [PATCH 4/5] Update src/types/app.ts Co-authored-by: Anmol Thapar --- src/types/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/app.ts b/src/types/app.ts index 4ffc869..476e64b 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -13,7 +13,7 @@ export type GroutDatasetMetadata = { }; // We only support tile datasets at the moment -export type datasetTypes = "tile"; +export type DatasetTypes = "tile"; // Data type of metadata response - currently provides only the dataset names and levels for tile data, but will // eventually include other types of metadata From be540b40a181dab36bc11116f6afd6d2dd6fb3a8 Mon Sep 17 00:00:00 2001 From: Emma Date: Fri, 20 Dec 2024 16:41:25 +0000 Subject: [PATCH 5/5] update type reference --- src/types/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/app.ts b/src/types/app.ts index 476e64b..d5bff95 100644 --- a/src/types/app.ts +++ b/src/types/app.ts @@ -18,7 +18,7 @@ export type DatasetTypes = "tile"; // Data type of metadata response - currently provides only the dataset names and levels for tile data, but will // eventually include other types of metadata export interface GroutMetadata { - datasets: Record>; + datasets: Record>; } export interface AppLocals {