diff options
Diffstat (limited to 'src/app/db/service/pivot/methods.js')
| -rw-r--r-- | src/app/db/service/pivot/methods.js | 330 |
1 files changed, 330 insertions, 0 deletions
diff --git a/src/app/db/service/pivot/methods.js b/src/app/db/service/pivot/methods.js new file mode 100644 index 0000000..1f54345 --- /dev/null +++ b/src/app/db/service/pivot/methods.js @@ -0,0 +1,330 @@ +/** + * Pivot Table Service API Methods + * @module app/db/service/pivot/methods + */ + +import * as db from "app/db/query"; +import { + buildPaginationResponse, + getOffsetAndLimit, + getSort, + getQueryFilters, + reduceValidColumns, + tableNameToModelName, +} from "app/db/helpers"; +import { + indexPivotTable, + getPivotModels, + handleCreateOne, + handleCreateMany, +} from "app/db/service/pivot/helpers"; +import { PERMISSIONS } from "app/constants"; +import debugModule from "debug"; + +/** + * Debug logger + */ +const debug = debugModule("shoebox:service:pivot"); + +/** + * API to index a model via a pivot table. Allows pagination, filtering. + */ +export function index(service) { + const { queryBuilder } = service; + const { pivotColumns, paginate } = service.options; + const { + parent, + Model, + ChildModel, + pivotTableName, + childTableName, + parentIdAttribute, + childIdAttribute, + parentPivotRelation, + childRelation, + } = getPivotModels(service); + + const pivotAttribute = Model.prototype.idAttribute.replace(/_id$/, ""); + // console.log(pivotIdAttribute, pivotAttribute); + + return async function indexPivotMiddleware(request, response, next) { + const { query } = request; + const withRelated = query.related ? query.related.split(",") : []; + + const filters = getQueryFilters(query); + const hasFilters = Object.keys(filters).length > 0; + const { offset, limit } = getOffsetAndLimit(query, paginate); + const { sortField, sortDirection, sortCount } = getSort(query, ChildModel); + + // Fetch the immediate parent of the pivot table based on the name of the parent resource. + // This instance is added to the `request.parents` object when performing the permissions check. + const parentInstance = request.parents[parent.resource]; + + // Fetch the children of the pivot table. + let childData; + + if (sortCount) { + // Sort by the count of a secondary pivot relation on the child + const sortTableName = childTableName + "_" + sortField; + const sortOrderName = sortField + "_count"; + const sortTableModel = request.bookshelf.model( + tableNameToModelName(childTableName + "_" + sortField) + ); + if (!sortTableModel) { + return next(new Error("No such pivot table")); + } + let query = request.bookshelf.knex + .from(childTableName) + .select( + childTableName + ".*", + request.bookshelf + .knex(sortTableName) + .count("*") + .whereRaw("?? = ??", [ + sortTableName + "." + childIdAttribute, + childTableName + "." + childIdAttribute, + ]) + .as(sortOrderName) + ) + .leftJoin( + pivotTableName, + pivotTableName + "." + childIdAttribute, + childTableName + "." + childIdAttribute + ) + .where( + pivotTableName + "." + parentIdAttribute, + "=", + parentInstance.id + ); + + if (hasFilters) { + query.andWhere((builder) => + queryBuilder(builder, pivotColumns, filters) + ); + } + + childData = await query + .orderBy(sortOrderName, sortDirection) + .offset(offset) + .limit(limit); + } else { + // Typical sort + childData = await parentInstance + .related(childRelation) + .query((builder) => { + if (hasFilters) { + builder.where((builder) => + queryBuilder(builder, pivotColumns, filters) + ); + } + builder.orderBy(sortField, sortDirection); + if (limit) { + builder.limit(limit); + } + if (offset) { + builder.offset(offset); + } + return builder; + }) + .fetch({ withRelated }); + + // Fetch the pivot table in case there are any necessary values on it + await Promise.all( + childData.map(async (item) => { + item.set(pivotAttribute, await item.pivot.fetch()); + }) + ); + } + + // Count the pivot table and generate pagination + let rowCount; + if (hasFilters) { + rowCount = await parentInstance + .related(childRelation) + .where((builder) => queryBuilder(builder, pivotColumns, filters)) + .count(); + } else { + rowCount = await parentInstance.related(parentPivotRelation).count(); + } + const pagination = buildPaginationResponse({ rowCount, query, paginate }); + + response.locals = { data: childData, pagination, query }; + next(); + }; +} + +/** + * API to fetch a single relation via the pivot table. + */ +export function show(service) { + const { + parent, + Model, + parentIdAttribute, + childIdAttribute, + pivotChildRelation, + } = getPivotModels(service); + + return async function showPivotMiddleware(request, response, next) { + const { query, parents } = request; + const parentInstance = parents[parent.resource]; + const withRelated = query.related ? query.related.split(",") : []; + + let child, data; + + try { + data = await db.show({ + Model: Model, + field: childIdAttribute, + objectID: parseInt(request.params.id), + criteria: { + [parentIdAttribute]: parentInstance.get(parentIdAttribute), + }, + withRelated, + }); + } catch (error) { + debug(`${service.resource} Error fetching pivot`); + debug(error); + return next(error); + } + + if (!data) { + response.locals = {}; + next(); + } + + try { + child = await data.related(pivotChildRelation).fetch(); + } catch (error) { + debug(`${service.resource} Error fetching child`); + debug(error); + return next(error); + } + + response.locals = { child, data }; + next(); + }; +} + +/** + * API to insert a new record + */ +export function create(service) { + const { Model, parentIdAttribute, childIdAttribute } = getPivotModels( + service + ); + return async function createPivotMiddleware(request, response, next) { + const { params } = request; + let data; + const body = reduceValidColumns( + request.body, + service.columns, + service.options.privateFields + ); + if (service.options.parent) { + service.idAttributes.forEach((idAttribute) => { + if (idAttribute in params && idAttribute in service.columns) { + body[idAttribute] = parseInt(params[idAttribute]); + } + }); + } + try { + const instances = await indexPivotTable({ + Model, + parentIdAttribute, + parentId: body[parentIdAttribute], + childIdAttribute, + childId: body[childIdAttribute], + }); + if (Array.isArray(body[childIdAttribute])) { + data = await handleCreateMany({ + Model: service.Model, + data: body, + childIdAttribute, + instances, + }); + } else { + data = await handleCreateOne({ + Model: service.Model, + data: body, + instances, + }); + } + } catch (error) { + debug(`${service.resource} create error`); + console.error(error); + return next(error); + } + response.locals = { data }; + next(); + }; +} + +/** + * API to update a single record + */ +export function update(service) { + return async function updatePivotMiddleware(request, response, next) { + const { data: instance } = response.locals; + const { user, permission } = request; + if ( + permission === PERMISSIONS.ALLOW_FOR_OWNER && + instance.get("user_id") !== user.user_id + ) { + return next(new Error("PermissionsError")); + } + const body = reduceValidColumns( + request.body, + service.columns, + service.options.privateFields + ); + let data; + try { + data = await db.update({ + instance, + data: body, + }); + } catch (error) { + debug(`${service.resource} update error`); + debug(error); + next(new Error(error)); + } + response.locals = { data }; + next(); + }; +} + +/** + * API to destroy a pivot table relation. + */ +export function destroy(service) { + const { Model, parentIdAttribute, childIdAttribute } = getPivotModels( + service + ); + return async function destroyPivotMiddleware(request, response, next) { + const idsToDelete = request.params.id || request.body[childIdAttribute]; + try { + const instances = await indexPivotTable({ + Model, + parentIdAttribute, + parentId: request.params[parentIdAttribute], + childIdAttribute, + childId: idsToDelete, + }); + await Promise.all(instances.map((instance) => instance.destroy())); + } catch (error) { + debug(`${service.resource} destroy error`); + debug(error); + return next(new Error(error)); + } + response.locals.success = true; + response.locals.id = idsToDelete; + next(); + }; +} + +/** + * API to destroy many records. Note that the normal destroy API accomplishes this. + * @type {Function} + */ +export const destroyMany = destroy; |
