implementation of knex crud handler

This commit is contained in:
Timo Hocker 2020-04-17 11:03:15 +02:00
parent 374940757a
commit 8b8fbb3ec5
6 changed files with 235 additions and 45 deletions

5
lib/HttpHandler.ts Normal file
View File

@ -0,0 +1,5 @@
import { Router } from 'express';
export class HttpHandler {
public abstract register_handlers(router: Router): void;
}

View File

@ -1,39 +1,30 @@
import { Request, Response } from 'express'; import { Request, Response, Router } from 'express';
import { http } from '@scode/consts'; import { http } from '@scode/consts';
import { try_parse_json } from '@scode/utilities';
import { KnexCrudOptions } from './KnexCrudOptions'; import { KnexCrudOptions } from './KnexCrudOptions';
import { CrudHandler } from './CrudHandler'; 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 table: string;
protected columns: Array<string>; protected columns: Array<string>;
protected options: KnexCrudOptions; protected options: KnexCrudOptions;
protected knex: Knex;
public constructor ( public constructor (
table: string, table: string,
columns: Array<string>, columns: Array<string>,
options: KnexCrudOptions = {} options: KnexCrudOptions = {}
) { ) {
super ();
this.table = table; this.table = table;
this.columns = columns; this.columns = columns;
this.options = options; this.options = options;
if (this.columns.filter ((val) => val.toLowerCase () === 'id').length > 0) {
throw new Error (
'the column id cannot be made available to modification'
);
} }
private call_auth (
auth?: Function,
req: Request,
res: Response
): Promise<boolean> {
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;
} }
protected validate_body ( protected validate_body (
@ -42,32 +33,138 @@ export class KnexCrudHandler implements CrudHandler {
): Promise<object> | object { ): Promise<object> | object {
if (typeof req.body === 'undefined') { if (typeof req.body === 'undefined') {
res.status (http.status_bad_request); res.status (http.status_bad_request);
res.end (); res.end ('body was undefined');
return null; 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>|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<void> { public async create (req: Request, res: Response): Promise<void> {
if (!await this.call_auth (this.options.create_authentication, req, res)) if (!await this.options.create_authorization (req, res))
return;
if (!await this.call_auth (this.options.create_authorization, req, res))
return; return;
const body_data = await this.validate_body (req, res); const body_data = await this.validate_body (req, res);
if (body_data === null) if (body_data === null)
return; 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<void> { public async read (req: Request, res: Response): Promise<void> {
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<void> { public async update (req: Request, res: Response): Promise<void> {
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<void> { public async delete (req: Request, res: Response): Promise<void> {
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<void> {
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);
} }
} }

View File

@ -1,25 +1,88 @@
import { Request, Response } from 'express'; import { Request, Response } from 'express';
type Authentication = {
(req: Request, res: Response, next: Function);
(req: Request, res: Response): Promise<boolean>;
}
type Authorization = { type Authorization = {
(req: Request, res: Response, next: Function); (req: Request, res: Response, next: Function);
(req: Request, res: Response): Promise<boolean>; (req: Request, res: Response): Promise<boolean>;
} }
export class KnexCrudOptions { type AuthRunner = {
public create_authentication?: Authentication; (req: Request, res: Response): Promise<boolean>;
public read_authentication?: Authentication; }
public update_authentication?: Authentication;
public delete_authentication?: Authentication;
public create_authorization?: Authorization; export class KnexCrudOptions {
public read_authorization?: Authorization; private _general_authorization?: Authorization;
public update_authorization?: Authorization; private _create_authorization?: Authorization;
public delete_authorization?: Authorization; private _read_authorization?: Authorization;
private _update_authorization?: Authorization;
private _delete_authorization?: Authorization;
public optional_columns?: Array<string>; public optional_columns?: Array<string>;
private get_auth_runner (
auth?: Authorization
): AuthRunner {
if (typeof auth === 'undefined')
return (): Promise<boolean> => new Promise ((r) => r (true));
return (): Promise<boolean> => 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<boolean> => {
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<boolean> => {
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<boolean> => {
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<boolean> => {
const res = (await general (req, res)) && (await specific (req, res));
return res;
};
}
public set delete_authorization (value: Authorization): void{
this._delete_authorization = value;
}
} }

31
lib/KnexInterface.ts Normal file
View File

@ -0,0 +1,31 @@
/* eslint-disable @typescript-eslint/naming-convention */
interface KnexQuery {
insert(data: object): Promise;
update(data: object): Promise;
select(...columns: Array<string>): 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<string>): KnexQuery;
}
type Knex = {
(table: string): KnexQuery;
}
export { Knex, KnexQuery };

View File

@ -29,7 +29,6 @@
}, },
"dependencies": { "dependencies": {
"@scode/consts": "^1.1.4", "@scode/consts": "^1.1.4",
"@scode/utilities": "^1.0.21",
"@types/express": "^4.17.6", "@types/express": "^4.17.6",
"express": "^4.17.1" "express": "^4.17.1"
} }

View File

@ -155,11 +155,6 @@
eslint-plugin-node "^11.0.0" eslint-plugin-node "^11.0.0"
eslint-plugin-sort-requires-by-path "^1.0.2" 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": "@stryker-mutator/api@^3.1.0":
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/@stryker-mutator/api/-/api-3.1.0.tgz#7eb6f1e1f2af17ff0425c6aa0d4d244ca822e972" resolved "https://registry.yarnpkg.com/@stryker-mutator/api/-/api-3.1.0.tgz#7eb6f1e1f2af17ff0425c6aa0d4d244ca822e972"