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