Skip to content

Commit

Permalink
Merge branch 'main' into foundation-proxy-save
Browse files Browse the repository at this point in the history
  • Loading branch information
jbolda committed Sep 12, 2024
2 parents d9d5279 + 56ac29c commit f372260
Show file tree
Hide file tree
Showing 12 changed files with 335 additions and 15 deletions.
5 changes: 5 additions & 0 deletions .changes/foundation-sim-logger.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@simulacrum/foundation-simulator": minor:feat
---

All routes now add a log to the simulation state on every visit. This assists in tracking hits on each simulation route.
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.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Simulacrum removes these constraints from your process by allowing you to simula

* [auth0](packages/auth0) - [@simulacrum/auth0-simulator](https://www.npmjs.com/package/@simulacrum/auth0-simulator)
* [ldap](packages/ldap) - [@simulacrum/ldap-simulator](https://www.npmjs.com/package/@simulacrum/ldap-simulator)
* [github-api](packages/github-api) - [@simulacrum/github-api-simulator](https://www.npmjs.com/package/@simulacrum/github-api-simulator)

## Usage

Expand Down
5 changes: 5 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

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
1 change: 1 addition & 0 deletions packages/foundation/example/extensiveServer/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const inputSelectors = ({
};

export const extendStore = {
logs: false,
actions: inputActions,
selectors: inputSelectors,
schema: inputSchema,
Expand Down
1 change: 1 addition & 0 deletions packages/foundation/example/singleFileServer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ export const simulation = createFoundationSimulationServer({
},
],
extendStore: {
logs: false,
actions: ({ thunks, schema }) => {
// TODO attempt to remove this type as a requirement
let upsertTest = thunks.create<AnyState>(
Expand Down
1 change: 1 addition & 0 deletions packages/foundation/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
},
"dependencies": {
"ajv-formats": "^3.0.1",
"cors": "^2.8.5",
"express": "^4.19.2",
"fdir": "^6.2.0",
"http-proxy-middleware": "^3.0.0",
Expand Down
160 changes: 148 additions & 12 deletions packages/foundation/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import express from "express";
import cors from "cors";
import type {
Request as ExpressRequest,
Response as ExpressResponse,
NextFunction as ExpressNextFunction,
} from "express";
import type { ILayer, IRoute } from "express-serve-static-core";
import { fdir } from "fdir";
import fs from "node:fs";
import path from "node:path";
Expand All @@ -26,14 +29,18 @@ import type {
import type {
ExtendSimulationSchemaInput,
ExtendSimulationSchema,
SimulationRoute,
} from "./store/schema";
import type { RecursivePartial } from "./store/types";
import { apiProxy } from "./middleware/proxy";
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 @@ -95,13 +102,61 @@ export function createFoundationSimulationServer<
return () => {
let app = express();

if (process.env.SIM_PROXY || proxyAndSave) {
if (proxyAndSave) {
app.use(apiProxy(proxyAndSave));
}

app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: false }));

let simulationStore = createSimulationStore(extendStore);

app.use((req, res, next) => {
// add each response to the internal log
simulationStore.store.dispatch(
simulationStore.actions.simulationLog({
method: req.method,
url: req.url,
query: req.query,
body: req.body,
})
);
next();
});

if (extendRouter) {
extendRouter(app, simulationStore);

if (app?._router?.stack) {
const layers: IRoute[] = app._router.stack
.map((stack: ILayer) => stack.route)
.filter(Boolean);

const simulationRoutes = [];
for (let layer of layers) {
for (let stack of layer.stack) {
simulationRoutes.push(
simulationStore.schema.simulationRoutes.add({
[`${stack.method}:${layer.path}`]: {
type: "JSON",
url: layer.path,
method: stack.method as SimulationRoute["method"],
calls: 0,
defaultCode: 200,
responses: [200],
},
})
);
}
}

simulationStore.store.dispatch(
simulationStore.actions.batchUpdater(simulationRoutes)
);
}
}

if (serveJsonFiles) {
const jsonFiles = new fdir()
.filter((path, _isDirectory) => path.endsWith(".json"))
Expand All @@ -111,19 +166,33 @@ export function createFoundationSimulationServer<
.sync();

if (jsonFiles.length > 0) {
const simulationRoutes = [];
for (let jsonFile of jsonFiles) {
const route = jsonFile.slice(0, jsonFile.length - 5);
const route = `/${jsonFile.slice(0, jsonFile.length - 5)}`;
const filename = path.join(serveJsonFiles, jsonFile);
app.get(`/${route}`, (_req, res) => {
app.get(route, function staticJson(_req, res) {
res.setHeader("content-type", "application/json");
fs.createReadStream(filename).pipe(res);
});

simulationRoutes.push(
simulationStore.schema.simulationRoutes.add({
[`get:${route}`]: {
type: "JSON",
url: route,
method: "get",
calls: 0,
defaultCode: 200,
responses: [200],
},
})
);
}
}
}

if (extendRouter) {
extendRouter(app, simulationStore);
simulationStore.store.dispatch(
simulationStore.actions.batchUpdater(simulationRoutes)
);
}
}

if (openapi) {
Expand Down Expand Up @@ -173,13 +242,79 @@ 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 === "/" ? "" : 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
);
});
}
}

// return simulation helper page
app.get("/", (req, res) => {
let routes = simulationStore.schema.simulationRoutes.selectTableAsList(
simulationStore.store.getState()
);
let logs = simulationStore.schema.simulationLogs.selectTableAsList(
simulationStore.store.getState()
);
if (routes.length === 0) {
res.sendStatus(404);
} else {
res.status(200).send(generateRoutesHTML(routes, logs));
}
});
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 All @@ -196,6 +331,7 @@ export function createFoundationSimulationServer<

return {
server,
simulationStore,
ensureClose: async () => {
await new Promise<void>((resolve) => {
server.once("close", resolve);
Expand Down
Loading

0 comments on commit f372260

Please sign in to comment.