transloadit 6.4.0 → 6.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -5
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +61 -5
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/types/builtinTemplates.d.ts +15 -3
- package/dist/alphalib/types/builtinTemplates.d.ts.map +1 -1
- package/dist/alphalib/types/builtinTemplates.js +23 -3
- package/dist/alphalib/types/builtinTemplates.js.map +1 -1
- package/dist/cli/OutputCtl.d.ts +1 -0
- package/dist/cli/OutputCtl.d.ts.map +1 -1
- package/dist/cli/OutputCtl.js +1 -1
- package/dist/cli/OutputCtl.js.map +1 -1
- package/dist/cli/commands/BaseCommand.d.ts +3 -2
- package/dist/cli/commands/BaseCommand.d.ts.map +1 -1
- package/dist/cli/commands/BaseCommand.js +27 -3
- package/dist/cli/commands/BaseCommand.js.map +1 -1
- package/dist/cli/commands/assemblies.d.ts +21 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +584 -91
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/templates.d.ts +1 -1
- package/dist/cli/commands/templates.d.ts.map +1 -1
- package/dist/cli/commands/templates.js +57 -67
- package/dist/cli/commands/templates.js.map +1 -1
- package/dist/cli/errors.d.ts +8 -0
- package/dist/cli/errors.d.ts.map +1 -0
- package/dist/cli/errors.js +28 -0
- package/dist/cli/errors.js.map +1 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +22 -0
- package/dist/cli/helpers.js.map +1 -1
- package/dist/cli/intentRuntime.d.ts +1 -0
- package/dist/cli/intentRuntime.d.ts.map +1 -1
- package/dist/cli/intentRuntime.js +9 -3
- package/dist/cli/intentRuntime.js.map +1 -1
- package/dist/cli/resultFiles.d.ts +1 -0
- package/dist/cli/resultFiles.d.ts.map +1 -1
- package/dist/cli/resultFiles.js.map +1 -1
- package/dist/cli/resultUrls.d.ts +2 -1
- package/dist/cli/resultUrls.d.ts.map +1 -1
- package/dist/cli/resultUrls.js +6 -3
- package/dist/cli/resultUrls.js.map +1 -1
- package/dist/cli/stepsInput.d.ts.map +1 -1
- package/dist/cli/stepsInput.js +17 -4
- package/dist/cli/stepsInput.js.map +1 -1
- package/dist/cli/types.d.ts +66 -0
- package/dist/cli/types.d.ts.map +1 -1
- package/dist/cli/types.js +29 -0
- package/dist/cli/types.js.map +1 -1
- package/dist/tus.d.ts +7 -2
- package/dist/tus.d.ts.map +1 -1
- package/dist/tus.js +58 -4
- package/dist/tus.js.map +1 -1
- package/package.json +1 -1
- package/src/Transloadit.ts +65 -4
- package/src/alphalib/types/builtinTemplates.ts +33 -3
- package/src/cli/OutputCtl.ts +2 -1
- package/src/cli/commands/BaseCommand.ts +29 -7
- package/src/cli/commands/assemblies.ts +775 -118
- package/src/cli/commands/templates.ts +93 -98
- package/src/cli/errors.ts +34 -0
- package/src/cli/helpers.ts +21 -0
- package/src/cli/intentRuntime.ts +10 -3
- package/src/cli/resultFiles.ts +1 -0
- package/src/cli/resultUrls.ts +11 -3
- package/src/cli/stepsInput.ts +19 -4
- package/src/cli/types.ts +35 -0
- package/src/tus.ts +72 -5
|
@@ -11,11 +11,13 @@ import { Command, Option } from 'clipanion'
|
|
|
11
11
|
import rreaddir from 'recursive-readdir'
|
|
12
12
|
import { z } from 'zod'
|
|
13
13
|
|
|
14
|
+
import { ApiError } from '../../ApiError.ts'
|
|
14
15
|
import { tryCatch } from '../../alphalib/tryCatch.ts'
|
|
16
|
+
import { CliInputError } from '../errors.ts'
|
|
15
17
|
import { createReadStream, formatAPIError, streamToBuffer } from '../helpers.ts'
|
|
16
18
|
import { createCliStepsSchema, parseStepsInputJson } from '../stepsInput.ts'
|
|
17
19
|
import ModifiedLookup from '../template-last-modified.ts'
|
|
18
|
-
import {
|
|
20
|
+
import { isTransloaditAPIError } from '../types.ts'
|
|
19
21
|
import { AuthenticatedCommand } from './BaseCommand.ts'
|
|
20
22
|
|
|
21
23
|
const rreaddirAsync = promisify(rreaddir)
|
|
@@ -54,27 +56,29 @@ export interface TemplateSyncOptions {
|
|
|
54
56
|
recursive?: boolean
|
|
55
57
|
}
|
|
56
58
|
|
|
59
|
+
async function readTemplateInput<T>(input: Promise<T>): Promise<T> {
|
|
60
|
+
const [error, content] = await tryCatch(input)
|
|
61
|
+
if (error) {
|
|
62
|
+
throw new CliInputError(`Cannot read Template input: ${error.message}`, { cause: error })
|
|
63
|
+
}
|
|
64
|
+
return content
|
|
65
|
+
}
|
|
66
|
+
|
|
57
67
|
export async function create(
|
|
58
68
|
output: IOutputCtl,
|
|
59
69
|
client: Transloadit,
|
|
60
70
|
{ name, file }: TemplateCreateOptions,
|
|
61
71
|
): Promise<unknown> {
|
|
62
|
-
|
|
63
|
-
const buf = await streamToBuffer(createReadStream(file))
|
|
72
|
+
const buf = await readTemplateInput(streamToBuffer(createReadStream(file)))
|
|
64
73
|
|
|
65
|
-
|
|
74
|
+
const steps = parseStepsInputJson(buf.toString(), output)
|
|
66
75
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
} catch (err) {
|
|
74
|
-
const error = ensureError(err)
|
|
75
|
-
output.error(error.message)
|
|
76
|
-
throw err
|
|
77
|
-
}
|
|
76
|
+
const result = await client.createTemplate({
|
|
77
|
+
name,
|
|
78
|
+
template: { steps } as TemplateContent,
|
|
79
|
+
})
|
|
80
|
+
output.print(result.id, result)
|
|
81
|
+
return result
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
export async function get(
|
|
@@ -84,11 +88,7 @@ export async function get(
|
|
|
84
88
|
): Promise<void> {
|
|
85
89
|
const requests = templates.map((template) => client.getTemplate(template))
|
|
86
90
|
|
|
87
|
-
const
|
|
88
|
-
if (err) {
|
|
89
|
-
output.error(formatAPIError(err))
|
|
90
|
-
throw err
|
|
91
|
-
}
|
|
91
|
+
const results = await Promise.all(requests)
|
|
92
92
|
|
|
93
93
|
for (const result of results) {
|
|
94
94
|
output.print(result, result)
|
|
@@ -100,53 +100,49 @@ export async function modify(
|
|
|
100
100
|
client: Transloadit,
|
|
101
101
|
{ template, name, file }: TemplateModifyOptions,
|
|
102
102
|
): Promise<void> {
|
|
103
|
-
|
|
104
|
-
const buf = await streamToBuffer(createReadStream(file))
|
|
103
|
+
const buf = await readTemplateInput(streamToBuffer(createReadStream(file)))
|
|
105
104
|
|
|
106
|
-
|
|
107
|
-
|
|
105
|
+
let steps: TemplateContent['steps'] | null = null
|
|
106
|
+
let newName = name
|
|
108
107
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!name || buf.length === 0) {
|
|
114
|
-
const tpl = await client.getTemplate(template)
|
|
115
|
-
if (!name) newName = tpl.name
|
|
116
|
-
if (buf.length === 0 && tpl.content.steps) {
|
|
117
|
-
steps = tpl.content.steps as TemplateContent['steps']
|
|
118
|
-
}
|
|
119
|
-
}
|
|
108
|
+
if (buf.length > 0) {
|
|
109
|
+
steps = parseStepsInputJson(buf.toString(), output) as TemplateContent['steps']
|
|
110
|
+
}
|
|
120
111
|
|
|
121
|
-
|
|
122
|
-
|
|
112
|
+
if (!name || buf.length === 0) {
|
|
113
|
+
const tpl = await client.getTemplate(template)
|
|
114
|
+
if (!name) newName = tpl.name
|
|
115
|
+
if (buf.length === 0 && tpl.content.steps) {
|
|
116
|
+
steps = tpl.content.steps as TemplateContent['steps']
|
|
123
117
|
}
|
|
118
|
+
}
|
|
124
119
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
// Steps (validated) is assignable to StepsInput at runtime; cast for TS
|
|
128
|
-
template: { steps } as TemplateContent,
|
|
129
|
-
})
|
|
130
|
-
} catch (err) {
|
|
131
|
-
output.error(formatAPIError(err))
|
|
132
|
-
throw err
|
|
120
|
+
if (steps === null) {
|
|
121
|
+
throw new CliInputError('No steps to update template with')
|
|
133
122
|
}
|
|
123
|
+
|
|
124
|
+
await client.editTemplate(template, {
|
|
125
|
+
name: newName,
|
|
126
|
+
// Steps (validated) is assignable to StepsInput at runtime; cast for TS
|
|
127
|
+
template: { steps } as TemplateContent,
|
|
128
|
+
})
|
|
134
129
|
}
|
|
135
130
|
|
|
136
131
|
async function deleteTemplates(
|
|
137
132
|
output: IOutputCtl,
|
|
138
133
|
client: Transloadit,
|
|
139
134
|
{ templates }: TemplateDeleteOptions,
|
|
140
|
-
): Promise<
|
|
141
|
-
await Promise.
|
|
142
|
-
templates.map(
|
|
143
|
-
const [err] = await tryCatch(client.deleteTemplate(template))
|
|
144
|
-
if (err) {
|
|
145
|
-
output.error(formatAPIError(err))
|
|
146
|
-
throw err
|
|
147
|
-
}
|
|
148
|
-
}),
|
|
135
|
+
): Promise<number | undefined> {
|
|
136
|
+
const results = await Promise.allSettled(
|
|
137
|
+
templates.map((template) => client.deleteTemplate(template)),
|
|
149
138
|
)
|
|
139
|
+
let exitCode: number | undefined
|
|
140
|
+
for (const [index, result] of results.entries()) {
|
|
141
|
+
if (result.status === 'fulfilled') continue
|
|
142
|
+
output.error(`Template ${templates[index]}: ${formatAPIError(result.reason)}`)
|
|
143
|
+
exitCode = 1
|
|
144
|
+
}
|
|
145
|
+
return exitCode
|
|
150
146
|
}
|
|
151
147
|
|
|
152
148
|
// Export with `delete` alias for external consumers
|
|
@@ -281,54 +277,54 @@ export async function sync(
|
|
|
281
277
|
|
|
282
278
|
const modified = new ModifiedLookup(client)
|
|
283
279
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
if (!
|
|
288
|
-
|
|
289
|
-
throw new Error(`Template file has no id and no steps: ${template.file}`)
|
|
290
|
-
}
|
|
291
|
-
return download(template)
|
|
280
|
+
await Promise.all(
|
|
281
|
+
templates.map(async (template) => {
|
|
282
|
+
if (!('steps' in template.data)) {
|
|
283
|
+
if (!template.data.transloadit_template_id) {
|
|
284
|
+
throw new CliInputError(`Template file has no id and no steps: ${template.file}`)
|
|
292
285
|
}
|
|
286
|
+
return download(template)
|
|
287
|
+
}
|
|
293
288
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
}),
|
|
313
|
-
)
|
|
314
|
-
} catch (err) {
|
|
315
|
-
if (isTransloaditAPIError(err)) {
|
|
316
|
-
if (err.code === 'SERVER_404' || (err.response && err.response.statusCode === 404)) {
|
|
317
|
-
throw new Error(`Template file references nonexistent template: ${template.file}`)
|
|
289
|
+
if (!template.data.transloadit_template_id) return upload(template)
|
|
290
|
+
|
|
291
|
+
const stats = await readTemplateInput(fsp.stat(template.file))
|
|
292
|
+
const fileModified = stats.mtime
|
|
293
|
+
|
|
294
|
+
let templateModified: Date
|
|
295
|
+
const templateId = template.data.transloadit_template_id
|
|
296
|
+
try {
|
|
297
|
+
await client.getTemplate(templateId)
|
|
298
|
+
templateModified = await new Promise<Date>((resolve, reject) =>
|
|
299
|
+
modified.byId(templateId, (err, res) => {
|
|
300
|
+
if (err) {
|
|
301
|
+
reject(err)
|
|
302
|
+
} else if (res) {
|
|
303
|
+
resolve(res)
|
|
304
|
+
} else {
|
|
305
|
+
reject(new Error('No date returned'))
|
|
318
306
|
}
|
|
319
|
-
}
|
|
320
|
-
|
|
307
|
+
}),
|
|
308
|
+
)
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (
|
|
311
|
+
(err instanceof ApiError &&
|
|
312
|
+
(err.code === 'TEMPLATE_NOT_FOUND' || err.cause?.response?.statusCode === 404)) ||
|
|
313
|
+
(isTransloaditAPIError(err) &&
|
|
314
|
+
(err.code === 'SERVER_404' || err.response?.statusCode === 404))
|
|
315
|
+
) {
|
|
316
|
+
throw new CliInputError(
|
|
317
|
+
`Template file references nonexistent template: ${template.file}`,
|
|
318
|
+
{ cause: err },
|
|
319
|
+
)
|
|
321
320
|
}
|
|
321
|
+
throw err
|
|
322
|
+
}
|
|
322
323
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
),
|
|
324
|
+
if (fileModified > templateModified) return upload(template)
|
|
325
|
+
return download(template)
|
|
326
|
+
}),
|
|
327
327
|
)
|
|
328
|
-
if (err) {
|
|
329
|
-
output.error(err)
|
|
330
|
-
throw err
|
|
331
|
-
}
|
|
332
328
|
|
|
333
329
|
async function upload(template: TemplateFile): Promise<void> {
|
|
334
330
|
const params = {
|
|
@@ -479,10 +475,9 @@ export class TemplatesDeleteCommand extends AuthenticatedCommand {
|
|
|
479
475
|
templateIds = Option.Rest({ required: 1 })
|
|
480
476
|
|
|
481
477
|
protected async run(): Promise<number | undefined> {
|
|
482
|
-
await deleteTemplates(this.output, this.client, {
|
|
478
|
+
return await deleteTemplates(this.output, this.client, {
|
|
483
479
|
templates: this.templateIds,
|
|
484
480
|
})
|
|
485
|
-
return undefined
|
|
486
481
|
}
|
|
487
482
|
}
|
|
488
483
|
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { ZodIssue } from 'zod'
|
|
2
|
+
|
|
3
|
+
/** An input error the CLI can explain without an internal-error stack trace. */
|
|
4
|
+
export class CliInputError extends Error {
|
|
5
|
+
override name = 'CliInputError'
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function describeIssues(issues: ZodIssue[]): string[] {
|
|
9
|
+
return issues.flatMap((issue) => {
|
|
10
|
+
if (issue.code === 'invalid_union') {
|
|
11
|
+
const descriptions = issue.unionErrors.flatMap((error) => describeIssues(error.issues))
|
|
12
|
+
// Interpolation is an alternative to literal Robot parameters, not a required input format.
|
|
13
|
+
const literals = descriptions.filter(
|
|
14
|
+
(description) => !description.endsWith(': Must be a full interpolation string'),
|
|
15
|
+
)
|
|
16
|
+
return literals.length > 0 ? literals : descriptions
|
|
17
|
+
}
|
|
18
|
+
const message =
|
|
19
|
+
issue.code === 'invalid_enum_value'
|
|
20
|
+
? `Expected one of: ${issue.options.join(', ')}`
|
|
21
|
+
: issue.message
|
|
22
|
+
return [`${issue.path.join('.') || '(root)'}: ${message}`]
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Summarize validation paths and remedies without duplicating Zod's union diagnostics. */
|
|
27
|
+
export function formatSchemaIssues(issues: ZodIssue[]): string {
|
|
28
|
+
const descriptions = [...new Set(describeIssues(issues))]
|
|
29
|
+
const remaining = descriptions.length - 10
|
|
30
|
+
return [
|
|
31
|
+
...descriptions.slice(0, 10),
|
|
32
|
+
...(remaining > 0 ? [`${remaining} more validation error${remaining === 1 ? '' : 's'}`] : []),
|
|
33
|
+
].join('; ')
|
|
34
|
+
}
|
package/src/cli/helpers.ts
CHANGED
|
@@ -8,8 +8,10 @@ import { homedir, userInfo } from 'node:os'
|
|
|
8
8
|
import path from 'node:path'
|
|
9
9
|
|
|
10
10
|
import { parse as parseDotenv } from 'dotenv'
|
|
11
|
+
import { ParseError, RequestError } from 'got'
|
|
11
12
|
import { z } from 'zod'
|
|
12
13
|
|
|
14
|
+
import { ApiError } from '../ApiError.ts'
|
|
13
15
|
import { isAPIError } from './types.ts'
|
|
14
16
|
|
|
15
17
|
/** API signing algorithms supported by CLI credentials and device authorization. */
|
|
@@ -535,6 +537,25 @@ export async function readCliInput(options: ReadCliInputOptions): Promise<CliInp
|
|
|
535
537
|
}
|
|
536
538
|
|
|
537
539
|
export function formatAPIError(err: unknown): string {
|
|
540
|
+
if (err instanceof RequestError) {
|
|
541
|
+
// ParseError messages contain response fragments and signed request URLs.
|
|
542
|
+
if (err instanceof ParseError) return `${err.code}: The API returned invalid JSON.`
|
|
543
|
+
const requestUrl = err.options.url
|
|
544
|
+
const message =
|
|
545
|
+
requestUrl instanceof URL
|
|
546
|
+
? err.message.replaceAll(requestUrl.href, `${requestUrl.origin}${requestUrl.pathname}`)
|
|
547
|
+
: err.message
|
|
548
|
+
return `${err.code}: ${message}`
|
|
549
|
+
}
|
|
550
|
+
if (err instanceof ApiError && !err.assemblySslUrl) {
|
|
551
|
+
const requestUrl = err.cause?.options.url
|
|
552
|
+
const url = typeof requestUrl === 'string' ? new URL(requestUrl) : requestUrl
|
|
553
|
+
if (url && /\/assemblies\/[^/]+$/.test(url.pathname)) {
|
|
554
|
+
// Request URLs can carry signed query parameters; only expose the Assembly location.
|
|
555
|
+
return `${err.message} (Assembly URL: ${url.origin}${url.pathname})`
|
|
556
|
+
}
|
|
557
|
+
if (err.assemblyId) return `${err.message} (Assembly ID: ${err.assemblyId})`
|
|
558
|
+
}
|
|
538
559
|
if (isAPIError(err)) {
|
|
539
560
|
return `${err.error}: ${err.message}`
|
|
540
561
|
}
|
package/src/cli/intentRuntime.ts
CHANGED
|
@@ -33,6 +33,7 @@ export interface PreparedIntentInputs {
|
|
|
33
33
|
cleanup: Array<() => Promise<void>>
|
|
34
34
|
hasTransientInputs: boolean
|
|
35
35
|
inputs: string[]
|
|
36
|
+
transientInputPaths?: string[]
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
export interface IntentSingleStepExecutionDefinition {
|
|
@@ -252,6 +253,7 @@ export async function prepareIntentInputs({
|
|
|
252
253
|
cleanup: prepared.cleanup,
|
|
253
254
|
hasTransientInputs: true,
|
|
254
255
|
inputs,
|
|
256
|
+
transientInputPaths: Object.values(prepared.files),
|
|
255
257
|
}
|
|
256
258
|
}
|
|
257
259
|
|
|
@@ -383,14 +385,15 @@ async function executeIntentCommand({
|
|
|
383
385
|
} as AssembliesCreateOptions['stepsData'],
|
|
384
386
|
}
|
|
385
387
|
|
|
386
|
-
const { hasFailures, resultUrls } = await assembliesCommands.create(output, client, {
|
|
388
|
+
const { assemblies, hasFailures, resultUrls } = await assembliesCommands.create(output, client, {
|
|
387
389
|
...createOptions,
|
|
388
390
|
output: outputPath ?? null,
|
|
389
391
|
outputMode,
|
|
390
392
|
...executionOptions,
|
|
393
|
+
printUrls,
|
|
391
394
|
})
|
|
392
395
|
if (printUrls) {
|
|
393
|
-
printResultUrls(output, resultUrls)
|
|
396
|
+
printResultUrls(output, resultUrls, assemblies)
|
|
394
397
|
}
|
|
395
398
|
return hasFailures ? 1 : undefined
|
|
396
399
|
}
|
|
@@ -766,9 +769,13 @@ abstract class GeneratedFileIntentCommandBase extends GeneratedIntentCommandBase
|
|
|
766
769
|
}
|
|
767
770
|
|
|
768
771
|
const effectiveOutputPath = this.getEffectiveOutputPath(rawValues)
|
|
772
|
+
const createOptions = this.getCreateOptions(effectivePreparedInputs.inputs)
|
|
773
|
+
if (effectivePreparedInputs.transientInputPaths != null) {
|
|
774
|
+
createOptions.transientInputPaths = effectivePreparedInputs.transientInputPaths
|
|
775
|
+
}
|
|
769
776
|
return await executeIntentCommand({
|
|
770
777
|
client: this.client,
|
|
771
|
-
createOptions
|
|
778
|
+
createOptions,
|
|
772
779
|
definition: this.getIntentDefinition(),
|
|
773
780
|
output: this.output,
|
|
774
781
|
outputMode: this.getEffectiveOutputMode(rawValues, effectiveOutputPath),
|
package/src/cli/resultFiles.ts
CHANGED
package/src/cli/resultUrls.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IOutputCtl } from './OutputCtl.ts'
|
|
2
2
|
import type { NormalizedAssemblyResults } from './resultFiles.ts'
|
|
3
|
+
import type { AssemblySummary } from './types.ts'
|
|
3
4
|
|
|
4
5
|
import { normalizeAssemblyResults } from './resultFiles.ts'
|
|
5
6
|
|
|
@@ -64,10 +65,17 @@ export function formatResultUrlRows(rows: readonly ResultUrlRow[]): string {
|
|
|
64
65
|
.join('\n')
|
|
65
66
|
}
|
|
66
67
|
|
|
67
|
-
export function printResultUrls(
|
|
68
|
-
|
|
68
|
+
export function printResultUrls(
|
|
69
|
+
output: IOutputCtl,
|
|
70
|
+
rows: readonly ResultUrlRow[],
|
|
71
|
+
assemblies?: readonly AssemblySummary[],
|
|
72
|
+
): void {
|
|
73
|
+
if (rows.length === 0 && !assemblies?.length) {
|
|
69
74
|
return
|
|
70
75
|
}
|
|
71
76
|
|
|
72
|
-
output.print(formatResultUrlRows(rows)
|
|
77
|
+
output.print(rows.length > 0 ? formatResultUrlRows(rows) : undefined, {
|
|
78
|
+
urls: rows,
|
|
79
|
+
...(assemblies?.length ? { assemblies } : {}),
|
|
80
|
+
})
|
|
73
81
|
}
|
package/src/cli/stepsInput.ts
CHANGED
|
@@ -5,9 +5,11 @@ import fsp from 'node:fs/promises'
|
|
|
5
5
|
|
|
6
6
|
import { z } from 'zod'
|
|
7
7
|
|
|
8
|
+
import { tryCatch } from '../alphalib/tryCatch.ts'
|
|
8
9
|
import { robotsSchema } from '../alphalib/types/robots/_index.ts'
|
|
9
10
|
import { refineDocumentExtractOcr } from '../alphalib/types/robots/document-extract.ts'
|
|
10
11
|
import { refineTransloaditImportSelector } from '../alphalib/types/robots/transloadit-import.ts'
|
|
12
|
+
import { CliInputError, formatSchemaIssues } from './errors.ts'
|
|
11
13
|
|
|
12
14
|
const stepsWrapperSchema = z.object({ steps: z.record(z.unknown()) })
|
|
13
15
|
|
|
@@ -35,7 +37,12 @@ export function parseStepsInputJson(
|
|
|
35
37
|
content: string,
|
|
36
38
|
output: Pick<IOutputCtl, 'warn'> = console,
|
|
37
39
|
): StepsInput {
|
|
38
|
-
|
|
40
|
+
let parsed: unknown
|
|
41
|
+
try {
|
|
42
|
+
parsed = JSON.parse(content)
|
|
43
|
+
} catch (error) {
|
|
44
|
+
throw new CliInputError('Invalid steps format: expected valid JSON', { cause: error })
|
|
45
|
+
}
|
|
39
46
|
const wrapped = stepsWrapperSchema.safeParse(parsed)
|
|
40
47
|
let stepsInput = parsed
|
|
41
48
|
// A step named “steps” has a robot, unlike the top-level instructions wrapper.
|
|
@@ -43,13 +50,18 @@ export function parseStepsInputJson(
|
|
|
43
50
|
// --steps only supplies steps; other instructions must not be silently discarded.
|
|
44
51
|
const validatedWrapper = stepsWrapperSchema.strict().safeParse(parsed)
|
|
45
52
|
if (!validatedWrapper.success) {
|
|
46
|
-
throw new
|
|
53
|
+
throw new CliInputError(
|
|
54
|
+
`Invalid steps format: ${formatSchemaIssues(validatedWrapper.error.issues)}`,
|
|
55
|
+
{ cause: validatedWrapper.error },
|
|
56
|
+
)
|
|
47
57
|
}
|
|
48
58
|
stepsInput = validatedWrapper.data.steps
|
|
49
59
|
}
|
|
50
60
|
const validated = createCliStepsSchema(output).safeParse(stepsInput)
|
|
51
61
|
if (!validated.success) {
|
|
52
|
-
throw new
|
|
62
|
+
throw new CliInputError(`Invalid steps format: ${formatSchemaIssues(validated.error.issues)}`, {
|
|
63
|
+
cause: validated.error,
|
|
64
|
+
})
|
|
53
65
|
}
|
|
54
66
|
|
|
55
67
|
const parsedSteps = stepsInput as Record<string, Record<string, unknown>>
|
|
@@ -72,6 +84,9 @@ export async function readStepsInputFile(
|
|
|
72
84
|
filePath: string,
|
|
73
85
|
output: Pick<IOutputCtl, 'warn'> = console,
|
|
74
86
|
): Promise<StepsInput> {
|
|
75
|
-
const content = await fsp.readFile(filePath, 'utf8')
|
|
87
|
+
const [error, content] = await tryCatch(fsp.readFile(filePath, 'utf8'))
|
|
88
|
+
if (error) {
|
|
89
|
+
throw new CliInputError(`Cannot read steps input: ${error.message}`, { cause: error })
|
|
90
|
+
}
|
|
76
91
|
return parseStepsInputJson(content, output)
|
|
77
92
|
}
|
package/src/cli/types.ts
CHANGED
|
@@ -2,6 +2,41 @@ import type { Steps } from '../alphalib/types/template.ts'
|
|
|
2
2
|
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
|
+
const ignoredErrorDetailsSchema = z
|
|
6
|
+
.union([
|
|
7
|
+
z.string(),
|
|
8
|
+
z.object({
|
|
9
|
+
code: z.string().optional().catch(undefined),
|
|
10
|
+
message: z.string().optional().catch(undefined),
|
|
11
|
+
}),
|
|
12
|
+
])
|
|
13
|
+
.optional()
|
|
14
|
+
.catch(undefined)
|
|
15
|
+
|
|
16
|
+
const ignoredErrorSummarySchema = z
|
|
17
|
+
.object({
|
|
18
|
+
step: z.string().nullable().optional().catch(undefined),
|
|
19
|
+
phase: z.string().optional().catch(undefined),
|
|
20
|
+
error: ignoredErrorDetailsSchema,
|
|
21
|
+
message: z.string().optional().catch(undefined),
|
|
22
|
+
})
|
|
23
|
+
.catch({})
|
|
24
|
+
|
|
25
|
+
/** Project unchecked API responses to CLI diagnostics without nested request metadata. */
|
|
26
|
+
export const assemblySummarySchema = z.object({
|
|
27
|
+
assembly_id: z.string().optional().catch(undefined),
|
|
28
|
+
assembly_url: z.string().nullable().optional().catch(undefined),
|
|
29
|
+
assembly_ssl_url: z.string().nullable().optional().catch(undefined),
|
|
30
|
+
ok: z.string().nullable().optional().catch(undefined),
|
|
31
|
+
error: z.string().optional().catch(undefined),
|
|
32
|
+
message: z.string().optional().catch(undefined),
|
|
33
|
+
ignored_error_count: z.number().finite().int().nonnegative().default(0).catch(0),
|
|
34
|
+
ignored_errors: z.array(ignoredErrorSummarySchema).default([]).catch([]),
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
/** Public diagnostics from one Assembly, excluding instructions and request internals. */
|
|
38
|
+
export type AssemblySummary = z.infer<typeof assemblySummarySchema>
|
|
39
|
+
|
|
5
40
|
// Zod schemas for runtime validation
|
|
6
41
|
const APIErrorSchema = z.object({
|
|
7
42
|
error: z.string(),
|
package/src/tus.ts
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import type { Readable } from 'node:stream'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
FileSource,
|
|
5
|
+
HttpRequest,
|
|
6
|
+
HttpResponse,
|
|
7
|
+
OnSuccessPayload,
|
|
8
|
+
SliceResult,
|
|
9
|
+
UploadOptions,
|
|
10
|
+
} from 'tus-js-client'
|
|
4
11
|
|
|
5
12
|
import type { AssemblyStatus } from './alphalib/types/assemblyStatus.ts'
|
|
6
13
|
import type { UploadProgress } from './Transloadit.ts'
|
|
7
14
|
|
|
15
|
+
import { ReadStream } from 'node:fs'
|
|
8
16
|
import { stat } from 'node:fs/promises'
|
|
9
17
|
import { basename } from 'node:path'
|
|
10
18
|
|
|
11
19
|
import debug from 'debug'
|
|
12
20
|
import pMap from 'p-map'
|
|
13
|
-
import { Upload } from 'tus-js-client'
|
|
21
|
+
import { defaultOptions, Upload } from 'tus-js-client'
|
|
14
22
|
|
|
15
23
|
const log = debug('transloadit')
|
|
16
24
|
const logWarn = debug('transloadit:warn')
|
|
@@ -31,6 +39,13 @@ interface SendTusRequestOptions {
|
|
|
31
39
|
signal?: AbortSignal
|
|
32
40
|
uploadUrls?: Record<string, string>
|
|
33
41
|
uploadBehavior?: UploadBehavior
|
|
42
|
+
onFileStreamOpened?: (stream: ReadStream) => void
|
|
43
|
+
onFileSliceStarted?: (slice: Promise<unknown>) => void
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
interface SendTusRequestResult {
|
|
47
|
+
uploadUrls: Record<string, string>
|
|
48
|
+
uploadsComplete?: Promise<unknown>
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
export async function sendTusRequest({
|
|
@@ -42,7 +57,9 @@ export async function sendTusRequest({
|
|
|
42
57
|
signal,
|
|
43
58
|
uploadUrls,
|
|
44
59
|
uploadBehavior = 'await',
|
|
45
|
-
|
|
60
|
+
onFileStreamOpened,
|
|
61
|
+
onFileSliceStarted,
|
|
62
|
+
}: SendTusRequestOptions): Promise<SendTusRequestResult> {
|
|
46
63
|
const streamLabels = Object.keys(streamsMap)
|
|
47
64
|
|
|
48
65
|
let totalBytes = 0
|
|
@@ -203,6 +220,25 @@ export async function sendTusRequest({
|
|
|
203
220
|
fieldname: label,
|
|
204
221
|
filename,
|
|
205
222
|
},
|
|
223
|
+
httpStack: {
|
|
224
|
+
createRequest(method: string, url: string): HttpRequest {
|
|
225
|
+
const request = defaultOptions.httpStack.createRequest(method, url)
|
|
226
|
+
const send = request.send.bind(request)
|
|
227
|
+
request.send = (body: unknown): Promise<HttpResponse> => {
|
|
228
|
+
// tus awaits pre-request hooks, so queued aborts must be checked at send itself.
|
|
229
|
+
if (signal?.aborted) {
|
|
230
|
+
// File setup can finish after abort() and reset tus's aborted state.
|
|
231
|
+
tusUpload.abort()
|
|
232
|
+
return Promise.reject(new Error('Upload aborted'))
|
|
233
|
+
}
|
|
234
|
+
return send(body)
|
|
235
|
+
}
|
|
236
|
+
return request
|
|
237
|
+
},
|
|
238
|
+
getName(): string {
|
|
239
|
+
return defaultOptions.httpStack.getName()
|
|
240
|
+
},
|
|
241
|
+
},
|
|
206
242
|
onError: reject,
|
|
207
243
|
onProgress: onTusProgress,
|
|
208
244
|
onSuccess: resolve,
|
|
@@ -218,6 +254,36 @@ export async function sendTusRequest({
|
|
|
218
254
|
}
|
|
219
255
|
},
|
|
220
256
|
}
|
|
257
|
+
if (onFileStreamOpened) {
|
|
258
|
+
// tus opens a separate ReadStream for each chunk; abort() leaves those streams open.
|
|
259
|
+
// Track those chunks even when the original file stream belongs to the caller.
|
|
260
|
+
tusOptions.fileReader = {
|
|
261
|
+
async openFile(input: unknown, chunkSize: number): Promise<FileSource> {
|
|
262
|
+
const source = await defaultOptions.fileReader.openFile(input, chunkSize)
|
|
263
|
+
return {
|
|
264
|
+
size: source.size,
|
|
265
|
+
async slice(start: number, end: number): Promise<SliceResult> {
|
|
266
|
+
if (signal?.aborted) throw new Error('Upload aborted')
|
|
267
|
+
const reading = source.slice(start, end).then((chunk) => {
|
|
268
|
+
// tus-js-client declares platform-specific chunk values as any.
|
|
269
|
+
const value: unknown = chunk.value
|
|
270
|
+
if (value instanceof ReadStream) onFileStreamOpened(value)
|
|
271
|
+
if (signal?.aborted) throw new Error('Upload aborted')
|
|
272
|
+
return chunk
|
|
273
|
+
})
|
|
274
|
+
// A file slice can open its stream before the promise resolves.
|
|
275
|
+
if (input instanceof ReadStream && input.path != null) {
|
|
276
|
+
onFileSliceStarted?.(reading)
|
|
277
|
+
}
|
|
278
|
+
return await reading
|
|
279
|
+
},
|
|
280
|
+
close() {
|
|
281
|
+
source.close()
|
|
282
|
+
},
|
|
283
|
+
}
|
|
284
|
+
},
|
|
285
|
+
}
|
|
286
|
+
}
|
|
221
287
|
// tus-js-client doesn't like undefined/null
|
|
222
288
|
if (size != null) tusOptions.uploadSize = size
|
|
223
289
|
if (chunkSize) tusOptions.chunkSize = chunkSize
|
|
@@ -255,13 +321,14 @@ export async function sendTusRequest({
|
|
|
255
321
|
|
|
256
322
|
await Promise.all(uploadUrlPromises)
|
|
257
323
|
|
|
324
|
+
const uploadsComplete = Promise.allSettled(completionPromises)
|
|
258
325
|
if (uploadBehavior === 'await') {
|
|
259
326
|
await Promise.all(completionPromises)
|
|
260
327
|
} else {
|
|
261
|
-
|
|
328
|
+
uploadsComplete.catch((err) => {
|
|
262
329
|
logWarn('Background upload failed', err)
|
|
263
330
|
})
|
|
264
331
|
}
|
|
265
332
|
|
|
266
|
-
return { uploadUrls: uploadUrlsResult }
|
|
333
|
+
return { uploadUrls: uploadUrlsResult, uploadsComplete }
|
|
267
334
|
}
|