Centralized script management for Workspace

100% agree! A lot of my requests here are directly related to this.

Here are some things I can’t currently do with no-code:

  • “Reach through” a Lookup to update the referenced entity.
  • Change the Status of a related entity.
  • Use References to look up a related entity:
// If a Task's "Project" field is uninitialized, and there is a "Meeting Notes"
// that references the Task, set the Task's "Project" field from the MN's "Default Project".

// Script parameters
const ENTITY_LINK_FIELD = 'Project'    // Task field that needs to be set
const PARENT_TYPE       = 'Project/Project'  // Type we want to find a link to (value for)
const REF_ENTITY_TYPES  = ['Clients/Meeting Notes']  // Types that have "Default Project" field
const REF_ENTITY_LINK_FIELD = 'Default Project'  // Name of the field to copy

const fibery = context.getService('fibery')
// const log = console.log

// Iterate through the entities
for (const entity of args.currentEntities) {
    // Set the Project field if empty
    if (entity[ENTITY_LINK_FIELD].id == null) {
        // Get all references to the Task
        const entityWithRefs = await fibery.getEntityById(entity.type, entity.id, ['References'])
        // Look for a reference to to a type with "Default Project" field (i.e. Meeting Notes)
        for (const ref of entityWithRefs['References']) {
            // Get referencing doc
            const refDoc  = await fibery.getEntityById('Collaboration~Documents/Reference', ref['Id'], ['FromEntityId', 'FromEntityType'])
            const refType = refDoc['FromEntityType'].name
            if (refType == PARENT_TYPE) {
                // The referring entity is the actual PARENT_TYPE (so its ID can be used directly)
                // I.e. a Task was created/referenced in a Project's Rich Text field
                const refEntity = await fibery.getEntityById(refType, refDoc['FromEntityId'], ['Name'])
                // log(` refType == PARENT_TYPE: ${refType}, ${refDoc['FromEntityId']} ${refEntity.id}`)
                await fibery.updateEntity(entity.type, entity.id, { [ENTITY_LINK_FIELD]: refEntity.id })
                break
            } else if (REF_ENTITY_TYPES.includes(refType)) {
                // Found a suitable referring entity (i.e. that has a "Default Project" field)
                const refEntity = await fibery.getEntityById(refType, refDoc['FromEntityId'], ['Name', REF_ENTITY_LINK_FIELD])
                if (refEntity != null && refEntity[REF_ENTITY_LINK_FIELD].id != null) {
                    const refId = refEntity[REF_ENTITY_LINK_FIELD].id
                    await fibery.updateEntity(entity.type, entity.id, { [ENTITY_LINK_FIELD]: refId })
                    break
                }
            }
        }
    }
}
  • Call an external webhook
  • Do something to every entity in a Collection, conditional upon the entity’s State, etc.
  • Trim blanks from a field (I tried a Formula, didn’t work :man_shrugging:)
  • Access an entity’s UUID
  • Treat a Formula field like a Lookup/Reference (if it returns an entity).
  • Do anything with Documents (like create & link).
  • Work with Rich Text:
// Create and link a new Meeting Notes entity to a Client

const log = console.log
const dump = (obj) => JSON.stringify(obj, null, 2)
const fibery = context.getService('fibery')
const MEETING_NOTES_TYPE = 'Clients/Meeting Notes'
const TEMPLATE_DOC_ID = "b62eaad0-e02b-11ec-b6d4-31b05e8bc8ae"
const template_DocName = 'Meeting Notes TEMPLATE'
const fmt = 'html'
function assert(b, msg) { if (!b) throw new Error(msg) }

// Retrieve the contents of the Meeting Notes TEMPLATE Document

const template_Secret = await getDocumentSecret(TEMPLATE_DOC_ID)
assert(template_Secret, `"${template_DocName}" Document not found - ID "${TEMPLATE_DOC_ID}"`)
const template = await fibery.getDocumentContent(template_Secret, fmt)
assert(template, `No content retrieved for "${template_DocName}" Document - ID "${TEMPLATE_DOC_ID}"`)

for (const client of args.currentEntities) {
    // Create a new Meeting Notes entity and link it to the Client
    const meetNotes = await fibery.createEntity(MEETING_NOTES_TYPE, { 'Client ID': client['Public ID'] })

    // Build the new Meeting Notes Description content via mustache macro replacement of the Template's contents
    const macros = {}
    const newDescription = template.replace(/\{\{\s*(\w+)\s*\}\}/g,
        (match, name) => (typeof macros[name] === 'string' ? macros[name] : ''))
    //log('new Description: ' + text)

    // Update the new Meeting Notes Description content
    await fibery.setDocumentContent(meetNotes.Description.Secret, newDescription, fmt)
}

async function getRichTextContent(entity, fieldName, fmt) {
    const secret = entity[fieldName].Secret
    return await fibery.getDocumentContent(secret, fmt)
}

async function getDocumentSecret(docId) {
    const view = await fibery.getEntityById('fibery/view', docId, ['fibery/meta']);
    return view['fibery/meta']['documentSecret'];
}
  • De-duplication (looping, comparing, deleting, etc) – i.e. enforcing the uniqueness of entities by a key value (post-hoc is better than nothing!)
// Search for identical Roles (by Name) and delete all but the oldest one
const fibery = context.getService('fibery')
const entity_type = args.currentEntities[0].type
const SPACE_NAME = 'Users'

for (const entity of args.currentEntities) {
    // Find all identical Roles
    const query = `{findRoles( name:{is: "${entity.name}"} orderBy:{creationDate: ASC}) {id}}`
    const result = await fibery.graphql(SPACE_NAME, query)
    // Delete any duplicates
    if (result && result.data.findRoles.length > 1) {
        const roles = result.data.findRoles
        for (let i = 1; i < roles.length; i++) {
            // console.log(`Deleting: ${roles[i].id}`)
            await fibery.deleteEntity(entity_type, roles[i].id)
        }
    }
}
  • Because no-code Rule conditions can only test an entity’s own fields (i.e. they can’t test the fields of related entities or use arbitrary Formulas), I am forced to bring in more fields from related entities as Lookups/Formulas, and I resist doing this because it litters up my DB’s with all sorts of unwanted fields.

  • The lack of a “Description” or “Comments” area for each DB’s Automations page (and for individual Rules/Buttons) is frustrating. I would really hate to inherit a complex system like this with such little documentation. Scripts at least allow me to do that.

:bulb: Ideally it should be just as easy to “synthesize” a join anywhere (instead of needing to manually construct a new DB to represent it). E.g. in a Table view, have the ability show any arbitrarily related fields (as columns) AND allow the user to edit them; or in automations, allow any related fields (and formulas) to be tested in the Rule conditions (we need formulas there!). I think this ability alone would relieve most of the architecture/code limitation issues I struggle with – big things like lack of polymorphism.

That, plus the ability to hide fields (I know it’s coming), but that’s really more of a band-aid to paper over the need to drag in so many fields with Lookups/Formulas.

6 Likes