async function uploadAppSyncFiles()

in packages/amplify-provider-awscloudformation/src/upload-appsync-files.js [31:224]


async function uploadAppSyncFiles(context, resourcesToUpdate, allResources, options = {}) {
  const allApiResourceToUpdate = resourcesToUpdate.filter(resource => resource.service === 'AppSync');
  const allApiResources = allResources.filter(resource => resource.service === 'AppSync');
  const { defaultParams, useDeprecatedParameters } = options;
  const backEndDir = context.amplify.pathManager.getBackendDirPath();
  const projectBucket = getProjectBucket(context);

  const getDeploymentRootKey = async resourceDir => {
    let deploymentSubKey;
    if (useDeprecatedParameters) {
      deploymentSubKey = new Date().getTime();
    } else {
      deploymentSubKey = await hashDirectory(resourceDir);
    }
    const deploymentRootKey = `${ROOT_APPSYNC_S3_KEY}/${deploymentSubKey}`;
    return deploymentRootKey;
  };

  const getApiKeyConfigured = () => {
    const projectDetails = context.amplify.getProjectDetails();
    const appSyncAPIs = Object.keys(projectDetails.amplifyMeta.api).reduce((acc, apiName) => {
      const api = projectDetails.amplifyMeta.api[apiName];
      if (api.service === 'AppSync') {
        acc.push({ ...api, name: apiName });
      }
      return acc;
    }, []);

    const appSyncApi = appSyncAPIs && appSyncAPIs.length && appSyncAPIs.length > 0 ? appSyncAPIs[0] : undefined;

    let hasApiKey = false;

    if (appSyncApi) {
      // Check for legacy security configuration and multi-auth as well
      const { authConfig, securityType } = appSyncApi.output;

      if (securityType && securityType === 'API_KEY') {
        hasApiKey = true;
      } else if (authConfig) {
        if (authConfig.defaultAuthentication.authenticationType === 'API_KEY') {
          hasApiKey = true;
        } else if (
          authConfig.additionalAuthenticationProviders &&
          authConfig.additionalAuthenticationProviders.find(p => p.authenticationType === 'API_KEY')
        ) {
          hasApiKey = true;
        }
      }
    }

    return hasApiKey;
  };

  const writeUpdatedParametersJson = (resource, rootKey) => {
    const { category, resourceName } = resource;
    // Read parameters.json, add timestamps, and write to build/parameters.json
    const parametersFilePath = path.join(backEndDir, category, resourceName, PARAM_FILE_NAME);
    const currentParameters = defaultParams || {};
    const apiKeyConfigured = getApiKeyConfigured();
    currentParameters.CreateAPIKey = apiKeyConfigured ? 1 : 0;
    if (fs.existsSync(parametersFilePath)) {
      try {
        const paramFile = fs.readFileSync(parametersFilePath).toString();
        const personalParams = JSON.parse(paramFile);
        Object.assign(currentParameters, personalParams);

        // If authRoleName parameter not present, add it
        if (!currentParameters.authRoleName) {
          currentParameters.authRoleName = {
            Ref: 'AuthRoleName',
          };
        }

        // If unauthRoleName parameter not present, add it
        if (!currentParameters.unauthRoleName) {
          currentParameters.unauthRoleName = {
            Ref: 'UnauthRoleName',
          };
        }

        if (personalParams.CreateAPIKey !== undefined && personalParams.APIKeyExpirationEpoch !== undefined) {
          context.print.warning(
            'APIKeyExpirationEpoch and CreateAPIKey parameters should not used together because it can cause ' +
              'unwanted behavior. In the future APIKeyExpirationEpoch will be removed, use CreateAPIKey instead.',
          );
        }

        // If the customer explicitly disabled API Key creation via legacy setting, show a warning and
        // honor the setting.
        if (personalParams.APIKeyExpirationEpoch) {
          if (personalParams.APIKeyExpirationEpoch === -1) {
            currentParameters.CreateAPIKey = 0;

            delete currentParameters.APIKeyExpirationEpoch;

            context.print.warning(
              "APIKeyExpirationEpoch parameter's -1 value is deprecated to disable " +
                'the API Key creation. In the future CreateAPIKey parameter replaces this behavior.',
            );
          } else {
            currentParameters.CreateAPIKey = 1;
          }
        }

        // We've to honor if customers are setting CreateAPIKey to 0 in their parameters file
        // to preserve the same behavior if APIKeyExpirationEpoch would be -1, so if it
        // was defined then its already copied over to currentParameters and we'll not overwrite it
        // based on the security configuration.
        if (personalParams.CreateAPIKey === undefined) {
          currentParameters.CreateAPIKey = apiKeyConfigured ? 1 : 0;
        }
      } catch (e) {
        context.print.error(`Could not parse parameters file at "${parametersFilePath}"`);
      }
    }
    if (!useDeprecatedParameters) {
      Object.assign(currentParameters, {
        S3DeploymentBucket: projectBucket,
        S3DeploymentRootKey: rootKey,
      });
    }

    // As a safety mechanism when dealing with migrations and diff versions,
    // make sure only expected parameters are passed.
    const cfFilePath = path.join(backEndDir, category, resourceName, 'build', CF_FILE_NAME);
    try {
      const cfFileContents = fs.readFileSync(cfFilePath).toString();
      const cfTemplateJson = JSON.parse(cfFileContents);
      const paramKeys = Object.keys(currentParameters);
      for (let keyIndex = 0; keyIndex < paramKeys.length; keyIndex++) {
        const paramKey = paramKeys[keyIndex];
        if (!cfTemplateJson.Parameters[paramKey]) {
          delete currentParameters[paramKey];
        }
      }
    } catch (e) {
      context.print.warning(`Could not read cloudformation template at path: ${cfFilePath}`);
    }

    const jsonString = JSON.stringify(currentParameters, null, 4);
    const buildDirectoryPath = path.join(backEndDir, category, resourceName, 'build');
    const parametersOutputFilePath = path.join(buildDirectoryPath, PARAM_FILE_NAME);
    fsext.ensureDirSync(buildDirectoryPath);
    fs.writeFileSync(parametersOutputFilePath, jsonString, 'utf8');
  };

  // There can only be one appsync resource
  if (allApiResourceToUpdate.length > 0) {
    const resource = allApiResourceToUpdate[0];
    const { category, resourceName } = resource;
    const resourceDir = path.normalize(path.join(backEndDir, category, resourceName));
    const resourceBuildDir = path.normalize(path.join(resourceDir, 'build'));

    const deploymentRootKey = await getDeploymentRootKey(resourceDir);
    writeUpdatedParametersJson(resource, deploymentRootKey);

    // Upload build/* to S3.
    const s3Client = await S3.getInstance(context);
    if (!fs.existsSync(resourceBuildDir)) {
      return;
    }
    const spinner = new ora('Uploading files...');
    spinner.start();
    await TransformPackage.uploadAPIProject({
      directory: resourceBuildDir,
      upload: async blob => {
        const { Key, Body } = blob;
        const fullKey = `${deploymentRootKey}/${Key}`;
        logger('uploadAppSyncFiles.upload.s3Client.uploadFile', [{ Key }])();

        return await s3Client.uploadFile(
          {
            Key: fullKey,
            Body,
          },
          false,
        );
      },
    });
    spinner.stop();
  } else if (allApiResources.length > 0) {
    // We need to update the parameters file even when we are not deploying the API
    // category to fix a bug around deployments on CI/CD platforms. Basically if a
    // build has not run on this machine before and we are updating a non-api category,
    // the params will not have the S3DeploymentRootKey parameter and will fail.
    // This block uses the consistent hash to fill params so the push
    // succeeds when the api category has not been built on this machine.
    const resource = allApiResources[0];
    const { category, resourceName } = resource;
    const resourceDir = path.normalize(path.join(backEndDir, category, resourceName));
    const deploymentRootKey = await getDeploymentRootKey(resourceDir);
    writeUpdatedParametersJson(resource, deploymentRootKey);
  }
}