src/domain/file/usecase/upload-files/uploadFiles.ts

Summary

Maintainability
A
1 hr
Test Coverage
import short from 'short-uuid'
import type { ReduxStore, ThunkResult } from 'domain/file/store/store'
import type { UploadFiles } from 'domain/file/command/uploadFiles'
import type { FileExecutorHandler, FilePort } from 'domain/file/port/filePort'
import type { DeepReadonly } from 'superTypes'
import type { FileEntity } from 'domain/file/entity/file'
import { ErrorMapper } from 'domain/error/mapper/error.mapper'
import { ThrowErrorActions } from 'domain/common/actionCreators'
import { UnspecifiedError, UploadError } from 'domain/file/entity/error'
import type { AppState } from 'domain/file/store/appState'
import { removeAllFiles } from '../remove-all-files/removeAllFiles'
import type { TaskRegisterReceivedPayload } from 'domain/task/event/taskRegisterReceived'
import { RegisterTaskActions } from 'domain/task/usecase/register-task/actionCreators'
import { getFilesSizeByIds } from 'domain/file/store/selector/file.selector'
import { AmendTaskStatusActions } from 'domain/task/usecase/amend-task-status/actionCreators'
import { SetTaskProgressValueActions } from 'domain/task/usecase/set-task-progress-value/actionCreators'
import { truthy } from 'utils'

const fileTaskType = 'file#upload-files'

type StoreParams = { state: AppState; dispatch: ReduxStore['dispatch'] }

const dispatchError = (error: unknown, dispatch: DeepReadonly<StoreParams['dispatch']>): void => {
  const errorToDispatch = ErrorMapper.mapRawErrorToEntity(error)
  dispatch(ThrowErrorActions.errorThrown(errorToDispatch))
  dispatch(removeAllFiles())
}

const dispatchProgressCurrentValue =
  (taskId: string, dispatch: DeepReadonly<StoreParams['dispatch']>) =>
  (value?: number): void => {
    value !== undefined &&
      dispatch(
        SetTaskProgressValueActions.taskProgressValueSetReceived({
          id: taskId,
          progressValue: value
        })
      )
  }

const executeGatewayUpload = async (
  file: FileEntity,
  target: string,
  fileGateway: FilePort,
  taskId: string,
  dispatch: DeepReadonly<StoreParams['dispatch']>
): Promise<void> => {
  try {
    await fileGateway.execute(async (handler: DeepReadonly<FileExecutorHandler>) => {
      const { id, name, size, stream, type }: FileEntity = file

      await handler.upload(
        { id, name, size, stream, type },
        target,
        dispatchProgressCurrentValue(taskId, dispatch)
      )
    })

    dispatch(AmendTaskStatusActions.taskStatusAmendReceived({ id: taskId, status: 'success' }))
  } catch (error: unknown) {
    const uploadError =
      error instanceof UploadError
        ? new UploadError(error.message, { ...error.context, taskId })
        : new UnspecifiedError(
            `Oops... An unspecified error occured when trying to execute a file upload for the task <${taskId}>`
          )
    dispatch(AmendTaskStatusActions.taskStatusAmendReceived({ id: taskId, status: 'error' }))
    throw uploadError
  }
}

const processUpload = async (
  files: DeepReadonly<FileEntity<string>[]>,
  target: string,
  fileGateway: FilePort,
  storeParams: DeepReadonly<StoreParams>
): Promise<void[]> => {
  const promises: Promise<void>[] = []
  const { state, dispatch }: StoreParams = storeParams

  files.forEach((file: FileEntity) => {
    const taskToRegister: TaskRegisterReceivedPayload = {
      id: short.generate(),
      type: fileTaskType,
      progress: {
        min: 0,
        max: getFilesSizeByIds(state, [file.id]),
        current: 0
      }
    }
    dispatch(RegisterTaskActions.taskRegisterReceived(taskToRegister))
    promises.push(executeGatewayUpload(file, target, fileGateway, taskToRegister.id, dispatch))
  })
  return Promise.all(promises)
}

/**
 * Upload files to any server.
 * @param uploadFilesPayload An object containing the id of the file gateway to use,
 * the target represented by either a root folder / bucket / etc. in which the file must be uploaded
 * on server (e.g 'MyBucketName' or '/rootFolderPath/'),
 * and optionally an array of file ids to be uploaded.
 * @return A Redux async ThunkResult.
 */
export const uploadFiles =
  (uploadFilesPayload: DeepReadonly<UploadFiles>): ThunkResult<Promise<void>> =>
  // eslint-disable-next-line @typescript-eslint/typedef
  async (dispatch, getState, { fileRegistryGateway }) => {
    try {
      const storeParams: StoreParams = { state: getState(), dispatch }
      const { target, filePortId, fileIds = [] }: DeepReadonly<UploadFiles> = uploadFilesPayload
      const fileEntitiesById = storeParams.state.file.byId
      const hasFileIds = fileIds.length > 0
      const fileGateway = fileRegistryGateway.get(filePortId)
      const isCommandPayloadValid = fileIds.every((fileId: string) => fileEntitiesById.has(fileId))

      if (!fileEntitiesById.size) {
        throw new UnspecifiedError(`Oops... It looks like there are no file(s) to upload...`)
      }

      if (hasFileIds && !isCommandPayloadValid) {
        throw new UnspecifiedError(
          `Oops... At least one provided file id does not exist... Check the provided list: ${JSON.stringify(
            fileIds
          )}...`
        )
      }

      if (!target.length) {
        throw new UnspecifiedError(
          `Oops... It seems that the provided target is empty... So we cannot perform a file upload...`
        )
      }

      const files = hasFileIds
        ? fileIds.map((id: string) => fileEntitiesById.get(id)).filter(truthy)
        : fileEntitiesById.toIndexedSeq().toArray()
      await processUpload(files, target, fileGateway, storeParams)
      dispatch(removeAllFiles())
    } catch (error: unknown) {
      dispatchError(error, dispatch)
    }
  }