Enterprise-CMCS/macpro-mako

View on GitHub
react-app/src/api/submissionService.ts

Summary

Maintainability
A
1 hr
Test Coverage
F
58%
import { OneMacUser } from "@/api";
import { buildActionUrl, SubmissionServiceEndpoint } from "@/utils";
import { useMutation, UseMutationOptions } from "@tanstack/react-query";
import { API } from "aws-amplify";
import { Action, Attachment, AttachmentKey, Authority, ReactQueryApiError } from "shared-types";
import { seaToolFriendlyTimestamp } from "shared-utils";

export type SubmissionServiceParameters<T> = {
  data: T;
  endpoint: SubmissionServiceEndpoint;
  user: OneMacUser | undefined;
  authority: Authority | string;
};
type SubmissionServiceResponse = {
  body: {
    message: string;
  };
};
type PreSignedURL = {
  url: string;
  key: string;
  bucket: string;
};
export type UploadRecipe = PreSignedURL & {
  data: File;
  title: AttachmentKey;
  name: string;
};
type AttachmentKeyValue = { attachmentKey: AttachmentKey; file: File };

/** Pass in an array of UploadRecipes and get a back-end compatible object
 * to store attachment data */
export const buildAttachmentObject = (recipes?: UploadRecipe[]) => {
  if (!recipes) return null;
  return recipes
    .map(
      (r) =>
        ({
          key: r.key,
          filename: r.name,
          title: r.title,
          bucket: r.bucket,
          uploadDate: Date.now(),
        } as Attachment),
    )
    .flat();
};

/** Builds the payload for submission based on which variant a developer has
 * configured the {@link submit} function with */
export const buildSubmissionPayload = <T extends Record<string, unknown>>(
  data: T,
  user: OneMacUser | undefined,
  endpoint: SubmissionServiceEndpoint,
  authority: Authority | string,
  attachments?: UploadRecipe[],
) => {
  const userDetails = {
    submitterEmail: user?.user?.email ?? "N/A",
    submitterName:
      user?.user?.given_name && user?.user?.family_name
        ? `${user.user.given_name} ${user.user.family_name}`
        : "N/A",
  };
  const baseProperties = {
    authority: authority,
    origin: "mako",
  };

  switch (endpoint) {
    case "/appk":
      return {
        ...data,
        ...userDetails,
        ...baseProperties,
        authority: Authority["1915c"],
        proposedEffectiveDate: seaToolFriendlyTimestamp(data.proposedEffectiveDate as Date),
        attachments: attachments ? buildAttachmentObject(attachments) : null,
      };
    case buildActionUrl(Action.REMOVE_APPK_CHILD):
      return {
        ...data,
        ...baseProperties,
        ...userDetails,
        authority: Authority["1915c"],
      };
    case "/submit":
      return {
        ...data,
        ...baseProperties,
        ...userDetails,
        proposedEffectiveDate: seaToolFriendlyTimestamp(data.proposedEffectiveDate as Date),
        attachments: attachments ? buildAttachmentObject(attachments) : null,
        state: (data.id as string).split("-")[0],
      };
    case buildActionUrl(Action.RESPOND_TO_RAI):
    case buildActionUrl(Action.ENABLE_RAI_WITHDRAW):
    case buildActionUrl(Action.DISABLE_RAI_WITHDRAW):
    case buildActionUrl(Action.WITHDRAW_RAI):
    case buildActionUrl(Action.WITHDRAW_PACKAGE):
    case buildActionUrl(Action.TEMP_EXTENSION):
    case buildActionUrl(Action.UPDATE_ID):
    default:
      return {
        ...baseProperties,
        ...userDetails,
        ...data,
        attachments: attachments ? buildAttachmentObject(attachments) : null,
      };
  }
};

export const buildAttachmentKeyValueArr = (
  attachments: Record<string, File[]>,
): AttachmentKeyValue[] =>
  Object.entries(attachments)
    .filter(([, val]) => val !== undefined && (val as File[]).length)
    .map(([key, value]) => {
      return (value as File[]).map((file) => ({
        attachmentKey: key as AttachmentKey,
        file: file,
      }));
    })
    .flat();

export const urlsToRecipes = (
  urls: PreSignedURL[],
  attachments: AttachmentKeyValue[],
): UploadRecipe[] =>
  urls.map((obj, idx) => ({
    ...obj, // Spreading the presigned url
    data: attachments[idx].file, // The attachment file object
    // Add your attachments object key and file label value to the attachmentTitleMap
    // for this transform to work. Else the title will just be the object key.
    title: attachments[idx].attachmentKey,
    name: attachments[idx].file.name,
  }));

/** A useful interface for submitting form data to our submission service */
export const submit = async <T extends Record<string, unknown>>({
  data,
  endpoint,
  user,
  authority,
}: SubmissionServiceParameters<T>): Promise<SubmissionServiceResponse> => {
  if (data?.attachments) {
    // Drop nulls and non arrays
    const attachments = buildAttachmentKeyValueArr(data.attachments as Record<string, File[]>);
    // Generate a presigned url for each attachment
    const preSignedURLs: PreSignedURL[] = await Promise.all(
      attachments.map((attachment) =>
        API.post("os", "/getUploadUrl", {
          body: {
            fileName: attachment.file.name,
          },
        }),
      ),
    );
    // For each attachment, add name, title, and a presigned url... and push to uploadRecipes
    const uploadRecipes: UploadRecipe[] = urlsToRecipes(preSignedURLs, attachments);
    // Upload attachments
    await Promise.all(
      uploadRecipes.map(async ({ url, data }) => {
        await fetch(url, {
          body: data,
          method: "PUT",
        });
      }),
    );
    // Submit form data
    return await API.post("os", endpoint, {
      body: buildSubmissionPayload(data, user, endpoint, authority, uploadRecipes),
    });
  } else {
    // Submit form data
    return await API.post("os", endpoint, {
      body: buildSubmissionPayload(data, user, endpoint, authority),
    });
  }
};

/** A useful interface for using react-query with our submission service. If you
 * are using react-hook-form's `form.handleSubmit()` pattern, bypass this and just
 * use {@link submit}. */
export const useSubmissionService = <T extends Record<string, unknown>>(
  config: SubmissionServiceParameters<T>,
  options?: UseMutationOptions<SubmissionServiceResponse, ReactQueryApiError>,
) =>
  useMutation<SubmissionServiceResponse, ReactQueryApiError>(
    ["submit"],
    () => submit(config),
    options,
  );