Update to make sure all linked record updates occur in sync with the API call.
This commit is contained in:
parent
af31dfc978
commit
bfe2bb5a23
|
@ -1,5 +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")
|
||||||
|
|
||||||
exports.fetch = async function(ctx) {
|
exports.fetch = async function(ctx) {
|
||||||
const db = new CouchDB(ctx.user.instanceId)
|
const db = new CouchDB(ctx.user.instanceId)
|
||||||
|
@ -53,21 +54,6 @@ exports.save = async function(ctx) {
|
||||||
const result = await db.post(modelToSave)
|
const result = await db.post(modelToSave)
|
||||||
modelToSave._rev = result.rev
|
modelToSave._rev = result.rev
|
||||||
|
|
||||||
const { schema } = ctx.request.body
|
|
||||||
for (let key of Object.keys(schema)) {
|
|
||||||
// model has a linked record
|
|
||||||
if (schema[key].type === "link") {
|
|
||||||
// create the link field in the other model
|
|
||||||
const linkedModel = await db.get(schema[key].modelId)
|
|
||||||
linkedModel.schema[modelToSave.name] = {
|
|
||||||
name: modelToSave.name,
|
|
||||||
type: "link",
|
|
||||||
modelId: modelToSave._id,
|
|
||||||
}
|
|
||||||
await db.put(linkedModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
designDoc.views = {
|
designDoc.views = {
|
||||||
...designDoc.views,
|
...designDoc.views,
|
||||||
|
@ -80,6 +66,12 @@ exports.save = async function(ctx) {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
|
// update linked records
|
||||||
|
await updateLinksForModel({
|
||||||
|
instanceId,
|
||||||
|
eventType: EventType.MODEL_SAVE,
|
||||||
|
model: modelToSave,
|
||||||
|
})
|
||||||
|
|
||||||
// syntactic sugar for event emission
|
// syntactic sugar for event emission
|
||||||
modelToSave.modelId = modelToSave._id
|
modelToSave.modelId = modelToSave._id
|
||||||
|
@ -106,20 +98,16 @@ exports.destroy = async function(ctx) {
|
||||||
records.rows.map(record => ({ _id: record.id, _deleted: true }))
|
records.rows.map(record => ({ _id: record.id, _deleted: true }))
|
||||||
)
|
)
|
||||||
|
|
||||||
// Delete linked record fields in dependent models
|
|
||||||
for (let key of Object.keys(modelToDelete.schema)) {
|
|
||||||
const { type, modelId } = modelToDelete.schema[key]
|
|
||||||
if (type === "link") {
|
|
||||||
const linkedModel = await db.get(modelId)
|
|
||||||
delete linkedModel.schema[modelToDelete.name]
|
|
||||||
await db.put(linkedModel)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete the "all" view
|
// delete the "all" view
|
||||||
const designDoc = await db.get("_design/database")
|
const designDoc = await db.get("_design/database")
|
||||||
delete designDoc.views[modelViewId]
|
delete designDoc.views[modelViewId]
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
|
// update linked records
|
||||||
|
await updateLinksForModel({
|
||||||
|
instanceId,
|
||||||
|
eventType: EventType.MODEL_DELETE,
|
||||||
|
model: modelToDelete,
|
||||||
|
})
|
||||||
|
|
||||||
// syntactic sugar for event emission
|
// syntactic sugar for event emission
|
||||||
modelToDelete.modelId = modelToDelete._id
|
modelToDelete.modelId = modelToDelete._id
|
||||||
|
|
|
@ -1,6 +1,11 @@
|
||||||
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 {
|
||||||
|
EventType,
|
||||||
|
updateLinksForRecord,
|
||||||
|
getLinkDocuments,
|
||||||
|
} = require("../../db/linkedRecords")
|
||||||
|
|
||||||
validateJs.extend(validateJs.validators.datetime, {
|
validateJs.extend(validateJs.validators.datetime, {
|
||||||
parse: function(value) {
|
parse: function(value) {
|
||||||
|
@ -41,6 +46,14 @@ exports.patch = async function(ctx) {
|
||||||
const response = await db.put(record)
|
const response = await db.put(record)
|
||||||
record._rev = response.rev
|
record._rev = response.rev
|
||||||
record.type = "record"
|
record.type = "record"
|
||||||
|
await updateLinksForRecord({
|
||||||
|
instanceId,
|
||||||
|
eventType: EventType.RECORD_UPDATE,
|
||||||
|
record,
|
||||||
|
modelId: record.modelId,
|
||||||
|
model,
|
||||||
|
})
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, model)
|
ctx.eventEmitter.emitRecord(`record:update`, instanceId, record, model)
|
||||||
ctx.body = record
|
ctx.body = record
|
||||||
|
@ -89,29 +102,13 @@ exports.save = async function(ctx) {
|
||||||
record.type = "record"
|
record.type = "record"
|
||||||
const response = await db.post(record)
|
const response = await db.post(record)
|
||||||
record._rev = response.rev
|
record._rev = response.rev
|
||||||
|
await updateLinksForRecord({
|
||||||
// create links in other tables
|
instanceId,
|
||||||
for (let key in record) {
|
eventType: EventType.RECORD_SAVE,
|
||||||
if (model.schema[key] && model.schema[key].type === "link") {
|
record,
|
||||||
const linked = await db.allDocs({
|
modelId: record.modelId,
|
||||||
include_docs: true,
|
model,
|
||||||
keys: record[key],
|
})
|
||||||
})
|
|
||||||
|
|
||||||
// add this record to the linked records in attached models
|
|
||||||
const linkedDocs = linked.rows.map(row => {
|
|
||||||
const doc = row.doc
|
|
||||||
return {
|
|
||||||
...doc,
|
|
||||||
[model.name]: doc[model.name]
|
|
||||||
? [...doc[model.name], record._id]
|
|
||||||
: [record._id],
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await db.bulkDocs(linkedDocs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model)
|
ctx.eventEmitter.emitRecord(`record:save`, instanceId, record, model)
|
||||||
|
@ -179,6 +176,13 @@ exports.destroy = async function(ctx) {
|
||||||
}
|
}
|
||||||
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
ctx.body = await db.remove(ctx.params.recordId, ctx.params.revId)
|
||||||
ctx.status = 200
|
ctx.status = 200
|
||||||
|
await updateLinksForRecord({
|
||||||
|
instanceId,
|
||||||
|
eventType: EventType.RECORD_DELETE,
|
||||||
|
record,
|
||||||
|
modelId: record.modelId,
|
||||||
|
})
|
||||||
|
|
||||||
// for automations include the record that was deleted
|
// for automations include the record that was deleted
|
||||||
ctx.record = record
|
ctx.record = record
|
||||||
ctx.eventEmitter &&
|
ctx.eventEmitter &&
|
||||||
|
@ -201,7 +205,7 @@ async function validate({ instanceId, modelId, record, model }) {
|
||||||
model = await db.get(modelId)
|
model = await db.get(modelId)
|
||||||
}
|
}
|
||||||
const errors = {}
|
const errors = {}
|
||||||
for (let fieldName in model.schema) {
|
for (let fieldName of Object.keys(model.schema)) {
|
||||||
const res = validateJs.single(
|
const res = validateJs.single(
|
||||||
record[fieldName],
|
record[fieldName],
|
||||||
model.schema[fieldName].constraints
|
model.schema[fieldName].constraints
|
||||||
|
@ -210,3 +214,28 @@ async function validate({ instanceId, modelId, record, model }) {
|
||||||
}
|
}
|
||||||
return { valid: Object.keys(errors).length === 0, errors }
|
return { valid: Object.keys(errors).length === 0, errors }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exports.fetchLinkedRecords = async function(ctx) {
|
||||||
|
const instanceId = ctx.user.instanceId
|
||||||
|
const modelId = ctx.params.modelId
|
||||||
|
const fieldName = ctx.params.fieldName
|
||||||
|
const recordId = ctx.params.recordId
|
||||||
|
if (instanceId == null || modelId == null || recordId == null) {
|
||||||
|
ctx.status = 400
|
||||||
|
ctx.body = {
|
||||||
|
status: 400,
|
||||||
|
error:
|
||||||
|
"Cannot handle request, URI params have not been successfully prepared.",
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let records = await getLinkDocuments({
|
||||||
|
instanceId,
|
||||||
|
modelId,
|
||||||
|
fieldName,
|
||||||
|
recordId,
|
||||||
|
includeDoc: true,
|
||||||
|
})
|
||||||
|
ctx.status = 200
|
||||||
|
ctx.body = { records: records }
|
||||||
|
}
|
||||||
|
|
|
@ -45,7 +45,7 @@ describe("/models", () => {
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(async (err, res) => {
|
.end(async (err, res) => {
|
||||||
expect(res.res.statusMessage).toEqual("Model TestModel saved successfully.");
|
expect(res.res.statusMessage).toEqual("Model TestModel saved successfully.");
|
||||||
expect(res.body.name).toEqual("TestModel");
|
expect(res.body.name).toEqual("TestModel");
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
@ -201,10 +201,10 @@ describe("/models", () => {
|
||||||
.expect('Content-Type', /json/)
|
.expect('Content-Type', /json/)
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.end(async (_, res) => {
|
.end(async (_, res) => {
|
||||||
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
|
expect(res.res.statusMessage).toEqual(`Model ${testModel._id} deleted.`);
|
||||||
const dependentModel = await getDocument(instance._id, linkedModel._id)
|
const dependentModel = await getDocument(instance._id, linkedModel._id)
|
||||||
expect(dependentModel.schema.TestModel).not.toBeDefined();
|
expect(dependentModel.schema.TestModel).not.toBeDefined();
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,10 @@ function LinkDocument(
|
||||||
fieldName2,
|
fieldName2,
|
||||||
recordId2
|
recordId2
|
||||||
) {
|
) {
|
||||||
|
// build the ID out of unique references to this link document
|
||||||
|
this._id = `${modelId1}/${modelId2}/${fieldName1}/${fieldName2}/${recordId1}/${recordId2}`
|
||||||
|
// required for referencing in view
|
||||||
|
this.type = "link"
|
||||||
this.doc1 = {
|
this.doc1 = {
|
||||||
modelId: modelId1,
|
modelId: modelId1,
|
||||||
fieldName: fieldName1,
|
fieldName: fieldName1,
|
||||||
|
@ -34,18 +38,12 @@ function LinkDocument(
|
||||||
}
|
}
|
||||||
|
|
||||||
class LinkController {
|
class LinkController {
|
||||||
/**
|
constructor({ instanceId, modelId, record, model }) {
|
||||||
* Create a new link controller which can be used to handle link updates for an event.
|
|
||||||
* @param {string} instanceId The instance in which updates will be carried out.
|
|
||||||
* @param {{modelId: string, model: object|undefined, record: object|undefined}} eventData data about
|
|
||||||
* what has occurred to drive this update - events are emitted when an operation that matters occurs.
|
|
||||||
*/
|
|
||||||
constructor(instanceId, eventData) {
|
|
||||||
this._instanceId = instanceId
|
this._instanceId = instanceId
|
||||||
this._db = new CouchDB(instanceId)
|
this._db = new CouchDB(instanceId)
|
||||||
this._modelId = eventData.modelId
|
this._modelId = modelId
|
||||||
this._record = eventData.record
|
this._record = record
|
||||||
this._model = eventData.model
|
this._model = model
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -106,9 +104,8 @@ class LinkController {
|
||||||
const field = model.schema[fieldName]
|
const field = model.schema[fieldName]
|
||||||
if (field.type === "link") {
|
if (field.type === "link") {
|
||||||
// get link docs to compare against
|
// get link docs to compare against
|
||||||
let currentLinkIds = await this.getLinkDocs(fieldName, record._id).map(
|
let linkDocs = await this.getLinkDocs(fieldName, record._id)
|
||||||
doc => doc._id
|
let currentLinkIds = linkDocs.map(doc => doc._id)
|
||||||
)
|
|
||||||
let toLinkIds = record[fieldName]
|
let toLinkIds = record[fieldName]
|
||||||
for (let linkId of toLinkIds) {
|
for (let linkId of toLinkIds) {
|
||||||
if (currentLinkIds.indexOf(linkId) === -1) {
|
if (currentLinkIds.indexOf(linkId) === -1) {
|
||||||
|
@ -144,7 +141,11 @@ class LinkController {
|
||||||
async recordDeleted() {
|
async recordDeleted() {
|
||||||
const record = this._record
|
const record = this._record
|
||||||
// get link docs to compare against
|
// get link docs to compare against
|
||||||
let toDelete = await this.getLinkDocs(null, record._id).map(doc => {
|
let linkDocs = await this.getLinkDocs(null, record._id)
|
||||||
|
if (linkDocs.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let toDelete = linkDocs.map(doc => {
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
|
@ -196,8 +197,13 @@ class LinkController {
|
||||||
await this._db.put(linkedModel)
|
await this._db.put(linkedModel)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// get link docs to compare against
|
||||||
|
let linkDocs = await this.getLinkDocs()
|
||||||
|
if (linkDocs.length === 0) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
// get link docs for this model and configure for deletion
|
// get link docs for this model and configure for deletion
|
||||||
let toDelete = await this.getLinkDocs().map(doc => {
|
let toDelete = linkDocs.map(doc => {
|
||||||
return {
|
return {
|
||||||
...doc,
|
...doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
const emitter = require("../../events")
|
|
||||||
const InMemoryQueue = require("../../utilities/queue/inMemoryQueue")
|
|
||||||
const LinkController = require("./LinkController")
|
const LinkController = require("./LinkController")
|
||||||
const CouchDB = require("../index")
|
const CouchDB = require("../index")
|
||||||
|
|
||||||
|
@ -16,55 +14,85 @@ const EventType = {
|
||||||
MODEL_DELETE: "model:delete",
|
MODEL_DELETE: "model:delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
const linkedRecordQueue = new InMemoryQueue("linkedRecordQueue")
|
module.exports.EventType = EventType
|
||||||
|
|
||||||
function createEmitterCallback(eventName) {
|
/**
|
||||||
emitter.on(eventName, function(event) {
|
* Update link documents for a model - this is to be called by the model controller when a model is being changed.
|
||||||
if (!event || !event.record || !event.record.modelId) {
|
* @param {EventType} eventType states what type of model change is occurring, means this can be expanded upon in the
|
||||||
return
|
* future quite easily (all updates go through one function).
|
||||||
}
|
* @param {string} instanceId The ID of the instance in which the model change is occurring.
|
||||||
linkedRecordQueue.add({
|
* @param {object} model The model which is changing, whether it is being deleted, created or updated.
|
||||||
type: eventName,
|
* @returns {Promise<null>} When the update is complete this will respond successfully.
|
||||||
event,
|
*/
|
||||||
})
|
module.exports.updateLinksForModel = async ({
|
||||||
})
|
eventType,
|
||||||
}
|
instanceId,
|
||||||
|
model,
|
||||||
for (let typeKey of Object.keys(EventType)) {
|
}) => {
|
||||||
createEmitterCallback(EventType[typeKey])
|
|
||||||
}
|
|
||||||
|
|
||||||
linkedRecordQueue.process(async job => {
|
|
||||||
let data = job.data
|
|
||||||
// can't operate without these properties
|
// can't operate without these properties
|
||||||
if (data.instanceId == null || data.modelId == null) {
|
if (instanceId == null || model == null) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
// link controller exists to help manage state, the operation
|
let linkController = new LinkController({
|
||||||
// of updating links is a particularly stateful task
|
instanceId,
|
||||||
let linkController = new LinkController(data.instanceId, data)
|
modelId: model._id,
|
||||||
// model doesn't have links, can stop here
|
model,
|
||||||
|
})
|
||||||
if (!(await linkController.doesModelHaveLinkedFields())) {
|
if (!(await linkController.doesModelHaveLinkedFields())) {
|
||||||
return
|
return null
|
||||||
}
|
}
|
||||||
// carry out the logic at a top level so that we can handle
|
switch (eventType) {
|
||||||
// multiple operations for a single queue entry if desired
|
|
||||||
switch (data.type) {
|
|
||||||
case EventType.RECORD_SAVE:
|
|
||||||
case EventType.RECORD_UPDATE:
|
|
||||||
await linkController.recordSaved()
|
|
||||||
break
|
|
||||||
case EventType.RECORD_DELETE:
|
|
||||||
await linkController.recordDeleted()
|
|
||||||
break
|
|
||||||
case EventType.MODEL_SAVE:
|
case EventType.MODEL_SAVE:
|
||||||
await linkController.modelSaved()
|
await linkController.modelSaved()
|
||||||
break
|
break
|
||||||
case EventType.MODEL_DELETE:
|
case EventType.MODEL_DELETE:
|
||||||
await linkController.modelDeleted()
|
await linkController.modelDeleted()
|
||||||
break
|
break
|
||||||
|
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<null>} When the update is complete this will respond successfully.
|
||||||
|
*/
|
||||||
|
module.exports.updateLinksForRecord = async ({
|
||||||
|
eventType,
|
||||||
|
instanceId,
|
||||||
|
record,
|
||||||
|
modelId,
|
||||||
|
model,
|
||||||
|
}) => {
|
||||||
|
// can't operate without these properties
|
||||||
|
if (instanceId == null || modelId == null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
let linkController = new LinkController({
|
||||||
|
instanceId,
|
||||||
|
modelId,
|
||||||
|
model,
|
||||||
|
record,
|
||||||
|
})
|
||||||
|
if (!(await linkController.doesModelHaveLinkedFields())) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
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."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the linking documents, not the linked documents themselves.
|
* Gets the linking documents, not the linked documents themselves.
|
||||||
|
@ -104,6 +132,10 @@ module.exports.getLinkDocuments = async ({
|
||||||
params = { startKey: [modelId, 1], endKey: [modelId, 1, {}] }
|
params = { startKey: [modelId, 1], endKey: [modelId, 1, {}] }
|
||||||
}
|
}
|
||||||
params.include_docs = !!includeDoc
|
params.include_docs = !!includeDoc
|
||||||
const response = await db.query("database/by_link", params)
|
try {
|
||||||
return response.rows.map(row => row.doc)
|
const response = await db.query("database/by_link", params)
|
||||||
|
return response.rows.map(row => row.doc)
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue