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)
        }
    }
}
1 Like

Thank you for sharing, lots to digest :blush:

2 Likes