Skip to content

Commit

Permalink
impr: use tsrest for dev endpoints (@fehmer)
Browse files Browse the repository at this point in the history
  • Loading branch information
fehmer committed Aug 23, 2024
1 parent e2d5744 commit cc83896
Show file tree
Hide file tree
Showing 13 changed files with 105 additions and 77 deletions.
Empty file.
6 changes: 6 additions & 0 deletions backend/scripts/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ 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",
},
],
},

Expand Down
16 changes: 10 additions & 6 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 @@ -15,10 +15,14 @@ import {
PersonalBest,
PersonalBests,
} from "@monkeytype/contracts/schemas/shared";
import {
GenerateDataRequest,
GenerateDataResponse,
} from "@monkeytype/contracts/dev";

type GenerateDataOptions = {
firstTestTimestamp: Date;
lastTestTimestamp: Date;
firstTestTimestamp: Date | number;
lastTestTimestamp: Date | number;
minTestsPerDay: number;
maxTestsPerDay: number;
};
Expand All @@ -31,8 +35,8 @@ const CREATE_RESULT_DEFAULT_OPTIONS: GenerateDataOptions = {
};

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 +46,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
49 changes: 18 additions & 31 deletions backend/src/api/routes/dev.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,22 @@
import { Router } from "express";
import joi from "joi";
import { createTestData } from "../controllers/dev";
import { isDevEnvironment } from "../../utils/misc";
import { devContract } from "@monkeytype/contracts/dev";
import { initServer } from "@ts-rest/express";
import { validate } from "../../middlewares/configuration";
import { validateRequest } from "../../middlewares/validation";
import { asyncHandler } from "../../middlewares/utility";

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

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

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)
);
const s = initServer();

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
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,
}
);
2 changes: 2 additions & 0 deletions packages/contracts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { publicContract } from "./public";
import { leaderboardsContract } from "./leaderboards";
import { resultsContract } from "./results";
import { configurationContract } from "./configuration";
import { devContract } from "./dev";

const c = initContract();

Expand All @@ -21,4 +22,5 @@ export const contract = c.router({
leaderboards: leaderboardsContract,
results: resultsContract,
configuration: configurationContract,
dev: devContract,
});
5 changes: 3 additions & 2 deletions packages/contracts/src/schemas/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ export type OpenApiTag =
| "public"
| "leaderboards"
| "results"
| "configuration";
| "configuration"
| "dev";

export type EndpointMetadata = {
/** Authentication options, by default a bearer token is required. */
Expand All @@ -18,7 +19,7 @@ export type EndpointMetadata = {
};

export type RequestAuthenticationOptions = {
/** Endpoint is accessible without any authentication. If `false` bearer authentication is required. */
/** Endpoint is accessible without any authentication. If `false` bearer authentication is requuired. */
isPublic?: boolean;
/** Endpoint is accessible with ape key authentication in _addition_ to the bearer authentication. */
acceptApeKeys?: boolean;
Expand Down

0 comments on commit cc83896

Please sign in to comment.