AssemblyOptions.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. import ErrorWithCause from '@uppy/utils/lib/ErrorWithCause'
  2. import type { Body, Meta, UppyFile } from '@uppy/utils/lib/UppyFile'
  3. import type { AssemblyParameters, Opts, AssemblyOptions as Options } from '.'
  4. /**
  5. * Check that Assembly parameters are present and include all required fields.
  6. */
  7. function validateParams(params?: AssemblyParameters | null): void {
  8. if (params == null) {
  9. throw new Error('Transloadit: The `params` option is required.')
  10. }
  11. if (typeof params === 'string') {
  12. try {
  13. // eslint-disable-next-line no-param-reassign
  14. params = JSON.parse(params)
  15. } catch (err) {
  16. // Tell the user that this is not an Uppy bug!
  17. throw new ErrorWithCause(
  18. 'Transloadit: The `params` option is a malformed JSON string.',
  19. { cause: err },
  20. )
  21. }
  22. }
  23. if (!params!.auth || !params!.auth.key) {
  24. throw new Error(
  25. 'Transloadit: The `params.auth.key` option is required. ' +
  26. 'You can find your Transloadit API key at https://transloadit.com/c/template-credentials',
  27. )
  28. }
  29. }
  30. export type OptionsWithRestructuredFields = Omit<Options, 'fields'> & {
  31. fields: Record<string, string | number>
  32. }
  33. /**
  34. * Combine Assemblies with the same options into a single Assembly for all the
  35. * relevant files.
  36. */
  37. function dedupe(
  38. list: Array<
  39. { fileIDs: string[]; options: OptionsWithRestructuredFields } | undefined
  40. >,
  41. ) {
  42. const dedupeMap: Record<
  43. string,
  44. { fileIDArrays: string[][]; options: OptionsWithRestructuredFields }
  45. > = Object.create(null)
  46. for (const { fileIDs, options } of list.filter(Boolean) as Array<{
  47. fileIDs: string[]
  48. options: OptionsWithRestructuredFields
  49. }>) {
  50. const id = JSON.stringify(options)
  51. if (id in dedupeMap) {
  52. dedupeMap[id].fileIDArrays.push(fileIDs)
  53. } else {
  54. dedupeMap[id] = {
  55. options,
  56. fileIDArrays: [fileIDs],
  57. }
  58. }
  59. }
  60. return Object.values(dedupeMap).map(({ options, fileIDArrays }) => ({
  61. options,
  62. fileIDs: fileIDArrays.flat(1),
  63. }))
  64. }
  65. async function getAssemblyOptions<M extends Meta, B extends Body>(
  66. file: UppyFile<M, B> | null,
  67. options: Opts<M, B>,
  68. ): Promise<OptionsWithRestructuredFields> {
  69. const assemblyOptions = (
  70. typeof options.assemblyOptions === 'function' ?
  71. await options.assemblyOptions(file, options)
  72. : options.assemblyOptions) as OptionsWithRestructuredFields
  73. validateParams(assemblyOptions.params)
  74. const { fields } = assemblyOptions
  75. if (Array.isArray(fields)) {
  76. assemblyOptions.fields =
  77. file == null ?
  78. {}
  79. : Object.fromEntries(
  80. fields.map((fieldName) => [fieldName, file.meta[fieldName]]),
  81. )
  82. } else if (fields == null) {
  83. assemblyOptions.fields = {}
  84. }
  85. return assemblyOptions
  86. }
  87. /**
  88. * Turn Transloadit plugin options and a list of files into a list of Assembly
  89. * options.
  90. */
  91. class AssemblyOptions<M extends Meta, B extends Body> {
  92. opts: Opts<M, B>
  93. files: UppyFile<M, B>[]
  94. constructor(files: UppyFile<M, B>[], opts: Opts<M, B>) {
  95. this.files = files
  96. this.opts = opts
  97. }
  98. /**
  99. * Generate a set of Assemblies that will handle the upload.
  100. * Returns a Promise for an object with keys:
  101. * - fileIDs - an array of file IDs to add to this Assembly
  102. * - options - Assembly options
  103. */
  104. async build(): Promise<
  105. { fileIDs: string[]; options: OptionsWithRestructuredFields }[]
  106. > {
  107. const options = this.opts
  108. if (this.files.length > 0) {
  109. return Promise.all(
  110. this.files.map(async (file) => {
  111. if (file == null) return undefined
  112. const assemblyOptions = await getAssemblyOptions(file, options)
  113. // We check if the file is present here again, because it could had been
  114. // removed during the await, e.g. if the user hit cancel while we were
  115. // waiting for the options.
  116. if (file == null) return undefined
  117. return {
  118. fileIDs: [file.id],
  119. options: assemblyOptions,
  120. }
  121. }),
  122. ).then(dedupe)
  123. }
  124. if (options.alwaysRunAssembly) {
  125. // No files, just generate one Assembly
  126. const assemblyOptions = await getAssemblyOptions(null, options)
  127. return [
  128. {
  129. fileIDs: [],
  130. options: assemblyOptions,
  131. },
  132. ]
  133. }
  134. // If there are no files and we do not `alwaysRunAssembly`,
  135. // don't do anything.
  136. return []
  137. }
  138. }
  139. export default AssemblyOptions
  140. export { validateParams }