import { InferType, Parser, inferFromSampleValue, object } from 'cast.ts' import debug from 'debug' import { Router, Request, Response } from 'express' import { readFileSync, writeFileSync } from 'fs' import { join } from 'path' import { env } from './env' import { HttpError } from './error' import { checkAdmin, getJWT, JWTPayload } from './jwt' import { proxy } from './proxy' let clientDir = join('..', 'client', 'src', 'api') let configFile = join(clientDir, 'config.ts') let clientWsTypesFile = join(clientDir, 'ws-types.ts') let serverWsTypesFile = join('src', 'ws', 'types.ts') const emptyParser = object({}) type Result = T | Promise export function defModule(options: { name: string; apiPrefix?: string }) { let moduleName = options.name let log = debug(moduleName) log.enabled = true let clientFile = join(clientDir, `${moduleName}.ts`) let router = Router() let apiPrefix = options.apiPrefix ?? `/api/${moduleName}` let code = ` // This file is generated automatically // Don't edit this file directly import { post } from './utils' let api_origin = '${apiPrefix}' ` function defAPI( api: { name: string sampleInput?: Input sampleOutput?: Output inputParser?: Parser> outputParser?: Parser> transformInputForLog?: (input: Partial>) => unknown transformOutputForLog?: (output: Partial>) => unknown } & ( | { jwt: true role?: 'admin' fn?: ( input: InferType, jwt: JWTPayload, ) => Result>> } | { jwt?: false fn?: ( input: InferType, jwt: JWTPayload | null, ) => Result>> } ), ) { let name = api.name let Name = name[0].toUpperCase() + name.slice(1) const inputParser = (api?.inputParser || (api?.sampleInput ? inferFromSampleValue(api.sampleInput) : emptyParser)) as Parser> const outputParser = (api?.outputParser || (api?.sampleOutput ? inferFromSampleValue(api.sampleOutput) : emptyParser)) as Parser> const InputType = inputParser.type const OutputType = outputParser.type code += ` export type ${Name}Input = ${InputType} export type ${Name}Output = ${OutputType}` if (api.jwt) { code += ` export function ${name}(input: ${Name}Input & { token: string }): Promise<${Name}Output & { error?: string }> { let { token, ...body } = input return post(api_origin + '/${name}', body, token) } ` } else { code += ` export function ${name}(input: ${Name}Input): Promise<${Name}Output & { error?: string }> { return post(api_origin + '/${name}', input) } ` } function getSampleInput() { return ( api.sampleInput ?? api.inputParser?.sampleValue ?? api.inputParser?.randomSample() ) } function getSampleOutput() { return ( api.sampleOutput ?? api.outputParser?.sampleValue ?? api.outputParser?.randomSample() ) } let requestHandler = async (req: Request, res: Response) => { let startTime = Date.now() let input: InferType | undefined let output: InferType | { error: string } let jwt: JWTPayload | null = null try { input = inputParser.parse(req.body) log( name, api.transformInputForLog ? api.transformInputForLog(input) : input, ) if (!api.fn) { res.status(501) res.json(getSampleOutput()) return } if (api.jwt) { jwt = getJWT(req) if (api.role == 'admin') checkAdmin(jwt) output = await api.fn(input, jwt) } else { try { jwt = getJWT(req) } catch (error) { // api from guest } output = await api.fn(input, jwt) } output = outputParser.parse(output) } catch (e: any) { let err = e as HttpError if (!err.statusCode) log(err) res.status(err.statusCode || 500) let error = HttpError.toString(err) output = { error } } let endTime = Date.now() res.json(output) if (!input) { input = req.body } proxy.log.push({ rpc: name, input: JSON.stringify( api.transformInputForLog && input ? api.transformInputForLog(input) : input, ), output: JSON.stringify( api.transformOutputForLog && output && typeof output == 'object' && !('error' in output) ? api.transformOutputForLog(output) : output, ), time_used: endTime - startTime, user_id: jwt ? jwt.id : null, user_agent: req.headers['user-agent'] || null, }) } router.post('/' + name, requestHandler) return { ...api, requestHandler, inputParser, outputParser, getSampleInput, getSampleOutput, } } function saveClient() { if (env.NODE_ENV != 'development') return saveConfig({ file: configFile, }) saveFile({ file: clientFile, code, }) } return { defAPI, saveClient, apiPrefix, router, } } function saveConfig(options: { file: string }) { let code = ` // This file is generated automatically // Don't edit this file directly export let server_origin = '${env.ORIGIN}' export function setServerOrigin(origin: string) { server_origin = origin } ` saveFile({ file: options.file, code }) } function saveFile(options: { file: string; code: string }) { let { file, code } = options code = code.trim() + '\n' try { let content = readFileSync(file).toString() if (content == code) return } catch (error) { // e.g. file not exist } writeFileSync(file, code) console.log('saved to', file) } export function saveWsTypes(args: { wsClientMessageParser: Parser wsServerMessageParser: Parser }) { let code = ` // This file is generated automatically // Don't edit this file directly export const Ping = '1' export const Pong = '2' export const Send = '3' export type WSClientMessage = ${args.wsClientMessageParser.type} export type WSServerMessage = ${args.wsServerMessageParser.type} ` saveFile({ file: clientWsTypesFile, code }) saveFile({ file: serverWsTypesFile, code }) }