Skip to content

Commit

Permalink
Add API support for querying events using a time range
Browse files Browse the repository at this point in the history
  • Loading branch information
coocos committed Apr 14, 2021
1 parent 0cfa3aa commit d226e76
Show file tree
Hide file tree
Showing 5 changed files with 130 additions and 43 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,13 @@ There's essentially a single, simple endpoint which you can use to query the ser
]
```

You probably shouldn't call this endpoint if you've been running the service for a while, because it will return _all_ events it has persisted from the feed. This could be tens of thousands of events! However, can use filters to narrow down your search. For example, to narrow down to a specific a time range:
You probably shouldn't call this endpoint if you've been running the service for a while, because it will return _all_ events it has persisted from the feed. This could be tens of thousands of events! However, can use filters to narrow down your search. For example, to narrow down to a specific a time range using ISO-8601:

- `GET /api/v1/events/?start=2020-04-05T00:00:00&end=2020-04-06T00:00:00` will return all events which occurred during 5th of April 2020.
- `GET /api/v1/events/?start=2020-04-05T00:00:00.000Z&end=2020-04-06T00:00:00.000Z` will return all events which occurred during 5th of April 2020.

Or you could narrow down by event location and a start time:
Or you could narrow down by using both location and start time:

- `GET /api/v1/events/?start=2020-04-01T00:00:00&location=Helsinki` will return all events which have occurred in Helsinki since 1st of April 2020.
- `GET /api/v1/events/?start=2020-04-01T00:00:00.000Z&location=Helsinki` will return all events which have occurred in Helsinki since 1st of April 2020.

## WebSocket API

Expand Down
19 changes: 15 additions & 4 deletions src/controllers/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,18 @@ import logger from "../logger";
import { events } from "../services/events";

export function validateListFilters(): ValidationChain[] {
return [query("location").isString().optional()];
return [
query("location")
.isString()
.notEmpty()
.withMessage("location cannot be empty")
.optional(),
query(["start", "end"])
.isISO8601()
.optional()
.withMessage("date needs to be formatted in ISO-8601")
.toDate(),
];
}

export async function listEvents(
Expand All @@ -14,14 +25,14 @@ export async function listEvents(
try {
const errors = validationResult(req);
if (!errors.isEmpty()) {
res.json({
res.status(400).json({
error: errors.array(),
});
return;
}
const allEvents = await events.find(req.query);
const foundEvents = await events.find(req.query);
res.json(
allEvents.map(({ type, location, time }) => ({
foundEvents.map(({ type, location, time }) => ({
type,
location,
time: time.toISOString(),
Expand Down
77 changes: 47 additions & 30 deletions src/routes/events.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,65 @@ import { events } from "../services/events";
jest.mock("../services/events");

describe("/events", () => {
const mockEvents = events as jest.Mocked<typeof events>;
mockEvents.find.mockResolvedValue([
{
location: "Kokkola",
type: "tulipalo muu: pieni",
time: new Date("2021-04-14T15:21:40.000Z"),
hash: "19dbf51923807992bc11e3a7826523487cee4773",
},
{
location: "Imatra",
type: "tulipalo muu: keskisuuri",
time: new Date("2021-04-14T15:27:01.000Z"),
hash: "35179fd03e62ba2f063ff6e45d8d6508b868e3c8",
},
]);

it("returns all events", async () => {
const mockEvents = events as jest.Mocked<typeof events>;
mockEvents.find.mockResolvedValue([
{
type: "rakennuspalo: keskisuuri",
location: "Tuusula",
time: new Date("2021-01-31T22:00:00.000Z"),
hash: "2a39407ee0570aae8f3ba2842e11aa28ce0f5d9f",
},
]);
const response = await request(app).get("/api/v1/events");
expect(response.status).toBe(200);
expect(response.body).toEqual([
{
type: "rakennuspalo: keskisuuri",
location: "Tuusula",
time: "2021-01-31T22:00:00.000Z",
location: "Kokkola",
type: "tulipalo muu: pieni",
time: "2021-04-14T15:21:40.000Z",
},
{
location: "Imatra",
type: "tulipalo muu: keskisuuri",
time: "2021-04-14T15:27:01.000Z",
},
]);
expect(mockEvents.find).toBeCalledWith({});
});

it("returns events for given location", async () => {
const mockEvents = events as jest.Mocked<typeof events>;
mockEvents.find.mockResolvedValue([
{
type: "rakennuspalo: keskisuuri",
location: "Tuusula",
time: new Date("2021-01-31T22:00:00.000Z"),
hash: "2a39407ee0570aae8f3ba2842e11aa28ce0f5d9f",
},
]);
const response = await request(app).get("/api/v1/events?location=Tuusula");
const response = await request(app).get("/api/v1/events?location=Kokkola");
expect(response.status).toBe(200);
expect(mockEvents.find).toBeCalledWith({
location: "Kokkola",
});
});

it("returns events for given start time", async () => {
const response = await request(app).get(
"/api/v1/events?start=2021-04-14T15:27:00.000Z"
);
expect(response.status).toBe(200);
expect(mockEvents.find).toBeCalledWith({
start: new Date("2021-04-14T15:27:00.000Z"),
});
});

it("returns events for given end time", async () => {
const response = await request(app).get(
"/api/v1/events?end=2021-04-14T15:25:00.000Z"
);
expect(response.status).toBe(200);
expect(response.body).toEqual([
{
type: "rakennuspalo: keskisuuri",
location: "Tuusula",
time: "2021-01-31T22:00:00.000Z",
},
]);
expect(mockEvents.find).toBeCalledWith({
location: "Tuusula",
end: new Date("2021-04-14T15:25:00.000Z"),
});
});
});
61 changes: 56 additions & 5 deletions src/services/events.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ describe("Service", () => {
await db.destroy();
});

it("adds event", async () => {
it("stores event", async () => {
await events.add({
location: "Kemi",
type: "tulipalo muu: pieni",
Expand All @@ -31,6 +31,7 @@ describe("Service", () => {
},
]);
});

it("filters events based on location", async () => {
const newEvents = [
{
Expand All @@ -49,8 +50,55 @@ describe("Service", () => {
for (const event of newEvents) {
await events.add(event);
}
expect(await events.find({ location: "Helsinki" })).toEqual([newEvents[0]]);
expect(await events.find({ location: "Kemi" })).toEqual([newEvents[1]]);
const [helsinki, kemi] = newEvents;
expect(await events.find({ location: "Helsinki" })).toEqual([helsinki]);
expect(await events.find({ location: "Kemi" })).toEqual([kemi]);
});

it("filters events based on start time", async () => {
const newEvents = [
{
type: "rakennuspalo: pieni",
location: "Helsinki",
time: new Date("2021-01-01T15:40:52.000Z"),
hash: "169a28a0c1979d7c105490bde4e30ce5b64418a5",
},
{
location: "Kemi",
type: "tulipalo muu: pieni",
time: new Date("2021-01-02T22:00:00.000Z"),
hash: "dffab7da0d65580c2c09ee683dd8f18632fa27c1",
},
];
for (const event of newEvents) {
await events.add(event);
}
const [first, second] = newEvents;
expect(await events.find({ start: first.time })).toEqual([first, second]);
expect(await events.find({ start: second.time })).toEqual([second]);
});

it("filters events based on an end time", async () => {
const newEvents = [
{
type: "rakennuspalo: pieni",
location: "Helsinki",
time: new Date("2021-01-01T15:40:52.000Z"),
hash: "169a28a0c1979d7c105490bde4e30ce5b64418a5",
},
{
location: "Kemi",
type: "tulipalo muu: pieni",
time: new Date("2021-01-02T22:00:00.000Z"),
hash: "dffab7da0d65580c2c09ee683dd8f18632fa27c1",
},
];
for (const event of newEvents) {
await events.add(event);
}
const [first, second] = newEvents;
expect(await events.find({ end: first.time })).toEqual([first]);
expect(await events.find({ end: second.time })).toEqual([first, second]);
});

it("indicates if event exists", async () => {
Expand All @@ -60,14 +108,17 @@ describe("Service", () => {
time: new Date("2021-01-31T22:00:00.000Z"),
hash: "dffab7da0d65580c2c09ee683dd8f18632fa27c1",
});
let eventExists = await events.exists({
const eventExists = await events.exists({
location: "Kemi",
type: "tulipalo muu: pieni",
time: new Date("2021-01-31T22:00:00.000Z"),
hash: "dffab7da0d65580c2c09ee683dd8f18632fa27c1",
});
expect(eventExists).toBe(true);
eventExists = await events.exists({
});

it("indicates if event does not exist", async () => {
const eventExists = await events.exists({
type: "rakennuspalo: pieni",
location: "Helsinki",
time: new Date("2021-03-01T15:40:52.000Z"),
Expand Down
8 changes: 8 additions & 0 deletions src/services/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Event = {

type QueryFilters = {
location?: string;
start?: Date;
end?: Date;
};

export const events = {
Expand All @@ -44,6 +46,12 @@ export const events = {
"locations.name": filters.location,
});
}
if (filters.start !== undefined) {
events = events.where("time", ">=", filters.start);
}
if (filters.end !== undefined) {
events = events.where("time", "<=", filters.end);
}
return (await events) as RescueEvent[];
},
async exists(event: RescueEvent): Promise<boolean> {
Expand Down

0 comments on commit d226e76

Please sign in to comment.