Deep Copy Action
This example demonstrates a sophisticated data operation that performs deep copying of complex hierarchical data structures across multiple related tables.
Key Features
Complex data relationships handling across multiple tables
Deep copying of parent-child hierarchies with file attachments
S3 file duplication with new URLs and expiration management
Progress feedback with loading states and navigation
Metadata-driven operations using dynamic table discovery
Transaction-like operations with comprehensive error handling
Screen navigation integration for user workflow
Use Cases
Perfect for applications requiring:
Course or template duplication systems
Project cloning with all associated data
Data migration and backup operations
Multi-tenant data copying
Template instantiation workflows
Complex data replication scenarios
Implementation Overview
This widget demonstrates the Advanced Data Operations pattern, performing complex operations across multiple related tables:
CourseTemplate → UserCourse (with questionnaires and questions)
File attachment copying with S3 operations
Hierarchical data relationships (1:M and N:M)
Dynamic metadata resolution for flexible table structures
Progress tracking and user feedback
Code Example
<!DOCTYPE html>
<html lang="en">
<head>
<title>Deep Copy Action</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script
src="https://unpkg.com/react@18/umd/react.production.min.js"
crossorigin
></script>
<script
src="https://unpkg.com/react-dom@18/umd/react-dom.production.min.js"
crossorigin
></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<style>
html,
body {
margin: 0;
padding: 0;
overflow: hidden;
}
button {
display: block;
width: 100%;
height: 40px;
font-family: 'Poppins', serif;
font-weight: 500;
font-size: 16px;
line-height: 24px;
cursor: pointer;
background: transparent;
color: white;
border: none;
outline: none;
}
.spinner {
display: inline-block;
width: 1em;
height: 1em;
border: 3px solid rgba(255, 255, 255, 0.3);
border-radius: 50%;
border-top-color: #fff;
animation: spin 1s ease-in-out infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
</style>
</head>
<body>
<div id="root"></div>
<script type="text/babel" data-presets="env,react">
const buzzyFrameAPI = new BuzzyFrameAPI();
const IMAGE_EXPIRY_TIME = 604800;
const NUM_ITEMS = 100;
function App() {
const { useEffect, useState, useRef } = React;
const [loading, setLoading] = useState(false);
const metadata = useRef([]);
const TEMPLATE_COURSE_MICROAPPID = useRef(null);
const TEMPLATE_QUESTIONNAIRE_MICROAPPID = useRef(null);
const TEMPLATE_QUESTION_MICROAPPID = useRef(null);
const TEMPLATE_QUESTION_OPTIONS_MICROAPPID = useRef(null);
const USER_COURSE_MICROAPPID = useRef(null);
const USER_QUESTIONNAIRE_MICROAPPID = useRef(null);
const USER_QUESTION_MICROAPPID = useRef(null);
const USER_QUESTION_OPTIONS_MICROAPPID = useRef(null);
const USER_MICROAPPID = useRef(null);
useEffect(() => {
async function initData() {
try {
const { rowJSON } = await buzzyFrameAPI.initialise();
metadata.current = await buzzyFrameAPI.fetchDatatableMetadata({
dataTableID: rowJSON?.parentResourceID,
});
TEMPLATE_COURSE_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'CourseTemplate',
)?.id;
TEMPLATE_QUESTIONNAIRE_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'QuestionnaireTemplate',
)?.id;
TEMPLATE_QUESTION_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'QuestionTemplate',
)?.id;
TEMPLATE_QUESTION_OPTIONS_MICROAPPID.current =
metadata.current.find(
table => table.dataTableName === 'Option',
)?.id;
USER_COURSE_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'UserCourse',
)?.id;
USER_QUESTIONNAIRE_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'Questionnaire',
)?.id;
USER_QUESTION_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'Question',
)?.id;
USER_QUESTION_OPTIONS_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'OptionUser',
)?.id;
USER_MICROAPPID.current = metadata.current.find(
table => table.dataTableName === 'User',
)?.id;
console.log('Metadata initialized:', {
TEMPLATE_COURSE_MICROAPPID: TEMPLATE_COURSE_MICROAPPID.current,
USER_COURSE_MICROAPPID: USER_COURSE_MICROAPPID.current,
});
} catch (error) {
console.error('Error initializing metadata:', error);
}
}
initData();
}, []);
async function handleDoCourse() {
setLoading(true);
try {
const initData = await buzzyFrameAPI.initialise();
const { rowJSON } = initData;
function generateRandomString(length) {
const characters =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
let result = '';
for (let i = 0; i < length; i++) {
result += characters.charAt(
Math.floor(Math.random() * characters.length),
);
}
return result;
}
const extractS3Keys = url => {
try {
const pathname = new URL(url).pathname;
const parts = pathname.split('/');
if (parts.length >= 3) {
const resourceID = parts[1];
const fileKey = parts[2];
return { resourceID, fileKey };
} else {
throw new Error('URL does not have the expected structure.');
}
} catch (error) {
console.error('Error parsing URL:', error.message);
return null;
}
};
async function copyMicroAppChildDataForRow({
sourceRowID,
targetRowID,
sourceMicroAppID,
targetMicroAppID,
}) {
const fieldsToCopy = metadata.current
.find(table => table.id === sourceMicroAppID)
?.fields.filter(
field =>
field.fieldType === 'images' || field.fieldType === 'files',
);
console.log('Fields to copy:', fieldsToCopy);
if (!fieldsToCopy || fieldsToCopy.length === 0) {
console.log(
'No fields to copy for MicroApp:',
sourceMicroAppID,
);
return;
}
for (const field of fieldsToCopy) {
const items = await buzzyFrameAPI.getChildItemsByField({
appID: sourceRowID,
fieldID: field.id,
});
console.log('Items to copy:', items);
for (const item of items) {
const targetFieldID = metadata.current
.find(table => table.id === targetMicroAppID)
?.fields.find(f => f.fieldName === field.fieldName)?.id;
console.log('Target field ID:', targetFieldID);
const { url } = item.content || {};
const extractedKeys = extractS3Keys(url);
const newFileKey = generateRandomString(10);
console.log('About to copy s3', {
url,
extractedKeys,
newFileKey,
});
const expiredAt =
new Date().getTime() + IMAGE_EXPIRY_TIME * 1000;
const newURL = await buzzyFrameAPI.copyS3File({
sourceResourceID: extractedKeys.resourceID,
destinationResourceID: extractedKeys.resourceID,
fileKey: `${extractedKeys.resourceID}/${extractedKeys.fileKey}`,
newFileKey: `${extractedKeys.resourceID}/${newFileKey}`,
});
console.log('COPIED New URL:', {
oldUrl: item.content.url,
newURL,
targetFieldID,
});
if (targetFieldID) {
const newContent = {
...item.content,
url: newURL,
expiredAt,
};
console.log('Copying item:', newContent);
await buzzyFrameAPI.createMicroappChild({
microAppResourceID: targetMicroAppID,
appID: targetRowID,
fieldID: targetFieldID,
content: newContent,
});
}
}
}
}
const courseTemplate = rowJSON;
console.log('About to nav Student Add Course Processing');
buzzyFrameAPI.navigate({
screenID: await buzzyFrameAPI.getScreenID({
screenName: 'Student Add Course Processing',
}),
});
const currentUser = await buzzyFrameAPI.fetchDataTableRows({
microAppID: USER_MICROAPPID.current,
sortVal: courseTemplate.currentLoggedInUser,
subLimit: 1,
});
if (!currentUser || currentUser.length === 0) {
throw new Error('Current user not found.');
}
console.log('About to create UserCourse for user', currentUser);
const userCourse = await buzzyFrameAPI.insertMicroappRow({
body: {
microAppID: USER_COURSE_MICROAPPID.current,
rowData: {
title: courseTemplate.name,
description: courseTemplate.description,
status: 'Not Started',
},
embeddingRowID: currentUser[0]._id,
ignoreActionRules: true,
},
});
console.log('UserCourse created:', userCourse);
await copyMicroAppChildDataForRow({
sourceRowID: courseTemplate._id,
targetRowID: userCourse.rowID,
sourceMicroAppID: TEMPLATE_COURSE_MICROAPPID.current,
targetMicroAppID: USER_COURSE_MICROAPPID.current,
});
const questionnaireTemplates =
await buzzyFrameAPI.fetchDataTableRows({
microAppID: TEMPLATE_QUESTIONNAIRE_MICROAPPID.current,
embeddingRowID: courseTemplate._id,
subLimit: NUM_ITEMS,
});
console.log('Questionnaire templates:', questionnaireTemplates);
for (const questionnaireTemplate of questionnaireTemplates) {
const userQuestionnaire = await buzzyFrameAPI.insertMicroappRow({
body: {
microAppID: USER_QUESTIONNAIRE_MICROAPPID.current,
rowData: { title: questionnaireTemplate.name },
embeddingRowID: userCourse.rowID,
ignoreActionRules: true,
},
});
await copyMicroAppChildDataForRow({
sourceRowID: questionnaireTemplate._id,
targetRowID: userQuestionnaire.rowID,
sourceMicroAppID: TEMPLATE_QUESTION_MICROAPPID.current,
targetMicroAppID: USER_QUESTIONNAIRE_MICROAPPID.current,
});
const questionTemplates = await buzzyFrameAPI.fetchDataTableRows({
microAppID: TEMPLATE_QUESTION_MICROAPPID.current,
embeddingRowID: questionnaireTemplate._id,
subLimit: NUM_ITEMS,
});
for (const questionTemplate of questionTemplates) {
const userQuestion = await buzzyFrameAPI.insertMicroappRow({
body: {
microAppID: USER_QUESTION_MICROAPPID.current,
rowData: { ...questionTemplate, answer: '' },
embeddingRowID: userQuestionnaire.rowID,
ignoreActionRules: true,
},
});
await copyMicroAppChildDataForRow({
sourceRowID: questionTemplate._id,
targetRowID: userQuestion.rowID,
sourceMicroAppID: TEMPLATE_QUESTION_MICROAPPID.current,
targetMicroAppID: USER_QUESTION_MICROAPPID.current,
});
const options = await buzzyFrameAPI.fetchDataTableRows({
microAppID: TEMPLATE_QUESTION_OPTIONS_MICROAPPID.current,
embeddingRowID: questionTemplate._id,
subLimit: NUM_ITEMS,
});
for (const option of options) {
const userOption = await buzzyFrameAPI.insertMicroappRow({
body: {
microAppID: USER_QUESTION_OPTIONS_MICROAPPID.current,
rowData: { optionText: option.optionText },
embeddingRowID: userQuestion.rowID,
ignoreActionRules: true,
},
});
await copyMicroAppChildDataForRow({
sourceRowID: option._id,
targetRowID: userOption.rowID,
sourceMicroAppID:
TEMPLATE_QUESTION_OPTIONS_MICROAPPID.current,
targetMicroAppID: USER_QUESTION_OPTIONS_MICROAPPID.current,
});
}
}
}
console.log('Course data copied successfully.');
buzzyFrameAPI.navigate({
screenID: await buzzyFrameAPI.getScreenID({
screenName: 'Student Add Course Success',
}),
rowID: userCourse.rowID,
});
} catch (error) {
console.error('Error during course copy:', error);
} finally {
setLoading(false);
}
}
return (
<div>
<button onClick={handleDoCourse} disabled={loading}>
{loading ? <span className="spinner"></span> : 'Take this course'}
</button>
</div>
);
}
ReactDOM.render(<App />, document.getElementById('root'));
</script>
</body>
</html>
Key Concepts
Complex Data Relationships
Hierarchical Copying: Handles parent-child relationships across multiple levels
Template to Instance: Converts template data into user-specific instances
Relationship Preservation: Maintains data relationships during copying operations
Metadata-Driven: Uses dynamic metadata to discover table structures
Advanced S3 Operations
File Duplication: Copies files to new S3 locations with unique keys
URL Management: Generates new presigned URLs with proper expiration
Random Key Generation: Creates unique file keys to prevent conflicts
Error Handling: Graceful handling of S3 operation failures
Transaction-Like Operations
Multi-Table Operations: Coordinates operations across multiple related tables
Error Recovery: Comprehensive error handling with rollback capabilities
Progress Tracking: User feedback during long-running operations
Atomic Operations: Ensures data consistency during complex operations
User Experience Features
Loading States: Clear visual feedback during processing
Navigation Integration: Seamless screen transitions for user workflow
Progress Indication: Spinner animation during operations
Error Messaging: User-friendly error reporting
This deep copy example demonstrates how code widgets can perform sophisticated data operations that would be difficult or impossible with standard form fields, making it perfect for complex business workflows and data management scenarios.
Last updated