Copy/Create/Paste Theme Graphql

Hello Shopify Team.

I placed this in graphql troubleshooting as this pertains to manipulating theme using only graphql calls.

What is the proper (advisable) way for copying the main theme files and pasting the main theme files into a duplicate theme using admin graphql?

GOAL
To fully automate the ability to duplicate the main theme and inject (upsert) theme files into duplicated theme that correlate to my shopify application while all being handled in graphql.

My resolution:

  1. I query all themes.
  2. Find MAIN theme.
  3. Query theme with MAIN theme id by using bulkoperation query theme.
    – This is where I get lost.
  4. ?? Pass: Main Theme data such as filename/contentType into StagedUploadsCreate.
  5. Chunk Main theme data array into 250’s to make it acceptable for StagedUploadsCreate.
  6. Upload content from Main theme data … on OnlineStoreThemeFileBodyText- content into each corresponding URL that was generated from stageduploadscreate.
    – Im truly lost at this point:
  7. ?? ThemeCreate Mutation? Theme create requires a external url or a or a staged upload URL.

Questions: I dont know how to use stageuploadscreate if themecreate mutation accepts one source URL: string and not an array of URL’s from stageuploadCreate. StageuploadsCreate resource argument donest have a zip value since that’s what needs to be applied to themeCreate.
Reference for question: themeCreate - GraphQL Admin

My attempt is built in typescript/remix shopify template and broken up in mulitple functions. Here is my code:

export async function RunMutationThemeFilesCopy(admin: any) {
  try {
    // Query All themes
    const queryThemes: any = await bulkOperationQueryThemes(admin);

    // Parse Data
    const queryThemesData = queryThemes
      .split("\n")
      .filter((line: string) => line.trim() !== "")
      .map((line: string) => {
        try {
          return JSON.parse(line);
        } catch (error) {
          console.error(
            `Error parsing JSON line: "${line}". Where: RunMutationThemeFilesCopy: queryThemesData: error: ${error}`,
          );
          return null;
        }
      })
      .filter((item: any) => item !== null);

    console.log(queryThemesData);

    // Find the theme with role: "MAIN"
    const mainTheme = queryThemesData.find((line: any) => line.role === "MAIN");

    if (!mainTheme) {
      throw new Error("No MAIN theme found");
    }

    // MIGHT need to create a fresh theme if no existing theme is found.
    console.log("Main Theme:", mainTheme);

    // Query "MAIN" theme for all files
    const queryMainTheme: any = await bulkOperationQueryTheme(admin, mainTheme);

    // Parse DATA
    const queryMainThemeData = queryMainTheme
      .split("\n")
      .filter((line: string) => line.trim() !== "")
      .map((line: string) => {
        try {
          return JSON.parse(line);
        } catch (error) {
          console.error(
            `Error parsing JSON line: "${line}". Where: RunMutationThemeFilesCopy: queryThemesData: error: ${error}`,
          );
          return null;
        }
      })
      .filter((item: any) => item !== null);

    console.log(queryMainThemeData);

    // Now Staged Upload Create?
    const stagedTarget = await stagedUploadsCreate2(admin, queryMainThemeData);
    //const stagedUploadPath = `${stagedTarget.parameters.find((p: { name: string }) => p.name === "key").value}`;
    
    console.log(stagedTarget);

    // And Then Theme Create?
    // const themeCreate = await RunMutationThemeCreate(admin, stagedUploadPath);
  } catch (error) {
    console.error(`Try/Catch Error: RunMutationThemeFileCopy: error: ${error}`);
  }
}

Relevant Mutation Functions

//?https://shopify.dev/docs/api/admin-graphql/latest/queries/themes
export async function bulkOperationQueryThemes(admin: any) {
  try {
    const response = await admin.graphql(`
    mutation {
      bulkOperationRunQuery(
        query: "
        {
          themes {
            edges {
              node {
                name
                id
                role
              }
            }
          }
        }
        "
      ) {
        bulkOperation {
          id
          status
        }
        userErrors {
          field
          message
        }
      }
    }
    `);
    const data = await response.json();
    const userErrors = data.data.bulkOperationRunQuery.userErrors;

    if (userErrors.length > 0) {
      console.error(`Error: ${userErrors}`);
      throw new Error("BulkOperationRunQuery failed on themes");
    }
    const bulkOperationID = data.data.bulkOperationRunQuery.bulkOperation.id;
    const fileUrl = await bulkOperationQueryStatus(bulkOperationID, admin);

    if (fileUrl) {
      const fileResponse = await fetch(fileUrl);
      if (fileResponse.ok) {
        const fileContent = await fileResponse.text();
        return fileContent;
      } else {
        throw new Error(`Failed to fetch file: ${fileResponse.statusText}`);
      }
    } else {
      throw new Error("No file URL found for the completed operation.");
    }
  } catch (error) {
    console.error(`Try/Catch Error: bulkOperationQueryThemes: ${error}`);
  }
}

export async function bulkOperationQueryTheme(admin: any, mainTheme: any) {
  try {
    const id = mainTheme.id;
    const response = await admin.graphql(`
      mutation {
        bulkOperationRunQuery(
          query: """
            {
              theme (id: "${id}") {
                id
                name
                role
                files {
                  edges {
                    node {
                      filename
                      contentType
                      body {
                        ... on OnlineStoreThemeFileBodyText {
                          content
                        }
                      }
                    }
                  }
                }
              }
            }
            """
        ) {
          bulkOperation {
            id
            status
          }
          userErrors {
            field 
            message 
          }
        }
      }
      `);
    const data = await response.json();
    const userErrors = data.data.bulkOperationRunQuery.userErrors;

    const bulkOperationID = data.data.bulkOperationRunQuery.bulkOperation.id;

    const fileUrl = await bulkOperationQueryStatus(bulkOperationID, admin);

    if (fileUrl) {
      const fileResponse = await fetch(fileUrl);
      if (fileResponse.ok) {
        const fileContent = await fileResponse.text();
        return fileContent;
      } else {
        throw new Error(`Failed to fetch file: ${fileResponse.statusText}`);
      }
    } else {
      throw new Error("No file URL found for the completed operation.");
    }
  } catch (error) {
    console.error(`Try/Catch Error: queryTheme: ${error}`);
  }
}

// For Copying and Pasting Theme
export async function stagedUploadsCreate2(admin: any, incomingData: any) {
  try {
    const inputArray = incomingData.map((line: any) => ({
      filename: line.filename,
      mimeType: line.contentType,
      resource: "FILE",
      httpMethod: "POST",
    }));

    const chunkSize = 250;
    const chunks = Sanitization.chunkArray(inputArray, chunkSize);
    const allStagedTargets = [];
    for (const chu of chunks) {
      const response = await admin.graphql(
        `
      mutation stagedUploadsCreate($input:
      [StagedUploadInput!]!) {
        stagedUploadsCreate(input: $input) {
          stagedTargets {
            url
            resourceUrl
            parameters{
              name
              value
            }
          }
        }
      }`,
        { variables: { input: chu } },
      );
      const data = await response.json();
      if (!data.data) throw new Error("Failed to reserve upload space.");
      allStagedTargets.push(data.data.stagedUploadsCreate.stagedTargets);
    }
    return allStagedTargets;
  } catch (error) {
    console.error(`Try/Catch Error: stagedUploadsCreate2: Error: ${error}`);
  }
}

Hey shopfiy team. You guys can close this discussion post. I came up with a solution.

1 Like