From 672094b177b481c55a235199da127aed52d8c8c6 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 | 120 ++++++++---------- 4 files changed, 85 insertions(+), 95 deletions(-) diff --git a/packages/server/src/api/controllers/model.js b/packages/server/src/api/controllers/model.js index 909c7b631..f223e0a99 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 a27746b56..7348356a8 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 38ec97cc7..94986c8db 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 6ecd5b5a2..3258df6d1 100644 --- a/packages/server/src/db/linkedRecords/index.js +++ b/packages/server/src/db/linkedRecords/index.js @@ -17,58 +17,55 @@ 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 +exports.createLinkView = async instanceId => { + const db = new CouchDB(instanceId) + const designDoc = await db.get("_design/database") + const view = { + map: function(doc) { + if (doc.type === "link") { + let doc1 = doc.doc1 + let doc2 = doc.doc2 + emit([doc1.modelId, 1, doc1.fieldName, doc1.recordId], doc2.recordId) + emit([doc2.modelId, 1, doc2.fieldName, doc2.recordId], doc1.recordId) + emit([doc1.modelId, 2, doc1.recordId], doc2.recordId) + emit([doc2.modelId, 2, doc2.recordId], doc1.recordId) + } + }.toString(), } - 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." + designDoc.views = { + ...designDoc.views, + by_link: view, } + await db.put(designDoc) } /** - * 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 + * 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 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 {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 that was - * operated upon, cleaned up and prepared for writing to DB. + * @returns {Promise} When the update is complete this will respond successfully. Returns the record for + * record operations and the model for model operations. */ -exports.updateLinksForRecord = async ({ +exports.updateLinks = async ({ eventType, instanceId, record, modelId, model, }) => { - // can't operate without these properties - if (instanceId == null || modelId == null || record == null) { - return record + // make sure model ID is set + if (model != null) { + modelId = model._id } let linkController = new LinkController({ instanceId, @@ -85,6 +82,10 @@ exports.updateLinksForRecord = async ({ 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." } @@ -96,10 +97,10 @@ exports.updateLinksForRecord = async ({ * @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) => { +exports.attachLinkInfo = async (instanceId, records) => { let recordPromises = [] for (let record of records) { - recordPromises.push(exports.attachLinkInfoToRecord(instanceId, record)) + recordPromises.push(exports.attachLinkInfo(instanceId, record)) } return await Promise.all(recordPromises) } @@ -110,7 +111,20 @@ exports.attachLinkInfoToRecords = async (instanceId, records) => { * @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) => { +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 @@ -126,8 +140,8 @@ exports.attachLinkInfoToRecord = async (instanceId, 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 } + if (record[doc.fieldName].count == null) { + record[doc.fieldName].count = 1 } else { record[doc.fieldName].count++ } @@ -135,28 +149,6 @@ exports.attachLinkInfoToRecord = async (instanceId, record) => { return record } -exports.createLinkView = async instanceId => { - const db = new CouchDB(instanceId) - const designDoc = await db.get("_design/database") - const view = { - map: function(doc) { - if (doc.type === "link") { - let doc1 = doc.doc1 - let doc2 = doc.doc2 - emit([doc1.modelId, 1, doc1.fieldName, doc1.recordId], doc2.recordId) - emit([doc2.modelId, 1, doc2.fieldName, doc2.recordId], doc1.recordId) - emit([doc1.modelId, 2, doc1.recordId], doc2.recordId) - emit([doc2.modelId, 2, doc2.recordId], doc1.recordId) - } - }.toString(), - } - designDoc.views = { - ...designDoc.views, - by_link: view, - } - await db.put(designDoc) -} - /** * Gets the linking documents, not the linked documents themselves. * @param {string} instanceId The instance in which we are searching for linked records.