diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/ExampleOutput/Model.ts b/src/ExampleOutput/Model.ts index fe0e9eb..146a8c3 100644 --- a/src/ExampleOutput/Model.ts +++ b/src/ExampleOutput/Model.ts @@ -3,12 +3,15 @@ nextId: number; } -export interface GraphNode { - id: number; +export interface GraphNodeInput { name: string; description: string; } +export interface GraphNode extends GraphNodeInput { + id: number; +} + export interface GraphNodePayload extends GraphNode { ref: string; } @@ -23,3 +26,10 @@ export function findSingle(graph: Graph, id: number) { return graph.nodes.find(n => n.id === id); } + +export function append(collection: Graph, item: GraphNodeInput): GraphNode { + const newItem: GraphNode = {...item, id: collection.nextId}; + collection.nextId++; + collection.nodes.push(newItem); + return newItem; +} diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/ExampleOutput/Model.ts b/src/ExampleOutput/Model.ts index fe0e9eb..146a8c3 100644 --- a/src/ExampleOutput/Model.ts +++ b/src/ExampleOutput/Model.ts @@ -3,12 +3,15 @@ nextId: number; } -export interface GraphNode { - id: number; +export interface GraphNodeInput { name: string; description: string; } +export interface GraphNode extends GraphNodeInput { + id: number; +} + export interface GraphNodePayload extends GraphNode { ref: string; } @@ -23,3 +26,10 @@ export function findSingle(graph: Graph, id: number) { return graph.nodes.find(n => n.id === id); } + +export function append(collection: Graph, item: GraphNodeInput): GraphNode { + const newItem: GraphNode = {...item, id: collection.nextId}; + collection.nextId++; + collection.nodes.push(newItem); + return newItem; +} diff --git a/src/MiddlewareGenerator.ts b/src/MiddlewareGenerator.ts index 6731183..23d5a4b 100644 --- a/src/MiddlewareGenerator.ts +++ b/src/MiddlewareGenerator.ts @@ -1,7 +1,7 @@ import { Description, FileWrite } from "./app"; import { dataAccessModuleName } from "./DataAccessGenerator"; import { modelModuleName } from "./ModelGenerator"; -import { Collection } from "./MySchema"; +import { Collection, Entity, Property } from "./MySchema"; import { collectionRoute } from "./OpenApiGenerator"; export function middlewareModuleName(collection: Collection): string { @@ -16,6 +16,8 @@ const definitions = [ defineImports(collection), defineCreateMiddlewareFunction(collection), + defineGeneralFunctions, + defineInputParsers(collection.entities), ]; return { location: middlewareModuleName(collection) + ".ts", @@ -26,20 +28,22 @@ function defineImports(collection: Collection): string { return `import express from "express"; import * as DataAccess from "./${dataAccessModuleName(collection)}"; -import { findSingle, toPayload } from "./${modelModuleName(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}/:id(\\d+)", (req, res) => { - const id = Number.parseInt(req.params.id, undefined); + 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"); @@ -53,6 +57,117 @@ 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 { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: \`Required member "\${fieldName}" could not be found.\` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = 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);`; } \ No newline at end of file diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/ExampleOutput/Model.ts b/src/ExampleOutput/Model.ts index fe0e9eb..146a8c3 100644 --- a/src/ExampleOutput/Model.ts +++ b/src/ExampleOutput/Model.ts @@ -3,12 +3,15 @@ nextId: number; } -export interface GraphNode { - id: number; +export interface GraphNodeInput { name: string; description: string; } +export interface GraphNode extends GraphNodeInput { + id: number; +} + export interface GraphNodePayload extends GraphNode { ref: string; } @@ -23,3 +26,10 @@ export function findSingle(graph: Graph, id: number) { return graph.nodes.find(n => n.id === id); } + +export function append(collection: Graph, item: GraphNodeInput): GraphNode { + const newItem: GraphNode = {...item, id: collection.nextId}; + collection.nextId++; + collection.nodes.push(newItem); + return newItem; +} diff --git a/src/MiddlewareGenerator.ts b/src/MiddlewareGenerator.ts index 6731183..23d5a4b 100644 --- a/src/MiddlewareGenerator.ts +++ b/src/MiddlewareGenerator.ts @@ -1,7 +1,7 @@ import { Description, FileWrite } from "./app"; import { dataAccessModuleName } from "./DataAccessGenerator"; import { modelModuleName } from "./ModelGenerator"; -import { Collection } from "./MySchema"; +import { Collection, Entity, Property } from "./MySchema"; import { collectionRoute } from "./OpenApiGenerator"; export function middlewareModuleName(collection: Collection): string { @@ -16,6 +16,8 @@ const definitions = [ defineImports(collection), defineCreateMiddlewareFunction(collection), + defineGeneralFunctions, + defineInputParsers(collection.entities), ]; return { location: middlewareModuleName(collection) + ".ts", @@ -26,20 +28,22 @@ function defineImports(collection: Collection): string { return `import express from "express"; import * as DataAccess from "./${dataAccessModuleName(collection)}"; -import { findSingle, toPayload } from "./${modelModuleName(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}/:id(\\d+)", (req, res) => { - const id = Number.parseInt(req.params.id, undefined); + 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"); @@ -53,6 +57,117 @@ 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 { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: \`Required member "\${fieldName}" could not be found.\` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = 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);`; } \ No newline at end of file diff --git a/src/ModelGenerator.ts b/src/ModelGenerator.ts index 147ca0e..f4afba6 100644 --- a/src/ModelGenerator.ts +++ b/src/ModelGenerator.ts @@ -1,5 +1,6 @@ import { FileWrite } from "./app"; import { Collection, Entity } from "./MySchema"; +import { collectionRoute } from "./OpenApiGenerator"; export function generateModel(collections: Collection[]): FileWrite[] { @@ -18,6 +19,7 @@ defineEntity(collection.entities), definePayload(collection), defineFindFunction(collection), + defineAppendFunction(collection), ]; return { location: fileName, @@ -35,9 +37,12 @@ function defineEntity(entity: Entity): string { const propDefinitions = entity.properties.map(p => `\t${p.key}: ${p.type};`).join("\n"); - return `export interface ${entity.name} { - id: number; + return `export interface ${entity.name}Input { ${propDefinitions} +} + +export interface ${entity.name} extends ${entity.name}Input { + id: number; }`; } @@ -50,7 +55,7 @@ export function toPayload(item: ${entity.name}): ${entity.name}Payload { return { ...item, - ref: \`/${collection.name.toLowerCase()}/\${item.id}\`, + ref: \`${collectionRoute(collection)}/\${item.id}\`, } }`; } @@ -60,3 +65,12 @@ return collection.items.find(n => n.id === id); }`; } + +function defineAppendFunction(collection: Collection): string { + return `export function append(collection: ${collection.name}, item: ${collection.entities.name}Input): ${collection.entities.name} { + const newItem: ${collection.entities.name} = {...item, id: collection.nextId}; + collection.nextId++; + collection.items.push(newItem); + return newItem; +}`; +} diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/ExampleOutput/Model.ts b/src/ExampleOutput/Model.ts index fe0e9eb..146a8c3 100644 --- a/src/ExampleOutput/Model.ts +++ b/src/ExampleOutput/Model.ts @@ -3,12 +3,15 @@ nextId: number; } -export interface GraphNode { - id: number; +export interface GraphNodeInput { name: string; description: string; } +export interface GraphNode extends GraphNodeInput { + id: number; +} + export interface GraphNodePayload extends GraphNode { ref: string; } @@ -23,3 +26,10 @@ export function findSingle(graph: Graph, id: number) { return graph.nodes.find(n => n.id === id); } + +export function append(collection: Graph, item: GraphNodeInput): GraphNode { + const newItem: GraphNode = {...item, id: collection.nextId}; + collection.nextId++; + collection.nodes.push(newItem); + return newItem; +} diff --git a/src/MiddlewareGenerator.ts b/src/MiddlewareGenerator.ts index 6731183..23d5a4b 100644 --- a/src/MiddlewareGenerator.ts +++ b/src/MiddlewareGenerator.ts @@ -1,7 +1,7 @@ import { Description, FileWrite } from "./app"; import { dataAccessModuleName } from "./DataAccessGenerator"; import { modelModuleName } from "./ModelGenerator"; -import { Collection } from "./MySchema"; +import { Collection, Entity, Property } from "./MySchema"; import { collectionRoute } from "./OpenApiGenerator"; export function middlewareModuleName(collection: Collection): string { @@ -16,6 +16,8 @@ const definitions = [ defineImports(collection), defineCreateMiddlewareFunction(collection), + defineGeneralFunctions, + defineInputParsers(collection.entities), ]; return { location: middlewareModuleName(collection) + ".ts", @@ -26,20 +28,22 @@ function defineImports(collection: Collection): string { return `import express from "express"; import * as DataAccess from "./${dataAccessModuleName(collection)}"; -import { findSingle, toPayload } from "./${modelModuleName(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}/:id(\\d+)", (req, res) => { - const id = Number.parseInt(req.params.id, undefined); + 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"); @@ -53,6 +57,117 @@ 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 { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: \`Required member "\${fieldName}" could not be found.\` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = 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);`; } \ No newline at end of file diff --git a/src/ModelGenerator.ts b/src/ModelGenerator.ts index 147ca0e..f4afba6 100644 --- a/src/ModelGenerator.ts +++ b/src/ModelGenerator.ts @@ -1,5 +1,6 @@ import { FileWrite } from "./app"; import { Collection, Entity } from "./MySchema"; +import { collectionRoute } from "./OpenApiGenerator"; export function generateModel(collections: Collection[]): FileWrite[] { @@ -18,6 +19,7 @@ defineEntity(collection.entities), definePayload(collection), defineFindFunction(collection), + defineAppendFunction(collection), ]; return { location: fileName, @@ -35,9 +37,12 @@ function defineEntity(entity: Entity): string { const propDefinitions = entity.properties.map(p => `\t${p.key}: ${p.type};`).join("\n"); - return `export interface ${entity.name} { - id: number; + return `export interface ${entity.name}Input { ${propDefinitions} +} + +export interface ${entity.name} extends ${entity.name}Input { + id: number; }`; } @@ -50,7 +55,7 @@ export function toPayload(item: ${entity.name}): ${entity.name}Payload { return { ...item, - ref: \`/${collection.name.toLowerCase()}/\${item.id}\`, + ref: \`${collectionRoute(collection)}/\${item.id}\`, } }`; } @@ -60,3 +65,12 @@ return collection.items.find(n => n.id === id); }`; } + +function defineAppendFunction(collection: Collection): string { + return `export function append(collection: ${collection.name}, item: ${collection.entities.name}Input): ${collection.entities.name} { + const newItem: ${collection.entities.name} = {...item, id: collection.nextId}; + collection.nextId++; + collection.items.push(newItem); + return newItem; +}`; +} diff --git a/src/OpenApiGenerator.ts b/src/OpenApiGenerator.ts index b59cb56..c060754 100644 --- a/src/OpenApiGenerator.ts +++ b/src/OpenApiGenerator.ts @@ -6,19 +6,28 @@ [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; }; -function generateSchemas(entities: Entity[]): APISchemas { - return fromEntites(entities.map(generateSchema)); +function flatten(acc: T[], val: T[]): T[] { + return acc.concat(val); } -function generateSchema(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject] { - const schema: OpenAPIV3.NonArraySchemaObject = { +function generateSchemas(entities: Entity[]): APISchemas { + return fromEntites(entities.map(generateEntitySchemas).reduce(flatten, [])); +} + +function generateEntitySchemas(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject][] { + const payloadSchema: OpenAPIV3.NonArraySchemaObject = { type: "object", required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), }; + const inputSchema: OpenAPIV3.NonArraySchemaObject = { + type: "object", + required: entity.properties.filter(p => !p.isNullable).map(p => p.key), + properties: fromEntites(entity.properties.map(generateProperty)), + }; return [ - entity.name, - schema, + [entity.name, payloadSchema], + [entity.name + "Input", inputSchema], ]; } @@ -64,12 +73,14 @@ const route = collectionRoute(collection); const path: OpenAPIV3.PathItemObject = { get: generateGetAllOperation(collection), + post: generateCreateOperation(collection), }; return [route, path]; } function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], responses: { "200": { description: `Lists all ${collection.name}`, @@ -89,10 +100,43 @@ return operation; } +function generateCreateOperation(collection: Collection): OpenAPIV3.OperationObject { + const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], + requestBody: { + required: true, + description: "Data for the new " + collection.entities.name, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/" + collection.entities.name + "Input" + } + } + } + }, + responses: { + "201": { + description: `Creates a new ${collection.entities.name}`, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/" + collection.entities.name + } + } + } + }, + "400": { + $ref: "#/components/responses/BadRequest", + }, + } + } + return operation; +} + function generateEntityPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { const route = itemRoute(collection); const path: OpenAPIV3.PathItemObject = { - get: generateGetSpecificOperation(collection.entities), + get: generateGetSpecificOperation(collection), }; return [route, path]; } @@ -105,8 +149,10 @@ return entity.name.toLowerCase() + "Id"; } -function generateGetSpecificOperation(entity: Entity): OpenAPIV3.OperationObject { +function generateGetSpecificOperation(collection: Collection): OpenAPIV3.OperationObject { + const entity: Entity = collection.entities; const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], parameters: [ { in: "path", @@ -148,6 +194,18 @@ } }; +const BadRequestResponse: OpenAPIV3.ResponseObject = { + description: "The supplied data in invalid. See the response text for more information.", + content: { + "text/plain": { + schema: { + type: "string", + example: "A required field is missing.", + } + } + } +} + export function generateOpenAPI(description: Description): OpenAPIV3.Document { const doc: OpenAPIV3.Document = { openapi: "3.0.0", @@ -159,7 +217,8 @@ components: { schemas: generateSchemas(description.collections.map(c => c.entities)), responses: { - NotFound: NotFoundResponse + NotFound: NotFoundResponse, + BadRequest: BadRequestResponse } }, paths: generatePaths(description.collections), diff --git a/src/DataAccessGenerator.ts b/src/DataAccessGenerator.ts index 4fc8142..96ef56a 100644 --- a/src/DataAccessGenerator.ts +++ b/src/DataAccessGenerator.ts @@ -53,7 +53,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { @@ -81,7 +81,7 @@ return `function parse${entity.name}(data: any): ${entity.name} { return { - id: parsePrimitiveProperty<${entity.name}, number>("int", "id", 0, data), + id: parsePrimitiveProperty<${entity.name}, number>("number", "id", 0, data), ${propParser} }; }`; @@ -94,7 +94,7 @@ function defineParseCollection(collection: Collection): string { return `function parse${collection.name}(data: any): ${collection.name} { return { - nextId: parsePrimitiveProperty<${collection.name}, number>("int", "nextId", 1, data), + nextId: parsePrimitiveProperty<${collection.name}, number>("number", "nextId", 1, data), items: parseArrayProperty<${collection.name}, ${collection.entities.name}>("items", parse${collection.entities.name}, data), }; }`; diff --git a/src/ExampleOutput/DataAccess.ts b/src/ExampleOutput/DataAccess.ts index f98ce06..a25cac4 100644 --- a/src/ExampleOutput/DataAccess.ts +++ b/src/ExampleOutput/DataAccess.ts @@ -15,7 +15,7 @@ function parseGraph(data: any): Graph { return { - nextId: parsePrimitiveProperty("int", "nextId", 1, data), + nextId: parsePrimitiveProperty("number", "nextId", 1, data), nodes: parseArrayProperty("nodes", parseGraphNode, data), }; } @@ -23,7 +23,7 @@ function parseGraphNode(data: any): GraphNode { return { - id: parsePrimitiveProperty("int", "id", 0, data), + id: parsePrimitiveProperty("number", "id", 0, data), name: parsePrimitiveProperty("string", "name", "", data), description: parsePrimitiveProperty("string", "description", "", data), }; @@ -42,7 +42,7 @@ return []; } -function parsePrimitiveProperty(type: T extends string ? "string" : "int", fieldName: keyof O, defaultValue: T, obj: any): T +export function parsePrimitiveProperty(type: T extends string ? "string" : "number", fieldName: keyof O, defaultValue: T, obj: any): T { if(fieldName in obj) { diff --git a/src/ExampleOutput/Middleware.ts b/src/ExampleOutput/Middleware.ts index 2a50fc3..bfd59d5 100644 --- a/src/ExampleOutput/Middleware.ts +++ b/src/ExampleOutput/Middleware.ts @@ -1,6 +1,83 @@ import express from "express"; import * as DataAccess from "./DataAccess"; -import { findSingle, toPayload } from "./Model"; +import { append, findSingle, GraphNodeInput, toPayload } from "./Model"; +import { Result } from "../ExampleOutput/Result"; + +function parseNotNull(data: any): Result { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: `Required member "${fieldName}" could not be found.` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = members[key]; + if (!member.isSuccessful) { + return member; + } + obj[key] = member.value; + } + } + return { isSuccessful: true, value: (obj as T) }; +} + +const parseName = compose(parseRequiredMember("name"), parseString); +const parseDescription = compose(parseRequiredMember("description"), parseString); + +function parseGraphNodeInput(data: any): Result { + const obj = andThen(parseNotNull(data), parseObject); + const members: ParseObject = { + name: andThen(obj, parseName), + description: andThen(obj, parseDescription), + }; + return resolve(members); +} export function graphMiddleware({ fileName }: { fileName: string }) { const graph = DataAccess.load(fileName); @@ -23,5 +100,24 @@ res.send(nodes.map(toPayload)); }); + route.post("/nodes", (req, res) => { + try { + const parsed = parseGraphNodeInput(req.body); + if(!parsed.isSuccessful) { + res.status(400).send(parsed.errorMessage); + return; + } + const item = parsed.value; + const added = append(graph, item); + DataAccess.save(fileName, graph); + const payload = toPayload(added); + res.status(201).send(payload); + } catch (error) { + console.error(error); + res.status(500).send("Internal Server Error."); + return; + } + }); + return route; } diff --git a/src/ExampleOutput/Model.ts b/src/ExampleOutput/Model.ts index fe0e9eb..146a8c3 100644 --- a/src/ExampleOutput/Model.ts +++ b/src/ExampleOutput/Model.ts @@ -3,12 +3,15 @@ nextId: number; } -export interface GraphNode { - id: number; +export interface GraphNodeInput { name: string; description: string; } +export interface GraphNode extends GraphNodeInput { + id: number; +} + export interface GraphNodePayload extends GraphNode { ref: string; } @@ -23,3 +26,10 @@ export function findSingle(graph: Graph, id: number) { return graph.nodes.find(n => n.id === id); } + +export function append(collection: Graph, item: GraphNodeInput): GraphNode { + const newItem: GraphNode = {...item, id: collection.nextId}; + collection.nextId++; + collection.nodes.push(newItem); + return newItem; +} diff --git a/src/MiddlewareGenerator.ts b/src/MiddlewareGenerator.ts index 6731183..23d5a4b 100644 --- a/src/MiddlewareGenerator.ts +++ b/src/MiddlewareGenerator.ts @@ -1,7 +1,7 @@ import { Description, FileWrite } from "./app"; import { dataAccessModuleName } from "./DataAccessGenerator"; import { modelModuleName } from "./ModelGenerator"; -import { Collection } from "./MySchema"; +import { Collection, Entity, Property } from "./MySchema"; import { collectionRoute } from "./OpenApiGenerator"; export function middlewareModuleName(collection: Collection): string { @@ -16,6 +16,8 @@ const definitions = [ defineImports(collection), defineCreateMiddlewareFunction(collection), + defineGeneralFunctions, + defineInputParsers(collection.entities), ]; return { location: middlewareModuleName(collection) + ".ts", @@ -26,20 +28,22 @@ function defineImports(collection: Collection): string { return `import express from "express"; import * as DataAccess from "./${dataAccessModuleName(collection)}"; -import { findSingle, toPayload } from "./${modelModuleName(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}/:id(\\d+)", (req, res) => { - const id = Number.parseInt(req.params.id, undefined); + 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"); @@ -53,6 +57,117 @@ 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 { + if (data == null) return { isSuccessful: false, errorMessage: "Input is null." }; + else return { isSuccessful: true, value: data }; +} + +function parseObject(data: any): Result { + if (typeof data === "object") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Input is not an object." }; +} + +const parseRequiredMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: false, errorMessage: \`Required member "\${fieldName}" could not be found.\` }; + } +} + +const parseNullableMember = (fieldName: keyof T) => (data: any): Result => { + if (fieldName in data) { + return { isSuccessful: true, value: data[fieldName] }; + } + else { + return { isSuccessful: true, value: null }; + } +} + +function parseString(data: any): Result { + if (data == null || typeof data === "string") return { isSuccessful: true, value: data }; + else return { isSuccessful: false, errorMessage: "Value is not a string." }; +} + +function andThen(result: Result, nextFn: (data: T) => Result): Result { + if (!result.isSuccessful) return result; + return nextFn(result.value); +} + +type ParseObject = { + [P in keyof T]: Result +}; + +function compose(first: (obj:R) => Result, second: (obj: S) => Result): (obj:R) => Result { + return (obj:R): Result => { + const firstResult = first(obj); + if(!firstResult.isSuccessful) return firstResult; + return second(firstResult.value); + }; +} + +function resolve(members: ParseObject): Result { + const obj: Partial = {}; + for (const key in members) { + if (Object.prototype.hasOwnProperty.call(members, key)) { + const member: Result]> = 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);`; } \ No newline at end of file diff --git a/src/ModelGenerator.ts b/src/ModelGenerator.ts index 147ca0e..f4afba6 100644 --- a/src/ModelGenerator.ts +++ b/src/ModelGenerator.ts @@ -1,5 +1,6 @@ import { FileWrite } from "./app"; import { Collection, Entity } from "./MySchema"; +import { collectionRoute } from "./OpenApiGenerator"; export function generateModel(collections: Collection[]): FileWrite[] { @@ -18,6 +19,7 @@ defineEntity(collection.entities), definePayload(collection), defineFindFunction(collection), + defineAppendFunction(collection), ]; return { location: fileName, @@ -35,9 +37,12 @@ function defineEntity(entity: Entity): string { const propDefinitions = entity.properties.map(p => `\t${p.key}: ${p.type};`).join("\n"); - return `export interface ${entity.name} { - id: number; + return `export interface ${entity.name}Input { ${propDefinitions} +} + +export interface ${entity.name} extends ${entity.name}Input { + id: number; }`; } @@ -50,7 +55,7 @@ export function toPayload(item: ${entity.name}): ${entity.name}Payload { return { ...item, - ref: \`/${collection.name.toLowerCase()}/\${item.id}\`, + ref: \`${collectionRoute(collection)}/\${item.id}\`, } }`; } @@ -60,3 +65,12 @@ return collection.items.find(n => n.id === id); }`; } + +function defineAppendFunction(collection: Collection): string { + return `export function append(collection: ${collection.name}, item: ${collection.entities.name}Input): ${collection.entities.name} { + const newItem: ${collection.entities.name} = {...item, id: collection.nextId}; + collection.nextId++; + collection.items.push(newItem); + return newItem; +}`; +} diff --git a/src/OpenApiGenerator.ts b/src/OpenApiGenerator.ts index b59cb56..c060754 100644 --- a/src/OpenApiGenerator.ts +++ b/src/OpenApiGenerator.ts @@ -6,19 +6,28 @@ [key: string]: OpenAPIV3.ReferenceObject | OpenAPIV3.ArraySchemaObject | OpenAPIV3.NonArraySchemaObject; }; -function generateSchemas(entities: Entity[]): APISchemas { - return fromEntites(entities.map(generateSchema)); +function flatten(acc: T[], val: T[]): T[] { + return acc.concat(val); } -function generateSchema(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject] { - const schema: OpenAPIV3.NonArraySchemaObject = { +function generateSchemas(entities: Entity[]): APISchemas { + return fromEntites(entities.map(generateEntitySchemas).reduce(flatten, [])); +} + +function generateEntitySchemas(entity: Entity): [string, OpenAPIV3.NonArraySchemaObject][] { + const payloadSchema: OpenAPIV3.NonArraySchemaObject = { type: "object", required: entity.properties.filter(p => !p.isNullable).map(p => p.key).concat(["id", "ref"]), properties: fromEntites(addIdProperties(entity.properties.map(generateProperty))), }; + const inputSchema: OpenAPIV3.NonArraySchemaObject = { + type: "object", + required: entity.properties.filter(p => !p.isNullable).map(p => p.key), + properties: fromEntites(entity.properties.map(generateProperty)), + }; return [ - entity.name, - schema, + [entity.name, payloadSchema], + [entity.name + "Input", inputSchema], ]; } @@ -64,12 +73,14 @@ const route = collectionRoute(collection); const path: OpenAPIV3.PathItemObject = { get: generateGetAllOperation(collection), + post: generateCreateOperation(collection), }; return [route, path]; } function generateGetAllOperation(collection: Collection): OpenAPIV3.OperationObject { const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], responses: { "200": { description: `Lists all ${collection.name}`, @@ -89,10 +100,43 @@ return operation; } +function generateCreateOperation(collection: Collection): OpenAPIV3.OperationObject { + const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], + requestBody: { + required: true, + description: "Data for the new " + collection.entities.name, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/" + collection.entities.name + "Input" + } + } + } + }, + responses: { + "201": { + description: `Creates a new ${collection.entities.name}`, + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/" + collection.entities.name + } + } + } + }, + "400": { + $ref: "#/components/responses/BadRequest", + }, + } + } + return operation; +} + function generateEntityPath(collection: Collection): [string, OpenAPIV3.PathItemObject] { const route = itemRoute(collection); const path: OpenAPIV3.PathItemObject = { - get: generateGetSpecificOperation(collection.entities), + get: generateGetSpecificOperation(collection), }; return [route, path]; } @@ -105,8 +149,10 @@ return entity.name.toLowerCase() + "Id"; } -function generateGetSpecificOperation(entity: Entity): OpenAPIV3.OperationObject { +function generateGetSpecificOperation(collection: Collection): OpenAPIV3.OperationObject { + const entity: Entity = collection.entities; const operation: OpenAPIV3.OperationObject = { + tags: [collection.name], parameters: [ { in: "path", @@ -148,6 +194,18 @@ } }; +const BadRequestResponse: OpenAPIV3.ResponseObject = { + description: "The supplied data in invalid. See the response text for more information.", + content: { + "text/plain": { + schema: { + type: "string", + example: "A required field is missing.", + } + } + } +} + export function generateOpenAPI(description: Description): OpenAPIV3.Document { const doc: OpenAPIV3.Document = { openapi: "3.0.0", @@ -159,7 +217,8 @@ components: { schemas: generateSchemas(description.collections.map(c => c.entities)), responses: { - NotFound: NotFoundResponse + NotFound: NotFoundResponse, + BadRequest: BadRequestResponse } }, paths: generatePaths(description.collections), diff --git a/src/server.ts b/src/server.ts index 367d5a4..e230ff5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,10 +1,11 @@ import express from "express"; import { graphMiddleware } from "./ExampleOutput/Middleware"; -import {join} from "path"; +import { join } from "path"; import { charactersMiddleware } from "./TestOutput/CharactersMiddleware"; import * as swaggerUi from "swagger-ui-express"; import * as swaggerDocument from "./ExampleOutput/openapi.json"; import * as swaggerDocumentTest from "./TestOutput/openapi.json"; +import bodyParser from "body-parser"; const port = 8080; const webRoot = "/"; @@ -14,8 +15,10 @@ const app = express(); app.disable("x-powered-by"); -app.use(graphMiddleware({fileName: join(__dirname, "..", "src", "ExampleOutput", "graph.json")})) -app.use(charactersMiddleware({fileName: join(__dirname, "..", "src", "TestOutput", "characters.json")})) +app.use(bodyParser.json()); + +app.use(graphMiddleware({ fileName: join(__dirname, "..", "src", "ExampleOutput", "graph.json") })) +app.use(charactersMiddleware({ fileName: join(__dirname, "..", "src", "TestOutput", "characters.json") })) app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument));