extract shared contracts

This commit is contained in:
Christian Fehmer 2024-05-31 17:47:51 +02:00
parent 1fcb8748c0
commit 093c176530
No known key found for this signature in database
GPG key ID: FE53784A69964062
35 changed files with 195 additions and 204 deletions

View file

@ -24,6 +24,6 @@
"./**/*.ts",
"./**/*.spec.ts",
"./setup-tests.ts",
"../../shared-types/**/*.d.ts"
"../../shared/types/**/*.d.ts"
]
}

View file

@ -1,11 +1,10 @@
import * as ConfigDAL from "../../dal/config";
import { MonkeyTypes } from "../../types/types";
import { MonkeyResponse, MonkeyResponse2 } from "../../utils/monkey-response";
import {
ConfigType,
GetConfig,
GetTestConfigParams,
GetTestConfigQuery,
} from "../schemas/config.contract";
} from "@shared/contract/config.contract";
export async function getConfig(
req: MonkeyTypes.Request
@ -18,7 +17,7 @@ export async function getConfig(
export async function getConfigV2(
req: MonkeyTypes.Request2
): Promise<MonkeyResponse2<ConfigType>> {
): Promise<MonkeyResponse2<GetConfig>> {
const { uid } = req.ctx.decodedToken;
const data = await ConfigDAL.getConfig(uid);
@ -27,7 +26,7 @@ export async function getConfigV2(
export async function getTestConfigV2(
req: MonkeyTypes.Request2<never, GetTestConfigQuery, GetTestConfigParams>
): Promise<MonkeyResponse2<ConfigType>> {
): Promise<MonkeyResponse2<GetConfig>> {
const { noCache, includes } = req.query;
const { id } = req.params;
const { uid } = req.ctx.decodedToken;

View file

@ -2,11 +2,7 @@ import _ from "lodash";
import * as UserDAL from "../../dal/user";
import MonkeyError from "../../utils/error";
import Logger from "../../utils/logger";
import {
EmptyMonkeyResponse2,
MonkeyResponse,
MonkeyResponse2,
} from "../../utils/monkey-response";
import { MonkeyResponse, MonkeyResponse2 } from "../../utils/monkey-response";
import * as DiscordUtils from "../../utils/discord";
import {
MILLISECONDS_IN_DAY,
@ -33,8 +29,7 @@ import * as AuthUtil from "../../utils/auth";
import * as Dates from "date-fns";
import { UTCDateMini } from "@date-fns/utc";
import * as BlocklistDal from "../../dal/blocklist";
import { UserCreateType, UserType } from "../schemas/user.contract";
import { MonkeyTypes } from "../../types/types";
import { UserCreateType, UserType } from "@shared/contract/user.contract";
async function verifyCaptcha(captcha: string): Promise<void> {
if (!(await verify(captcha))) {

View file

@ -5,11 +5,11 @@ import {
} from "../../middlewares/auth";
import * as RateLimit from "../../middlewares/rate-limit";
import * as ConfigController from "../controllers/config";
import { configContract as configsContract } from "../schemas/config.contract";
import { configContract } from "./../../../../shared/contract/config.contract";
import { callHandler } from "./index2";
const s = initServer();
export const configRoutes = s.router(configsContract, {
export const configRoutes = s.router(configContract, {
get: {
middleware: [authenticateRequest(), RateLimit.configGet],
handler: (r) => callHandler(ConfigController.getConfigV2)(r),

View file

@ -1,20 +1,15 @@
import { AppRoute, AppRouter } from "@ts-rest/core";
import {
TsRestRequest,
TsRestRequestHandler,
createExpressEndpoints,
initServer,
} from "@ts-rest/express";
import { IRouter, NextFunction } from "express";
import {
MonkeyResponse2,
MonkeyStatusAware,
} from "../../utils/monkey-response";
import { contract } from "../schemas/index.contract";
import { RateLimitRequestHandler } from "express-rate-limit";
import { MonkeyResponse2 } from "../../utils/monkey-response";
import { configRoutes } from "./configsV2";
import { userRoutes } from "./usersV2";
import { MonkeyTypes } from "../../types/types";
import { RateLimitRequestHandler } from "express-rate-limit";
import { AppRoute, AppRouter } from "@ts-rest/core";
import { contract } from "./../../../../shared/contract/index.contract";
const s = initServer();
const router = s.router(contract, {

View file

@ -5,7 +5,7 @@ import { authenticateRequest } from "../../middlewares/auth";
import * as RateLimit from "../../middlewares/rate-limit";
import * as UserController from "../controllers/user";
import { userContract } from "../schemas/user.contract";
import { userContract } from "./../../../../shared/contract/user.contract";
import { callHandler } from "./index2";
const s = initServer();

View file

@ -15,7 +15,6 @@ import crypto from "crypto";
import { performance } from "perf_hooks";
import { TsRestRequest, TsRestRequestHandler } from "@ts-rest/express";
import { AppRoute, AppRouter } from "@ts-rest/core";
import { MonkeyTypes } from "../types/types";
type RequestAuthenticationOptions = {
isPublic?: boolean;

View file

@ -4,7 +4,6 @@ import { Response, NextFunction } from "express";
import { RateLimiterMemory } from "rate-limiter-flexible";
import rateLimit, { Options } from "express-rate-limit";
import { isDevEnvironment } from "../utils/misc";
import { MonkeyTypes } from "../types/types";
const REQUEST_MULTIPLIER = isDevEnvironment() ? 100 : 1;

View file

@ -1,10 +1,8 @@
import { AppRoute, AppRouter, Prettify } from "@ts-rest/core";
import { TsRestRequest } from "@ts-rest/express";
import { Router } from "express";
type ObjectId = import("mongodb").ObjectId;
type ExpressRequest = import("express").Request;
type TsRestRequest = import("@ts-rest/express").TsRestRequest<any>;
type AppRoute = import("@ts-rest/core").AppRoute;
type AppRouter = import("@ts-rest/core").AppRouter;
declare namespace MonkeyTypes {
type DecodedToken = {
@ -18,21 +16,21 @@ declare namespace MonkeyTypes {
decodedToken: DecodedToken;
};
type Request2<TBody = never, TQuery = never, TParams = never> = {
body: Readonly<TBody>;
query: Readonly<TQuery>;
params: TParams;
ctx: Readonly<Context>;
raw: TsRestRequest<any>;
};
type Request = {
ctx: Readonly<Context>;
} & ExpressRequest;
type Request2<TBody = any, TQuery = any, TParams = any> = {
body: Readonly<TBody>;
query: Readonly<TQuery>;
params: TParams;
ctx: Readonly<Context>;
raw: TsRestRequest;
};
type RequestTsRest<T extends AppRoute | AppRouter = any> = {
ctx: Readonly<Context>;
} & TsRestRequest<T>;
} & TsRestRequest;
type DBUser = Omit<
SharedTypes.User,

View file

@ -1,7 +1,7 @@
import { v4 as uuidv4 } from "uuid";
import { isDevEnvironment } from "./misc";
import { MonkeyStatusAware } from "./monkey-response";
import { MonkeyErrorType } from "../api/schemas/common.contract";
import { MonkeyErrorType } from "@shared/contract/common.contract";
class MonkeyError extends Error implements MonkeyStatusAware, MonkeyErrorType {
status: number;

View file

@ -1,6 +1,5 @@
import _ from "lodash";
import uaparser from "ua-parser-js";
import { ExpressRequest, MonkeyTypes } from "../types/types";
import { TsRestExpressOptions, TsRestRequest } from "@ts-rest/express";
//todo split this file into smaller util files (grouped by functionality)

View file

@ -1,6 +1,6 @@
import { Response } from "express";
import { isCustomCode } from "../constants/monkey-status-codes";
import { MonkeyResonseType as MonkeyResponseType } from "../api/schemas/common.contract";
import { MonkeyResonseType as MonkeyResponseType } from "@shared/contract/common.contract";
export interface MonkeyStatusAware {
status: number;

View file

@ -1,6 +1,5 @@
import "dotenv/config";
import { Counter, Histogram, Gauge } from "prom-client";
import { ExpressRequest, MonkeyTypes } from "../types/types";
import { TsRestRequest } from "@ts-rest/express";
const auth = new Counter({

View file

@ -19,18 +19,14 @@
"noFallthroughCasesInSwitch": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@shared/*": ["../shared-types/*"]
"@shared/*": ["../shared/*"]
}
},
"ts-node": {
"files": true
},
"files": [
"./src/types/types.d.ts",
"../shared-types/types.d.ts",
"../shared-types/config.d.ts"
],
"include": ["./src/**/*", "../shared-types/**/*.d.ts"],
"files": ["./src/types/types.d.ts"],
"include": ["./src/**/*", "../shared/**/*"],
"exclude": [
"node_modules",
"build",

View file

@ -20,9 +20,5 @@
"files": true
},
"files": ["../src/ts/types/types.d.ts", "vitest.d.ts"],
"include": [
"./**/*.spec.ts",
"./setup-tests.ts",
"../../shared-types/**/*.d.ts"
]
"include": ["./**/*.spec.ts", "./setup-tests.ts", "../../shared/**/*.d.ts"]
}

View file

@ -0,0 +1,60 @@
import { initClient } from "@ts-rest/core";
import {
GetConfig,
configContract,
} from "./../../../../../shared/contract/config.contract";
import { getAuthenticatedUser, isAuthenticated } from "../../firebase";
import { getIdToken } from "firebase/auth";
import { Axios, AxiosError, AxiosResponse, Method, isAxiosError } from "axios";
export default class Users {
private client;
constructor(baseUrl: string, axios: Axios) {
this.client = initClient(configContract, {
baseUrl,
jsonQuery: true,
//TODO extract
api: async ({ path, method, headers, body }) => {
const token = isAuthenticated()
? await getIdToken(getAuthenticatedUser())
: "";
try {
const result = await axios.request({
method: method as Method,
url: path,
headers: {
...headers,
Authorization: `Bearer ${token}`,
},
data: body,
});
return {
status: result.status,
body: result.data,
headers: result.headers,
};
} catch (e: Error | AxiosError | unknown) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
return {
status: response.status,
body: response.data,
headers: response.headers,
};
}
throw e;
}
},
});
}
async get(): Promise<GetConfig> {
return (await this.client.get()).body;
}
getClient() {
return this.client;
}
}

View file

@ -9,6 +9,7 @@ import ApeKeys from "./ape-keys";
import Public from "./public";
import Configuration from "./configuration";
import UsersV2 from "./usersV2";
import ConfigsV2 from "./configsV2";
export default {
Configs,
@ -22,4 +23,5 @@ export default {
ApeKeys,
Configuration,
UsersV2,
ConfigsV2,
};

View file

@ -1,5 +1,8 @@
import { initClient } from "@ts-rest/core";
import { GetUserType, userContract } from "../../contract-temp/user.contract";
import {
GetUserType,
userContract,
} from "./../../../../../shared/contract/user.contract";
import { getAuthenticatedUser, isAuthenticated } from "../../firebase";
import { getIdToken } from "firebase/auth";
import { Axios, AxiosError, AxiosResponse, Method, isAxiosError } from "axios";
@ -11,8 +14,8 @@ export default class Users {
this.client = initClient(userContract, {
baseUrl,
jsonQuery: true,
//TODO extract
api: async ({ path, method, headers, body }) => {
console.log("####", { path, method, headers, body });
const token = isAuthenticated()
? await getIdToken(getAuthenticatedUser())
: "";
@ -31,7 +34,7 @@ export default class Users {
body: result.data,
headers: result.headers,
};
} catch (e: Error | AxiosError | any) {
} catch (e: Error | AxiosError | unknown) {
if (isAxiosError(e)) {
const error = e as AxiosError;
const response = error.response as AxiosResponse;
@ -50,4 +53,8 @@ export default class Users {
async getData(): Promise<GetUserType> {
return (await this.client.get()).body;
}
getClient() {
return this.client;
}
}

View file

@ -25,7 +25,8 @@ const Ape = {
publicStats: new endpoints.Public(httpClient),
apeKeys: new endpoints.ApeKeys(httpClient),
configuration: new endpoints.Configuration(httpClient),
usersV2: new endpoints.UsersV2(BASE_URL, axiosClient),
usersV2: new endpoints.UsersV2(BASE_URL, axiosClient).getClient(),
configsV2: new endpoints.ConfigsV2(BASE_URL, axiosClient).getClient(),
};
export default Ape;

View file

@ -1,14 +0,0 @@
import { z } from "zod";
export const MonkeyResponseSchema = z.object({
message: z.string(),
status: z.number().int(),
});
export type MonkeyResonseType = z.infer<typeof MonkeyResponseSchema>;
export const MonkeyErrorSchema = z.object({
status: z.number().int(),
errorId: z.string(),
uid: z.string().optional(),
});
export type MonkeyErrorType = z.infer<typeof MonkeyErrorSchema>;

View file

@ -1,51 +0,0 @@
import { initContract } from "@ts-rest/core";
import { z } from "zod";
import { MonkeyResponseSchema } from "./common.contract";
const c = initContract();
const ConfigSchema = z.object({
test: z.string().readonly(),
});
export type ConfigType = z.infer<typeof ConfigSchema>;
const GetConfigSchema = MonkeyResponseSchema.extend({ data: ConfigSchema });
export type GetConfig = z.infer<typeof GetConfigSchema>;
const GetTestConfigParamsSchema = z.object({
id: z.string(),
});
export type GetTestConfigParams = z.infer<typeof GetTestConfigParamsSchema>;
const GetTestConfigQuerySchema = z.object({
noCache: z.boolean().optional().default(false),
includes: z
.array(z.enum(["server", "client"] as const))
.optional()
.default(["server"]),
});
export type GetTestConfigQuery = z.infer<typeof GetTestConfigQuerySchema>;
export const configContract = c.router(
{
get: {
method: "GET",
path: "/",
responses: {
200: GetConfigSchema,
},
},
getTest: {
method: "GET",
path: "/test/:id/",
pathParams: GetTestConfigParamsSchema,
query: GetTestConfigQuerySchema,
responses: {
200: GetConfigSchema,
},
},
},
{
pathPrefix: "/v2/configs",
strictStatusCodes: true,
}
);

View file

@ -1,10 +0,0 @@
import { initContract } from "@ts-rest/core";
import { userContract } from "./user.contract";
import { configContract } from "./config.contract";
const c = initContract();
export const contract = c.router({
users: userContract,
configs: configContract,
});

View file

@ -1,49 +0,0 @@
import { initContract } from "@ts-rest/core";
import { z } from "zod";
import { MonkeyResponseSchema, MonkeyErrorSchema } from "./common.contract";
const c = initContract();
const UserSchema = z.object({
uid: z.string().readonly(),
name: z.string(),
email: z.string().email(),
});
export type UserType = z.infer<typeof UserSchema>;
const UserCreateSchema = UserSchema.pick({
name: true,
email: true,
}).extend({
captcha: z.string(),
});
export type UserCreateType = z.infer<typeof UserCreateSchema>;
const GetUserSchema = MonkeyResponseSchema.extend({ data: UserSchema });
export type GetUserType = z.infer<typeof GetUserSchema>;
export const userContract = c.router(
{
signup: {
method: "POST",
path: "/signup",
body: UserCreateSchema,
responses: {
200: MonkeyResponseSchema,
400: MonkeyErrorSchema,
},
},
get: {
method: "GET",
path: "/",
responses: {
200: GetUserSchema,
//404: MonkeyErrorSchema,
},
},
},
{
pathPrefix: "/v2/users",
strictStatusCodes: true,
}
);

View file

@ -53,6 +53,16 @@ export async function initSnapshot(): Promise<
const snap = { ...defaultSnap };
try {
if (!isAuthenticated()) return false;
//EXAMPLE call
const testConfig = await Ape.configsV2.getTest({
params: { id: "456" },
query: {
includes: ["client", "server"],
noCache: true,
},
});
console.log("####", testConfig.body);
// if (ActivePage.get() === "loading") {
// LoadingPage.updateBar(22.5);
// } else {
@ -61,8 +71,8 @@ export async function initSnapshot(): Promise<
// LoadingPage.updateText("Downloading user...");
const [userResponse, configResponse, presetsResponse] = await Promise.all([
Ape.usersV2.getData(),
Ape.configs.get(),
Ape.usersV2.get(),
Ape.configsV2.get(),
Ape.presets.get(),
]);
@ -70,14 +80,14 @@ export async function initSnapshot(): Promise<
if (userResponse.status !== 200) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw {
message: `${userResponse.message} (user)`,
message: `${userResponse.body.message} (user)`,
responseCode: userResponse.status,
};
}
if (configResponse.status !== 200) {
// eslint-disable-next-line @typescript-eslint/no-throw-literal
throw {
message: `${configResponse.message} (config)`,
message: `${configResponse.body.message} (config)`,
responseCode: configResponse.status,
};
}
@ -89,8 +99,8 @@ export async function initSnapshot(): Promise<
};
}
const userData = userResponse.data;
const configData = configResponse.data;
const userData = userResponse.body.data as any; //TODO
const configData = configResponse.body.data;
const presetsData = presetsResponse.data;
if (userData === null) {

View file

@ -33,9 +33,9 @@
"allowUnreachableCode": false,
"skipLibCheck": false,
"paths": {
"@shared/*": ["../shared-types/*"]
"@shared/*": ["../shared/*"]
}
},
"include": ["./src/**/*.ts", "../shared-types/**/*.d.ts"],
"include": ["./src/**/*.ts", "../shared/**/*.d.ts"],
"exclude": ["node_modules", "build", "setup-tests.ts", "**/*.spec.ts"]
}

View file

@ -9,24 +9,19 @@
"path": "frontend"
},
{
"name": "shared-types",
"path": "shared-types"
},
{
"name": "contract",
"path": "contract"
"name": "shared",
"path": "shared"
},
{
"name": "root",
"path": "./"
"path": "."
}
],
"settings": {
"files.exclude": {
"frontend": true,
"backend": true,
"shared-types": true,
"contract": true
"shared": true
},
"search.exclude": {
//defaults

View file

@ -7,8 +7,6 @@ const c = initContract();
const ConfigSchema = z.object({
test: z.string().readonly(),
});
export type ConfigType = z.infer<typeof ConfigSchema>;
const GetConfigSchema = MonkeyResponseSchema.extend({ data: ConfigSchema });
export type GetConfig = z.infer<typeof GetConfigSchema>;

35
shared/package-lock.json generated Normal file
View file

@ -0,0 +1,35 @@
{
"name": "contract",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "contract",
"dependencies": {
"@ts-rest/core": "3.45.2",
"zod": "3.23.8"
}
},
"node_modules/@ts-rest/core": {
"version": "3.45.2",
"resolved": "https://registry.npmjs.org/@ts-rest/core/-/core-3.45.2.tgz",
"integrity": "sha512-Eiv+Sa23MbsAd1Gx9vNJ+IFCDyLZNdJ+UuGMKbFvb+/NmgcBR1VL1UIVtEkd5DJxpYMMd8SLvW91RgB2TS8iPw==",
"peerDependencies": {
"zod": "^3.22.3"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/zod": {
"version": "3.23.8",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
}
}
}

15
shared/package.json Normal file
View file

@ -0,0 +1,15 @@
{
"name": "contract",
"types": "./dist/index.d.ts",
"exports": {
".": "./dist/index.js"
},
"dependencies": {
"@ts-rest/core": "3.45.2",
"zod": "3.23.8"
},
"scripts": {
"build": "tsc",
"dev": "tsc --watch"
}
}

17
shared/tsconfig.json Normal file
View file

@ -0,0 +1,17 @@
{
"compilerOptions": {
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"module": "ESNext", //TODO CommonJS
"moduleResolution": "Bundler", //TODO remove?
"preserveWatchOutput": true,
"skipLibCheck": true,
"strict": true,
"rootDir": ".",
"outDir": "dist",
"types": ["node"],
"declaration": true
},
"exclude": ["node_modules", "./dist/**/*"]
}