Skip to content

Commit

Permalink
add simulationRoutes to store, display at base / route
Browse files Browse the repository at this point in the history
  • Loading branch information
jbolda committed Jul 22, 2024
1 parent f4c44e8 commit 974671e
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 7 deletions.
5 changes: 5 additions & 0 deletions .changes/foundation-simulator-route-list.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@simulacrum/foundation-simulator": minor:feat
---

To improve transparency and flexibility, we now include a page at the root that lists all of the routes, and the ability to signal which response to return.
11 changes: 9 additions & 2 deletions packages/foundation/example/extensiveServer/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ const openapiSchemaFromRealEndpoint = {
200: {
description: "All of the dogs",
},
404: {
description: "The dogs have gone missing!",
},
},
},
},
Expand Down Expand Up @@ -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(
Expand Down
76 changes: 71 additions & 5 deletions packages/foundation/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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<string, SimulationHandlerFunctions>;
export type {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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<string, SimulationRoute>);
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<string, Partial<SimulationRoute>>;
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" }));

Expand Down
85 changes: 85 additions & 0 deletions packages/foundation/src/routeTemplate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { SimulationRoute } from "./store/schema";

const responseSubmit = (routeId: string, response: number) => /* HTML */ `<form
action=""
method="post"
>
<input type="submit" name="${routeId}" value="${response}" />
</form>`;
const routeToId = (route: SimulationRoute) => `${route.method}:${route.url}`;

export const generateRoutesHTML = (routes: SimulationRoute[]) => {
return /* HTML */ `<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>Simulation Server Routes</title>
<style>
html {
font-size: 16px;
line-height: 1.5;
background-color: #fff;
color: #000;
}
body {
margin: 0 auto;
max-width: 720px;
padding: 0 16px;
font-family: sans-serif;
}
a {
text-decoration: none;
}
.routes {
display: grid;
grid-template-columns: 20px auto auto auto;
column-gap: 15px;
}
.route-actions {
display: flex;
gap: 5px;
}
li {
margin-bottom: 8px;
}
/* Dark mode styles */
@media (prefers-color-scheme: dark) {
html {
background-color: #1e293b;
color: #fff;
}
a {
}
}
</style>
</head>
<body>
<main class="my-12">
<h1>Simulation Routes</h1>
<div class="routes">
${routes
.map(
(route) =>
`<span>${route.method.toUpperCase()}</span><a href=${
route.url
}>${route.url}</a><span>returns: ${
route.defaultCode
}, called ${
route.calls
} times</span><div class="route-actions">${route.responses
.map((response) =>
responseSubmit(routeToId(route), response)
)
.join("")}</div>`
)
.join("\n")}
</div>
</main>
</body>
</html>`;
};
19 changes: 19 additions & 0 deletions packages/foundation/src/store/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ export type ExtendSimulationSchemaInput<T> = ({
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<ExtendedSimulationSchema>(
inputSchema: ExtendSimulationSchemaInput<ExtendedSimulationSchema>
) {
Expand All @@ -16,6 +25,16 @@ export function generateSchemaWithInputSlices<ExtendedSimulationSchema>(
let schemaAndInitialState = createSchema({
cache: immerSlice.table({ empty: {} }),
loaders: immerSlice.loaders(),
simulationRoutes: immerSlice.table<SimulationRoute>({
empty: {
type: "Explicit",
url: "",
method: "get",
calls: 0,
defaultCode: 200,
responses: [200],
},
}),
...slices,
});

Expand Down

0 comments on commit 974671e

Please sign in to comment.