Merge branch 'linked-records' of github.com:Budibase/budibase into linked-records
This commit is contained in:
commit
b5d38dd6e4
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
|
@ -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",
|
||||||
|
|
|
@ -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,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue