Browse Source

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.

master
mike12345567 6 years ago
parent
commit
672094b177
  1. 10
      packages/server/src/api/controllers/model.js
  2. 30
      packages/server/src/api/controllers/record.js
  3. 20
      packages/server/src/db/linkedRecords/LinkController.js
  4. 120
      packages/server/src/db/linkedRecords/index.js

10
packages/server/src/api/controllers/model.js

@ -1,6 +1,6 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const { EventType, updateLinksForModel } = require("../../db/linkedRecords") const linkRecords = require("../../db/linkedRecords")
exports.fetch = async function(ctx) { exports.fetch = async function(ctx) {
const db = new CouchDB(ctx.user.instanceId) const db = new CouchDB(ctx.user.instanceId)
@ -66,9 +66,9 @@ exports.save = async function(ctx) {
}, },
} }
// update linked records // update linked records
await updateLinksForModel({ await linkRecords.updateLinks({
instanceId, instanceId,
eventType: EventType.MODEL_SAVE, eventType: linkRecords.EventType.MODEL_SAVE,
model: modelToSave, model: modelToSave,
}) })
await db.put(designDoc) await db.put(designDoc)
@ -99,9 +99,9 @@ exports.destroy = async function(ctx) {
) )
// update linked records // update linked records
await updateLinksForModel({ await linkRecords.updateLinks({
instanceId, instanceId,
eventType: EventType.MODEL_DELETE, eventType: linkRecords.EventType.MODEL_DELETE,
model: modelToDelete, model: modelToDelete,
}) })
// delete the "all" view // delete the "all" view

30
packages/server/src/api/controllers/record.js

@ -1,13 +1,7 @@
const CouchDB = require("../../db") const CouchDB = require("../../db")
const validateJs = require("validate.js") const validateJs = require("validate.js")
const newid = require("../../db/newid") const newid = require("../../db/newid")
const { const linkRecords = require("../../db/linkedRecords")
EventType,
updateLinksForRecord,
getLinkDocuments,
attachLinkInfoToRecord,
attachLinkInfoToRecords,
} = require("../../db/linkedRecords")
validateJs.extend(validateJs.validators.datetime, { validateJs.extend(validateJs.validators.datetime, {
parse: function(value) { parse: function(value) {
@ -46,9 +40,9 @@ exports.patch = async function(ctx) {
} }
// returned record is cleaned and prepared for writing to DB // returned record is cleaned and prepared for writing to DB
record = await updateLinksForRecord({ record = await linkRecords.updateLinks({
instanceId, instanceId,
eventType: EventType.RECORD_UPDATE, eventType: linkRecords.EventType.RECORD_UPDATE,
record, record,
modelId: record.modelId, modelId: record.modelId,
model, model,
@ -102,9 +96,9 @@ exports.save = async function(ctx) {
return return
} }
record = await updateLinksForRecord({ record = await linkRecords.updateLinks({
instanceId, instanceId,
eventType: EventType.RECORD_SAVE, eventType: linkRecords.EventType.RECORD_SAVE,
record, record,
modelId: record.modelId, modelId: record.modelId,
model, model,
@ -140,7 +134,7 @@ exports.fetchView = async function(ctx) {
response.rows = response.rows.map(row => row.doc) 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) { exports.fetchModelRecords = async function(ctx) {
@ -149,7 +143,7 @@ exports.fetchModelRecords = async function(ctx) {
const response = await db.query(`database/all_${ctx.params.modelId}`, { const response = await db.query(`database/all_${ctx.params.modelId}`, {
include_docs: true, include_docs: true,
}) })
ctx.body = await attachLinkInfoToRecords( ctx.body = await linkRecords.attachLinkInfo(
instanceId, instanceId,
response.rows.map(row => row.doc) response.rows.map(row => row.doc)
) )
@ -162,7 +156,7 @@ exports.search = async function(ctx) {
include_docs: true, include_docs: true,
...ctx.request.body, ...ctx.request.body,
}) })
ctx.body = await attachLinkInfoToRecords( ctx.body = await linkRecords.attachLinkInfo(
instanceId, instanceId,
response.rows.map(row => row.doc) 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") ctx.throw(400, "Supplied modelId does not match the records modelId")
return return
} }
ctx.body = await attachLinkInfoToRecord(instanceId, record) ctx.body = await linkRecords.attachLinkInfo(instanceId, record)
} }
exports.destroy = async function(ctx) { 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") ctx.throw(400, "Supplied modelId doesn't match the record's modelId")
return return
} }
await updateLinksForRecord({ await linkRecords.updateLinks({
instanceId, instanceId,
eventType: EventType.RECORD_DELETE, eventType: linkRecords.EventType.RECORD_DELETE,
record, record,
modelId: record.modelId, modelId: record.modelId,
}) })
@ -244,7 +238,7 @@ exports.fetchLinkedRecords = async function(ctx) {
return return
} }
// get the link docs // get the link docs
const linkDocIds = await getLinkDocuments({ const linkDocIds = await linkRecords.getLinkDocuments({
instanceId, instanceId,
modelId, modelId,
fieldName, fieldName,

20
packages/server/src/db/linkedRecords/LinkController.js

@ -102,14 +102,18 @@ class LinkController {
const record = this._record const record = this._record
const operations = [] const operations = []
for (let fieldName of Object.keys(model.schema)) { 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] const field = model.schema[fieldName]
if (field.type === "link") { if (
field.type === "link" &&
recordField != null &&
recordField.length !== 0
) {
// get link docs to compare against // get link docs to compare against
const linkDocIds = await this.getLinkDocs(false, fieldName, record._id) const linkDocIds = await this.getLinkDocs(false, fieldName, record._id)
// get the links this record wants to make // iterate through the link IDs in the record field, see if any don't exist already
const toLinkIds = record[fieldName] for (let linkId of recordField) {
// iterate through them and find any which don't exist, create them
for (let linkId of toLinkIds) {
if (linkDocIds.indexOf(linkId) === -1) { if (linkDocIds.indexOf(linkId) === -1) {
operations.push( operations.push(
new LinkDocument( new LinkDocument(
@ -124,14 +128,14 @@ class LinkController {
} }
// work out any that need to be deleted // work out any that need to be deleted
const toDeleteIds = linkDocIds.filter( const toDeleteIds = linkDocIds.filter(
id => toLinkIds.indexOf(id) === -1 id => recordField.indexOf(id) === -1
) )
operations.concat( operations.concat(
toDeleteIds.map(id => ({ _id: id, _deleted: true })) toDeleteIds.map(id => ({ _id: id, _deleted: true }))
) )
} }
// remove the field from the record, shouldn't store it // replace this field with a simple entry to denote there are links
delete record[fieldName] record[fieldName] = { type: "link" }
} }
} }
await this._db.bulkDocs(operations) await this._db.bulkDocs(operations)

120
packages/server/src/db/linkedRecords/index.js

@ -17,58 +17,55 @@ const EventType = {
exports.EventType = 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. * Creates the link view for the instance, this will overwrite the existing one, but this should only
* @param {EventType} eventType states what type of model change is occurring, means this can be expanded upon in the * be called if it is found that the view does not exist.
* future quite easily (all updates go through one function). * @param {string} instanceId The instance to which the view should be added.
* @param {string} instanceId The ID of the instance in which the model change is occurring. * @returns {Promise<void>} The view now exists, please note that the next view of this query will actually build it,
* @param {object} model The model which is changing, whether it is being deleted, created or updated. * so it may be slow.
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the model that was
* operated upon.
*/ */
exports.updateLinksForModel = async ({ eventType, instanceId, model }) => { exports.createLinkView = async instanceId => {
// can't operate without these properties const db = new CouchDB(instanceId)
if (instanceId == null || model == null) { const designDoc = await db.get("_design/database")
return model const view = {
} map: function(doc) {
let linkController = new LinkController({ if (doc.type === "link") {
instanceId, let doc1 = doc.doc1
modelId: model._id, let doc2 = doc.doc2
model, emit([doc1.modelId, 1, doc1.fieldName, doc1.recordId], doc2.recordId)
}) emit([doc2.modelId, 1, doc2.fieldName, doc2.recordId], doc1.recordId)
if (!(await linkController.doesModelHaveLinkedFields())) { emit([doc1.modelId, 2, doc1.recordId], doc2.recordId)
return model emit([doc2.modelId, 2, doc2.recordId], doc1.recordId)
}
}.toString(),
} }
switch (eventType) { designDoc.views = {
case EventType.MODEL_SAVE: ...designDoc.views,
return await linkController.modelSaved() by_link: view,
case EventType.MODEL_DELETE:
return await linkController.modelDeleted()
default:
throw "Type of event is not known, linked record handler requires update."
} }
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. * Update link documents for a record or model - this is to be called by the API controller when a change is occurring.
* @param {EventType} eventType states what type of record change is occurring, means this can be expanded upon in the * @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). * 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 {string} instanceId The ID of the instance in which the change 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 changed.
* @param {string} modelId The ID of the of the model which is being updated. * * @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. * @param {object|null} model If the model has already been retrieved this can be used to reduce database gets.
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the record that was * @returns {Promise<object>} When the update is complete this will respond successfully. Returns the record for
* operated upon, cleaned up and prepared for writing to DB. * record operations and the model for model operations.
*/ */
exports.updateLinksForRecord = async ({ exports.updateLinks = async ({
eventType, eventType,
instanceId, instanceId,
record, record,
modelId, modelId,
model, model,
}) => { }) => {
// can't operate without these properties // make sure model ID is set
if (instanceId == null || modelId == null || record == null) { if (model != null) {
return record modelId = model._id
} }
let linkController = new LinkController({ let linkController = new LinkController({
instanceId, instanceId,
@ -85,6 +82,10 @@ exports.updateLinksForRecord = async ({
return await linkController.recordSaved() return await linkController.recordSaved()
case EventType.RECORD_DELETE: case EventType.RECORD_DELETE:
return await linkController.recordDeleted() return await linkController.recordDeleted()
case EventType.MODEL_SAVE:
return await linkController.modelSaved()
case EventType.MODEL_DELETE:
return await linkController.modelDeleted()
default: default:
throw "Type of event is not known, linked record handler requires update." 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. * @param {object[]} records A list records to be updated with link info.
* @returns {Promise<object[]>} The updated records (this may be the same if no links were found). * @returns {Promise<object[]>} 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 = [] let recordPromises = []
for (let record of records) { for (let record of records) {
recordPromises.push(exports.attachLinkInfoToRecord(instanceId, record)) recordPromises.push(exports.attachLinkInfo(instanceId, record))
} }
return await Promise.all(recordPromises) 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). * @param {object} record The record itself which is to be updated with info (if applicable).
* @returns {Promise<object>} The updated record (this may be the same if no links were found). * @returns {Promise<object>} 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 recordId = record._id
const modelId = record.modelId const modelId = record.modelId
// get all links for record, ignore fieldName for now // get all links for record, ignore fieldName for now
@ -126,8 +140,8 @@ exports.attachLinkInfoToRecord = async (instanceId, record) => {
for (let linkDoc of linkDocs) { for (let linkDoc of linkDocs) {
// work out which link pertains to this record // work out which link pertains to this record
const doc = linkDoc.doc1.recordId === recordId ? linkDoc.doc1 : linkDoc.doc2 const doc = linkDoc.doc1.recordId === recordId ? linkDoc.doc1 : linkDoc.doc2
if (record[doc.fieldName] == null || record[doc.fieldName].count == null) { if (record[doc.fieldName].count == null) {
record[doc.fieldName] = { type: "link", count: 1 } record[doc.fieldName].count = 1
} else { } else {
record[doc.fieldName].count++ record[doc.fieldName].count++
} }
@ -135,28 +149,6 @@ exports.attachLinkInfoToRecord = async (instanceId, record) => {
return 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. * Gets the linking documents, not the linked documents themselves.
* @param {string} instanceId The instance in which we are searching for linked records. * @param {string} instanceId The instance in which we are searching for linked records.

Loading…
Cancel
Save