From 8b8fbb3ec5db12309baccbbfc2ca228bd468a1bf Mon Sep 17 00:00:00 2001 From: Timo Hocker Date: Fri, 17 Apr 2020 11:03:15 +0200 Subject: [PATCH] implementation of knex crud handler --- lib/HttpHandler.ts | 5 ++ lib/KnexCrudHandler.ts | 147 ++++++++++++++++++++++++++++++++++------- lib/KnexCrudOptions.ts | 91 +++++++++++++++++++++---- lib/KnexInterface.ts | 31 +++++++++ package.json | 1 - yarn.lock | 5 -- 6 files changed, 235 insertions(+), 45 deletions(-) create mode 100644 lib/HttpHandler.ts create mode 100644 lib/KnexInterface.ts diff --git a/lib/HttpHandler.ts b/lib/HttpHandler.ts new file mode 100644 index 0000000..2499f93 --- /dev/null +++ b/lib/HttpHandler.ts @@ -0,0 +1,5 @@ +import { Router } from 'express'; + +export class HttpHandler { + public abstract register_handlers(router: Router): void; +} diff --git a/lib/KnexCrudHandler.ts b/lib/KnexCrudHandler.ts index c83aaf5..9602729 100644 --- a/lib/KnexCrudHandler.ts +++ b/lib/KnexCrudHandler.ts @@ -1,39 +1,30 @@ -import { Request, Response } from 'express'; +import { Request, Response, Router } from 'express'; import { http } from '@scode/consts'; -import { try_parse_json } from '@scode/utilities'; import { KnexCrudOptions } from './KnexCrudOptions'; import { CrudHandler } from './CrudHandler'; +import { Knex } from './KnexInterface'; +import { HttpHandler } from './HttpHandler'; -export class KnexCrudHandler implements CrudHandler { +export class KnexCrudHandler extends HttpHandler implements CrudHandler { protected table: string; protected columns: Array; protected options: KnexCrudOptions; + protected knex: Knex; public constructor ( table: string, columns: Array, options: KnexCrudOptions = {} ) { + super (); this.table = table; this.columns = columns; this.options = options; - } - - private call_auth ( - auth?: Function, - req: Request, - res: Response - ): Promise { - if (typeof auth === 'undefined') - return true; - - const promise = new Promise ((resolve) => { - const result = auth (req, res, resolve); - if (typeof result !== 'undefined') - resolve (result); - }); - - return promise; + if (this.columns.filter ((val) => val.toLowerCase () === 'id').length > 0) { + throw new Error ( + 'the column id cannot be made available to modification' + ); + } } protected validate_body ( @@ -42,32 +33,138 @@ export class KnexCrudHandler implements CrudHandler { ): Promise | object { if (typeof req.body === 'undefined') { res.status (http.status_bad_request); - res.end (); + res.end ('body was undefined'); return null; } - return try_parse_json (req.body); + try { + return JSON.parse (req.body); + } + catch (e) { + res.status (http.status_bad_request); + res.end ('invalid json input'); + } + return null; + } + + protected ensure_data ( + data: object, + res: Response, + fail_on_undef = true + ): Promise|object { + const obj = {}; + const keys = Object.keys (data); + for (const col of this.columns) { + if (!keys.includes (col) && fail_on_undef) { + res.status (http.status_bad_request) + .end (`missing field: ${col}`); + return null; + } + obj[col] = data[col]; + } + for (const col of this.options.optional_columns) + obj[col] = data[col]; + + return obj; } public async create (req: Request, res: Response): Promise { - if (!await this.call_auth (this.options.create_authentication, req, res)) - return; - if (!await this.call_auth (this.options.create_authorization, req, res)) + if (!await this.options.create_authorization (req, res)) return; const body_data = await this.validate_body (req, res); if (body_data === null) return; + + const db_data = await this.ensure_data (body_data, res); + if (db_data === null) + return; + + const inserted = await this.knex (this.table) + .returning ('id') + .insert (db_data); + + res.status (http.status_created) + .end (inserted[0]); } public async read (req: Request, res: Response): Promise { + if (!await this.options.read_authorization (req, res)) + return; + if (typeof req.headers.id === 'undefined') { + res.status (http.status_bad_request) + .end ('id undefined'); + return; + } + + const json = await this.knex (this.table) + .select ([ + 'id', + ...this.columns + ]) + .where ({ id: req.headers.id }); + + res.status (json.length > 0 ? http.status_ok : http.status_not_found) + .json (json[0]); } public async update (req: Request, res: Response): Promise { + if (!await this.options.update_authorization (req, res)) + return; + const body_data = await this.validate_body (req, res); + if (body_data === null) + return; + + const db_data = await this.ensure_data (body_data, res, false); + if (db_data === null) + return; + + if (typeof req.headers.id === 'undefined') { + res.status (http.status_bad_request) + .end ('id undefined'); + return; + } + + await this.knex (this.table) + .where ({ id: req.headers.id }) + .update (db_data); + + res.status (http.status_ok) + .end (inserted[0]); } public async delete (req: Request, res: Response): Promise { + if (!await this.options.delete_authorization (req, res)) + return; + if (typeof req.headers.id === 'undefined') { + res.status (http.status_bad_request) + .end ('id undefined'); + return; + } + + await this.knex (this.table) + .where ({ id: req.headers.id }) + .delete (); + + res.status (http.status_ok) + .end (); + } + + protected async create_or_update ( + req: Request, + res: Response + ): Promise { + if (typeof req.headers.id === 'undefined') + await this.create (req, res); + else + await this.update (req, res); + } + + public register_handlers (router: Router): void { + router.post ('/', this.create_or_update); + router.get ('/', this.read); + router.delete ('/', this.delete); } } diff --git a/lib/KnexCrudOptions.ts b/lib/KnexCrudOptions.ts index 50ef151..905905a 100644 --- a/lib/KnexCrudOptions.ts +++ b/lib/KnexCrudOptions.ts @@ -1,25 +1,88 @@ import { Request, Response } from 'express'; -type Authentication = { - (req: Request, res: Response, next: Function); - (req: Request, res: Response): Promise; -} - type Authorization = { (req: Request, res: Response, next: Function); (req: Request, res: Response): Promise; } -export class KnexCrudOptions { - public create_authentication?: Authentication; - public read_authentication?: Authentication; - public update_authentication?: Authentication; - public delete_authentication?: Authentication; +type AuthRunner = { + (req: Request, res: Response): Promise; +} - public create_authorization?: Authorization; - public read_authorization?: Authorization; - public update_authorization?: Authorization; - public delete_authorization?: Authorization; +export class KnexCrudOptions { + private _general_authorization?: Authorization; + private _create_authorization?: Authorization; + private _read_authorization?: Authorization; + private _update_authorization?: Authorization; + private _delete_authorization?: Authorization; public optional_columns?: Array; + + private get_auth_runner ( + auth?: Authorization + ): AuthRunner { + if (typeof auth === 'undefined') + return (): Promise => new Promise ((r) => r (true)); + return (): Promise => new Promise ((resolve) => { + const result = auth (req, res, resolve); + if (typeof result !== 'undefined') + resolve (result); + }); + } + + public set general_authorization (value: Authorization): void{ + this._general_authorization = value; + } + + public get create_authorization (): AuthRunner { + const general = this.get_auth_runner (this._general_authorization); + const specific = this.get_auth_runner (this._create_authorization); + return async (req, res): Promise => { + const res = (await general (req, res)) && (await specific (req, res)); + return res; + }; + } + + public set create_authorization (value: Authorization): void{ + this._create_authorization = value; + } + + public get read_authorization (): AuthRunner { + const general = this.get_auth_runner (this._general_authorization); + const specific = this.get_auth_runner (this._read_authorization); + return async (req, res): Promise => { + const res = (await general (req, res)) && (await specific (req, res)); + return res; + }; + } + + public set read_authorization (value: Authorization): void{ + this._read_authorization = value; + } + + public get update_authorization (): AuthRunner { + const general = this.get_auth_runner (this._general_authorization); + const specific = this.get_auth_runner (this._update_authorization); + return async (req, res): Promise => { + const res = (await general (req, res)) && (await specific (req, res)); + return res; + }; + } + + public set update_authorization (value: Authorization): void{ + this._update_authorization = value; + } + + public get delete_authorization (): AuthRunner { + const general = this.get_auth_runner (this._general_authorization); + const specific = this.get_auth_runner (this._delete_authorization); + return async (req, res): Promise => { + const res = (await general (req, res)) && (await specific (req, res)); + return res; + }; + } + + public set delete_authorization (value: Authorization): void{ + this._delete_authorization = value; + } } diff --git a/lib/KnexInterface.ts b/lib/KnexInterface.ts new file mode 100644 index 0000000..8b072d8 --- /dev/null +++ b/lib/KnexInterface.ts @@ -0,0 +1,31 @@ +/* eslint-disable @typescript-eslint/naming-convention */ + +interface KnexQuery { + insert(data: object): Promise; + update(data: object): Promise; + select(...columns: Array): Promise; + delete(): Promise; + where(filter: object): KnexQuery; + whereNot(filter: object): KnexQuery; + whereIn(filter: object): KnexQuery; + whereNotIn(filter: object): KnexQuery; + whereNull(filter: object): KnexQuery; + whereNotNull(filter: object): KnexQuery; + whereExists(filter: object): KnexQuery; + whereNotExists(filter: object): KnexQuery; + whereBetween(filter: object): KnexQuery; + whereNotBetween(filter: object): KnexQuery; + join(table: string, col: string, on: string): KnexQuery; + leftJoin(table: string, col: string, on: string): KnexQuery; + rightJoin(table: string, col: string, on: string): KnexQuery; + leftOuterJoin(table: string, col: string, on: string): KnexQuery; + rightOuterJoin(table: string, col: string, on: string): KnexQuery; + fullOuterJoin(table: string, col: string, on: string): KnexQuery; + returning(column: string | Array): KnexQuery; +} + +type Knex = { + (table: string): KnexQuery; +} + +export { Knex, KnexQuery }; diff --git a/package.json b/package.json index 20605fe..1a2b746 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,6 @@ }, "dependencies": { "@scode/consts": "^1.1.4", - "@scode/utilities": "^1.0.21", "@types/express": "^4.17.6", "express": "^4.17.1" } diff --git a/yarn.lock b/yarn.lock index 883b1a2..46ef491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -155,11 +155,6 @@ eslint-plugin-node "^11.0.0" eslint-plugin-sort-requires-by-path "^1.0.2" -"@scode/utilities@^1.0.21": - version "1.0.21" - resolved "https://npm.scode.ovh/@scode%2futilities/-/utilities-1.0.21.tgz#adbc50720515df16fc05bc97b56cb9ba35499e0a" - integrity sha512-2F5mSTN7/2w5YQgamp2nan6q75Nln6bjAVBOvTrrQMbhWkZnieKzgt3g9M8GnsICVefmk5IPguiZhWPVYLmQxw== - "@stryker-mutator/api@^3.1.0": version "3.1.0" resolved "https://registry.yarnpkg.com/@stryker-mutator/api/-/api-3.1.0.tgz#7eb6f1e1f2af17ff0425c6aa0d4d244ca822e972"