Merge branch 'linked-records' of github.com:Budibase/budibase into linked-records

This commit is contained in:
Andrew Kingston 2020-09-30 16:50:15 +01:00
commit b5d38dd6e4
4 changed files with 104 additions and 101 deletions

View File

@ -86,6 +86,15 @@ exports.save = async function(ctx) {
const existingRecord = record._rev && (await db.get(record._id)) const existingRecord = record._rev && (await db.get(record._id))
// make sure link records are up to date
record = await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_SAVE,
record,
modelId: record.modelId,
model,
})
if (existingRecord) { if (existingRecord) {
const response = await db.put(record) const response = await db.put(record)
record._rev = response.rev record._rev = response.rev
@ -96,13 +105,6 @@ exports.save = async function(ctx) {
return return
} }
record = await linkRecords.updateLinks({
instanceId,
eventType: linkRecords.EventType.RECORD_SAVE,
record,
modelId: record.modelId,
model,
})
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
@ -170,7 +172,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 linkRecords.attachLinkInfoSingleRecord(instanceId, record) ctx.body = await linkRecords.attachLinkInfo(instanceId, record)
} }
exports.destroy = async function(ctx) { exports.destroy = async function(ctx) {
@ -222,11 +224,10 @@ 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) { exports.fetchEnrichedRecord = async function(ctx) {
const instanceId = ctx.user.instanceId const instanceId = ctx.user.instanceId
const db = new CouchDB(instanceId) const db = new CouchDB(instanceId)
const modelId = ctx.params.modelId const modelId = ctx.params.modelId
const fieldName = ctx.params.fieldName
const recordId = ctx.params.recordId const recordId = ctx.params.recordId
if (instanceId == null || modelId == null || recordId == null) { if (instanceId == null || modelId == null || recordId == null) {
ctx.status = 400 ctx.status = 400
@ -237,18 +238,35 @@ exports.fetchLinkedRecords = async function(ctx) {
} }
return return
} }
// // need model to work out where links go in record
const modelAndRecord = await Promise.all([db.get(modelId), db.get(recordId)])
const model = modelAndRecord[0]
const record = modelAndRecord[1]
// get the link docs // get the link docs
const linkDocIds = await linkRecords.getLinkDocuments({ const linkVals = await linkRecords.getLinkDocuments({
instanceId, instanceId,
modelId, modelId,
fieldName,
recordId, recordId,
}) })
// now get the docs from the all docs index // look up the actual records based on the ids
const response = await db.allDocs({ const response = await db.allDocs({
include_docs: true, include_docs: true,
keys: linkDocIds, keys: linkVals.map(linkVal => linkVal.id),
}) })
ctx.body = response.rows.map(row => row.doc) // need to include the IDs in these records for any links they may have
let linkedRecords = await linkRecords.attachLinkInfo(
instanceId,
response.rows.map(row => row.doc)
)
// insert the link records in the correct place throughout the main record
for (let fieldName of Object.keys(model.schema)) {
let field = model.schema[fieldName]
if (field.type === "link") {
record[fieldName] = linkedRecords.filter(
linkRecord => linkRecord.modelId === field.modelId
)
}
}
ctx.body = record
ctx.status = 200 ctx.status = 200
} }

View File

@ -7,14 +7,9 @@ const router = Router()
router router
.get( .get(
"/api/:modelId/:recordId/:fieldName/links", "/api/:modelId/:recordId/enrich",
authorized(READ_MODEL, ctx => ctx.params.modelId), authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchLinkedRecords recordController.fetchEnrichedRecord
)
.get(
"/api/:modelId/:recordId/links",
authorized(READ_MODEL, ctx => ctx.params.modelId),
recordController.fetchLinkedRecords
) )
.get( .get(
"/api/:modelId/records", "/api/:modelId/records",

View File

@ -79,13 +79,12 @@ class LinkController {
/** /**
* Utility function for main getLinkDocuments function - refer to it for functionality. * Utility function for main getLinkDocuments function - refer to it for functionality.
*/ */
getLinkDocs(includeDocs, fieldName = null, recordId = null) { getLinkDocs(recordId = null) {
return linkedRecords.getLinkDocuments({ return linkedRecords.getLinkDocuments({
instanceId: this._instanceId, instanceId: this._instanceId,
modelId: this._modelId, modelId: this._modelId,
fieldName,
recordId, recordId,
includeDocs, includeDocs: false,
}) })
} }
@ -101,6 +100,8 @@ class LinkController {
const model = await this.model() const model = await this.model()
const record = this._record const record = this._record
const operations = [] const operations = []
// get link docs to compare against
const linkVals = await this.getLinkDocs(record._id)
for (let fieldName of Object.keys(model.schema)) { for (let fieldName of Object.keys(model.schema)) {
// get the links this record wants to make // get the links this record wants to make
const recordField = record[fieldName] const recordField = record[fieldName]
@ -110,8 +111,11 @@ class LinkController {
recordField != null && recordField != null &&
recordField.length !== 0 recordField.length !== 0
) { ) {
// get link docs to compare against // check which links actual pertain to the update in this record
const linkDocIds = await this.getLinkDocs(false, fieldName, record._id) let linkDocIds = linkVals.filter(
linkVal => linkVal.fieldName === fieldName
)
linkDocIds = linkDocIds.map(linkVal => linkVal.id)
// iterate through the link IDs in the record field, see if any don't exist already // iterate through the link IDs in the record field, see if any don't exist already
for (let linkId of recordField) { for (let linkId of recordField) {
if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) { if (linkId && linkId !== "" && linkDocIds.indexOf(linkId) === -1) {
@ -135,7 +139,7 @@ class LinkController {
) )
} }
// replace this field with a simple entry to denote there are links // replace this field with a simple entry to denote there are links
record[fieldName] = { type: "link" } delete record[fieldName]
} }
} }
await this._db.bulkDocs(operations) await this._db.bulkDocs(operations)
@ -151,13 +155,15 @@ class LinkController {
async recordDeleted() { async recordDeleted() {
const record = this._record const record = this._record
// need to get the full link docs to be be able to delete it // need to get the full link docs to be be able to delete it
const linkDocs = await this.getLinkDocs(true, null, record._id) const linkDocIds = await this.getLinkDocs(record._id).map(
if (linkDocs.length === 0) { linkVal => linkVal.id
)
if (linkDocIds.length === 0) {
return null return null
} }
const toDelete = linkDocs.map(doc => { const toDelete = linkDocIds.map(id => {
return { return {
...doc, _id: id,
_deleted: true, _deleted: true,
} }
}) })
@ -211,14 +217,14 @@ class LinkController {
} }
} }
// need to get the full link docs to delete them // need to get the full link docs to delete them
const linkDocs = await this.getLinkDocs(true) const linkDocIds = await this.getLinkDocs().map(linkVal => linkVal.id)
if (linkDocs.length === 0) { if (linkDocIds.length === 0) {
return null return null
} }
// get link docs for this model and configure for deletion // get link docs for this model and configure for deletion
const toDelete = linkDocs.map(doc => { const toDelete = linkDocIds.map(id => {
return { return {
...doc, _id: id,
_deleted: true, _deleted: true,
} }
}) })

View File

@ -32,10 +32,14 @@ exports.createLinkView = async instanceId => {
if (doc.type === "link") { if (doc.type === "link") {
let doc1 = doc.doc1 let doc1 = doc.doc1
let doc2 = doc.doc2 let doc2 = doc.doc2
emit([doc1.modelId, 1, doc1.fieldName, doc1.recordId], doc2.recordId) emit([doc1.modelId, doc1.recordId], {
emit([doc2.modelId, 1, doc2.fieldName, doc2.recordId], doc1.recordId) id: doc2.recordId,
emit([doc1.modelId, 2, doc1.recordId], doc2.recordId) fieldName: doc1.fieldName,
emit([doc2.modelId, 2, doc2.recordId], doc1.recordId) })
emit([doc2.modelId, doc2.recordId], {
id: doc1.recordId,
fieldName: doc2.fieldName,
})
} }
}.toString(), }.toString(),
} }
@ -92,62 +96,50 @@ exports.updateLinks = async ({
} }
} }
/**
* 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.attachLinkInfoSingleRecord(instanceId, record))
}
return await Promise.all(recordPromises)
}
/** /**
* Update a record with information about the links that pertain to it. * 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 {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). * @param {object} records The record(s) themselves which is to be updated with info (if applicable). This can be
* @returns {Promise<object>} The updated record (this may be the same if no links were found). * a single record object or an array of records - both will be handled.
* @returns {Promise<object>} The updated record (this may be the same if no links were found). If an array was input
* then an array will be output, object input -> object output.
*/ */
exports.attachLinkInfoSingleRecord = async (instanceId, record) => { exports.attachLinkInfo = async (instanceId, records) => {
// first check if the record has any link fields and set counts to zero // handle a single record as well as multiple
let hasLinkedRecords = false let wasArray = true
for (let fieldName of Object.keys(record)) { if (!(records instanceof Array)) {
let field = record[fieldName] records = [records]
if (field != null && field.type === "link") { wasArray = false
hasLinkedRecords = true
field.count = 0
} }
} // start by getting all the link values for performance reasons
// no linked records, can simply return let responses = await Promise.all(
if (!hasLinkedRecords) { records.map(record =>
return record exports.getLinkDocuments({
}
const recordId = record._id
const modelId = record.modelId
// get all links for record, ignore fieldName for now
const linkDocs = await exports.getLinkDocuments({
instanceId, instanceId,
modelId, modelId: record.modelId,
recordId, recordId: record._id,
includeDocs: true, includeDocs: false,
}) })
if (linkDocs == null || linkDocs.length === 0) { )
return record )
} // can just use an index to access responses, order maintained
for (let linkDoc of linkDocs) { let index = 0
// now iterate through the records and all field information
for (let record of records) {
// get all links for record, ignore fieldName for now
const linkVals = responses[index++]
for (let linkVal of linkVals) {
// 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 if (!(record[linkVal.fieldName] instanceof Array)) {
if (record[doc.fieldName] == null || isNaN(record[doc.fieldName].count)) { record[linkVal.fieldName] = [linkVal.id]
record[doc.fieldName] = { type: "link", count: 1 }
} else { } else {
record[doc.fieldName].count++ record[linkVal.fieldName].push(linkVal.id)
} }
} }
return record }
// if it was an array when it came in then handle it as an array in response
// otherwise return the first element as there was only one input
return wasArray ? records : records[0]
} }
/** /**
@ -167,25 +159,17 @@ exports.attachLinkInfoSingleRecord = async (instanceId, record) => {
exports.getLinkDocuments = async ({ exports.getLinkDocuments = async ({
instanceId, instanceId,
modelId, modelId,
fieldName,
recordId, recordId,
includeDocs, includeDocs,
}) => { }) => {
const db = new CouchDB(instanceId) const db = new CouchDB(instanceId)
let params let params
if (fieldName != null && recordId != null) { if (recordId != null) {
params = { key: [modelId, 1, fieldName, recordId] } params = { key: [modelId, recordId] }
} else if (fieldName != null && recordId == null) {
params = {
startKey: [modelId, 1, fieldName],
endKey: [modelId, 1, fieldName, {}],
}
} else if (fieldName == null && recordId != null) {
params = { key: [modelId, 2, recordId] }
} }
// only model is known // only model is known
else { else {
params = { startKey: [modelId, 1], endKey: [modelId, 1, {}] } params = { startKey: [modelId], endKey: [modelId, {}] }
} }
params.include_docs = !!includeDocs params.include_docs = !!includeDocs
try { try {