OK all figured out: please ignore my last two requests: here’s the code, for reference: it parses the rich text field and stores all the referenced entities, then replaces them in-place with their (text) names: it then writes that (replaced) text into a second rich text field (to make it searchable) and then it populates a set of (Database-specific) collections with lists of all the referenced entities of each Database type. Code is currently tied to an action button because there seems no way to detect change of a rich text field:
// Developer reference is at api.fibery.io/#action-buttons
const logging = false
const create_searchable_details = true
const update_collection_fields = true
const create_outcome = false
// Lookup table for mapping typeId to entity Name and corresponding
// Referenced Entity field. Note: this table must be extended
// whenever a new Database is added (if it might be Referenced), or
// modified whenever a Database's Name is updated.
const lookup = {
'e5f3e990-815a-11ec-adbf-35e3df6ae56f': {
'name': 'Tag',
'field': 'Referenced Tags'
},
'53c77c70-7cb5-11ec-bdf4-9d8359eb2d89': {
'name': 'Person',
'field': 'Referenced People'
},
'bdefbc60-7cb6-11ec-bdf4-9d8359eb2d89': {
'name': 'Product',
'field': 'Referenced Products'
},
'ee89f9a0-7cb4-11ec-bdf4-9d8359eb2d89': {
'name': 'Organization',
'field': 'Referenced Organizations'
},
'87c49720-7cdc-11ec-bdf4-9d8359eb2d89': {
'name': 'Trial',
'field': 'Referenced Trials'
},
'54a09420-5dac-11eb-836f-5117371ac1ee': {
'name': 'Task',
'field': 'Referenced Tasks'
},
'8f95e150-70c7-11ec-b97d-972ab98146c5': {
'name': 'Epic',
'field': 'Referenced Epics'
},
'848451a0-78e9-11ec-9cbf-ed2f2cff2041': {
'name': 'Project',
'field': 'Referenced Projects'
},
'ad167630-7014-11ec-9df5-c99d2f84b20b': {
'name': 'Milestone',
'field': 'Referenced Milestones'
},
'efbd97f0-5dac-11eb-836f-5117371ac1ee': {
'name': 'Goal',
'field': 'Referenced Goals'
},
'1ad6b940-7cb4-11ec-bdf4-9d8359eb2d89': {
'name': 'Note',
'field': 'Referenced Notes'
}
}
// Create and initialize a field_names list with the names of the
// collection fields listing the corresponding referenced entities.
const field_names = []
for (var key in lookup) {
field_names.push(lookup[key]['field'])
}
// Recursively traverse the (rich text) document object, storing all
// entity references and then replacing them in-place with the names
// of the referenced entities.
async function traverse1(fibery, parent) {
let is_entity = false
for (let child_key in parent) {
const child = parent[child_key]
if (child_key == "type" && child == "entity") is_entity = true
if (typeof child === "object") {
if (is_entity && child_key == "attrs") {
const id = child['id']
const type_id = child['typeId']
if (logging) console.log("FOUND ENTITY: id: " + id + " type_id: " + type_id)
const inferred_type = lookup[type_id]['name']
const reference = await fibery.getEntityById(inferred_type, id, ['Name'])
found_refs[inferred_type].push(reference)
return reference['Name']
} else {
const name = await traverse1(fibery, child)
if (name !== null) parent[child_key] = { 'type': 'text', 'text': name }
}
}
}
return null
}
async function traverse(fibery, found_refs, parent) {
let is_entity = false
for (let child_key in parent) {
const child = parent[child_key]
if (child_key == "type" && child == "entity") is_entity = true
if (typeof child === "object") {
if (is_entity && child_key == "attrs") {
const id = child['id']
const type_id = child['typeId']
if (logging) console.log("FOUND ENTITY: id: " + id + " type_id: " + type_id)
const inferred_type = lookup[type_id]['name']
const reference = await fibery.getEntityById(inferred_type, id, ['Name'])
found_refs[inferred_type].push(reference)
return reference['Name']
} else {
const name = await traverse(fibery, found_refs, child)
if (name !== null) parent[child_key] = { 'type': 'text', 'text': name }
}
}
}
return null
}
// Remove all entity references in list from designated collection.
async function emptyReferenceList(fibery, entity, ref_type, refs) {
for (const ref of refs) {
if (logging) console.log("REMOVE " + ref_type + " REF: " + ref.Id)
await fibery.removeCollectionItem(entity.type, entity.id, ref_type, ref.Id)
}
}
// Add all entity references in list to designated collection.
async function setReferenceList(fibery, entity, ref_type, refs) {
for (const ref of refs) {
if (logging) console.log("ADD " + ref_type + " REF: " + ref.Id)
await fibery.addCollectionItem(entity.type, entity.id, ref_type, ref.Id)
}
}
// Fibery API is used to retrieve and update entities
const fibery = context.getService('fibery')
// affected entities are stored in args.currentEntities;
// to support batch actions they always come in an array
for (const entity of args.currentEntities) {
// an entity contains all fields apart from collections;
// to access a field refer to it by its UI name
// Note: we could also get a restricted copy of the entity contain only
// the Details field (+ Id) like this (but not necessary):
// const x = await fibery.getEntityById(entity.type, entity.id, ['Details'])
// That is also the ONLY way to get collection fields, eg:
// const entity2 = await fibery.getEntityById(entity.type, entity.id, ['Tags', 'Epic Tags'])
// const tags = entity2['Tags']
// const epic_tags = entity2['Epic Tags']
// We could also use that method to get the *incoming* references (entities
// which have referenced this entity):
// const entity2 = await fibery.getEntityById(entity.type, entity.id, ['References']);
// const references = entity2['References']
// although unfortunately there's no way to get a list of entities that this
// entity references: for that we have to crawl through the rich text fields, as below.
const details = entity['Details']
const searchable_details = entity['Searchable Details']
if (logging) {
console.log("Entity:")
console.log(entity)
console.log("Details:")
console.log(details)
}
const document = await fibery.getDocumentContent(details['secret'], "json")
const entity2 = await fibery.getEntityById(entity.type, entity.id, field_names)
if (logging) {
console.log("Document:")
console.log(document)
console.log("Entity + collection fields:")
console.log(entity2)
}
if (update_collection_fields) {
// Remove existing references (in any case, since if there's
// no rich text then all collection fields should be empty).
for (var key in lookup) {
const field_name = lookup[key]['field']
await emptyReferenceList(fibery, entity, field_name, entity2[field_name])
}
}
if (document != null) {
// Create and initialize a found_refs dictionary with the names of
// the referenced entities.
const found_refs = {}
for (var key in lookup) {
found_refs[lookup[key]['name']] = []
}
// Store all entity references found in document, replacing
// them in-place with the names of the referenced entities.
await traverse(fibery, found_refs, document)
if (logging) {
console.log("Stripped document:")
console.log(document)
}
if (create_searchable_details) {
// Update the search details field with the stripped text.
await fibery.setDocumentContent(searchable_details['secret'], document, "json")
}
if (create_outcome) {
// Update the (non-rich text) outcome field with the
// stripped text (this will strip all remaining structure,
// but the reference names will survive because they've
// already been replaced with text).
await fibery.updateEntity(entity.type, entity.id, {'Outcome': document})
}
if (update_collection_fields) {
// Update all the collection fields with the stored lists of
// referenced entities.
for (var key in lookup) {
const name = lookup[key]['name']
const field_name = lookup[key]['field']
await setReferenceList(fibery, entity, field_name, found_refs[name])
}
}
} else if (create_searchable_details) {
// There's no rich text, so the search text field must be empty.
await fibery.setDocumentContent(searchable_details['secret'], "", "md")
}
}
One remaining thing: it would be nice if I could make the collections that I’m auto-populating read-only (they should only ideally only change when entities are added to or removed from the rich text field, and the user certainly should not be able to modify them, because they would no longer by in sync with the rich text field). I see some references to marking fields as read-only in the API: does that apply to collections (relations)? Is it something that I can do as a one-off (programmatically)? Or even better via the UI?