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.
This commit is contained in:
parent
31943cc66b
commit
ac7374662c
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -17,124 +17,12 @@ 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 }) => {
|
|
||||||
// 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<object>} 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<object[]>} 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<object>} 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 => {
|
exports.createLinkView = async instanceId => {
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
|
@ -157,6 +45,110 @@ exports.createLinkView = async instanceId => {
|
||||||
await db.put(designDoc)
|
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<object>} 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<object[]>} 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<object>} 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.
|
* 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…
Reference in New Issue