import { Description, FileWrite } from "./app"; import { dataAccessModuleName } from "./DataAccessGenerator"; import { modelModuleName } from "./ModelGenerator"; import { Collection, Entity, Property } from "./MySchema"; import { collectionRoute } from "./OpenApiGenerator"; export function middlewareModuleName(collection: Collection): string { return `${collection.name}Middleware`; } export function generateMiddlewares(description: Description): FileWrite[] { return description.collections.map(generateMiddleware); } function generateMiddleware(collection: Collection): FileWrite { const definitions = [ defineImports(collection), defineCreateMiddlewareFunction(collection), defineGeneralFunctions, defineInputParsers(collection.entities), ]; return { location: middlewareModuleName(collection) + ".ts", content: definitions.join("\n\n") + "\n", }; } function defineImports(collection: Collection): string { return `import express from "express"; import * as DataAccess from "./${dataAccessModuleName(collection)}"; import { append, findSingle, ${collection.entities.name}Input, toPayload } from "./${modelModuleName(collection)}"; import { Result } from "../ExampleOutput/Result";`; } function defineCreateMiddlewareFunction(collection: Collection): string { const col = collection.name.toLowerCase(); const route = collectionRoute(collection); const entityId = collection.entities.name.toLowerCase() + "Id"; return `export function ${col}Middleware({ fileName }: { fileName: string }) { const ${col} = DataAccess.load(fileName); const route = express.Router(); // TODO get/post collection, get/post/delete single entity route.get("${route}/:${entityId}(\\\\d+)", (req, res) => { const id = Number.parseInt(req.params.${entityId}, undefined); const item = findSingle(${col}, id); if (item == null) { res.status(404).send("not found"); return; } res.send(toPayload(item)); }); route.get("${route}", (req, res) => { const items = ${col}.items; res.send(items.map(toPayload)); }); route.post("${route}", (req, res) => { try { const parsed = parse${collection.entities.name}Input(req.body); if(!parsed.isSuccessful) { res.status(400).send(parsed.errorMessage); return; } const item = parsed.value; const added = append(${col}, item); DataAccess.save(fileName, ${col}); const payload = toPayload(added); res.status(201).send(payload); } catch (error) { console.error(error); res.status(500).send("Internal Server Error."); return; } }); return route; }`; } const defineGeneralFunctions: string = `function parseNotNull(data: any): Result<any> { if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; else return { isSuccessful: true, value: data }; } function parseObject(data: any): Result<object> { if (typeof data === "object") return { isSuccessful: true, value: data }; else return { isSuccessful: false, errorMessage: "Input is not an object." }; } const parseRequiredMember = <T>(fieldName: keyof T) => (data: any): Result<any> => { if (fieldName in data) { return { isSuccessful: true, value: data[fieldName] }; } else { return { isSuccessful: false, errorMessage: \`Required member "\${fieldName}" could not be found.\` }; } } const parseNullableMember = <T>(fieldName: keyof T) => (data: any): Result<any> => { if (fieldName in data) { return { isSuccessful: true, value: data[fieldName] }; } else { return { isSuccessful: true, value: null }; } } function parseString(data: any): Result<string> { if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; else return { isSuccessful: false, errorMessage: "Value is not a string." }; } function andThen<T, R>(result: Result<T>, nextFn: (data: T) => Result<R>): Result<R> { if (!result.isSuccessful) return result; return nextFn(result.value); } type ParseObject<T extends {}> = { [P in keyof T]: Result<T[P]> }; function compose<R, S, T>(first: (obj:R) => Result<S>, second: (obj: S) => Result<T>): (obj:R) => Result<T> { return (obj:R): Result<T> => { const firstResult = first(obj); if(!firstResult.isSuccessful) return firstResult; return second(firstResult.value); }; } function resolve<T extends {}>(members: ParseObject<T>): Result<T> { const obj: Partial<T> = {}; for (const key in members) { if (Object.prototype.hasOwnProperty.call(members, key)) { const member: Result<T[Extract<keyof T, string>]> = members[key]; if (!member.isSuccessful) { return member; } obj[key] = member.value; } } return { isSuccessful: true, value: (obj as T) }; }`; function defineInputParsers(entity: Entity): string { const propParsers = entity.properties.map(p => definePropertyParser(entity, p)).join("\n"); return `${propParsers} function parse${entity.name}Input(data: any): Result<${entity.name}Input> { const obj = andThen(parseNotNull(data), parseObject); const members: ParseObject<${entity.name}Input> = { ${entity.properties.map(p => `${p.key}: andThen(obj, parse${p.key}),`).join("\n\t\t")} }; return resolve(members); }`; } function definePropertyParser(entity: Entity, property: Property): string { if(property.type === "string") { return defineStringPropertyParser(entity.name, property.key, property.isNullable); } else { throw new Error("Properties other than strings are not implemented yet."); } } function defineStringPropertyParser(typeName: string, propName: string, isNullable: boolean): string { const parser = isNullable ? "parseNullableMember" : "parseRequiredMember"; return `const parse${propName} = compose(${parser}<${typeName}Input>("${propName}"), parseString);`; }