Skip to content

Commit

Permalink
impr: use tsrest for dev endpoints (@fehmer) (#5800)
Browse files Browse the repository at this point in the history
!nuf
  • Loading branch information
fehmer authored Aug 23, 2024
1 parent 8863fb7 commit 30d440a
Show file tree
Hide file tree
Showing 14 changed files with 176 additions and 93 deletions.
59 changes: 59 additions & 0 deletions backend/__tests__/api/controllers/dev.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import request from "supertest";
import app from "../../../src/app";

import { ObjectId } from "mongodb";
import * as Misc from "../../../src/utils/misc";

const uid = new ObjectId().toHexString();
const mockApp = request(app);

describe("DevController", () => {
describe("generate testData", () => {
const isDevEnvironmentMock = vi.spyOn(Misc, "isDevEnvironment");

beforeEach(() => {
isDevEnvironmentMock.mockReset();
isDevEnvironmentMock.mockReturnValue(true);
});

it("should fail on prod", async () => {
//GIVEN
isDevEnvironmentMock.mockReturnValue(false);
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({ username: "test" })
.expect(503);
//THEN
expect(body.message).toEqual(
"Development endpoints are only available in DEV mode."
);
});
it("should fail without mandatory properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({})
.expect(422);

//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: [`"username" Required`],
});
});
it("should fail with unknown properties", async () => {
//WHEN
const { body } = await mockApp
.post("/dev/generateData")
.send({ username: "Bob", extra: "value" })
.expect(422);

//THEN
expect(body).toEqual({
message: "Invalid request data schema",
validationErrors: ["Unrecognized key(s) in object: 'extra'"],
});
});
});
});
7 changes: 7 additions & 0 deletions backend/scripts/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,13 @@ export function getOpenApi(): OpenAPIObject {
"x-displayName": "Server configuration",
"x-public": "yes",
},
{
name: "dev",
description:
"Development related endpoints. Only available on dev environment",
"x-displayName": "Development",
"x-public": "no",
},
],
},

Expand Down
39 changes: 17 additions & 22 deletions backend/src/api/controllers/dev.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MonkeyResponse } from "../../utils/monkey-response";
import { MonkeyResponse2 } from "../../utils/monkey-response";
import * as UserDal from "../../dal/user";
import FirebaseAdmin from "../../init/firebase-admin";
import Logger from "../../utils/logger";
Expand All @@ -9,30 +9,27 @@ import { roundTo2 } from "../../utils/misc";
import { ObjectId } from "mongodb";
import * as LeaderboardDal from "../../dal/leaderboards";
import MonkeyError from "../../utils/error";
import isNumber from "lodash/isNumber";

import {
Mode,
PersonalBest,
PersonalBests,
} from "@monkeytype/contracts/schemas/shared";
import {
GenerateDataRequest,
GenerateDataResponse,
} from "@monkeytype/contracts/dev";

type GenerateDataOptions = {
firstTestTimestamp: Date;
lastTestTimestamp: Date;
minTestsPerDay: number;
maxTestsPerDay: number;
};

const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = {
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())),
lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())),
const CREATE_RESULT_DEFAULT_OPTIONS = {
firstTestTimestamp: DateUtils.startOfDay(new UTCDate(Date.now())).valueOf(),
lastTestTimestamp: DateUtils.endOfDay(new UTCDate(Date.now())).valueOf(),
minTestsPerDay: 0,
maxTestsPerDay: 50,
};

export async function createTestData(
req: MonkeyTypes.Request
): Promise<MonkeyResponse> {
req: MonkeyTypes.Request2<undefined, GenerateDataRequest>
): Promise<GenerateDataResponse> {
const { username, createUser } = req.body;
const user = await getOrCreateUser(username, "password", createUser);

Expand All @@ -42,7 +39,7 @@ export async function createTestData(
await updateUser(uid);
await updateLeaderboard();

return new MonkeyResponse("test data created", { uid, email }, 200);
return new MonkeyResponse2("test data created", { uid, email });
}

async function getOrCreateUser(
Expand Down Expand Up @@ -73,20 +70,18 @@ async function getOrCreateUser(

async function createTestResults(
user: MonkeyTypes.DBUser,
configOptions: Partial<GenerateDataOptions>
configOptions: GenerateDataRequest
): Promise<void> {
const config = {
...CREATE_RESULT_DEFAULT_OPTIONS,
...configOptions,
};
if (isNumber(config.firstTestTimestamp))
config.firstTestTimestamp = toDate(config.firstTestTimestamp);
if (isNumber(config.lastTestTimestamp))
config.lastTestTimestamp = toDate(config.lastTestTimestamp);
const start = toDate(config.firstTestTimestamp);
const end = toDate(config.lastTestTimestamp);

const days = DateUtils.eachDayOfInterval({
start: config.firstTestTimestamp,
end: config.lastTestTimestamp,
start,
end,
}).map((day) => ({
timestamp: DateUtils.startOfDay(day),
amount: Math.round(random(config.minTestsPerDay, config.maxTestsPerDay)),
Expand Down
44 changes: 12 additions & 32 deletions backend/src/api/routes/dev.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,15 @@
import { Router } from "express";
import joi from "joi";
import { createTestData } from "../controllers/dev";
import { isDevEnvironment } from "../../utils/misc";
import { validate } from "../../middlewares/configuration";
import { validateRequest } from "../../middlewares/validation";
import { asyncHandler } from "../../middlewares/utility";
import { devContract } from "@monkeytype/contracts/dev";
import { initServer } from "@ts-rest/express";

const router = Router();
import * as DevController from "../controllers/dev";
import { callController } from "../ts-rest-adapter";
import { onlyAvailableOnDev } from "../../middlewares/utility";

router.use(
validate({
criteria: () => {
return isDevEnvironment();
},
invalidMessage: "Development endpoints are only available in DEV mode.",
})
);
const s = initServer();

router.post(
"/generateData",
validateRequest({
body: {
username: joi.string().required(),
createUser: joi.boolean().optional(),
firstTestTimestamp: joi.number().optional(),
lastTestTimestamp: joi.number().optional(),
minTestsPerDay: joi.number().optional(),
maxTestsPerDay: joi.number().optional(),
},
}),
asyncHandler(createTestData)
);

export default router;
export default s.router(devContract, {
generateData: {
middleware: [onlyAvailableOnDev()],
handler: async (r) => callController(DevController.createTestData)(r),
},
});
4 changes: 1 addition & 3 deletions backend/src/api/routes/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ const router = s.router(contract, {
leaderboards,
results,
configuration,
dev,
});

export function addApiRoutes(app: Application): void {
Expand Down Expand Up @@ -139,9 +140,6 @@ function applyDevApiRoutes(app: Application): void {
}
next();
});

//enable dev edpoints
app.use("/dev", dev);
}
}

Expand Down
11 changes: 11 additions & 0 deletions backend/src/middlewares/utility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import _ from "lodash";
import type { Request, Response, NextFunction, RequestHandler } from "express";
import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response";
import { recordClientVersion as prometheusRecordClientVersion } from "../utils/prometheus";
import { validate } from "./configuration";
import { isDevEnvironment } from "../utils/misc";

export const emptyMiddleware = (
_req: MonkeyTypes.Request,
Expand Down Expand Up @@ -49,3 +51,12 @@ export function recordClientVersion(): RequestHandler {
next();
};
}

export function onlyAvailableOnDev(): RequestHandler {
return validate({
criteria: () => {
return isDevEnvironment();
},
invalidMessage: "Development endpoints are only available in DEV mode.",
});
}
15 changes: 0 additions & 15 deletions frontend/src/ts/ape/endpoints/dev.ts

This file was deleted.

2 changes: 0 additions & 2 deletions frontend/src/ts/ape/endpoints/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import Quotes from "./quotes";
import Users from "./users";
import Dev from "./dev";

export default {
Quotes,
Users,
Dev,
};
4 changes: 3 additions & 1 deletion frontend/src/ts/ape/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,22 @@ import { buildHttpClient } from "./adapters/axios-adapter";
import { envConfig } from "../constants/env-config";
import { buildClient } from "./adapters/ts-rest-adapter";
import { contract } from "@monkeytype/contracts";
import { devContract } from "@monkeytype/contracts/dev";

const API_PATH = "";
const BASE_URL = envConfig.backendUrl;
const API_URL = `${BASE_URL}${API_PATH}`;

const httpClient = buildHttpClient(API_URL, 10_000);
const tsRestClient = buildClient(contract, BASE_URL, 10_000);
const devClient = buildClient(devContract, BASE_URL, 240_000);

// API Endpoints
const Ape = {
...tsRestClient,
users: new endpoints.Users(httpClient),
quotes: new endpoints.Quotes(httpClient),
dev: new endpoints.Dev(buildHttpClient(API_URL, 240_000)),
dev: devClient,
};

export default Ape;
14 changes: 0 additions & 14 deletions frontend/src/ts/ape/types/dev.d.ts

This file was deleted.

7 changes: 4 additions & 3 deletions frontend/src/ts/modals/simple-modals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
TextInput,
} from "../utils/simple-modal";
import { ShowOptions } from "../utils/animated-modal";
import { GenerateDataRequest } from "@monkeytype/contracts/dev";

type PopupKey =
| "updateEmail"
Expand Down Expand Up @@ -1308,7 +1309,7 @@ list.devGenerateData = new SimpleModal({
minTestsPerDay,
maxTestsPerDay
): Promise<ExecReturn> => {
const request: Ape.Dev.GenerateData = {
const request: GenerateDataRequest = {
username,
createUser: createUser === "true",
};
Expand All @@ -1321,11 +1322,11 @@ list.devGenerateData = new SimpleModal({
if (maxTestsPerDay !== undefined && maxTestsPerDay.length > 0)
request.maxTestsPerDay = Number.parseInt(maxTestsPerDay);

const result = await Ape.dev.generateData(request);
const result = await Ape.dev.generateData({ body: request });

return {
status: result.status === 200 ? 1 : -1,
message: result.message,
message: result.body.message,
hideOptions: {
clearModalChain: true,
},
Expand Down
58 changes: 58 additions & 0 deletions packages/contracts/src/dev.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { initContract } from "@ts-rest/core";
import { z } from "zod";
import {
CommonResponses,
EndpointMetadata,
responseWithData,
} from "./schemas/api";
import { IdSchema } from "./schemas/util";

export const GenerateDataRequestSchema = z.object({
username: z.string(),
createUser: z
.boolean()
.optional()
.describe(
"If `true` create user with <username>@example.com and password `password`. If false user has to exist."
),
firstTestTimestamp: z.number().int().nonnegative().optional(),
lastTestTimestamp: z.number().int().nonnegative().optional(),
minTestsPerDay: z.number().int().nonnegative().optional(),
maxTestsPerDay: z.number().int().nonnegative().optional(),
});
export type GenerateDataRequest = z.infer<typeof GenerateDataRequestSchema>;

export const GenerateDataResponseSchema = responseWithData(
z.object({
uid: IdSchema,
email: z.string().email(),
})
);
export type GenerateDataResponse = z.infer<typeof GenerateDataResponseSchema>;

const c = initContract();
export const devContract = c.router(
{
generateData: {
summary: "generate data",
description: "Generate test results for the given user.",
method: "POST",
path: "/generateData",
body: GenerateDataRequestSchema.strict(),
responses: {
200: GenerateDataResponseSchema,
},
},
},
{
pathPrefix: "/dev",
strictStatusCodes: true,
metadata: {
openApiTags: "dev",
authenticationOptions: {
isPublic: true,
},
} as EndpointMetadata,
commonResponses: CommonResponses,
}
);
Loading

0 comments on commit 30d440a

Please sign in to comment.