296 lines
7.8 KiB
TypeScript
296 lines
7.8 KiB
TypeScript
|
|
/* eslint-disable @typescript-eslint/ban-types,@typescript-eslint/no-explicit-any,max-params */
|
||
|
|
import * as Boom from "@hapi/boom";
|
||
|
|
import * as Hapi from "@hapi/hapi";
|
||
|
|
import { CrudRepository } from "../records/crud-repository";
|
||
|
|
import { createResponse } from "../helpers/response";
|
||
|
|
import {
|
||
|
|
PgRecordInfo,
|
||
|
|
UnsavedR,
|
||
|
|
SavedR,
|
||
|
|
KeyType,
|
||
|
|
} from "../records/record-info";
|
||
|
|
|
||
|
|
/**
|
||
|
|
*
|
||
|
|
* A generic controller that handles exposes a [[CrudRepository]] as HTTP
|
||
|
|
* endpoints with full POST, PUT, GET, DELETE semantics.
|
||
|
|
*
|
||
|
|
* The controller yanks the instance of the crud repository out of the request at runtime.
|
||
|
|
* This assumes you're following the pattern exposed with the hapi-pg-promise plugin.
|
||
|
|
*
|
||
|
|
* @typeParam ID The type of the id column
|
||
|
|
* @typeParam T The type of the record
|
||
|
|
*/
|
||
|
|
export abstract class AbstractCrudController<
|
||
|
|
TUnsavedR,
|
||
|
|
TSavedR extends TUnsavedR & IdKeyT,
|
||
|
|
IdKeyT extends object
|
||
|
|
> {
|
||
|
|
/**
|
||
|
|
* @param repoName the key at which the repository for the record can be accessed (that is, request.db[repoName])
|
||
|
|
* @param paramsIdField the placeholder used in the Hapi route for the id of the record
|
||
|
|
* @param dbDecoration the decorated function on the request to use (defaults to request.db())
|
||
|
|
*/
|
||
|
|
|
||
|
|
abstract repoName: string;
|
||
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||
|
|
// @ts-ignore
|
||
|
|
abstract paramsIdField = "id";
|
||
|
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||
|
|
// @ts-ignore
|
||
|
|
abstract dbDecoration = "db";
|
||
|
|
abstract recordType: PgRecordInfo<TUnsavedR, TSavedR, IdKeyT>;
|
||
|
|
|
||
|
|
repo(request: Hapi.Request): CrudRepository<TUnsavedR, TSavedR, IdKeyT> {
|
||
|
|
// @ts-expect-error
|
||
|
|
const db = request[this.dbDecoration];
|
||
|
|
if (!db)
|
||
|
|
throw Boom.badImplementation(
|
||
|
|
`CrudController for table ${this.recordType.tableName} could not find request decoration '${this.dbDecoration}'`
|
||
|
|
);
|
||
|
|
const repo = db()[this.repoName];
|
||
|
|
if (!repo)
|
||
|
|
throw Boom.badImplementation(
|
||
|
|
`CrudController for table ${this.recordType.tableName} could not find repository for '${this.dbDecoration}().${this.repoName}'`
|
||
|
|
);
|
||
|
|
return repo;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates a new record
|
||
|
|
*/
|
||
|
|
public create = async (
|
||
|
|
request: Hapi.Request,
|
||
|
|
toolkit: Hapi.ResponseToolkit
|
||
|
|
): Promise<any> => {
|
||
|
|
try {
|
||
|
|
// would love to know how to get rid of this double cast hack
|
||
|
|
const payload: TSavedR = <TSavedR>(<any>request.payload);
|
||
|
|
const data: TSavedR = await this.repo(request).insert(payload);
|
||
|
|
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
value: data,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} catch (error: any) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.badImplementation(error),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Updates a record by ID. This method can accept partial updates.
|
||
|
|
*/
|
||
|
|
public updateById = async (
|
||
|
|
request: Hapi.Request,
|
||
|
|
toolkit: Hapi.ResponseToolkit
|
||
|
|
): Promise<any> => {
|
||
|
|
try {
|
||
|
|
const payload: Partial<TSavedR> = <any>request.payload;
|
||
|
|
const id: IdKeyT = request.params[this.paramsIdField];
|
||
|
|
const updatedRow: TSavedR = await this.repo(request).updateById(
|
||
|
|
id,
|
||
|
|
payload
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!updatedRow) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.notFound(),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
value: updatedRow,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} catch (error: any) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.badImplementation(error),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return a record given its id.
|
||
|
|
*/
|
||
|
|
public getById = async (
|
||
|
|
request: Hapi.Request,
|
||
|
|
toolkit: Hapi.ResponseToolkit
|
||
|
|
): Promise<any> => {
|
||
|
|
try {
|
||
|
|
const id: IdKeyT = request.params[this.paramsIdField];
|
||
|
|
// @ts-expect-error
|
||
|
|
const row: TSavedR = await this.repo(request).findById(id);
|
||
|
|
|
||
|
|
if (!row) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.notFound(),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
value: row,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} catch (error: any) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.badImplementation(error),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Return all records.
|
||
|
|
*/
|
||
|
|
public getAll = async (
|
||
|
|
request: Hapi.Request,
|
||
|
|
toolkit: Hapi.ResponseToolkit
|
||
|
|
): Promise<any> => {
|
||
|
|
try {
|
||
|
|
const rows: TSavedR[] = await this.repo(request).findAll();
|
||
|
|
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
value: rows,
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} catch (error: any) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.badImplementation(error),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Delete a record given its id.
|
||
|
|
*/
|
||
|
|
public deleteById = async (
|
||
|
|
request: Hapi.Request,
|
||
|
|
toolkit: Hapi.ResponseToolkit
|
||
|
|
): Promise<any> => {
|
||
|
|
try {
|
||
|
|
const id: IdKeyT = request.params[this.paramsIdField];
|
||
|
|
|
||
|
|
const count = await this.repo(request).removeById(id);
|
||
|
|
|
||
|
|
if (count === 0) {
|
||
|
|
return createResponse(request, { boom: Boom.notFound() });
|
||
|
|
}
|
||
|
|
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
value: { id },
|
||
|
|
})
|
||
|
|
);
|
||
|
|
} catch (error: any) {
|
||
|
|
return toolkit.response(
|
||
|
|
createResponse(request, {
|
||
|
|
boom: Boom.badImplementation(error),
|
||
|
|
})
|
||
|
|
);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||
|
|
export function unboundCrudController<TRecordInfo extends PgRecordInfo>(
|
||
|
|
aRecordType: TRecordInfo
|
||
|
|
) {
|
||
|
|
return class CrudController extends AbstractCrudController<
|
||
|
|
UnsavedR<TRecordInfo>,
|
||
|
|
SavedR<TRecordInfo>,
|
||
|
|
KeyType<TRecordInfo>
|
||
|
|
> {
|
||
|
|
public readonly repoName: string;
|
||
|
|
public readonly paramsIdField;
|
||
|
|
public readonly dbDecoration;
|
||
|
|
public readonly recordType = aRecordType;
|
||
|
|
|
||
|
|
constructor(repoName: string, paramsIdField = "id", dbDecoration = "db") {
|
||
|
|
super();
|
||
|
|
this.repoName = repoName;
|
||
|
|
this.paramsIdField = paramsIdField;
|
||
|
|
this.dbDecoration = dbDecoration;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
|
||
|
|
export function CrudControllerBase<Rec extends PgRecordInfo>(recordType: Rec) {
|
||
|
|
return unboundCrudController<Rec>(recordType);
|
||
|
|
}
|
||
|
|
|
||
|
|
export const crudRoutesFor = (
|
||
|
|
name: string,
|
||
|
|
path: string,
|
||
|
|
controller: AbstractCrudController<any, any, any>,
|
||
|
|
idParam: string,
|
||
|
|
validate: Record<string, Hapi.RouteOptionsValidate>
|
||
|
|
): Hapi.ServerRoute[] => [
|
||
|
|
{
|
||
|
|
method: "POST",
|
||
|
|
path: `${path}`,
|
||
|
|
options: {
|
||
|
|
handler: controller.create,
|
||
|
|
validate: validate.create,
|
||
|
|
description: `Method that creates a new ${name}.`,
|
||
|
|
tags: ["api", name],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
method: "PUT",
|
||
|
|
path: `${path}/{${idParam}}`,
|
||
|
|
options: {
|
||
|
|
handler: controller.updateById,
|
||
|
|
validate: validate.updateById,
|
||
|
|
description: `Method that updates a ${name} by its id.`,
|
||
|
|
tags: ["api", name],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
method: "GET",
|
||
|
|
path: `${path}/{${idParam}}`,
|
||
|
|
options: {
|
||
|
|
handler: controller.getById,
|
||
|
|
validate: validate.getById,
|
||
|
|
description: `Method that gets a ${name} by its id.`,
|
||
|
|
tags: ["api", name],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
method: "GET",
|
||
|
|
path: `${path}`,
|
||
|
|
options: {
|
||
|
|
handler: controller.getAll,
|
||
|
|
description: `Method that gets all ${name}s.`,
|
||
|
|
tags: ["api", name],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
{
|
||
|
|
method: "DELETE",
|
||
|
|
path: `${path}/{${idParam}}`,
|
||
|
|
options: {
|
||
|
|
handler: controller.deleteById,
|
||
|
|
validate: validate.deleteById,
|
||
|
|
description: `Method that deletes a ${name} by its id.`,
|
||
|
|
tags: ["api", name],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|