How I Implemented "Entity Templates"

I’m sharing my implementation of an entity Templating pattern, which has a simple interface to trigger copying the fields from a Template entity to a new entity (including cloning its collections).

The pattern requires adding a “Template” relation (self-related) to a DB, which identifies a Template entity to copy from, and a Rule to do the copying.

I’ll illustrate the implementation using my “Project” DB:

I added the one-to-many self-relation “Template” (called “Template Target” on the other end) to my Project DB. The “Template” end is what you use to link a new Project to a Template Project that you want to copy the fields from, and the “Template Target” end is a collection that is ignored.

image

In a new “target” entity, assigning/linking an entity to the “Template” field triggers a Rule which copies the fields from the linked Template to the target entity where it was linked. A script handles cloning any collections from the Template to the target entity’s corresponding collections (if desired).

Because a “Template entity” is just a another entity, I need a way to distinguish template entities from regular entities. For this I use a generic “Tags” collection (which is used by most of my DBs). I have a Tag called “TEMPLATE”, and linking this Tag to an entity denotes it as a Template entity. This is useful for filtering Template entities from views (or selecting only Template entities).
image

The no-code part of the Rule (“Copy fields from assigned Template”) copies ordinary (non-collection) fields, as well as collections that don’t need to be cloned. The script handles cloning collection entities if needed, and copying Rich Text fields (which actually should probably be done in the no-code part).

Cloning entities from a Template’s collection takes advantage of the same pattern - for each child in a Template’s collection, a new child entity is created and added to the target parent’s collection; then this new child’s “Template” field is linked to its source child (in the Template collection), which causes the same pattern to be triggered, copying the fields from the template child to the new child.

This means the child’s DB also needs to implement the Template pattern, i.e. it must have its own Template relation and Rule. This Rule must be customized for each DB, to copy its fields appropriately.

Script:

// Populate the current "target" entity's collections with clones of the "child entities"
// in the corresponding collections of the linked Template entity.
const fibery = context.getService('fibery')

// Clone entities from these collections of the Template:
const cloneCollections = [
    { collectionName: 'Tasks'  , collectionType: 'Projects/Task' },
    { collectionName: 'Content', collectionType: 'Docs/Content'  },
    { collectionName: 'Pages'  , collectionType: 'Projects/Page' },
]
// NOTE: For collections where you want to link (not clone) the template collection's entities (many-to-*),
// just use the no-code part of the Rule to assign the collection from the Template to the Step-1-Entity -
// just like other (non-collection) fields are copied from the Template.

// Append the contents of the following Rich Text fields from the Template into the current "target" entity:
// (or you could overwrite, instead of appending)
const appendRichTexts = ['Description']

// I use the Tags field to mark an entity as a Template
const TAG_TYPE = 'Types/Tags', TAGS_FIELD = 'Tags'

// newParent is the "target" entity; newParent.Template is the Template entity (of the same type)
// whose contents we want to clone/duplicate into newParent's corresponding fields.
for (const newParent of args.currentEntities) {
    // console.log(`\nnewParent "${newParent.Name}": `, newParent)
    // Get all fields to clone from the Template entity
    // const ASSIGNEES_FIELD = "Assignees"
    const template = await fibery.getEntityById(newParent.type, newParent.Template.Id, [...appendRichTexts, ...cloneCollections.map(e => e.collectionName)]) // ASSIGNEES_FIELD,
    // console.log('\ntemplate: ', template)
    if (!template) {
        console.log(`newParent "${newParent.Name}" has empty Template - skipping`)
        continue
    }
    // For each Template collection to clone:
    for (const { collectionName, collectionType } of cloneCollections) {
        const currentCollection = template[collectionName]
        // console.log(`\ncurrentCollection "${collectionName}" [${currentCollection.length}]: `, currentCollection)

        // For each templateChild entity in this template collection, "clone" it -
        // which at this stage consists only of creating a new entity,
        // linking it to newParent, and also linking its "Template" field to the templateChild clone-source
        // This Template linking will trigger a Rule in the child DB which is responsible for actually copying
        // the new child entity's fields from the linked Template.
        for (const templateChild of currentCollection) {
            // console.log('\ntemplateChild: ', templateChild)
            const newChild = await fibery.createEntity(collectionType, {  // the child clone-to-be
                // Remove "TEMPLATE" from end of Name field
                Name:     templateChild.Name.replace(/\W+TEMPLATE\W*$/, ''),
                Template: templateChild.Id      // This causes field-copying to happen via a Rule in the child DB
            })
            // Add the new child (clone) entity to newParent's collection
            await fibery.addCollectionItem(newParent.type, newParent.Id, collectionName, newChild.Id)
        }
    }

    // Append the requested Rich Text fields from Template entity to newParent
    // (this could be done in the no-code part instead)
    for (const field of appendRichTexts) {
        const templateSecret = template[field].Secret
        const parentSecret   = newParent[field].Secret
        const content        = await fibery.getDocumentContent(templateSecret, 'html')
        await fibery.appendDocumentContent(parentSecret, content, 'html')       // Must use html to preserve formatting!
    }

    // Remove the TEMPLATE tag from newParent
    const newParent2 = await fibery.getEntityById(newParent.type, newParent.Id, [TAGS_FIELD])
    const newParentTags = newParent2['Tags']
    if (newParentTags) {
        const templateTag = newParentTags.find(e => e.Name.match(/TEMPLATE\s*$/))
        if (templateTag) {
            await fibery.removeCollectionItem(newParent.type, newParent.Id, TAGS_FIELD, templateTag.Id)
        }
    }
}
7 Likes

Thank you for sharing, lots to digest :blush:

3 Likes

Thanks a lot for sharing. I was trying to figure out how do I do simple template rich text field and this code definitely worked.
Have u found a way to update rich text field with no code?
Append to content doesn’t seem to work for me.

It should be possible by just using markdown. Something like {{Template.Description}} ought to work.

Yes, the no-code approach to copying/updating Rich Text fields does work.

But it can be tricky to get the correct syntax for field references, as it differs from the syntax used in formulas and JS (plus confusing documentation, and unhelpful or non-existent error messages).

1 Like

Perhaps our user guide section on markdown needs improving. Can you tell us what you think we are missing :pray:

This part of the documentation seems like it was written as a summary by someone familiar with the syntax and requirements, as opposed to a “User Guide” that has been developed for people approaching the area with no knowledge. A “User Guide” approach could usefully take the perspective that the audience is already familiar with Formulas (and/or scripts), and so include a section near the top about differences to be aware of.

It took me a while to realize that a relation field with spaces in its name is entered verbatim with the spaces, and without quotes or brackets, e.g. {{Relation Name.Field Name}}. This differs from other parts of the system such as Formulas/scripts, where a field name must be in brackets/quotes if it contains spaces.

Yes, this is demonstrated in the docs, but because it’s not pointed out explicitly (and because it differs from field syntax in other areas) and it’s quite unintuitive for a programmer :wink: it’s easy to miss. I am used to quickly scanning docs to pick up the important points, not always sitting down to read them entirely and carefully. I do make assumptions unconsciously – e.g. that field reference syntax will be consistent through a system.

Similarly the requirement to “declare” relations before referencing them is also documented, but easy to miss, since it also differs from how Formulas work.

In the past at least, feedback from error messages has unhelpful with syntax or other errors.

2 Likes

Hm… I tried that but maybe I was holding it wrong.
I’m trying to fill field on creation of entity and what I noticed after sometime playing with scripts is that lookup fields are updated with slight delay AFTER entity created/updated is fired.
I will try to see if I can use relationship directly.

If that’s the case, then maybe try to use the ‘Entity linked to…’ trigger to run the automation when a lookup field gets new content

was able to use {{Reference.Description}} from the docs.
Would be great to have some way to see what the value would be without running the rule. Some kind “run this script with a test entity with the same type”.
It gets tricky to understand why sometimes it doesn’t work.

2 Likes

I can’t edit my original post here, which is too bad, because I think this solution no longer works :cry:
(at least for cloning a self-related collection).

Fibery is not quite clever enough with automation loop detection, so it now prevents scripts like this from working, at least in certain cases.

1 Like