From ac7374662c8b2d73c40f57409c5110a8f8cd2855 Mon Sep 17 00:00:00 2001 From: mike12345567 Date: Tue, 29 Sep 2020 17:22:04 +0100 Subject: [PATCH] A small performance enhancement, storing in the record that it does have links, so that when retrieving info for records it can exit the process early if a record has no mention of links. --- packages/server/src/api/controllers/model.js | 10 +- packages/server/src/api/controllers/record.js | 30 +-- .../src/db/linkedRecords/LinkController.js | 20 +- packages/server/src/db/linkedRecords/index.js | 226 +++++++++--------- 4 files changed, 138 insertions(+), 148 deletions(-) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 909c7b631e..f223e0a99b 100644 --- a/packages/server/src/api/controllers/model.js +++ b/packages/server/src/api/controllers/model.js @@ -1,6 +1,6 @@ const CouchDB = require("../../db") const newid = require("../../db/newid") -const { EventType, updateLinksForModel } = require("../../db/linkedRecords") +const linkRecords = require("../../db/linkedRecords") exports.fetch = async function(ctx) { const db = new CouchDB(ctx.user.instanceId) @@ -66,9 +66,9 @@ exports.save = async function(ctx) { }, } // update linked records - await updateLinksForModel({ + await linkRecords.updateLinks({ instanceId, - eventType: EventType.MODEL_SAVE, + eventType: linkRecords.EventType.MODEL_SAVE, model: modelToSave, }) await db.put(designDoc) @@ -99,9 +99,9 @@ exports.destroy = async function(ctx) { ) // update linked records - await updateLinksForModel({ + await linkRecords.updateLinks({ instanceId, - eventType: EventType.MODEL_DELETE, + eventType: linkRecords.EventType.MODEL_DELETE, model: modelToDelete, }) // delete the "all" view diff --git a/packages/server/src/api/controllers/record.js b/packages/server/src/api/controllers/record.js index a27746b563..7348356a82 100644 --- a/packages/server/src/api/controllers/record.js +++ b/packages/server/src/api/controllers/record.js @@ -1,13 +1,7 @@ const CouchDB = require("../../db") const validateJs = require("validate.js") const newid = require("../../db/newid") -const { - EventType, - updateLinksForRecord, - getLinkDocuments, - attachLinkInfoToRecord, - attachLinkInfoToRecords, -} = require("../../db/linkedRecords") +const linkRecords = require("../../db/linkedRecords") validateJs.extend(validateJs.validators.datetime, { parse: function(value) { @@ -46,9 +40,9 @@ exports.patch = async function(ctx) { } // returned record is cleaned and prepared for writing to DB - record = await updateLinksForRecord({ + record = await linkRecords.updateLinks({ instanceId, - eventType: EventType.RECORD_UPDATE, + eventType: linkRecords.EventType.RECORD_UPDATE, record, modelId: record.modelId, model, @@ -102,9 +96,9 @@ exports.save = async function(ctx) { return } - record = await updateLinksForRecord({ + record = await linkRecords.updateLinks({ instanceId, - eventType: EventType.RECORD_SAVE, + eventType: linkRecords.EventType.RECORD_SAVE, record, modelId: record.modelId, model, @@ -140,7 +134,7 @@ exports.fetchView = async function(ctx) { response.rows = response.rows.map(row => row.doc) } - ctx.body = await attachLinkInfoToRecords(instanceId, response.rows) + ctx.body = await linkRecords.attachLinkInfo(instanceId, response.rows) } exports.fetchModelRecords = async function(ctx) { @@ -149,7 +143,7 @@ exports.fetchModelRecords = async function(ctx) { const response = await db.query(`database/all_${ctx.params.modelId}`, { include_docs: true, }) - ctx.body = await attachLinkInfoToRecords( + ctx.body = await linkRecords.attachLinkInfo( instanceId, response.rows.map(row => row.doc) ) @@ -162,7 +156,7 @@ exports.search = async function(ctx) { include_docs: true, ...ctx.request.body, }) - ctx.body = await attachLinkInfoToRecords( + ctx.body = await linkRecords.attachLinkInfo( instanceId, response.rows.map(row => row.doc) ) @@ -176,7 +170,7 @@ exports.find = async function(ctx) { ctx.throw(400, "Supplied modelId does not match the records modelId") return } - ctx.body = await attachLinkInfoToRecord(instanceId, record) + ctx.body = await linkRecords.attachLinkInfo(instanceId, record) } exports.destroy = async function(ctx) { @@ -187,9 +181,9 @@ exports.destroy = async function(ctx) { ctx.throw(400, "Supplied modelId doesn't match the record's modelId") return } - await updateLinksForRecord({ + await linkRecords.updateLinks({ instanceId, - eventType: EventType.RECORD_DELETE, + eventType: linkRecords.EventType.RECORD_DELETE, record, modelId: record.modelId, }) @@ -244,7 +238,7 @@ exports.fetchLinkedRecords = async function(ctx) { return } // get the link docs - const linkDocIds = await getLinkDocuments({ + const linkDocIds = await linkRecords.getLinkDocuments({ instanceId, modelId, fieldName, diff --git a/packages/server/src/db/linkedRecords/LinkController.js b/packages/server/src/db/linkedRecords/LinkController.js index 38ec97cc7c..94986c8dbc 100644 --- a/packages/server/src/db/linkedRecords/LinkController.js +++ b/packages/server/src/db/linkedRecords/LinkController.js @@ -102,14 +102,18 @@ class LinkController { const record = this._record const operations = [] for (let fieldName of Object.keys(model.schema)) { + // get the links this record wants to make + const recordField = record[fieldName] const field = model.schema[fieldName] - if (field.type === "link") { + if ( + field.type === "link" && + recordField != null && + recordField.length !== 0 + ) { // get link docs to compare against const linkDocIds = await this.getLinkDocs(false, fieldName, record._id) - // get the links this record wants to make - const toLinkIds = record[fieldName] - // iterate through them and find any which don't exist, create them - for (let linkId of toLinkIds) { + // iterate through the link IDs in the record field, see if any don't exist already + for (let linkId of recordField) { if (linkDocIds.indexOf(linkId) === -1) { operations.push( new LinkDocument( @@ -124,14 +128,14 @@ class LinkController { } // work out any that need to be deleted const toDeleteIds = linkDocIds.filter( - id => toLinkIds.indexOf(id) === -1 + id => recordField.indexOf(id) === -1 ) operations.concat( toDeleteIds.map(id => ({ _id: id, _deleted: true })) ) } - // remove the field from the record, shouldn't store it - delete record[fieldName] + // replace this field with a simple entry to denote there are links + record[fieldName] = { type: "link" } } } await this._db.bulkDocs(operations) diff --git a/packages/server/src/db/linkedRecords/index.js b/packages/server/src/db/linkedRecords/index.js index 6ecd5b5a2c..3258df6d15 100644 --- a/packages/server/src/db/linkedRecords/index.js +++ b/packages/server/src/db/linkedRecords/index.js @@ -17,124 +17,12 @@ const EventType = { exports.EventType = EventType /** - * Update link documents for a model - this is to be called by the model controller when a model is being changed. - * @param {EventType} eventType states what type of model change is occurring, means this can be expanded upon in the - * future quite easily (all updates go through one function). - * @param {string} instanceId The ID of the instance in which the model change is occurring. - * @param {object} model The model which is changing, whether it is being deleted, created or updated. - * @returns {Promise} When the update is complete this will respond successfully. Returns the model that was - * operated upon. + * Creates the link view for the instance, this will overwrite the existing one, but this should only + * be called if it is found that the view does not exist. + * @param {string} instanceId The instance to which the view should be added. + * @returns {Promise} The view now exists, please note that the next view of this query will actually build it, + * so it may be slow. */ -exports.updateLinksForModel = async ({ eventType, instanceId, model }) => { - // can't operate without these properties - if (instanceId == null || model == null) { - return model - } - let linkController = new LinkController({ - instanceId, - modelId: model._id, - model, - }) - if (!(await linkController.doesModelHaveLinkedFields())) { - return model - } - switch (eventType) { - case EventType.MODEL_SAVE: - return await linkController.modelSaved() - case EventType.MODEL_DELETE: - return await linkController.modelDeleted() - default: - throw "Type of event is not known, linked record handler requires update." - } -} - -/** - * Update link documents for a record - this is to be called by the record controller when a record is being changed. - * @param {EventType} eventType states what type of record change is occurring, means this can be expanded upon in the - * future quite easily (all updates go through one function). - * @param {string} instanceId The ID of the instance in which the record update is occurring. - * @param {object} record The record which is changing, e.g. created, updated or deleted. - * @param {string} modelId The ID of the of the model which is being updated. - * @param {object|null} model If the model has already been retrieved this can be used to reduce database gets. - * @returns {Promise} When the update is complete this will respond successfully. Returns the record that was - * operated upon, cleaned up and prepared for writing to DB. - */ -exports.updateLinksForRecord = async ({ - eventType, - instanceId, - record, - modelId, - model, -}) => { - // can't operate without these properties - if (instanceId == null || modelId == null || record == null) { - return record - } - let linkController = new LinkController({ - instanceId, - modelId, - model, - record, - }) - if (!(await linkController.doesModelHaveLinkedFields())) { - return record - } - switch (eventType) { - case EventType.RECORD_SAVE: - case EventType.RECORD_UPDATE: - return await linkController.recordSaved() - case EventType.RECORD_DELETE: - return await linkController.recordDeleted() - default: - throw "Type of event is not known, linked record handler requires update." - } -} - -/** - * Utility function to in parallel up a list of records with link info. - * @param {string} instanceId The instance in which this record has been created. - * @param {object[]} records A list records to be updated with link info. - * @returns {Promise} The updated records (this may be the same if no links were found). - */ -exports.attachLinkInfoToRecords = async (instanceId, records) => { - let recordPromises = [] - for (let record of records) { - recordPromises.push(exports.attachLinkInfoToRecord(instanceId, record)) - } - return await Promise.all(recordPromises) -} - -/** - * Update a record with information about the links that pertain to it. - * @param {string} instanceId The instance in which this record has been created. - * @param {object} record The record itself which is to be updated with info (if applicable). - * @returns {Promise} The updated record (this may be the same if no links were found). - */ -exports.attachLinkInfoToRecord = async (instanceId, record) => { - const recordId = record._id - const modelId = record.modelId - // get all links for record, ignore fieldName for now - const linkDocs = await exports.getLinkDocuments({ - instanceId, - modelId, - recordId, - includeDocs: true, - }) - if (linkDocs == null || linkDocs.length === 0) { - return record - } - for (let linkDoc of linkDocs) { - // work out which link pertains to this record - const doc = linkDoc.doc1.recordId === recordId ? linkDoc.doc1 : linkDoc.doc2 - if (record[doc.fieldName] == null || record[doc.fieldName].count == null) { - record[doc.fieldName] = { type: "link", count: 1 } - } else { - record[doc.fieldName].count++ - } - } - return record -} - exports.createLinkView = async instanceId => { const db = new CouchDB(instanceId) const designDoc = await db.get("_design/database") @@ -157,6 +45,110 @@ exports.createLinkView = async instanceId => { await db.put(designDoc) } +/** + * Update link documents for a record or model - this is to be called by the API controller when a change is occurring. + * @param {string} eventType states what type of change which is occurring, means this can be expanded upon in the + * future quite easily (all updates go through one function). + * @param {string} instanceId The ID of the instance in which the change is occurring. + * @param {string} modelId The ID of the of the model which is being changed. + * * @param {object|null} record The record which is changing, e.g. created, updated or deleted. + * @param {object|null} model If the model has already been retrieved this can be used to reduce database gets. + * @returns {Promise} When the update is complete this will respond successfully. Returns the record for + * record operations and the model for model operations. + */ +exports.updateLinks = async ({ + eventType, + instanceId, + record, + modelId, + model, +}) => { + // make sure model ID is set + if (model != null) { + modelId = model._id + } + let linkController = new LinkController({ + instanceId, + modelId, + model, + record, + }) + if (!(await linkController.doesModelHaveLinkedFields())) { + return record + } + switch (eventType) { + case EventType.RECORD_SAVE: + case EventType.RECORD_UPDATE: + return await linkController.recordSaved() + case EventType.RECORD_DELETE: + return await linkController.recordDeleted() + case EventType.MODEL_SAVE: + return await linkController.modelSaved() + case EventType.MODEL_DELETE: + return await linkController.modelDeleted() + default: + throw "Type of event is not known, linked record handler requires update." + } +} + +/** + * Utility function to in parallel up a list of records with link info. + * @param {string} instanceId The instance in which this record has been created. + * @param {object[]} records A list records to be updated with link info. + * @returns {Promise} The updated records (this may be the same if no links were found). + */ +exports.attachLinkInfo = async (instanceId, records) => { + let recordPromises = [] + for (let record of records) { + recordPromises.push(exports.attachLinkInfo(instanceId, record)) + } + return await Promise.all(recordPromises) +} + +/** + * Update a record with information about the links that pertain to it. + * @param {string} instanceId The instance in which this record has been created. + * @param {object} record The record itself which is to be updated with info (if applicable). + * @returns {Promise} The updated record (this may be the same if no links were found). + */ +exports.attachLinkInfo = async (instanceId, record) => { + // first check if the record has any link fields + let hasLinkedRecords = false + for (let fieldName of Object.keys(record)) { + let field = record[fieldName] + if (field != null && field.type === "link") { + hasLinkedRecords = true + break + } + } + // no linked records, can simply return + if (!hasLinkedRecords) { + return record + } + const recordId = record._id + const modelId = record.modelId + // get all links for record, ignore fieldName for now + const linkDocs = await exports.getLinkDocuments({ + instanceId, + modelId, + recordId, + includeDocs: true, + }) + if (linkDocs == null || linkDocs.length === 0) { + return record + } + for (let linkDoc of linkDocs) { + // work out which link pertains to this record + const doc = linkDoc.doc1.recordId === recordId ? linkDoc.doc1 : linkDoc.doc2 + if (record[doc.fieldName].count == null) { + record[doc.fieldName].count = 1 + } else { + record[doc.fieldName].count++ + } + } + return record +} + /** * Gets the linking documents, not the linked documents themselves. * @param {string} instanceId The instance in which we are searching for linked records.