Deep Copy (within a space)

Hi everyone - this is a working prototype of a deep copy function to enable projects to be copied within a space. My goal was to make it possible to maintain “templates” within each space that include both structure and content.

Since this is my first Fibery automation, it has several limitations – but hopefully, the code can be easily modified for other use cases. And it’s definitely a 0.01 version - the code needs refactoring and is a little verbose (cough).

Muchas gracias, 谢谢 and thanks to @Chr1sG, @Yuri_BC and @Matt_Blais for all your help - I’d still be banging my head on the table w/o your suggestions.

Fixed Database Hierarchy
Schema consists of projects, sections and tasks:

  • Projects 1:M sections
  • Sections 1:M tasks
  • Projects, sections, and tasks can also have recursive 1:M sub entities.

Features and Limitations
Supports:

  • Recursive copy of project-section-task hierarchy
  • Recursive copy of sub-projects, sections and tasks
  • Single-select, Boolean, and rich-text fields

Does not support:

  • M:M relationships
  • Documents
  • Fields other than those listed in the init constant section

Customization

  • The only mandatory config is the SPACE constant at the beginning.
  • As long as you’re OK with the schema hierarchy and the number and type of fields, then the space, database and field names are all configurable in the constant init section.
  • If you need to add more databases and/or fields, hopefully that’s self-evident in the script.

Planned

  • Support for M:M project-section-task hierarchy and M:M sub-projects, sections, tasks.
  • Support for auto-discovery of field types and names.
// FIBERY-TEMPLATE-SPACE - COMMON - AUTOMATION - DEEP COPY
// VERSIION - 0.01
// DATE - 2024.04.27

// VERSION NOTES
// 0.01
// - chg task field_cycle --> field_group

// init constant for databases
const SPACE = 'template - common - public';
const PROJECT_DATABASE = SPACE + '/Project';
const SECTION_DATABASE = SPACE + '/Section';
const TASK_DATABASE = SPACE + '/Task';

// constants for project field names
const PROJECT_FIELD_NAME = 'Name';
const PROJECT_FIELD_PARENT_PROJECT = 'Parent Project';

const PROJECT_FIELD_PROJECTS = 'Projects';  // sub projects
const PROJECT_FIELD_SECTIONS = 'Sections';  // direct dedscendant sections


const PROJECT_FIELD_DESCRIPTION = 'Project Overview';
const PROJECT_FIELD_STATE = 'State';


// constants for section field names
const SECTION_FIELD_NAME = 'Name';
const SECTION_FIELD_PARENT_PROJECT = 'Parent Project';
const SECTION_FIELD_PARENT_SECTION = 'Parent Section';

const SECTION_FIELD_SECTIONS = 'Sections';  // sub sections
const SECTION_FIELD_TASKS = 'Tasks';        // direct descendant tasks

const SECTION_FIELD_DESCRIPTION = 'Section Overview';

const SECTION_FIELD_STATE = 'State';
const SECTION_FIELD_PRIORITY = 'Priority';
const SECTION_FIELD_FOCUS = '!';


// constants for task field names
const TASK_FIELD_NAME = 'Name';
const TASK_FIELD_PARENT_PROJECT = 'Parent Project';
const TASK_FIELD_PARENT_SECTION = 'Parent Section';
const TASK_FIELD_PARENT_TASK = 'Parent Task';

const TASK_FIELD_TASKS = 'Tasks';   // sub tasks

const TASK_FIELD_DESCRIPTION = 'Task Overview';
const TASK_FIELD_STATE = 'State';
const TASK_FIELD_PRIORITY = 'Priority';
const TASK_FIELD_FOCUS = '!';

const TASK_FIELD_TYPE = 'Task Type';
const TASK_FIELD_GROUP = 'Task Group';
const TASK_FIELD_TEMPLATE = 'Template';

// Define maximum depth for conversion
const MAX_LEVEL = 3;

// Initialize Fibery service
const fibery = context.getService('fibery');

// Execute conversion process
await copyProjects();

// copy all projects
async function copyProjects() {
    let projectsSource = args.currentEntities;

    if (projectsSource && projectsSource.length > 0) {
        for (const projectSource of projectsSource) {

            // get project details
            // parent project is only available in project details
            const projectDetailsSource = await fetchProjectDetailsSource(
                PROJECT_DATABASE,
                projectSource
            );

            // only process primary projects (projects with no parent)
            // skip subprojects - they will be copied in the recursive project copy
            if (projectDetailsSource[PROJECT_FIELD_PARENT_PROJECT] &&
                projectDetailsSource[PROJECT_FIELD_PARENT_PROJECT].Id === null) {

                console.log('project source - project name - ',
                    projectDetailsSource[PROJECT_FIELD_NAME]);

                await copyProject(projectDetailsSource);
            }

        }
    }
}

// copy each project 
async function copyProject(projectDetailsSource, projectParentProjectIdSource = null, currLevel = 1) {
    if (currLevel > MAX_LEVEL) {
        return; // stop recursion if max level is exceeded
    }

    try {
        // init new entity data object
        const projectDetailsTarget = {};

        updateFieldCommon(
            PROJECT_FIELD_NAME,
            projectDetailsSource[PROJECT_FIELD_NAME] + ' (copy)',
            projectDetailsTarget
        );

        updateFieldCommon(
            PROJECT_FIELD_PARENT_PROJECT,
            projectParentProjectIdSource ? projectParentProjectIdSource : null,
            projectDetailsTarget
        );

        // init new entity
        const projectTarget = await fibery.createEntity(
            PROJECT_DATABASE,
            projectDetailsTarget
        );

        await updateFieldDescription(
            PROJECT_DATABASE,
            PROJECT_FIELD_DESCRIPTION,
            projectDetailsSource[PROJECT_FIELD_DESCRIPTION],
            projectTarget
        );

        await updateFieldState(
            PROJECT_DATABASE,
            PROJECT_FIELD_STATE,
            projectDetailsSource[PROJECT_FIELD_STATE],
            projectTarget
        );

        // copy project sections
        await copyProjectSections(projectDetailsSource, projectTarget);

        // copy each sub-project
        let projectSubProjectsSource = projectDetailsSource[PROJECT_FIELD_PROJECTS];

        if (projectSubProjectsSource && projectSubProjectsSource.length > 0) {
            for (const projectSubProjectSource of projectSubProjectsSource) {

                // get project details
                // parent project is only available in project details
                const projectSubProjectDetailsSource = await fetchProjectDetailsSource(
                    PROJECT_DATABASE,
                    projectSubProjectSource
                );

                // only process sub projects (projects with a parent)
                // skip primary projects
                if (projectSubProjectDetailsSource[PROJECT_FIELD_PARENT_PROJECT] &&
                    projectSubProjectDetailsSource[PROJECT_FIELD_PARENT_PROJECT].Id !== null) {

                    // console.log('\nproject source - sub-project name - ',
                    //     projectSubProjectDetailsSource[PROJECT_FIELD_NAME]);

                    await copyProject(
                        projectSubProjectDetailsSource,
                        projectTarget.Id,
                        currLevel + 1
                    );
                }


            }
        }


    } catch (error) {
        console.error('Error - Copy Project Function:', error);
    }
}


async function copyProjectSections(projectDetailsSource, projectTarget) {
    // init project section 
    let projectSectionsSource = projectDetailsSource[PROJECT_FIELD_SECTIONS];

    if (projectSectionsSource && projectSectionsSource.length > 0) {
        for (const projectSectionSource of projectSectionsSource) {

            // get section details
            // parent section is only available in section details
            const projectSectionDetailsSource = await fetchSectionDetailsSource(
                SECTION_DATABASE,
                projectSectionSource
            );


            // only process primary sections (sections with no parent)
            // skip subsections - they will be copied in the recursive section copy
            if (projectSectionDetailsSource[SECTION_FIELD_PARENT_SECTION] &&
                projectSectionDetailsSource[SECTION_FIELD_PARENT_SECTION].Id === null) {

                console.log('project source - section name - ',
                    projectSectionDetailsSource[SECTION_FIELD_NAME]);

                await copySection(
                    projectSectionDetailsSource,
                    projectTarget.Id

                );
            }
        }
    }
}

// copy each section 
async function copySection(sectionDetailsSource,
    sectionParentProjectIdTarget,
    sectionParentSectionIdTarget = null,
    currLevel = 1) {
    if (currLevel > MAX_LEVEL) {
        return; // stop recursion if max level is exceeded
    }

    // console.log('parent project id - ',sectionParentProjectIdTarget);
    // console.log('parent section id - ',sectionParentSectionIdTarget);


    try {
        // init new entity data object
        const sectionDetailsTarget = {};

        updateFieldCommon(
            SECTION_FIELD_NAME,
            sectionDetailsSource[SECTION_FIELD_NAME] + ' (copy)',
            sectionDetailsTarget
        );

        updateFieldParent(
            SECTION_FIELD_PARENT_PROJECT,
            sectionParentProjectIdTarget,
            sectionDetailsTarget
        );

        updateFieldParent(
            SECTION_FIELD_PARENT_SECTION,
            sectionParentSectionIdTarget,
            sectionDetailsTarget
        );

        // init new entity
        const sectionTarget = await fibery.createEntity(
            SECTION_DATABASE,
            sectionDetailsTarget
        );

        await updateFieldDescription(
            SECTION_DATABASE,
            SECTION_FIELD_DESCRIPTION,
            sectionDetailsSource[SECTION_FIELD_DESCRIPTION],
            sectionTarget
        );

        await updateFieldState(
            SECTION_DATABASE,
            SECTION_FIELD_STATE,
            sectionDetailsSource[SECTION_FIELD_STATE],
            sectionTarget
        );

        await updateFieldSelectSingle(
            SECTION_DATABASE,
            SECTION_FIELD_PRIORITY,
            sectionDetailsSource[SECTION_FIELD_PRIORITY],
            sectionTarget
        );

        await updateFieldCheckbox(
            SECTION_DATABASE,
            SECTION_FIELD_FOCUS,
            sectionDetailsSource[SECTION_FIELD_FOCUS],
            sectionTarget
        );


        // copy section tasks
        await copySectionTasks(sectionDetailsSource, sectionTarget);

        // copy each sub-section
        let sectionSubSectionsSource = sectionDetailsSource[SECTION_FIELD_SECTIONS];

        if (sectionSubSectionsSource && sectionSubSectionsSource.length > 0) {
            for (const sectionSubSectionSource of sectionSubSectionsSource) {

                // get section details
                // parent section is only available in section details
                const sectionSubSectionDetailsSource = await fetchSectionDetailsSource(
                    SECTION_DATABASE,
                    sectionSubSectionSource
                );

                // only process sub sections (sections with a parent)
                // skip primary sections
                if (sectionSubSectionDetailsSource[SECTION_FIELD_PARENT_SECTION] &&
                    sectionSubSectionDetailsSource[SECTION_FIELD_PARENT_SECTION].Id !== null) {

                    // console.log('\nsection source - sub-section name - ',
                    //     sectionSubSectionDetailsSource[SECTION_FIELD_NAME]);

                    await copySection(
                        sectionSubSectionDetailsSource,
                        sectionParentProjectIdTarget,
                        sectionTarget.Id,
                        currLevel + 1
                    );
                }
            }
        }


    } catch (error) {
        console.error('Error - Copy Section Function:', error);
    }
}


// copy section tasks
async function copySectionTasks(sectionDetailsSource, sectionTarget) {
    // init project section 
    let sectionTasksSource = sectionDetailsSource[SECTION_FIELD_TASKS];

    if (sectionTasksSource && sectionTasksSource.length > 0) {
        for (const sectionTaskSource of sectionTasksSource) {

            // get section details
            // parent section is only available in section details
            const sectionTaskDetailsSource = await fetchTaskDetailsSource(
                TASK_DATABASE,
                sectionTaskSource
            );


            // only process primary sections (sections with no parent)
            // skip subsections - they will be copied in the recursive section copy
            if (sectionTaskDetailsSource[TASK_FIELD_PARENT_TASK] &&
                sectionTaskDetailsSource[TASK_FIELD_PARENT_TASK].Id === null) {

                console.log('section source - task name - ',
                    sectionTaskDetailsSource[TASK_FIELD_NAME]);

                await copyTask(
                    sectionTaskDetailsSource,
                    sectionTarget[SECTION_FIELD_PARENT_PROJECT].Id,
                    sectionTarget.Id
                );
            }
        }
    }
}


// copy each task
async function copyTask(
    taskDetailsSource,
    taskParentProjectIdTarget,
    taskParentSectionIdTarget,
    taskParentTaskIdTarget = null,
    currLevel = 1) {
    if (currLevel > MAX_LEVEL) {
        return; // stop recursion if max level is exceeded
    }

    console.log('parent project id - ', taskParentProjectIdTarget);
    console.log('parent section id - ', taskParentSectionIdTarget);
    console.log('parent task id - ', taskParentTaskIdTarget);


    try {
        // init new entity data object
        const taskDetailsTarget = {};

        updateFieldCommon(
            TASK_FIELD_NAME,
            taskDetailsSource[TASK_FIELD_NAME] + ' (copy)',
            taskDetailsTarget
        );

        updateFieldParent(
            TASK_FIELD_PARENT_PROJECT,
            taskParentProjectIdTarget,
            taskDetailsTarget
        );

        updateFieldParent(
            TASK_FIELD_PARENT_SECTION,
            taskParentSectionIdTarget,
            taskDetailsTarget
        );

        updateFieldParent(
            TASK_FIELD_PARENT_TASK,
            taskParentTaskIdTarget,
            taskDetailsTarget
        );

        // init new entity
        const taskTarget = await fibery.createEntity(
            TASK_DATABASE,
            taskDetailsTarget
        );

        await updateFieldDescription(
            TASK_DATABASE,
            TASK_FIELD_DESCRIPTION,
            taskDetailsSource[TASK_FIELD_DESCRIPTION],
            taskTarget
        );

        await updateFieldState(
            TASK_DATABASE,
            TASK_FIELD_STATE,
            taskDetailsSource[TASK_FIELD_STATE],
            taskTarget
        );

        await updateFieldSelectSingle(
            TASK_DATABASE,
            TASK_FIELD_PRIORITY,
            taskDetailsSource[TASK_FIELD_PRIORITY],
            taskTarget
        );

        await updateFieldCheckbox(
            TASK_DATABASE,
            TASK_FIELD_FOCUS,
            taskDetailsSource[TASK_FIELD_FOCUS],
            taskTarget
        );

        await updateFieldSelectSingle(
            TASK_DATABASE,
            TASK_FIELD_TYPE,
            taskDetailsSource[TASK_FIELD_TYPE],
            taskTarget
        );

        await updateFieldSelectSingle(
            TASK_DATABASE,
            TASK_FIELD_GROUP,
            taskDetailsSource[TASK_FIELD_GROUP],
            taskTarget
        );

        await updateFieldCheckbox(
            TASK_DATABASE,
            TASK_FIELD_TEMPLATE,
            taskDetailsSource[TASK_FIELD_TEMPLATE],
            taskTarget
        );

        // copy task objects
        // end of the line - no child objects for task

        // copy each sub-task
        let taskSubTasksSource = taskDetailsSource[TASK_FIELD_TASKS];

        if (taskSubTasksSource && taskSubTasksSource.length > 0) {
            for (const taskSubTaskSource of taskSubTasksSource) {

                // get task details
                // parent task is only available in task details
                const taskSubTaskDetailsSource = await fetchTaskDetailsSource(
                    TASK_DATABASE,
                    taskSubTaskSource
                );

                // only process sub tasks (tasks with a parent)
                // skip primary tasks
                if (taskSubTaskDetailsSource[TASK_FIELD_PARENT_TASK] &&
                    taskSubTaskDetailsSource[TASK_FIELD_PARENT_TASK].Id !== null) {

                    console.log('\ntask source - sub-task name - ',
                        taskSubTaskDetailsSource[TASK_FIELD_NAME]);

                    await copyTask(
                        taskSubTaskDetailsSource,
                        taskParentProjectIdTarget,
                        taskParentSectionIdTarget,
                        taskTarget.Id,
                        currLevel + 1
                    );
                }
            }
        }


    } catch (error) {
        console.error('Error - Copy Task Function:', error);
    }
}



// FETCH FUNCTIONS
// fetch project details
async function fetchProjectDetailsSource(Database, projectSource) {
    return await fibery.getEntityById(
        Database,
        projectSource.Id,
        [
            PROJECT_FIELD_NAME,
            PROJECT_FIELD_PARENT_PROJECT,

            PROJECT_FIELD_PROJECTS,
            PROJECT_FIELD_SECTIONS,

            PROJECT_FIELD_DESCRIPTION,
            PROJECT_FIELD_STATE,
        ]
    );
}

// fetch section details
async function fetchSectionDetailsSource(Database, sectionSource) {
    return await fibery.getEntityById(
        Database,
        sectionSource.Id,
        [
            SECTION_FIELD_NAME,
            SECTION_FIELD_PARENT_PROJECT,
            SECTION_FIELD_PARENT_SECTION,
            SECTION_FIELD_SECTIONS,
            SECTION_FIELD_TASKS,
            SECTION_FIELD_DESCRIPTION,
            SECTION_FIELD_STATE,
            SECTION_FIELD_PRIORITY,
            SECTION_FIELD_FOCUS
        ]
    );
}

// fetch task details
async function fetchTaskDetailsSource(Database, taskSource) {
    return await fibery.getEntityById(
        Database,
        taskSource.Id,
        [
            TASK_FIELD_NAME,
            TASK_FIELD_PARENT_SECTION,
            TASK_FIELD_PARENT_TASK,
            TASK_FIELD_TASKS,
            TASK_FIELD_DESCRIPTION,
            TASK_FIELD_STATE,
            TASK_FIELD_PRIORITY,
            TASK_FIELD_FOCUS,
            TASK_FIELD_TYPE,
            TASK_FIELD_GROUP,
            TASK_FIELD_TEMPLATE
        ]
    );
}



// COMMON FUNCTIONS - USED BY ALL DATABASES
// common field update - generic
function updateFieldCommon(fieldName, fieldValueSource, entityTarget) {
    entityTarget[fieldName] = fieldValueSource;
}


// common field update - parent project / parent section / parent task
// - references parent from target project, section or task
// - not the source project, section or task
function updateFieldParent(fieldName, fieldValueTarget, entityTarget) {
    entityTarget[fieldName] = fieldValueTarget ? fieldValueTarget : null;

}


// common field update - description
async function updateFieldDescription(Database, fieldName, fieldValueSource, entityTarget) {
    let fieldValueTarget = null;


    if (fieldValueSource && fieldValueSource.Secret) {
        fieldValueTarget = await fibery.getDocumentContent(fieldValueSource.Secret, 'json');

        await fibery.setDocumentContent(
            entityTarget[fieldName].Secret,
            fieldValueTarget,
            'json');
    }
}

// commmon field update - state
async function updateFieldState(Database, fieldName, fieldValueSource, entityTarget) {
    let fieldValueTarget = fieldValueSource ? fieldValueSource : null;

    await fibery.setState(
        Database,
        entityTarget.id,
        fieldValueTarget.Name
    );
}

// commmon field update - single select
async function updateFieldSelectSingle(Database, fieldName, fieldValueSource, entityTarget) {


    if (fieldValueSource) {
        let fieldValueTarget = fieldValueSource;

        await fibery.updateEntity(
            Database,
            entityTarget.id,
            {
                [fieldName]: fieldValueTarget.Name
            }
        )

    }
}

// commmon field update - checkbox
async function updateFieldCheckbox(Database, fieldName, fieldValueSource, entityTarget) {


    if (fieldValueSource) {
        let fieldValueTarget = fieldValueSource ? true : false;

        await fibery.updateEntity(
            Database,
            entityTarget.id,
            {
                [fieldName]: fieldValueTarget
            }
        )

    }
}


Link to space template here.

3 Likes

Nice work!

Spotted a typo in your comment here (should be ‘checkbox’ not ‘single select’):

Haha - too many copy paste…:slight_smile:
Thx, I’ll fix it in the template.

1 Like