Merge branch 'linked-records' of github.com:Budibase/budibase into linked-records
This commit is contained in:
commit
9a9430e0cf
|
@ -19,12 +19,18 @@ exports.find = async function(ctx) {
|
||||||
exports.save = async function(ctx) {
|
exports.save = async function(ctx) {
|
||||||
const instanceId = ctx.user.instanceId
|
const instanceId = ctx.user.instanceId
|
||||||
const db = new CouchDB(instanceId)
|
const db = new CouchDB(instanceId)
|
||||||
|
const oldModelId = ctx.request.body._id
|
||||||
const modelToSave = {
|
const modelToSave = {
|
||||||
type: "model",
|
type: "model",
|
||||||
_id: newid(),
|
_id: newid(),
|
||||||
views: {},
|
views: {},
|
||||||
...ctx.request.body,
|
...ctx.request.body,
|
||||||
}
|
}
|
||||||
|
// get the model in its previous state for differencing
|
||||||
|
let oldModel = null
|
||||||
|
if (oldModelId) {
|
||||||
|
oldModel = await db.get(oldModelId)
|
||||||
|
}
|
||||||
|
|
||||||
// rename record fields when table column is renamed
|
// rename record fields when table column is renamed
|
||||||
const { _rename } = modelToSave
|
const { _rename } = modelToSave
|
||||||
|
@ -70,8 +76,11 @@ exports.save = async function(ctx) {
|
||||||
// update linked records
|
// update linked records
|
||||||
await linkRecords.updateLinks({
|
await linkRecords.updateLinks({
|
||||||
instanceId,
|
instanceId,
|
||||||
eventType: linkRecords.EventType.MODEL_SAVE,
|
eventType: oldModel
|
||||||
|
? linkRecords.EventType.MODEL_UPDATED
|
||||||
|
: linkRecords.EventType.MODEL_SAVE,
|
||||||
model: modelToSave,
|
model: modelToSave,
|
||||||
|
oldModel: oldModel,
|
||||||
})
|
})
|
||||||
await db.put(designDoc)
|
await db.put(designDoc)
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
const CouchDB = require("../index")
|
const CouchDB = require("../index")
|
||||||
const linkedRecords = require("./index")
|
const { IncludeDocs, getLinkDocuments } = require("./linkUtils")
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new link document structure which can be put to the database. It is important to
|
* Creates a new link document structure which can be put to the database. It is important to
|
||||||
|
@ -62,11 +62,14 @@ class LinkController {
|
||||||
/**
|
/**
|
||||||
* Checks if the model this was constructed with has any linking columns currently.
|
* Checks if the model this was constructed with has any linking columns currently.
|
||||||
* If the model has not been retrieved this will retrieve it based on the eventData.
|
* If the model has not been retrieved this will retrieve it based on the eventData.
|
||||||
|
* @params {object|null} model If a model that is not known to the link controller is to be tested.
|
||||||
* @returns {Promise<boolean>} True if there are any linked fields, otherwise it will return
|
* @returns {Promise<boolean>} True if there are any linked fields, otherwise it will return
|
||||||
* false.
|
* false.
|
||||||
*/
|
*/
|
||||||
async doesModelHaveLinkedFields() {
|
async doesModelHaveLinkedFields(model = null) {
|
||||||
const model = await this.model()
|
if (model == null) {
|
||||||
|
model = await this.model()
|
||||||
|
}
|
||||||
for (let fieldName of Object.keys(model.schema)) {
|
for (let fieldName of Object.keys(model.schema)) {
|
||||||
const { type } = model.schema[fieldName]
|
const { type } = model.schema[fieldName]
|
||||||
if (type === "link") {
|
if (type === "link") {
|
||||||
|
@ -79,12 +82,23 @@ 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(recordId = null) {
|
getRecordLinkDocs(recordId, includeDocs = IncludeDocs.EXCLUDE) {
|
||||||
return linkedRecords.getLinkDocuments({
|
return getLinkDocuments({
|
||||||
instanceId: this._instanceId,
|
instanceId: this._instanceId,
|
||||||
modelId: this._modelId,
|
modelId: this._modelId,
|
||||||
recordId,
|
recordId,
|
||||||
includeDocs: false,
|
includeDocs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function for main getLinkDocuments function - refer to it for functionality.
|
||||||
|
*/
|
||||||
|
getModelLinkDocs(includeDocs = IncludeDocs.EXCLUDE) {
|
||||||
|
return getLinkDocuments({
|
||||||
|
instanceId: this._instanceId,
|
||||||
|
modelId: this._modelId,
|
||||||
|
includeDocs,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -101,7 +115,7 @@ class LinkController {
|
||||||
const record = this._record
|
const record = this._record
|
||||||
const operations = []
|
const operations = []
|
||||||
// get link docs to compare against
|
// get link docs to compare against
|
||||||
const linkVals = await this.getLinkDocs(record._id)
|
const linkVals = await this.getRecordLinkDocs(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]
|
||||||
|
@ -155,15 +169,16 @@ 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 linkDocIds = await this.getLinkDocs(record._id).map(
|
const linkDocs = await this.getRecordLinkDocs(
|
||||||
linkVal => linkVal.id
|
record._id,
|
||||||
|
IncludeDocs.INCLUDE
|
||||||
)
|
)
|
||||||
if (linkDocIds.length === 0) {
|
if (linkDocs.length === 0) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const toDelete = linkDocIds.map(id => {
|
const toDelete = linkDocs.map(doc => {
|
||||||
return {
|
return {
|
||||||
_id: id,
|
...doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -171,6 +186,36 @@ class LinkController {
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove a field from a model as well as any linked records that pertained to it.
|
||||||
|
* @param {string} fieldName The field to be removed from the model.
|
||||||
|
* @returns {Promise<void>} The model has now been updated.
|
||||||
|
*/
|
||||||
|
async removeFieldFromModel(fieldName) {
|
||||||
|
let model = await this.model()
|
||||||
|
let field = model.schema[fieldName]
|
||||||
|
const linkDocs = await this.getModelLinkDocs(IncludeDocs.INCLUDE)
|
||||||
|
let toDelete = linkDocs.filter(linkDoc => {
|
||||||
|
let correctFieldName =
|
||||||
|
linkDoc.doc1.modelId === model._id
|
||||||
|
? linkDoc.doc1.fieldName
|
||||||
|
: linkDoc.doc2.fieldName
|
||||||
|
return correctFieldName === fieldName
|
||||||
|
})
|
||||||
|
await this._db.bulkDocs(
|
||||||
|
toDelete.map(doc => {
|
||||||
|
return {
|
||||||
|
...doc,
|
||||||
|
_deleted: true,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
// remove schema from other model
|
||||||
|
let linkedModel = this._db.get(field.modelId)
|
||||||
|
delete linkedModel[field.fieldName]
|
||||||
|
this._db.put(linkedModel)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a model is saved this will carry out the necessary operations to make sure
|
* When a model is saved this will carry out the necessary operations to make sure
|
||||||
* any linked models are notified and updated correctly.
|
* any linked models are notified and updated correctly.
|
||||||
|
@ -198,6 +243,26 @@ class LinkController {
|
||||||
return model
|
return model
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a model, this means if a field is removed need to handle removing from other table and removing
|
||||||
|
* any link docs that pertained to it.
|
||||||
|
* @param {object} oldModel The model before it was updated which can be used for differencing.
|
||||||
|
* @returns {Promise<Object>} The model which has been saved, same response as with the modelSaved function.
|
||||||
|
*/
|
||||||
|
async modelUpdated(oldModel) {
|
||||||
|
// first start by checking if any link columns have been deleted
|
||||||
|
const newModel = await this.model()
|
||||||
|
for (let fieldName of Object.keys(oldModel.schema)) {
|
||||||
|
const field = oldModel.schema[fieldName]
|
||||||
|
// this field has been removed from the model schema
|
||||||
|
if (field.type === "link" && newModel.schema[fieldName] == null) {
|
||||||
|
await this.removeFieldFromModel(fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// now handle as if its a new save
|
||||||
|
return this.modelSaved()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* When a model is deleted this will carry out the necessary operations to make sure
|
* When a model is deleted this will carry out the necessary operations to make sure
|
||||||
* any linked models have the joining column correctly removed as well as removing any
|
* any linked models have the joining column correctly removed as well as removing any
|
||||||
|
@ -217,14 +282,14 @@ class LinkController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// need to get the full link docs to delete them
|
// need to get the full link docs to delete them
|
||||||
const linkDocIds = await this.getLinkDocs().map(linkVal => linkVal.id)
|
const linkDocs = await this.getModelLinkDocs(IncludeDocs.INCLUDE)
|
||||||
if (linkDocIds.length === 0) {
|
if (linkDocs.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 = linkDocIds.map(id => {
|
const toDelete = linkDocs.map(doc => {
|
||||||
return {
|
return {
|
||||||
_id: id,
|
...doc,
|
||||||
_deleted: true,
|
_deleted: true,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
const LinkController = require("./LinkController")
|
const LinkController = require("./LinkController")
|
||||||
const CouchDB = require("../index")
|
const { IncludeDocs, getLinkDocuments, createLinkView } = require("./linkUtils")
|
||||||
const Sentry = require("@sentry/node")
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This functionality makes sure that when records with links are created, updated or deleted they are processed
|
* This functionality makes sure that when records with links are created, updated or deleted they are processed
|
||||||
|
@ -12,43 +11,15 @@ const EventType = {
|
||||||
RECORD_UPDATE: "record:update",
|
RECORD_UPDATE: "record:update",
|
||||||
RECORD_DELETE: "record:delete",
|
RECORD_DELETE: "record:delete",
|
||||||
MODEL_SAVE: "model:save",
|
MODEL_SAVE: "model:save",
|
||||||
|
MODEL_UPDATED: "model:updated",
|
||||||
MODEL_DELETE: "model:delete",
|
MODEL_DELETE: "model:delete",
|
||||||
}
|
}
|
||||||
|
|
||||||
exports.EventType = EventType
|
exports.EventType = EventType
|
||||||
|
// re-export utils here for ease of use
|
||||||
/**
|
exports.IncludeDocs = IncludeDocs
|
||||||
* Creates the link view for the instance, this will overwrite the existing one, but this should only
|
exports.getLinkDocuments = getLinkDocuments
|
||||||
* be called if it is found that the view does not exist.
|
exports.createLinkView = createLinkView
|
||||||
* @param {string} instanceId The instance to which the view should be added.
|
|
||||||
* @returns {Promise<void>} The view now exists, please note that the next view of this query will actually build it,
|
|
||||||
* so it may be slow.
|
|
||||||
*/
|
|
||||||
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, doc1.recordId], {
|
|
||||||
id: doc2.recordId,
|
|
||||||
fieldName: doc1.fieldName,
|
|
||||||
})
|
|
||||||
emit([doc2.modelId, doc2.recordId], {
|
|
||||||
id: doc1.recordId,
|
|
||||||
fieldName: doc2.fieldName,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}.toString(),
|
|
||||||
}
|
|
||||||
designDoc.views = {
|
|
||||||
...designDoc.views,
|
|
||||||
by_link: view,
|
|
||||||
}
|
|
||||||
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.
|
* Update link documents for a record or model - this is to be called by the API controller when a change is occurring.
|
||||||
|
@ -56,8 +27,9 @@ exports.createLinkView = async instanceId => {
|
||||||
* 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 change is occurring.
|
* @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 {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} 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.
|
||||||
|
* @param {object|null} oldModel If the model is being updated then the old model can be provided for differencing.
|
||||||
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the record for
|
* @returns {Promise<object>} When the update is complete this will respond successfully. Returns the record for
|
||||||
* record operations and the model for model operations.
|
* record operations and the model for model operations.
|
||||||
*/
|
*/
|
||||||
|
@ -67,6 +39,7 @@ exports.updateLinks = async ({
|
||||||
record,
|
record,
|
||||||
modelId,
|
modelId,
|
||||||
model,
|
model,
|
||||||
|
oldModel,
|
||||||
}) => {
|
}) => {
|
||||||
// make sure model ID is set
|
// make sure model ID is set
|
||||||
if (model != null) {
|
if (model != null) {
|
||||||
|
@ -78,7 +51,11 @@ exports.updateLinks = async ({
|
||||||
model,
|
model,
|
||||||
record,
|
record,
|
||||||
})
|
})
|
||||||
if (!(await linkController.doesModelHaveLinkedFields())) {
|
if (
|
||||||
|
!(await linkController.doesModelHaveLinkedFields()) &&
|
||||||
|
(oldModel == null ||
|
||||||
|
!(await linkController.doesModelHaveLinkedFields(oldModel)))
|
||||||
|
) {
|
||||||
return record
|
return record
|
||||||
}
|
}
|
||||||
switch (eventType) {
|
switch (eventType) {
|
||||||
|
@ -89,6 +66,8 @@ exports.updateLinks = async ({
|
||||||
return await linkController.recordDeleted()
|
return await linkController.recordDeleted()
|
||||||
case EventType.MODEL_SAVE:
|
case EventType.MODEL_SAVE:
|
||||||
return await linkController.modelSaved()
|
return await linkController.modelSaved()
|
||||||
|
case EventType.MODEL_UPDATED:
|
||||||
|
return await linkController.modelUpdated(oldModel)
|
||||||
case EventType.MODEL_DELETE:
|
case EventType.MODEL_DELETE:
|
||||||
return await linkController.modelDeleted()
|
return await linkController.modelDeleted()
|
||||||
default:
|
default:
|
||||||
|
@ -114,11 +93,11 @@ exports.attachLinkInfo = async (instanceId, records) => {
|
||||||
// start by getting all the link values for performance reasons
|
// start by getting all the link values for performance reasons
|
||||||
let responses = await Promise.all(
|
let responses = await Promise.all(
|
||||||
records.map(record =>
|
records.map(record =>
|
||||||
exports.getLinkDocuments({
|
getLinkDocuments({
|
||||||
instanceId,
|
instanceId,
|
||||||
modelId: record.modelId,
|
modelId: record.modelId,
|
||||||
recordId: record._id,
|
recordId: record._id,
|
||||||
includeDocs: false,
|
includeDocs: IncludeDocs.EXCLUDE,
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -141,50 +120,3 @@ exports.attachLinkInfo = async (instanceId, records) => {
|
||||||
// otherwise return the first element as there was only one input
|
// otherwise return the first element as there was only one input
|
||||||
return wasArray ? records : records[0]
|
return wasArray ? records : records[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the linking documents, not the linked documents themselves.
|
|
||||||
* @param {string} instanceId The instance in which we are searching for linked records.
|
|
||||||
* @param {string} modelId The model which we are searching for linked records against.
|
|
||||||
* @param {string|null} fieldName The name of column/field which is being altered, only looking for
|
|
||||||
* linking documents that are related to it. If this is not specified then the table level will be assumed.
|
|
||||||
* @param {string|null} recordId The ID of the record which we want to find linking documents for -
|
|
||||||
* if this is not specified then it will assume model or field level depending on whether the
|
|
||||||
* field name has been specified.
|
|
||||||
* @param {boolean|null} includeDocs whether to include docs in the response call, this is considerably slower so only
|
|
||||||
* use this if actually interested in the docs themselves.
|
|
||||||
* @returns {Promise<object[]>} This will return an array of the linking documents that were found
|
|
||||||
* (if any).
|
|
||||||
*/
|
|
||||||
exports.getLinkDocuments = async ({
|
|
||||||
instanceId,
|
|
||||||
modelId,
|
|
||||||
recordId,
|
|
||||||
includeDocs,
|
|
||||||
}) => {
|
|
||||||
const db = new CouchDB(instanceId)
|
|
||||||
let params
|
|
||||||
if (recordId != null) {
|
|
||||||
params = { key: [modelId, recordId] }
|
|
||||||
}
|
|
||||||
// only model is known
|
|
||||||
else {
|
|
||||||
params = { startKey: [modelId], endKey: [modelId, {}] }
|
|
||||||
}
|
|
||||||
params.include_docs = !!includeDocs
|
|
||||||
try {
|
|
||||||
const response = await db.query("database/by_link", params)
|
|
||||||
if (includeDocs) {
|
|
||||||
return response.rows.map(row => row.doc)
|
|
||||||
} else {
|
|
||||||
return response.rows.map(row => row.value)
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
// check if the view doesn't exist, it should for all new instances
|
|
||||||
if (err != null && err.name === "not_found") {
|
|
||||||
await exports.createLinkView(instanceId)
|
|
||||||
} else {
|
|
||||||
Sentry.captureException(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -0,0 +1,91 @@
|
||||||
|
const CouchDB = require("../index")
|
||||||
|
const Sentry = require("@sentry/node")
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Only needed so that boolean parameters are being used for includeDocs
|
||||||
|
* @type {{EXCLUDE: boolean, INCLUDE: boolean}}
|
||||||
|
*/
|
||||||
|
exports.IncludeDocs = {
|
||||||
|
INCLUDE: true,
|
||||||
|
EXCLUDE: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the link view for the instance, this will overwrite the existing one, but this should only
|
||||||
|
* be called if it is found that the view does not exist.
|
||||||
|
* @param {string} instanceId The instance to which the view should be added.
|
||||||
|
* @returns {Promise<void>} The view now exists, please note that the next view of this query will actually build it,
|
||||||
|
* so it may be slow.
|
||||||
|
*/
|
||||||
|
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, doc1.recordId], {
|
||||||
|
id: doc2.recordId,
|
||||||
|
fieldName: doc1.fieldName,
|
||||||
|
})
|
||||||
|
emit([doc2.modelId, doc2.recordId], {
|
||||||
|
id: doc1.recordId,
|
||||||
|
fieldName: doc2.fieldName,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}.toString(),
|
||||||
|
}
|
||||||
|
designDoc.views = {
|
||||||
|
...designDoc.views,
|
||||||
|
by_link: view,
|
||||||
|
}
|
||||||
|
await db.put(designDoc)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the linking documents, not the linked documents themselves.
|
||||||
|
* @param {string} instanceId The instance in which we are searching for linked records.
|
||||||
|
* @param {string} modelId The model which we are searching for linked records against.
|
||||||
|
* @param {string|null} fieldName The name of column/field which is being altered, only looking for
|
||||||
|
* linking documents that are related to it. If this is not specified then the table level will be assumed.
|
||||||
|
* @param {string|null} recordId The ID of the record which we want to find linking documents for -
|
||||||
|
* if this is not specified then it will assume model or field level depending on whether the
|
||||||
|
* field name has been specified.
|
||||||
|
* @param {boolean|null} includeDocs whether to include docs in the response call, this is considerably slower so only
|
||||||
|
* use this if actually interested in the docs themselves.
|
||||||
|
* @returns {Promise<object[]>} This will return an array of the linking documents that were found
|
||||||
|
* (if any).
|
||||||
|
*/
|
||||||
|
exports.getLinkDocuments = async ({
|
||||||
|
instanceId,
|
||||||
|
modelId,
|
||||||
|
recordId,
|
||||||
|
includeDocs,
|
||||||
|
}) => {
|
||||||
|
const db = new CouchDB(instanceId)
|
||||||
|
let params
|
||||||
|
if (recordId != null) {
|
||||||
|
params = { key: [modelId, recordId] }
|
||||||
|
}
|
||||||
|
// only model is known
|
||||||
|
else {
|
||||||
|
params = { startKey: [modelId], endKey: [modelId, {}] }
|
||||||
|
}
|
||||||
|
params.include_docs = !!includeDocs
|
||||||
|
try {
|
||||||
|
const response = await db.query("database/by_link", params)
|
||||||
|
if (includeDocs) {
|
||||||
|
return response.rows.map(row => row.doc)
|
||||||
|
} else {
|
||||||
|
return response.rows.map(row => row.value)
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
// check if the view doesn't exist, it should for all new instances
|
||||||
|
if (err != null && err.name === "not_found") {
|
||||||
|
await exports.createLinkView(instanceId)
|
||||||
|
} else {
|
||||||
|
Sentry.captureException(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue