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.