/** * 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;