diff --git a/packages/foundation/example/extensiveServer/openapi.ts b/packages/foundation/example/extensiveServer/openapi.ts index d25bd46c..7ee48661 100644 --- a/packages/foundation/example/extensiveServer/openapi.ts +++ b/packages/foundation/example/extensiveServer/openapi.ts @@ -16,6 +16,9 @@ const openapiSchemaFromRealEndpoint = { 200: { description: "All of the dogs", }, + 404: { + description: "The dogs have gone missing!", + }, }, }, }, @@ -127,11 +130,15 @@ function handlers( simulationStore: ExtendedSimulationStore ): SimulationHandlers { return { - getDogs: (_c, _r, response) => { + getDogs: (_c, request, response, _next, routeMetadata) => { let dogs = simulationStore.schema.dogs.select( simulationStore.store.getState() ); - response.status(200).json({ dogs }); + if (routeMetadata.defaultCode === 200) { + response.status(200).json({ dogs }); + } else { + response.sendStatus(routeMetadata.defaultCode); + } }, putDogs: (c, req, response) => { simulationStore.store.dispatch( diff --git a/packages/foundation/src/index.ts b/packages/foundation/src/index.ts index e6de65d1..2e0448df 100644 --- a/packages/foundation/src/index.ts +++ b/packages/foundation/src/index.ts @@ -2,6 +2,7 @@ import express from "express"; import type { Request as ExpressRequest, Response as ExpressResponse, + NextFunction as ExpressNextFunction, } from "express"; import { merge } from "lodash"; import OpenAPIBackend from "openapi-backend"; @@ -23,13 +24,17 @@ import type { import type { ExtendSimulationSchemaInput, ExtendSimulationSchema, + SimulationRoute, } from "./store/schema"; import type { RecursivePartial } from "./store/types"; +import { generateRoutesHTML } from "./routeTemplate"; type SimulationHandlerFunctions = ( context: OpenAPIBackendContext, request: ExpressRequest, - response: ExpressResponse + response: ExpressResponse, + next: ExpressNextFunction, + routeMetadata: SimulationRoute ) => void; export type SimulationHandlers = Record; export type { @@ -87,6 +92,7 @@ export function createFoundationSimulationServer< return () => { let app = express(); app.use(express.json()); + app.use(express.urlencoded({ extended: false })); let simulationStore = createSimulationStore(extendStore); if (extendRouter) { @@ -140,13 +146,73 @@ export function createFoundationSimulationServer< }); // initalize the backend - api.init(); - app.use((req, res, next) => - api.handleRequest(req as Request, req, res, next) - ); + api.init().then((init) => { + const router = init.router; + const operations = router.getOperations(); + const simulationRoutes = operations.reduce((routes, operation) => { + const url = `${router.apiRoot}${operation.path}`; + routes[`${operation.method}:${url}`] = { + type: "OpenAPI", + url, + method: operation.method as SimulationRoute["method"], + calls: 0, + defaultCode: 200, + responses: Object.keys(operation.responses ?? {}).map((key) => + parseInt(key) + ), + }; + return routes; + }, {} as Record); + simulationStore.store.dispatch( + simulationStore.actions.batchUpdater([ + simulationStore.schema.simulationRoutes.add(simulationRoutes), + ]) + ); + return init; + }); + app.use((req, res, next) => { + const routeId = `${req.method.toLowerCase()}:${req.path}`; + const routeMetadata = + simulationStore.schema.simulationRoutes.selectById( + simulationStore.store.getState(), + { + id: routeId, + } + ); + return api.handleRequest( + req as Request, + req, + res, + next, + routeMetadata + ); + }); } } + app.get("/", (req, res) => { + let routes = simulationStore.schema.simulationRoutes.selectTableAsList( + simulationStore.store.getState() + ); + if (routes.length === 0) { + res.sendStatus(404); + } else { + res.status(200).send(generateRoutesHTML(routes)); + } + }); + app.post("/", (req, res) => { + const formValue = req.body; + const entries = {} as Record>; + for (let [key, value] of Object.entries(formValue)) { + entries[key] = { defaultCode: parseInt(value as string) }; + } + simulationStore.store.dispatch( + simulationStore.actions.batchUpdater([ + simulationStore.schema.simulationRoutes.patch(entries), + ]) + ); + res.redirect("/"); + }); // if no extendRouter routes or openapi routes handle this, return 404 app.all("*", (req, res) => res.status(404).json({ error: "not found" })); diff --git a/packages/foundation/src/routeTemplate.ts b/packages/foundation/src/routeTemplate.ts new file mode 100644 index 00000000..5837206f --- /dev/null +++ b/packages/foundation/src/routeTemplate.ts @@ -0,0 +1,85 @@ +import type { SimulationRoute } from "./store/schema"; + +const responseSubmit = (routeId: string, response: number) => /* HTML */ `
+ +
`; +const routeToId = (route: SimulationRoute) => `${route.method}:${route.url}`; + +export const generateRoutesHTML = (routes: SimulationRoute[]) => { + return /* HTML */ ` + + + + Simulation Server Routes + + + +
+

Simulation Routes

+
+ ${routes + .map( + (route) => + `${route.method.toUpperCase()}${route.url}returns: ${ + route.defaultCode + }, called ${ + route.calls + } times
${route.responses + .map((response) => + responseSubmit(routeToId(route), response) + ) + .join("")}
` + ) + .join("\n")} +
+
+ + `; +}; diff --git a/packages/foundation/src/store/schema.ts b/packages/foundation/src/store/schema.ts index 4d148756..cbd5eecb 100644 --- a/packages/foundation/src/store/schema.ts +++ b/packages/foundation/src/store/schema.ts @@ -8,6 +8,15 @@ export type ExtendSimulationSchemaInput = ({ slice, }: ExtendSimulationSchema) => T; +export interface SimulationRoute { + type: "OpenAPI" | "Explicit"; + url: string; + method: "get" | "post" | "delete" | "patch"; + calls: number; + defaultCode: number; + responses: number[]; +} + export function generateSchemaWithInputSlices( inputSchema: ExtendSimulationSchemaInput ) { @@ -16,6 +25,16 @@ export function generateSchemaWithInputSlices( let schemaAndInitialState = createSchema({ cache: immerSlice.table({ empty: {} }), loaders: immerSlice.loaders(), + simulationRoutes: immerSlice.table({ + empty: { + type: "Explicit", + url: "", + method: "get", + calls: 0, + defaultCode: 200, + responses: [200], + }, + }), ...slices, });