import { Request, Response } from "express" import { checkId, create, getStatusCode, update } from "./edit" import { handleError } from "./http" import { LoadController } from "./LoadController" import { Attribute, Attributes, ErrorMessage } from "./metadata" import { resources, StringMap } from "./resources" import { buildAndCheckId, buildId } from "./view" export type Build = (res: Response, obj: T, isCreate?: boolean, isPatch?: boolean) => void export type Validate = (obj: T, resource?: StringMap, patch?: boolean) => Promise export type Save = (obj: T, ctx?: any) => Promise export interface GenericService { metadata?(): Attributes | undefined load(id: ID, ctx?: any): Promise create(obj: T, ctx?: any): Promise update(obj: T, ctx?: any): Promise patch?(obj: Partial, ctx?: any): Promise delete?(id: ID, ctx?: any): Promise } export class GenericController extends LoadController { metadata?: Attributes returnNumber?: boolean constructor( public service: GenericService, public build?: Build, public validate?: Validate, returnNumber?: boolean, ) { super(service) this.returnNumber = returnNumber if (service.metadata) { const m = service.metadata() if (m) { this.metadata = m } } this.create = this.create.bind(this) this.update = this.update.bind(this) this.patch = this.patch.bind(this) this.delete = this.delete.bind(this) if (!validate && resources.createValidator && this.metadata) { const v = resources.createValidator(this.metadata) this.validate = v.validate } } create(req: Request, res: Response): void { validateAndCreate(req, res, this.service.create, this.validate, this.build) } update(req: Request, res: Response): void { const id = buildAndCheckIdWithBody(req, res, this.keys, this.service.update) if (id) { validateAndUpdate(res, req.body, false, this.service.update, this.validate, undefined, this.build) } } patch(req: Request, res: Response): void { const id = buildAndCheckIdWithBody(req, res, this.keys, this.service.patch) if (id && this.service.patch) { validateAndUpdate(res, req.body, true, this.service.patch, this.validate, undefined, this.build) } } delete(req: Request, res: Response): void { const id = buildAndCheckId(req, res, this.keys) if (id) { if (!this.service.delete) { res.status(405).end("Method Not Allowed") } else { this.service .delete(id) .then((count) => { res.status(getDeleteStatus(count)).json(count).end() }) .catch((err) => handleError(err, res)) } } } } export function validateAndCreate(req: Request, res: Response, save: Save, validate?: Validate, build?: Build, returnNumber?: boolean): void { const obj = req.body if (!obj || obj === "") { res.status(400).end("The request body cannot be empty.") } else { if (validate) { validate(obj) .then((errors) => { if (errors && errors.length > 0) { res.status(getStatusCode(errors)).json(errors).end() } else { if (build) { build(res, obj, true) } create(res, obj, save, returnNumber) } }) .catch((err) => handleError(err, res)) } else { create(res, obj, save, returnNumber) } } } export function validateAndUpdate( res: Response, obj: T, isPatch: boolean, save: Save, validate?: Validate, resource?: StringMap, build?: Build, returnNumber?: boolean, ): void { if (validate) { validate(obj, resource, isPatch) .then((errors) => { if (errors && errors.length > 0) { res.status(getStatusCode(errors)).json(errors).end() } else { if (build) { build(res, obj, false, isPatch) } update(res, obj, save, returnNumber) } }) .catch((err) => handleError(err, res)) } else { update(res, obj, save, returnNumber) } } export function buildAndCheckIdWithBody(req: Request, res: Response, keys?: Attribute[], patch?: (obj: T, ctx?: any) => Promise): ID | undefined { const obj = req.body if (!obj || obj === "") { res.status(400).end("The request body cannot be empty.") return undefined } if (!patch) { res.status(405).end("Method Not Allowed") return undefined } const id = buildId(req, keys) if (!id) { res.status(400).end("Invalid parameters") return undefined } const ok = checkId(obj, id, keys) if (!ok) { res.status(400).end("body and url are not matched") return undefined } return id } export function getDeleteStatus(count: number): number { if (count > 0) { return 200 } else if (count === 0) { return 404 } else { return 409 } } export interface ModelConfig { id?: string payload?: string user?: string updatedBy?: string updatedAt?: string createdBy?: string createdAt?: string version?: string } export function useBuild(c: ModelConfig, generate?: () => string): Build { const b = new Builder( generate, c.id ? c.id : "", c.payload ? c.payload : "", c.user ? c.user : "", c.updatedBy ? c.updatedBy : "", c.updatedAt ? c.updatedAt : "", c.createdBy ? c.createdBy : "", c.createdAt ? c.createdAt : "", c.version ? c.version : "", ) return b.build } // tslint:disable-next-line:max-classes-per-file export class Builder { constructor( public generate: (() => string) | undefined, public id: string, public payload: string, public user: string, public updatedBy: string, public updatedAt: string, public createdBy: string, public createdAt: string, public version: string, ) { this.build = this.build.bind(this) } build(res: Response, obj: T, isCreate?: boolean, isPatch?: boolean): void { const o: any = obj let usr = "" if (this.user.length > 0) { if (this.payload.length > 0) { const payload = res.locals[this.payload] if (payload) { usr = payload[this.user] } } else { usr = res.locals[this.user] } } if (!usr) { usr = "" } const now = new Date() if (isCreate) { if (this.generate && this.id.length > 0) { o[this.id] = this.generate() } if (usr.length > 0) { if (this.createdAt.length > 0) { o[this.createdAt] = now } if (this.createdBy.length > 0) { o[this.createdBy] = usr } } if (this.version.length > 0) { o[this.version] = 1 } } else if (isPatch) { const keys = Object.keys(o) if (keys.length === 0) { return } } if (usr.length > 0) { if (this.updatedAt.length > 0) { o[this.updatedAt] = now } if (this.updatedBy.length > 0) { o[this.updatedBy] = usr } } } }