Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

mrc-6097 Metadata endpoint #4

Merged
merged 6 commits into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions src/controllers/metadataController.ts
Original file line number Diff line number Diff line change
@@ -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);
};
}
2 changes: 2 additions & 0 deletions src/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 7 additions & 3 deletions src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 { ConfigReader } from "./server/configReader";
import { GroutConfig } 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
Expand All @@ -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

Expand Down
19 changes: 19 additions & 0 deletions src/server/buildMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
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<TileDataset>
): GroutMetadata => {
const tileDatasetMetadata = {};
for (const datasetName of Object.keys(tileDatasets)) {
tileDatasetMetadata[datasetName] = {
levels: Object.keys(tileDatasets[datasetName])
};
}
return {
datasets: {
tile: tileDatasetMetadata
}
};
};
File renamed without changes.
6 changes: 3 additions & 3 deletions src/discover.ts → src/server/discover.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down
13 changes: 13 additions & 0 deletions src/types/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ export interface GroutConfig {
// We represent this as a dict, where the keys are the level names
export type TileDataset = Dict<TileDatabase>;

export type GroutDatasetMetadata = {
levels: string[];
};

// We only support tile datasets at the moment
export type datasetTypes = "tile";
EmmaLRussell marked this conversation as resolved.
Show resolved Hide resolved

// 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<datasetTypes, Dict<GroutDatasetMetadata>>;
}

export interface AppLocals {
tileDatasets: Dict<TileDataset>;
}
9 changes: 2 additions & 7 deletions tests/integration/index.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
Expand Down
10 changes: 10 additions & 0 deletions tests/integration/integrationTest.ts
Original file line number Diff line number Diff line change
@@ -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;
};
17 changes: 17 additions & 0 deletions tests/integration/metadata.spec.ts
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
});
});
});
28 changes: 28 additions & 0 deletions tests/unit/controllers/metadataController.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
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
);
});
});
6 changes: 6 additions & 0 deletions tests/unit/routes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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
);
Expand Down
35 changes: 35 additions & 0 deletions tests/unit/server/buildMetadata.spec.ts
Original file line number Diff line number Diff line change
@@ -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"]
}
}
}
});
});
});
Original file line number Diff line number Diff line change
@@ -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");
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
Expand All @@ -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
}));

Expand Down
Loading