@transloadit/node 4.2.0 → 4.3.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 +58 -0
- package/dist/Transloadit.d.ts +19 -0
- package/dist/Transloadit.d.ts.map +1 -1
- package/dist/Transloadit.js +22 -0
- package/dist/Transloadit.js.map +1 -1
- package/dist/alphalib/assembly-linter.d.ts +123 -0
- package/dist/alphalib/assembly-linter.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.js +1142 -0
- package/dist/alphalib/assembly-linter.js.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts +87 -0
- package/dist/alphalib/assembly-linter.lang.en.d.ts.map +1 -0
- package/dist/alphalib/assembly-linter.lang.en.js +326 -0
- package/dist/alphalib/assembly-linter.lang.en.js.map +1 -0
- package/dist/alphalib/object.d.ts +20 -0
- package/dist/alphalib/object.d.ts.map +1 -0
- package/dist/alphalib/object.js +23 -0
- package/dist/alphalib/object.js.map +1 -0
- package/dist/alphalib/stepParsing.d.ts +93 -0
- package/dist/alphalib/stepParsing.d.ts.map +1 -0
- package/dist/alphalib/stepParsing.js +1154 -0
- package/dist/alphalib/stepParsing.js.map +1 -0
- package/dist/alphalib/templateMerge.d.ts +4 -0
- package/dist/alphalib/templateMerge.d.ts.map +1 -0
- package/dist/alphalib/templateMerge.js +22 -0
- package/dist/alphalib/templateMerge.js.map +1 -0
- package/dist/cli/commands/assemblies.d.ts +20 -1
- package/dist/cli/commands/assemblies.d.ts.map +1 -1
- package/dist/cli/commands/assemblies.js +137 -2
- package/dist/cli/commands/assemblies.js.map +1 -1
- package/dist/cli/commands/auth.d.ts.map +1 -1
- package/dist/cli/commands/auth.js +19 -19
- package/dist/cli/commands/auth.js.map +1 -1
- package/dist/cli/commands/index.d.ts.map +1 -1
- package/dist/cli/commands/index.js +2 -1
- package/dist/cli/commands/index.js.map +1 -1
- package/dist/cli/docs/assemblyLintingExamples.d.ts +2 -0
- package/dist/cli/docs/assemblyLintingExamples.d.ts.map +1 -0
- package/dist/cli/docs/assemblyLintingExamples.js +10 -0
- package/dist/cli/docs/assemblyLintingExamples.js.map +1 -0
- package/dist/cli/helpers.d.ts +11 -0
- package/dist/cli/helpers.d.ts.map +1 -1
- package/dist/cli/helpers.js +29 -0
- package/dist/cli/helpers.js.map +1 -1
- package/dist/lintAssemblyInput.d.ts +10 -0
- package/dist/lintAssemblyInput.d.ts.map +1 -0
- package/dist/lintAssemblyInput.js +73 -0
- package/dist/lintAssemblyInput.js.map +1 -0
- package/dist/lintAssemblyInstructions.d.ts +29 -0
- package/dist/lintAssemblyInstructions.d.ts.map +1 -0
- package/dist/lintAssemblyInstructions.js +33 -0
- package/dist/lintAssemblyInstructions.js.map +1 -0
- package/package.json +5 -2
- package/src/Transloadit.ts +39 -0
- package/src/alphalib/assembly-linter.lang.en.ts +393 -0
- package/src/alphalib/assembly-linter.ts +1475 -0
- package/src/alphalib/object.ts +27 -0
- package/src/alphalib/stepParsing.ts +1465 -0
- package/src/alphalib/templateMerge.ts +32 -0
- package/src/alphalib/typings/json-to-ast.d.ts +34 -0
- package/src/cli/commands/assemblies.ts +161 -2
- package/src/cli/commands/auth.ts +19 -22
- package/src/cli/commands/index.ts +2 -0
- package/src/cli/docs/assemblyLintingExamples.ts +9 -0
- package/src/cli/helpers.ts +50 -0
- package/src/lintAssemblyInput.ts +89 -0
- package/src/lintAssemblyInstructions.ts +72 -0
|
@@ -0,0 +1,1465 @@
|
|
|
1
|
+
import type { ZodObject, ZodRawShape } from 'zod'
|
|
2
|
+
import { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import { entries } from './object.ts'
|
|
5
|
+
import { robotsSchema } from './types/robots/_index.ts'
|
|
6
|
+
import {
|
|
7
|
+
useParamArrayOfStringsSchema,
|
|
8
|
+
useParamArrayOfUseParamObjectSchema,
|
|
9
|
+
useParamObjectOfStepsSchema,
|
|
10
|
+
useParamStringSchema,
|
|
11
|
+
} from './types/robots/_instructions-primitives.ts'
|
|
12
|
+
import type { RobotFilePreviewInstructionsInput } from './types/robots/file-preview.ts'
|
|
13
|
+
import type { RobotFileServeInstructionsInput } from './types/robots/file-serve.ts'
|
|
14
|
+
import type { RobotS3StoreInstructionsInput } from './types/robots/s3-store.ts'
|
|
15
|
+
import type {
|
|
16
|
+
AssemblyInstructionsInput,
|
|
17
|
+
StepInput,
|
|
18
|
+
StepInputWithUse,
|
|
19
|
+
StepsInput,
|
|
20
|
+
} from './types/template.ts'
|
|
21
|
+
import { assemblyInstructionsSchema } from './types/template.ts'
|
|
22
|
+
import type { ZodIssueWithContext } from './zodParseWithContext.ts'
|
|
23
|
+
import { zodParseWithContext } from './zodParseWithContext.ts'
|
|
24
|
+
|
|
25
|
+
type StepInputRecord = StepInput & Record<string, unknown>
|
|
26
|
+
|
|
27
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
28
|
+
return typeof value === 'object' && value !== null
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Add this type to represent steps that don't require use
|
|
32
|
+
export type NoUseRobot =
|
|
33
|
+
| '/upload/handle'
|
|
34
|
+
| '/google/import'
|
|
35
|
+
| '/dropbox/import'
|
|
36
|
+
| '/supabase/import'
|
|
37
|
+
| '/swift/import'
|
|
38
|
+
| '/backblaze/import'
|
|
39
|
+
| '/ftp/import'
|
|
40
|
+
| '/cloudfiles/import'
|
|
41
|
+
| '/cloudflare/import'
|
|
42
|
+
| '/digitalocean/import'
|
|
43
|
+
| '/http/import'
|
|
44
|
+
| '/s3/import'
|
|
45
|
+
| '/azure/import'
|
|
46
|
+
| '/minio/import'
|
|
47
|
+
| '/wasabi/import'
|
|
48
|
+
| '/edgly/deliver'
|
|
49
|
+
| '/tlcdn/deliver'
|
|
50
|
+
| '/sftp/import'
|
|
51
|
+
|
|
52
|
+
export type StepInputWithoutUse = Omit<StepInput, 'use'> & {
|
|
53
|
+
robot: NoUseRobot
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function doesRobotSupportUse(
|
|
57
|
+
robot: string,
|
|
58
|
+
): robot is Exclude<StepInput['robot'], NoUseRobot> {
|
|
59
|
+
return (
|
|
60
|
+
robot !== '/upload/handle' &&
|
|
61
|
+
robot !== '/google/import' &&
|
|
62
|
+
robot !== '/dropbox/import' &&
|
|
63
|
+
robot !== '/supabase/import' &&
|
|
64
|
+
robot !== '/swift/import' &&
|
|
65
|
+
robot !== '/backblaze/import' &&
|
|
66
|
+
robot !== '/ftp/import' &&
|
|
67
|
+
robot !== '/cloudfiles/import' &&
|
|
68
|
+
robot !== '/cloudflare/import' &&
|
|
69
|
+
robot !== '/digitalocean/import' &&
|
|
70
|
+
robot !== '/http/import' &&
|
|
71
|
+
robot !== '/s3/import' &&
|
|
72
|
+
robot !== '/azure/import' &&
|
|
73
|
+
robot !== '/minio/import' &&
|
|
74
|
+
robot !== '/wasabi/import' &&
|
|
75
|
+
robot !== '/edgly/deliver' &&
|
|
76
|
+
robot !== '/tlcdn/deliver' &&
|
|
77
|
+
robot !== '/sftp/import'
|
|
78
|
+
)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function doesStepRobotSupportUse(step: StepInput): step is StepInputWithUse {
|
|
82
|
+
return 'robot' in step && doesRobotSupportUse(step.robot)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface ParsedTemplateField {
|
|
86
|
+
mostCommonExampleValue: FieldOccurrence['exampleValues'][number]
|
|
87
|
+
fieldName: string
|
|
88
|
+
value?: string
|
|
89
|
+
occurrences: FieldOccurrence[]
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface FieldOccurrence {
|
|
93
|
+
errors: string[]
|
|
94
|
+
exampleValues: (string | number | boolean)[]
|
|
95
|
+
leader: string
|
|
96
|
+
paramName: string
|
|
97
|
+
path: (number | string)[]
|
|
98
|
+
requiresDenoEval: boolean
|
|
99
|
+
rName: string
|
|
100
|
+
stepName: string
|
|
101
|
+
trailer: string
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ValidationError {
|
|
105
|
+
stepName: string
|
|
106
|
+
robotName: string
|
|
107
|
+
paramName: string
|
|
108
|
+
fieldNames: string[]
|
|
109
|
+
message: string
|
|
110
|
+
value: unknown
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface Recommendation {
|
|
114
|
+
id: string
|
|
115
|
+
robotName: string
|
|
116
|
+
description: string
|
|
117
|
+
applyFunction: (content: string) => string
|
|
118
|
+
iconSrc: string
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export interface InterpolatedTemplateError {
|
|
122
|
+
stepName: string
|
|
123
|
+
robotName: string
|
|
124
|
+
paramName: string
|
|
125
|
+
fieldNames: string[]
|
|
126
|
+
message: string
|
|
127
|
+
value: unknown
|
|
128
|
+
}
|
|
129
|
+
export function nonSignedSmartCDNUrl(
|
|
130
|
+
argWorkspaceSlug: string,
|
|
131
|
+
argTemplateSlug: string,
|
|
132
|
+
argInputField: string,
|
|
133
|
+
params: Record<string, string | number> = {},
|
|
134
|
+
) {
|
|
135
|
+
const workspaceSlug = encodeURIComponent(argWorkspaceSlug)
|
|
136
|
+
const templateSlug = encodeURIComponent(argTemplateSlug)
|
|
137
|
+
const inputField = encodeURIComponent(argInputField)
|
|
138
|
+
|
|
139
|
+
const queryParams = new URLSearchParams(
|
|
140
|
+
Object.fromEntries(entries(params).map(([key, value]) => [key, String(value)])),
|
|
141
|
+
)
|
|
142
|
+
queryParams.sort()
|
|
143
|
+
|
|
144
|
+
const nonSignedUrl = `https://${workspaceSlug}.tlcdn.com/${templateSlug}/${inputField}?${queryParams}`
|
|
145
|
+
return nonSignedUrl
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function simplifyUse(step: StepInput): StepInput {
|
|
149
|
+
if (!doesStepRobotSupportUse(step)) {
|
|
150
|
+
return step
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!('use' in step)) {
|
|
154
|
+
return step
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// const zodRes1 = useParamStringSchema.safeParse(step.use)
|
|
158
|
+
const zodRes2 = useParamArrayOfStringsSchema.safeParse(step.use)
|
|
159
|
+
// const zodRes3 = useParamArrayOfUseParamObjectSchema.safeParse(step.use)
|
|
160
|
+
// const zodRes4 = useParamObjectOfStepsSchema.safeParse(step.use)
|
|
161
|
+
|
|
162
|
+
if (zodRes2.success) {
|
|
163
|
+
// Turn single element array into a string
|
|
164
|
+
if (zodRes2.data.length === 1) {
|
|
165
|
+
step.use = zodRes2.data[0]
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return step
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getLastUsedStepName(step: StepInput): string | undefined {
|
|
173
|
+
if (!doesStepRobotSupportUse(step)) {
|
|
174
|
+
return undefined
|
|
175
|
+
}
|
|
176
|
+
if (!('use' in step)) {
|
|
177
|
+
return undefined
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const zodRes1 = useParamStringSchema.safeParse(step.use)
|
|
181
|
+
const zodRes2 = useParamArrayOfStringsSchema.safeParse(step.use)
|
|
182
|
+
const zodRes3 = useParamArrayOfUseParamObjectSchema.safeParse(step.use)
|
|
183
|
+
const zodRes4 = useParamObjectOfStepsSchema.safeParse(step.use)
|
|
184
|
+
|
|
185
|
+
if (zodRes1.success) {
|
|
186
|
+
return zodRes1.data
|
|
187
|
+
}
|
|
188
|
+
if (zodRes2.success) {
|
|
189
|
+
return zodRes2.data[zodRes2.data.length - 1]
|
|
190
|
+
}
|
|
191
|
+
if (zodRes3.success) {
|
|
192
|
+
return zodRes3.data[zodRes3.data.length - 1].name
|
|
193
|
+
}
|
|
194
|
+
if (zodRes4.success) {
|
|
195
|
+
const zodRes41 = useParamStringSchema.safeParse(zodRes4.data.steps)
|
|
196
|
+
const zodRes42 = useParamArrayOfStringsSchema.safeParse(zodRes4.data.steps)
|
|
197
|
+
const zodRes43 = useParamArrayOfUseParamObjectSchema.safeParse(zodRes4.data.steps)
|
|
198
|
+
|
|
199
|
+
if (zodRes41.success) {
|
|
200
|
+
return zodRes41.data
|
|
201
|
+
}
|
|
202
|
+
if (zodRes42.success) {
|
|
203
|
+
return zodRes42.data[zodRes42.data.length - 1]
|
|
204
|
+
}
|
|
205
|
+
if (zodRes43.success) {
|
|
206
|
+
return zodRes43.data[zodRes43.data.length - 1].name
|
|
207
|
+
}
|
|
208
|
+
throw new Error('Invalid use value')
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
return undefined
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
export function addUseReference(
|
|
215
|
+
step: StepInput,
|
|
216
|
+
newName: string,
|
|
217
|
+
opts?: {
|
|
218
|
+
leading?: boolean
|
|
219
|
+
silent?: boolean
|
|
220
|
+
},
|
|
221
|
+
): StepInput {
|
|
222
|
+
// const step = structuredClone(step)
|
|
223
|
+
const { leading = false } = opts ?? {}
|
|
224
|
+
|
|
225
|
+
if (!doesStepRobotSupportUse(step)) {
|
|
226
|
+
return step
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (!('use' in step)) {
|
|
230
|
+
// TypeScript now knows this step supports 'use' due to the doesStepRobotSupportUse check
|
|
231
|
+
// We need to return a new object with the use property
|
|
232
|
+
return { ...step, use: newName } as StepInput
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const zodRes1 = useParamStringSchema.safeParse(step.use)
|
|
236
|
+
const zodRes2 = useParamArrayOfStringsSchema.safeParse(step.use)
|
|
237
|
+
const zodRes3 = useParamArrayOfUseParamObjectSchema.safeParse(step.use)
|
|
238
|
+
const zodRes4 = useParamObjectOfStepsSchema.safeParse(step.use)
|
|
239
|
+
|
|
240
|
+
if (zodRes1.success) {
|
|
241
|
+
step.use = leading ? [newName, zodRes1.data] : [zodRes1.data, newName]
|
|
242
|
+
} else if (zodRes2.success) {
|
|
243
|
+
step.use = leading ? [newName, ...zodRes2.data] : [...zodRes2.data, newName]
|
|
244
|
+
} else if (zodRes3.success) {
|
|
245
|
+
step.use = leading ? [{ name: newName }, ...zodRes3.data] : [...zodRes3.data, { name: newName }]
|
|
246
|
+
} else if (zodRes4.success) {
|
|
247
|
+
const zodRes41 = useParamStringSchema.safeParse(zodRes4.data.steps)
|
|
248
|
+
const zodRes42 = useParamArrayOfStringsSchema.safeParse(zodRes4.data.steps)
|
|
249
|
+
const zodRes43 = useParamArrayOfUseParamObjectSchema.safeParse(zodRes4.data.steps)
|
|
250
|
+
|
|
251
|
+
if (zodRes41.success) {
|
|
252
|
+
step.use = leading
|
|
253
|
+
? { ...zodRes4.data, steps: [newName, zodRes41.data] }
|
|
254
|
+
: { ...zodRes4.data, steps: [zodRes41.data, newName] }
|
|
255
|
+
} else if (zodRes42.success) {
|
|
256
|
+
step.use = leading
|
|
257
|
+
? { ...zodRes4.data, steps: [newName, ...zodRes42.data] }
|
|
258
|
+
: { ...zodRes4.data, steps: [...zodRes42.data, newName] }
|
|
259
|
+
} else if (zodRes43.success) {
|
|
260
|
+
step.use = leading
|
|
261
|
+
? { ...zodRes4.data, steps: [{ name: newName }, ...zodRes43.data] }
|
|
262
|
+
: { ...zodRes4.data, steps: [...zodRes43.data, { name: newName }] }
|
|
263
|
+
} else {
|
|
264
|
+
throw new Error('Invalid use value')
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return simplifyUse(step)
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Helper function to update 'use' references
|
|
272
|
+
export function renameUseReferences(step: StepInput, oldName: string, newName: string): StepInput {
|
|
273
|
+
if (!('use' in step)) {
|
|
274
|
+
return step
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const zodRes1 = useParamStringSchema.safeParse(step.use)
|
|
278
|
+
const zodRes2 = useParamArrayOfStringsSchema.safeParse(step.use)
|
|
279
|
+
const zodRes3 = useParamArrayOfUseParamObjectSchema.safeParse(step.use)
|
|
280
|
+
const zodRes4 = useParamObjectOfStepsSchema.safeParse(step.use)
|
|
281
|
+
|
|
282
|
+
if (zodRes1.success) {
|
|
283
|
+
step.use = step.use === oldName ? newName : step.use
|
|
284
|
+
} else if (zodRes2.success) {
|
|
285
|
+
const newUse: z.infer<typeof useParamArrayOfStringsSchema> = []
|
|
286
|
+
for (const currentName of zodRes2.data) {
|
|
287
|
+
if (currentName === oldName) {
|
|
288
|
+
if (!newUse.includes(newName)) {
|
|
289
|
+
newUse.push(newName)
|
|
290
|
+
}
|
|
291
|
+
} else if (!newUse.includes(currentName)) {
|
|
292
|
+
newUse.push(currentName)
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
step.use = newUse
|
|
296
|
+
} else if (zodRes3.success) {
|
|
297
|
+
step.use = zodRes3.data.map((u) => ({
|
|
298
|
+
...u,
|
|
299
|
+
name: u.name === oldName ? newName : u.name,
|
|
300
|
+
}))
|
|
301
|
+
} else if (zodRes4.success) {
|
|
302
|
+
const zodRes41 = useParamStringSchema.safeParse(zodRes4.data.steps)
|
|
303
|
+
const zodRes42 = useParamArrayOfStringsSchema.safeParse(zodRes4.data.steps)
|
|
304
|
+
const zodRes43 = useParamArrayOfUseParamObjectSchema.safeParse(zodRes4.data.steps)
|
|
305
|
+
|
|
306
|
+
if (zodRes41.success) {
|
|
307
|
+
step.use = {
|
|
308
|
+
...zodRes4.data,
|
|
309
|
+
steps: zodRes41.data === oldName ? newName : zodRes41.data,
|
|
310
|
+
}
|
|
311
|
+
} else if (zodRes42.success) {
|
|
312
|
+
step.use = {
|
|
313
|
+
...zodRes4.data,
|
|
314
|
+
steps: zodRes42.data.map((u) => (u === oldName ? newName : u)),
|
|
315
|
+
}
|
|
316
|
+
} else if (zodRes43.success) {
|
|
317
|
+
step.use = {
|
|
318
|
+
...zodRes4.data,
|
|
319
|
+
steps: zodRes43.data.map((u) => ({
|
|
320
|
+
...u,
|
|
321
|
+
name: u.name === oldName ? newName : u.name,
|
|
322
|
+
})),
|
|
323
|
+
}
|
|
324
|
+
} else {
|
|
325
|
+
throw new Error('Invalid use value')
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return simplifyUse(step)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
export function removeUseReference(step: StepInput, nameToRemove: string): StepInput {
|
|
333
|
+
if (!('use' in step)) {
|
|
334
|
+
return step
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const zodRes1 = useParamStringSchema.safeParse(step.use)
|
|
338
|
+
const zodRes2 = useParamArrayOfStringsSchema.safeParse(step.use)
|
|
339
|
+
const zodRes3 = useParamArrayOfUseParamObjectSchema.safeParse(step.use)
|
|
340
|
+
const zodRes4 = useParamObjectOfStepsSchema.safeParse(step.use)
|
|
341
|
+
|
|
342
|
+
if (zodRes1.success) {
|
|
343
|
+
if (step.use === nameToRemove) {
|
|
344
|
+
return { ...step, use: [] }
|
|
345
|
+
}
|
|
346
|
+
return step
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (zodRes2.success) {
|
|
350
|
+
const newUse = zodRes2.data.filter((u) => u !== nameToRemove)
|
|
351
|
+
return simplifyUse({ ...step, use: newUse })
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if (zodRes3.success) {
|
|
355
|
+
const newUse = zodRes3.data.filter((u) => u.name !== nameToRemove)
|
|
356
|
+
return simplifyUse({ ...step, use: newUse })
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (zodRes4.success) {
|
|
360
|
+
const zodRes41 = useParamStringSchema.safeParse(zodRes4.data.steps)
|
|
361
|
+
const zodRes42 = useParamArrayOfStringsSchema.safeParse(zodRes4.data.steps)
|
|
362
|
+
const zodRes43 = useParamArrayOfUseParamObjectSchema.safeParse(zodRes4.data.steps)
|
|
363
|
+
|
|
364
|
+
if (zodRes41.success) {
|
|
365
|
+
if (zodRes41.data === nameToRemove) {
|
|
366
|
+
return { ...step, use: { ...zodRes4.data, steps: [] } }
|
|
367
|
+
}
|
|
368
|
+
return step
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (zodRes42.success) {
|
|
372
|
+
const newSteps = zodRes42.data.filter((u) => u !== nameToRemove)
|
|
373
|
+
return simplifyUse({ ...step, use: { ...zodRes4.data, steps: newSteps } })
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
if (zodRes43.success) {
|
|
377
|
+
const newSteps = zodRes43.data.filter((u) => u.name !== nameToRemove)
|
|
378
|
+
return simplifyUse({ ...step, use: { ...zodRes4.data, steps: newSteps } })
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
throw new Error('Invalid use value')
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return step
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isZodObject(schema: z.ZodType<unknown>): schema is ZodObject<ZodRawShape> {
|
|
388
|
+
return 'shape' in schema
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getSchemaForRobot(rName: string): z.ZodType<unknown> | undefined {
|
|
392
|
+
// Extract all robot schemas from the union and convert readonly array to regular array
|
|
393
|
+
const schemas = [...robotsSchema._def.options]
|
|
394
|
+
|
|
395
|
+
// Find the matching schema based on the robot name
|
|
396
|
+
return schemas.find((schema) => {
|
|
397
|
+
if (!isZodObject(schema)) return false
|
|
398
|
+
const shape = schema.shape
|
|
399
|
+
|
|
400
|
+
if (!('robot' in shape)) return false
|
|
401
|
+
const robotField = shape.robot
|
|
402
|
+
|
|
403
|
+
if (!(robotField instanceof z.ZodLiteral)) return false
|
|
404
|
+
|
|
405
|
+
return robotField.value === rName
|
|
406
|
+
})
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
interface ParseSafeTemplateOpts {
|
|
410
|
+
silent?: boolean
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export const getIndentation = (templateContent: string) => {
|
|
414
|
+
const lines = templateContent.split('\n')
|
|
415
|
+
|
|
416
|
+
// Find the first line with content and its indentation level
|
|
417
|
+
let baseIndent = ''
|
|
418
|
+
let firstContentLine = ''
|
|
419
|
+
for (const line of lines) {
|
|
420
|
+
const trimmed = line.trim()
|
|
421
|
+
if (trimmed.length > 0) {
|
|
422
|
+
firstContentLine = line
|
|
423
|
+
baseIndent = line.slice(0, line.length - trimmed.length)
|
|
424
|
+
break
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Find the first nested line's indentation
|
|
429
|
+
let nestedIndent = ''
|
|
430
|
+
let foundNested = false
|
|
431
|
+
for (const line of lines) {
|
|
432
|
+
const trimmed = line.trim()
|
|
433
|
+
if (trimmed.length === 0) continue
|
|
434
|
+
|
|
435
|
+
// Skip the first content line
|
|
436
|
+
if (line === firstContentLine) continue
|
|
437
|
+
|
|
438
|
+
const currentIndent = line.slice(0, line.length - trimmed.length)
|
|
439
|
+
if (currentIndent.length > baseIndent.length) {
|
|
440
|
+
nestedIndent = currentIndent.slice(baseIndent.length)
|
|
441
|
+
foundNested = true
|
|
442
|
+
break
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// If we found nested indentation, use that
|
|
447
|
+
if (foundNested) {
|
|
448
|
+
// For tabs, return the actual number of tabs used
|
|
449
|
+
if (nestedIndent.includes('\t')) {
|
|
450
|
+
return '\t'.repeat(nestedIndent.split('\t').length - 1)
|
|
451
|
+
}
|
|
452
|
+
// For spaces, return the actual number of spaces used
|
|
453
|
+
return nestedIndent
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Default to 2 spaces if no nested indentation found
|
|
457
|
+
return ' '
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
export class StepParsingError extends Error {
|
|
461
|
+
zodIssuesWithContext: ZodIssueWithContext[]
|
|
462
|
+
|
|
463
|
+
humanReadable: string
|
|
464
|
+
|
|
465
|
+
constructor(message: string, zodIssuesWithContext: ZodIssueWithContext[], humanReadable: string) {
|
|
466
|
+
super(message)
|
|
467
|
+
this.zodIssuesWithContext = zodIssuesWithContext
|
|
468
|
+
this.humanReadable = humanReadable
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function formatZodIssuesForLog(
|
|
473
|
+
issues: ZodIssueWithContext[],
|
|
474
|
+
): Array<{ path: (string | number)[]; code: string; message: string }> {
|
|
475
|
+
return issues.map((issue) => ({
|
|
476
|
+
path: issue.path,
|
|
477
|
+
code: issue.code,
|
|
478
|
+
message: issue.message,
|
|
479
|
+
}))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export const parseSafeTemplate = (
|
|
483
|
+
templateContent: string,
|
|
484
|
+
opts?: ParseSafeTemplateOpts,
|
|
485
|
+
): [StepParsingError, null] | [null, AssemblyInstructionsInput, string] => {
|
|
486
|
+
const silent = opts?.silent ?? (process.env.NODE_ENV ?? 'production') === 'production'
|
|
487
|
+
let parsed: unknown
|
|
488
|
+
let indent = ' '
|
|
489
|
+
try {
|
|
490
|
+
parsed = JSON.parse(templateContent)
|
|
491
|
+
|
|
492
|
+
indent = getIndentation(templateContent)
|
|
493
|
+
} catch (error) {
|
|
494
|
+
if (!silent) {
|
|
495
|
+
console.error('templateContent', { length: templateContent.length })
|
|
496
|
+
}
|
|
497
|
+
return [
|
|
498
|
+
new StepParsingError(
|
|
499
|
+
`Error parsing valid type from Template. ${error}. Input length: ${templateContent.length}`,
|
|
500
|
+
[],
|
|
501
|
+
'',
|
|
502
|
+
),
|
|
503
|
+
null,
|
|
504
|
+
]
|
|
505
|
+
}
|
|
506
|
+
const {
|
|
507
|
+
success,
|
|
508
|
+
errors: zodIssuesWithContext,
|
|
509
|
+
humanReadable,
|
|
510
|
+
} = zodParseWithContext(assemblyInstructionsSchema, parsed)
|
|
511
|
+
if (!success) {
|
|
512
|
+
if (!silent) {
|
|
513
|
+
console.error('zodIssuesWithContext', formatZodIssuesForLog(zodIssuesWithContext))
|
|
514
|
+
}
|
|
515
|
+
return [
|
|
516
|
+
new StepParsingError(
|
|
517
|
+
'Error validating Template against assemblyInstructionsSchema. ',
|
|
518
|
+
zodIssuesWithContext,
|
|
519
|
+
humanReadable,
|
|
520
|
+
),
|
|
521
|
+
null,
|
|
522
|
+
]
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// We won't return zodRes.data because that will add all the defaults to the
|
|
526
|
+
// original input. So if we know it will pass, we return the original input.
|
|
527
|
+
// One of the few cases where `as` is safe and allowed.
|
|
528
|
+
const safe = parsed as AssemblyInstructionsInput
|
|
529
|
+
return [null, safe, indent]
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
export function botNeedsInput(robotName: string, stepName?: string, step?: StepInput) {
|
|
533
|
+
if (robotName.endsWith('/import')) {
|
|
534
|
+
return false
|
|
535
|
+
}
|
|
536
|
+
if (robotName === '/upload/handle') {
|
|
537
|
+
return false
|
|
538
|
+
}
|
|
539
|
+
if (robotName === '/html/convert') {
|
|
540
|
+
if (step && 'url' in step && typeof step.url === 'string' && step.url) {
|
|
541
|
+
return false
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (robotName === '/text/speak') {
|
|
545
|
+
if (step && 'prompt' in step && typeof step.prompt === 'string' && step.prompt) {
|
|
546
|
+
return false
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
if (robotName === '/image/generate') {
|
|
550
|
+
return false
|
|
551
|
+
}
|
|
552
|
+
if (stepName === ':original') {
|
|
553
|
+
return false
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
return true
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
export function getFirstStepNameThatDoesNotNeedInput(
|
|
560
|
+
templateContent: string,
|
|
561
|
+
excludeBots: string[] = [],
|
|
562
|
+
): string {
|
|
563
|
+
// Used by functions that fix missing use parameters, which is
|
|
564
|
+
// a violation of our schema so we cannot use parseSafeTemplate
|
|
565
|
+
// here.
|
|
566
|
+
let parsed: unknown
|
|
567
|
+
try {
|
|
568
|
+
parsed = JSON.parse(templateContent)
|
|
569
|
+
} catch (_e) {
|
|
570
|
+
return ''
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (!isRecord(parsed) || !('steps' in parsed)) {
|
|
574
|
+
return ''
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
const stepsValue = (parsed as Record<string, unknown>).steps
|
|
578
|
+
if (!isRecord(stepsValue)) {
|
|
579
|
+
return ''
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const stepsRecord = stepsValue as Record<string, StepInputRecord>
|
|
583
|
+
|
|
584
|
+
return getFirstStepNameThatDoesNotNeedInputFromSteps(stepsRecord, excludeBots)
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
const getFirstStepNameThatDoesNotNeedInputFromSteps = (
|
|
588
|
+
steps: Record<string, StepInputRecord>,
|
|
589
|
+
excludeBots: string[] = [],
|
|
590
|
+
): string => {
|
|
591
|
+
return (
|
|
592
|
+
Object.keys(steps).find((stepName) => {
|
|
593
|
+
const step = steps[stepName]
|
|
594
|
+
return (
|
|
595
|
+
typeof step === 'object' &&
|
|
596
|
+
step !== null &&
|
|
597
|
+
typeof step.robot === 'string' &&
|
|
598
|
+
!botNeedsInput(step.robot, stepName, step) &&
|
|
599
|
+
!excludeBots.includes(step.robot)
|
|
600
|
+
)
|
|
601
|
+
}) ?? ''
|
|
602
|
+
)
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
function hasRobotInSteps(steps: StepsInput, rName: string | RegExp): boolean {
|
|
606
|
+
return Object.values(steps).some((step) => {
|
|
607
|
+
return typeof rName === 'string' ? step.robot === rName : rName.test(step.robot)
|
|
608
|
+
})
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
const hasSteps = (
|
|
612
|
+
template: AssemblyInstructionsInput,
|
|
613
|
+
): template is AssemblyInstructionsInput & { steps: StepsInput } => {
|
|
614
|
+
return typeof template.steps === 'object' && template.steps !== null
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
export const hasRobot = (
|
|
618
|
+
templateContent: string,
|
|
619
|
+
rName: string | RegExp,
|
|
620
|
+
silent?: boolean,
|
|
621
|
+
): boolean => {
|
|
622
|
+
const parseOpts = silent === undefined ? undefined : { silent }
|
|
623
|
+
const [templateError, template] = parseSafeTemplate(templateContent, parseOpts)
|
|
624
|
+
if (templateError) {
|
|
625
|
+
return false
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return Object.values(template.steps ?? {}).some((step) => {
|
|
629
|
+
return typeof rName === 'string' ? step.robot === rName : rName.test(step.robot)
|
|
630
|
+
})
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
export const doesContentRequireUpload = (templateContent: string, opts?: { silent: boolean }) => {
|
|
634
|
+
const hasFileUploadRobot = hasRobot(templateContent, '/file/upload', opts?.silent)
|
|
635
|
+
const hasOriginalStepName = templateContent.includes(':original')
|
|
636
|
+
|
|
637
|
+
if (hasFileUploadRobot || hasOriginalStepName) {
|
|
638
|
+
return true
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
return false
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
export const canAssemblyJustRun = (templateContent: string) => {
|
|
645
|
+
const firstStepNameThatDoesNotNeedInput = getFirstStepNameThatDoesNotNeedInput(templateContent, [
|
|
646
|
+
'/upload/handle',
|
|
647
|
+
])
|
|
648
|
+
|
|
649
|
+
if (firstStepNameThatDoesNotNeedInput !== '') {
|
|
650
|
+
return true
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
return false
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export function addOptimizeRobots(templateContent: string): string {
|
|
657
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
658
|
+
if (templateError) {
|
|
659
|
+
return templateContent
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const newSteps: StepsInput = {}
|
|
663
|
+
let hasResizeStep = false
|
|
664
|
+
|
|
665
|
+
for (const [stepName, step] of entries(template.steps)) {
|
|
666
|
+
newSteps[stepName] = step
|
|
667
|
+
|
|
668
|
+
if (step.robot === '/image/resize') {
|
|
669
|
+
hasResizeStep = true
|
|
670
|
+
const optimizeStepName = `${stepName}_optimized`
|
|
671
|
+
const optimizeStep: StepInput = {
|
|
672
|
+
robot: '/image/optimize',
|
|
673
|
+
use: [],
|
|
674
|
+
}
|
|
675
|
+
addUseReference(optimizeStep, stepName)
|
|
676
|
+
newSteps[optimizeStepName] = optimizeStep
|
|
677
|
+
|
|
678
|
+
// Update subsequent steps to use the new optimize step
|
|
679
|
+
for (const [, nextStep] of entries(template.steps)) {
|
|
680
|
+
renameUseReferences(nextStep, stepName, optimizeStepName)
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
if (!hasResizeStep) {
|
|
686
|
+
return templateContent
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
template.steps = newSteps
|
|
690
|
+
return JSON.stringify(template, null, indent)
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
export function removeRobots(templateContent: string, robots: string[] | RegExp = []): string {
|
|
694
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
695
|
+
if (templateError) {
|
|
696
|
+
return templateContent
|
|
697
|
+
}
|
|
698
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
699
|
+
return templateContent
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
const newSteps: StepsInput = {}
|
|
703
|
+
|
|
704
|
+
const firstImportStepName = Object.keys(template.steps).find((stepName) =>
|
|
705
|
+
template.steps?.[stepName]?.robot?.endsWith('/import'),
|
|
706
|
+
)
|
|
707
|
+
|
|
708
|
+
let usedBefore = ''
|
|
709
|
+
for (const [stepName, step] of entries(template.steps)) {
|
|
710
|
+
if (Array.isArray(robots) ? robots.includes(step.robot) : robots.test(step.robot)) {
|
|
711
|
+
usedBefore = stepName
|
|
712
|
+
continue
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
if (step.robot === '/file/serve' && firstImportStepName && usedBefore) {
|
|
716
|
+
// Copy step before modifying
|
|
717
|
+
const updatedStep = { ...step }
|
|
718
|
+
renameUseReferences(updatedStep, usedBefore, firstImportStepName)
|
|
719
|
+
newSteps[stepName] = updatedStep
|
|
720
|
+
} else {
|
|
721
|
+
newSteps[stepName] = step
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
return JSON.stringify({ ...template, steps: newSteps }, null, indent)
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
export function removeFileServeRobots(templateContent: string): string {
|
|
729
|
+
return removeRobots(templateContent, ['/file/serve'])
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
export function removeUploading(templateContent: string): string {
|
|
733
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
734
|
+
if (templateError) {
|
|
735
|
+
return templateContent
|
|
736
|
+
}
|
|
737
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
738
|
+
return templateContent
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
const newSteps: StepsInput = {}
|
|
742
|
+
|
|
743
|
+
const firstImportStepName = Object.keys(template.steps).find((stepName) =>
|
|
744
|
+
template.steps?.[stepName]?.robot?.endsWith('/import'),
|
|
745
|
+
)
|
|
746
|
+
|
|
747
|
+
for (const [stepName, step] of entries(template.steps)) {
|
|
748
|
+
if (step.robot !== '/upload/handle') {
|
|
749
|
+
newSteps[stepName] = firstImportStepName
|
|
750
|
+
? renameUseReferences(step, ':original', firstImportStepName)
|
|
751
|
+
: step
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
template.steps = newSteps
|
|
756
|
+
return JSON.stringify(template, null, indent)
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
export function getRobotList(templateContent: string): string[] {
|
|
760
|
+
const [templateError, template] = parseSafeTemplate(templateContent)
|
|
761
|
+
if (templateError) {
|
|
762
|
+
return []
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return entries(template.steps).map(([, step]) => step.robot)
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
export function addStorageRobot(content: string): string {
|
|
769
|
+
const [templateError2, template2, indent] = parseSafeTemplate(content)
|
|
770
|
+
if (templateError2) {
|
|
771
|
+
return content
|
|
772
|
+
}
|
|
773
|
+
if (!('steps' in template2) || typeof template2.steps !== 'object') {
|
|
774
|
+
return content
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// Find the last non-storage step
|
|
778
|
+
let lastNonStorageStep = ''
|
|
779
|
+
for (const [stepName, step] of entries(template2.steps)) {
|
|
780
|
+
if (!step.robot.endsWith('/store')) {
|
|
781
|
+
lastNonStorageStep = stepName
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
const storedStep: RobotS3StoreInstructionsInput = {
|
|
786
|
+
robot: '/s3/store' as const,
|
|
787
|
+
credentials: 'YOUR_S3_CREDENTIALS',
|
|
788
|
+
path: '/uploads/${file.id}/${file.name}',
|
|
789
|
+
use: [],
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
// Add references in order
|
|
793
|
+
if (lastNonStorageStep) {
|
|
794
|
+
addUseReference(storedStep, lastNonStorageStep)
|
|
795
|
+
}
|
|
796
|
+
if (template2.steps[':original']) {
|
|
797
|
+
addUseReference(storedStep, ':original')
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
template2.steps.stored = storedStep
|
|
801
|
+
|
|
802
|
+
return JSON.stringify(template2, null, indent)
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
export function addFilePreviewRobot(templateContent: string): string {
|
|
806
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
807
|
+
if (templateError) {
|
|
808
|
+
return templateContent
|
|
809
|
+
}
|
|
810
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
811
|
+
return templateContent
|
|
812
|
+
}
|
|
813
|
+
const steps = template.steps
|
|
814
|
+
const newSteps: StepsInput = {}
|
|
815
|
+
|
|
816
|
+
let importStepName: string | null = null
|
|
817
|
+
let serveStep: string | null = null
|
|
818
|
+
let width: RobotFilePreviewInstructionsInput['width']
|
|
819
|
+
let height: RobotFilePreviewInstructionsInput['height']
|
|
820
|
+
let format: RobotFilePreviewInstructionsInput['format']
|
|
821
|
+
|
|
822
|
+
// Identify steps and extract width
|
|
823
|
+
for (const [stepName, step] of entries(steps)) {
|
|
824
|
+
if (!botNeedsInput(step.robot)) {
|
|
825
|
+
importStepName = stepName
|
|
826
|
+
} else if (['/image/resize', '/video/thumb'].includes(step.robot)) {
|
|
827
|
+
if ('width' in step && (typeof step.width === 'number' || typeof step.width === 'string')) {
|
|
828
|
+
width = step.width
|
|
829
|
+
}
|
|
830
|
+
if (
|
|
831
|
+
'height' in step &&
|
|
832
|
+
(typeof step.height === 'number' || typeof step.height === 'string')
|
|
833
|
+
) {
|
|
834
|
+
height = step.height
|
|
835
|
+
}
|
|
836
|
+
if ('format' in step) {
|
|
837
|
+
if (step.format === 'png') {
|
|
838
|
+
format = 'png'
|
|
839
|
+
} else if (step.format === 'jpg') {
|
|
840
|
+
format = 'jpg'
|
|
841
|
+
} else if (step.format === 'gif') {
|
|
842
|
+
format = 'gif'
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
} else if (step.robot === '/file/serve') {
|
|
846
|
+
serveStep = stepName
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
// Add import step
|
|
851
|
+
if (importStepName) {
|
|
852
|
+
newSteps[importStepName] = steps[importStepName]
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
if (!width) {
|
|
856
|
+
if (serveStep) {
|
|
857
|
+
width = '${fields.w}'
|
|
858
|
+
} else {
|
|
859
|
+
width = 500
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
if (!height) {
|
|
863
|
+
if (serveStep) {
|
|
864
|
+
height = '${fields.h}'
|
|
865
|
+
} else {
|
|
866
|
+
height = 250
|
|
867
|
+
}
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
if (!format) {
|
|
871
|
+
format = 'png'
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
// Add preview step
|
|
875
|
+
newSteps.previewed = {
|
|
876
|
+
robot: '/file/preview',
|
|
877
|
+
use: [],
|
|
878
|
+
format,
|
|
879
|
+
width,
|
|
880
|
+
height,
|
|
881
|
+
resize_strategy: 'min_fit',
|
|
882
|
+
} satisfies RobotFilePreviewInstructionsInput
|
|
883
|
+
if (importStepName) {
|
|
884
|
+
addUseReference(newSteps.previewed, importStepName)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
// Update serve step
|
|
888
|
+
if (serveStep) {
|
|
889
|
+
newSteps[serveStep] = {
|
|
890
|
+
robot: '/file/serve',
|
|
891
|
+
use: [],
|
|
892
|
+
} satisfies RobotFileServeInstructionsInput
|
|
893
|
+
// Always use 'previewed' as the source for serve step
|
|
894
|
+
addUseReference(newSteps[serveStep], 'previewed')
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
template.steps = newSteps
|
|
898
|
+
return JSON.stringify(template, null, indent)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
export function getRecommendations(templateContent: string, silent?: boolean): Recommendation[] {
|
|
902
|
+
const parseOpts = silent === undefined ? undefined : { silent }
|
|
903
|
+
const [templateError, template] = parseSafeTemplate(templateContent, parseOpts)
|
|
904
|
+
if (templateError) {
|
|
905
|
+
return []
|
|
906
|
+
}
|
|
907
|
+
if (!hasSteps(template)) {
|
|
908
|
+
return []
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
const recommendations: Recommendation[] = []
|
|
912
|
+
const steps = template.steps
|
|
913
|
+
|
|
914
|
+
const stepCount = Object.keys(steps).length
|
|
915
|
+
const canJustRun = getFirstStepNameThatDoesNotNeedInputFromSteps(steps, ['/upload/handle']) !== ''
|
|
916
|
+
const hasFileServe = hasRobotInSteps(steps, '/file/serve')
|
|
917
|
+
const hasStorageRobot = hasRobotInSteps(steps, /\/store$/)
|
|
918
|
+
const hasImageResizeStep = hasRobotInSteps(steps, '/image/resize')
|
|
919
|
+
const hasVideoThumbStep = hasRobotInSteps(steps, '/video/thumb')
|
|
920
|
+
const hasImageResizeOrVideoThumb = hasImageResizeStep || hasVideoThumbStep
|
|
921
|
+
|
|
922
|
+
// Add the storage recommendation if needed
|
|
923
|
+
if (!hasStorageRobot && !hasFileServe) {
|
|
924
|
+
recommendations.push({
|
|
925
|
+
id: 'ADD_STORAGE',
|
|
926
|
+
robotName: 'Add Storage Robot',
|
|
927
|
+
description: 'Add a storage Robot to permanently save your processed files.',
|
|
928
|
+
applyFunction: (templateContent2) => addStorageRobot(templateContent2),
|
|
929
|
+
iconSrc: '/assets/images/robots/s3-store.png',
|
|
930
|
+
})
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
if (stepCount === 2 && canJustRun && hasFileServe) {
|
|
934
|
+
recommendations.push({
|
|
935
|
+
id: 'ADD_FILE_PREVIEW',
|
|
936
|
+
robotName: 'Add /file/preview',
|
|
937
|
+
description:
|
|
938
|
+
'You are serving assets directly to users. Consider adding a preview Step to reduce sizes and bandwidth usage.',
|
|
939
|
+
applyFunction: (templateContent2) => addFilePreviewRobot(templateContent2),
|
|
940
|
+
iconSrc: '/assets/images/robots/file-preview.png',
|
|
941
|
+
})
|
|
942
|
+
} else if (stepCount === 3 && canJustRun && hasFileServe && hasImageResizeOrVideoThumb) {
|
|
943
|
+
const robotToSwap = hasImageResizeStep ? '/image/resize' : '/video/thumb'
|
|
944
|
+
const imageOrVideo = hasImageResizeStep ? 'image' : 'video'
|
|
945
|
+
const negative = hasImageResizeStep ? 'video' : 'image'
|
|
946
|
+
|
|
947
|
+
recommendations.push({
|
|
948
|
+
id: 'REPLACE_THUMBESQUE_WITH_FILE_PREVIEW',
|
|
949
|
+
robotName: 'Use /file/preview',
|
|
950
|
+
description: `If you swap out the ${robotToSwap} Step for /file/preview, you can serve previews of not only ${imageOrVideo}s but also ${negative}s, audio, PDFs, and more.`,
|
|
951
|
+
applyFunction: (templateContent2) => addFilePreviewRobot(templateContent2),
|
|
952
|
+
iconSrc: '/assets/images/robots/file-preview.png',
|
|
953
|
+
})
|
|
954
|
+
} else if (
|
|
955
|
+
hasRobotInSteps(steps, '/image/resize') &&
|
|
956
|
+
!hasRobotInSteps(steps, '/image/optimize')
|
|
957
|
+
) {
|
|
958
|
+
recommendations.push({
|
|
959
|
+
id: 'ADD_IMAGE_OPTIMIZE',
|
|
960
|
+
robotName: 'Add /image/optimize',
|
|
961
|
+
description: 'Optimize your resized images to reduce file size without losing quality.',
|
|
962
|
+
applyFunction: (templateContent2) => addOptimizeRobots(templateContent2),
|
|
963
|
+
iconSrc: '/assets/images/robots/image-optimize.png',
|
|
964
|
+
})
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// Sort recommendations by id to ensure consistent order
|
|
968
|
+
return recommendations.sort((a, b) => a.id.localeCompare(b.id))
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
export function addFileServeRobot(templateContent: string): string {
|
|
972
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
973
|
+
if (templateError) {
|
|
974
|
+
return templateContent
|
|
975
|
+
}
|
|
976
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
977
|
+
return templateContent
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
const { steps } = template
|
|
981
|
+
|
|
982
|
+
let lastNonImportStepName = ''
|
|
983
|
+
let lastExportStepName = ''
|
|
984
|
+
|
|
985
|
+
// Find the last non-import step and any export step
|
|
986
|
+
for (const [stepName, step] of entries(steps)) {
|
|
987
|
+
if (!step.robot.endsWith('/import')) {
|
|
988
|
+
lastNonImportStepName = stepName
|
|
989
|
+
}
|
|
990
|
+
if (step.robot.includes('/store')) {
|
|
991
|
+
lastExportStepName = stepName
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
// If there's an export step, replace it with /file/serve
|
|
996
|
+
if (lastExportStepName) {
|
|
997
|
+
const lastExportStep = steps[lastExportStepName]
|
|
998
|
+
delete steps[lastExportStepName]
|
|
999
|
+
|
|
1000
|
+
const finalUse = getLastUsedStepName(lastExportStep)
|
|
1001
|
+
if (finalUse) {
|
|
1002
|
+
steps.served = {
|
|
1003
|
+
robot: '/file/serve',
|
|
1004
|
+
use: [],
|
|
1005
|
+
} satisfies RobotFileServeInstructionsInput
|
|
1006
|
+
addUseReference(steps.served, finalUse)
|
|
1007
|
+
}
|
|
1008
|
+
} else {
|
|
1009
|
+
// If no export step, append /file/serve as a new step
|
|
1010
|
+
steps.served = {
|
|
1011
|
+
robot: '/file/serve',
|
|
1012
|
+
use: [],
|
|
1013
|
+
} satisfies RobotFileServeInstructionsInput
|
|
1014
|
+
addUseReference(steps.served, lastNonImportStepName)
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
return JSON.stringify(template, null, indent)
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
export function addUploadHandleRobot(templateContent: string): string {
|
|
1021
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
1022
|
+
if (templateError) {
|
|
1023
|
+
return templateContent
|
|
1024
|
+
}
|
|
1025
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
1026
|
+
return templateContent
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
const steps = template.steps
|
|
1030
|
+
|
|
1031
|
+
const stepsOrder = Object.keys(steps)
|
|
1032
|
+
let firstImportStepName: string | null = null
|
|
1033
|
+
let firstImportStepIndex = -1
|
|
1034
|
+
|
|
1035
|
+
// Find the first import step
|
|
1036
|
+
for (let i = 0; i < stepsOrder.length; i++) {
|
|
1037
|
+
const stepName = stepsOrder[i]
|
|
1038
|
+
const step = steps[stepName]
|
|
1039
|
+
if (step.robot.endsWith('/import')) {
|
|
1040
|
+
firstImportStepName = stepName
|
|
1041
|
+
firstImportStepIndex = i
|
|
1042
|
+
break
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
const newSteps: StepsInput = {}
|
|
1047
|
+
if (firstImportStepName !== null && firstImportStepIndex >= 0) {
|
|
1048
|
+
// Replace the first import step with ':original' at the same position
|
|
1049
|
+
const stepsOrderNew = [...stepsOrder]
|
|
1050
|
+
stepsOrderNew[firstImportStepIndex] = ':original'
|
|
1051
|
+
|
|
1052
|
+
for (const stepName of stepsOrderNew) {
|
|
1053
|
+
if (stepName === ':original') {
|
|
1054
|
+
newSteps[stepName] = { robot: '/upload/handle' }
|
|
1055
|
+
} else {
|
|
1056
|
+
let step = steps[stepName]
|
|
1057
|
+
step = renameUseReferences(step, firstImportStepName, ':original')
|
|
1058
|
+
newSteps[stepName] = step
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
} else {
|
|
1062
|
+
// No import step, insert ':original' before the first step
|
|
1063
|
+
const stepsOrderNew = [':original', ...stepsOrder]
|
|
1064
|
+
let isFirstStep = true
|
|
1065
|
+
for (const stepName of stepsOrderNew) {
|
|
1066
|
+
if (stepName === ':original') {
|
|
1067
|
+
newSteps[stepName] = { robot: '/upload/handle' }
|
|
1068
|
+
} else {
|
|
1069
|
+
const step = steps[stepName]
|
|
1070
|
+
if (
|
|
1071
|
+
isFirstStep &&
|
|
1072
|
+
(!('use' in step) || (Array.isArray(step.use) && step.use.length === 0)) &&
|
|
1073
|
+
doesStepRobotSupportUse(step)
|
|
1074
|
+
) {
|
|
1075
|
+
step.use = ':original'
|
|
1076
|
+
isFirstStep = false
|
|
1077
|
+
}
|
|
1078
|
+
newSteps[stepName] = step
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
}
|
|
1082
|
+
|
|
1083
|
+
template.steps = newSteps
|
|
1084
|
+
return JSON.stringify(template, null, indent)
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
export function addImportRobot(templateContent: string): string {
|
|
1088
|
+
const [templateError, template, indent] = parseSafeTemplate(templateContent)
|
|
1089
|
+
if (templateError) {
|
|
1090
|
+
return templateContent
|
|
1091
|
+
}
|
|
1092
|
+
if (!('steps' in template) || typeof template.steps !== 'object') {
|
|
1093
|
+
return templateContent
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
const steps = template.steps
|
|
1097
|
+
const newSteps: StepsInput = {}
|
|
1098
|
+
|
|
1099
|
+
// Check if an import or html/convert robot already exists
|
|
1100
|
+
const hasImportRobot =
|
|
1101
|
+
Boolean(getFirstStepNameThatDoesNotNeedInput(templateContent)) &&
|
|
1102
|
+
!hasRobot(templateContent, '/upload/handle')
|
|
1103
|
+
if (hasImportRobot) {
|
|
1104
|
+
return templateContent // No changes needed
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const uploadHandleStepName = Object.keys(steps).find(
|
|
1108
|
+
(stepName) => steps[stepName].robot === '/upload/handle',
|
|
1109
|
+
)
|
|
1110
|
+
|
|
1111
|
+
// Find the first non-import-non-export step to determine media type
|
|
1112
|
+
let firstNonImportExportStep: StepInput | undefined
|
|
1113
|
+
for (const [, step] of entries(steps)) {
|
|
1114
|
+
if (
|
|
1115
|
+
!firstNonImportExportStep &&
|
|
1116
|
+
!step.robot.endsWith('/import') &&
|
|
1117
|
+
!step.robot.includes('/store') &&
|
|
1118
|
+
step.robot !== '/file/serve'
|
|
1119
|
+
) {
|
|
1120
|
+
firstNonImportExportStep = step
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Add the import step, replacing the upload/handle step if it exists
|
|
1125
|
+
const importStepName = 'imported'
|
|
1126
|
+
|
|
1127
|
+
let url: string
|
|
1128
|
+
|
|
1129
|
+
// Set URL based on the type of media being processed
|
|
1130
|
+
const robotName = firstNonImportExportStep?.robot || ''
|
|
1131
|
+
if (robotName.startsWith('/image/') || robotName === '/file/preview') {
|
|
1132
|
+
url = 'https://demos.transloadit.com/inputs/prinsengracht.jpg'
|
|
1133
|
+
} else if (robotName.startsWith('/video/')) {
|
|
1134
|
+
url = 'https://demos.transloadit.com/inputs/wave10.mp4'
|
|
1135
|
+
} else if (robotName.startsWith('/audio/')) {
|
|
1136
|
+
url = 'https://demos.transloadit.com/inputs/joakim_karud-rock_angel.mp3'
|
|
1137
|
+
} else if (robotName.startsWith('/document/')) {
|
|
1138
|
+
url = 'https://demos.transloadit.com/inputs/aws-cloud-best-practices.pdf'
|
|
1139
|
+
} else {
|
|
1140
|
+
url = 'https://demos.transloadit.com/inputs/prinsengracht.jpg'
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
newSteps[importStepName] = {
|
|
1144
|
+
robot: '/http/import',
|
|
1145
|
+
url,
|
|
1146
|
+
} satisfies StepInput
|
|
1147
|
+
|
|
1148
|
+
// Update references and add other steps
|
|
1149
|
+
for (const [stepName, step] of entries(steps)) {
|
|
1150
|
+
if (stepName !== uploadHandleStepName) {
|
|
1151
|
+
const updatedStep = { ...step }
|
|
1152
|
+
if (uploadHandleStepName) {
|
|
1153
|
+
renameUseReferences(updatedStep, uploadHandleStepName, importStepName)
|
|
1154
|
+
} else if (
|
|
1155
|
+
!('use' in updatedStep) ||
|
|
1156
|
+
(Array.isArray(updatedStep.use) && updatedStep.use.length === 0)
|
|
1157
|
+
) {
|
|
1158
|
+
addUseReference(updatedStep, importStepName)
|
|
1159
|
+
}
|
|
1160
|
+
newSteps[stepName] = updatedStep
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
template.steps = newSteps
|
|
1165
|
+
return JSON.stringify(template, null, indent)
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
export function addFieldsInput(templateContent: string): string {
|
|
1169
|
+
let parsed: unknown
|
|
1170
|
+
let indent = ' '
|
|
1171
|
+
try {
|
|
1172
|
+
parsed = JSON.parse(templateContent)
|
|
1173
|
+
indent = getIndentation(templateContent)
|
|
1174
|
+
} catch (_e) {
|
|
1175
|
+
return templateContent
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
if (!isRecord(parsed)) {
|
|
1179
|
+
return templateContent
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const parsedRecord = parsed as Record<string, unknown>
|
|
1183
|
+
const stepsValue = parsedRecord.steps
|
|
1184
|
+
if (!isRecord(stepsValue)) {
|
|
1185
|
+
return templateContent
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
const steps = stepsValue as Record<string, StepInputRecord>
|
|
1189
|
+
|
|
1190
|
+
for (const [, step] of entries(steps)) {
|
|
1191
|
+
if (step.robot === '/http/import') {
|
|
1192
|
+
if ('url' in step && typeof step.url === 'string') {
|
|
1193
|
+
if (!step.url.includes('${fields.input}')) {
|
|
1194
|
+
let url: URL
|
|
1195
|
+
try {
|
|
1196
|
+
url = new URL(step.url)
|
|
1197
|
+
url.pathname = '${fields.input}'
|
|
1198
|
+
step.url = url.toString().replaceAll('%7B', '{').replaceAll('%7D', '}')
|
|
1199
|
+
} catch (_e) {
|
|
1200
|
+
step.url = 'https://demos.transloadit.com/${fields.input}'
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
1203
|
+
} else {
|
|
1204
|
+
step.url = 'https://demos.transloadit.com/${fields.input}'
|
|
1205
|
+
}
|
|
1206
|
+
} else if (step.robot.endsWith('/import')) {
|
|
1207
|
+
if ('path' in step && typeof step.path === 'string') {
|
|
1208
|
+
if (!step.path.includes('${fields.input}')) {
|
|
1209
|
+
let url: URL
|
|
1210
|
+
try {
|
|
1211
|
+
url = new URL(step.path)
|
|
1212
|
+
url.pathname = '${fields.input}'
|
|
1213
|
+
step.path = url.toString().replaceAll('%7B', '{').replaceAll('%7D', '}')
|
|
1214
|
+
} catch (_e) {
|
|
1215
|
+
step.path = '${fields.input}'
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
} else {
|
|
1219
|
+
step.path = '${fields.input}'
|
|
1220
|
+
}
|
|
1221
|
+
break
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
parsedRecord.steps = steps
|
|
1226
|
+
|
|
1227
|
+
return JSON.stringify(parsedRecord, null, indent)
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
function getExampleValueForField(
|
|
1231
|
+
rName: string,
|
|
1232
|
+
paramName: string,
|
|
1233
|
+
): FieldOccurrence['exampleValues'][number][] {
|
|
1234
|
+
if (rName === '/http/import' && paramName === 'url') {
|
|
1235
|
+
return ['inputs/prinsengracht.jpg']
|
|
1236
|
+
}
|
|
1237
|
+
if (rName.endsWith('/import') && paramName === 'path') {
|
|
1238
|
+
return ['inputs/prinsengracht.jpg']
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
if (paramName === 'width') {
|
|
1242
|
+
return [400]
|
|
1243
|
+
}
|
|
1244
|
+
if (paramName === 'height') {
|
|
1245
|
+
return [180]
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
return []
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function getMostCommonExampleValue(
|
|
1252
|
+
occurrences: FieldOccurrence[],
|
|
1253
|
+
): FieldOccurrence['exampleValues'][number] {
|
|
1254
|
+
const exampleValues = occurrences.map((occurrence) => occurrence.exampleValues[0])
|
|
1255
|
+
const exampleValueCounts = new Map<FieldOccurrence['exampleValues'][number], number>()
|
|
1256
|
+
|
|
1257
|
+
for (const value of exampleValues) {
|
|
1258
|
+
exampleValueCounts.set(value, (exampleValueCounts.get(value) || 0) + 1)
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
let mostCommonValue: string | number | boolean | undefined
|
|
1262
|
+
let maxCount = 0
|
|
1263
|
+
|
|
1264
|
+
for (const [value, count] of exampleValueCounts.entries()) {
|
|
1265
|
+
if (count > maxCount) {
|
|
1266
|
+
maxCount = count
|
|
1267
|
+
mostCommonValue = value
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
if (mostCommonValue === undefined) {
|
|
1272
|
+
return ''
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
return mostCommonValue
|
|
1276
|
+
}
|
|
1277
|
+
|
|
1278
|
+
// Modify the extractFieldNamesFromTemplate function
|
|
1279
|
+
export function extractFieldNamesFromTemplate(templateContent: string): ParsedTemplateField[] {
|
|
1280
|
+
const [templateError, template] = parseSafeTemplate(templateContent)
|
|
1281
|
+
if (templateError) {
|
|
1282
|
+
return []
|
|
1283
|
+
}
|
|
1284
|
+
const fieldsMap = new Map<string, ParsedTemplateField>()
|
|
1285
|
+
|
|
1286
|
+
function traverse(value: unknown, path: (number | string)[], stepName: string, rName: string) {
|
|
1287
|
+
if (typeof value === 'string') {
|
|
1288
|
+
const matches = value.match(/\${fields\.([a-zA-Z0-9_]+)}/g)
|
|
1289
|
+
if (matches) {
|
|
1290
|
+
for (const match of matches) {
|
|
1291
|
+
const fieldName = match.slice(9, -1)
|
|
1292
|
+
let field = fieldsMap.get(fieldName)
|
|
1293
|
+
if (!field) {
|
|
1294
|
+
field = {
|
|
1295
|
+
fieldName,
|
|
1296
|
+
occurrences: [],
|
|
1297
|
+
mostCommonExampleValue: '',
|
|
1298
|
+
}
|
|
1299
|
+
fieldsMap.set(fieldName, field)
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
const parts = value.split(match)
|
|
1303
|
+
const [leader, trailer] = parts
|
|
1304
|
+
const paramName = String(path[0])
|
|
1305
|
+
|
|
1306
|
+
field.occurrences.push({
|
|
1307
|
+
stepName,
|
|
1308
|
+
exampleValues: getExampleValueForField(rName, paramName),
|
|
1309
|
+
rName,
|
|
1310
|
+
paramName,
|
|
1311
|
+
leader,
|
|
1312
|
+
trailer,
|
|
1313
|
+
requiresDenoEval: false,
|
|
1314
|
+
errors: [],
|
|
1315
|
+
path: [stepName, ...path],
|
|
1316
|
+
})
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
} else if (Array.isArray(value)) {
|
|
1320
|
+
for (const [index, item] of value.entries()) {
|
|
1321
|
+
traverse(item, [...path, index], stepName, rName)
|
|
1322
|
+
}
|
|
1323
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
1324
|
+
for (const [key, childValue] of entries(value)) {
|
|
1325
|
+
traverse(childValue, [...path, key], stepName, rName)
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
|
|
1330
|
+
for (const [stepName, step] of entries(template.steps || {})) {
|
|
1331
|
+
if (typeof step !== 'object' || step === null) {
|
|
1332
|
+
continue
|
|
1333
|
+
}
|
|
1334
|
+
if (!('robot' in step)) {
|
|
1335
|
+
continue
|
|
1336
|
+
}
|
|
1337
|
+
if (typeof step.robot !== 'string') {
|
|
1338
|
+
continue
|
|
1339
|
+
}
|
|
1340
|
+
const rName = step.robot || ''
|
|
1341
|
+
traverse(step, [], stepName, rName)
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
for (const field of fieldsMap.values()) {
|
|
1345
|
+
field.mostCommonExampleValue = getMostCommonExampleValue(field.occurrences)
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
return Array.from(fieldsMap.values())
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
function getFinalType(schema: z.ZodTypeAny): z.ZodTypeAny {
|
|
1352
|
+
if (schema instanceof z.ZodOptional || schema instanceof z.ZodNullable) {
|
|
1353
|
+
return getFinalType(schema.unwrap())
|
|
1354
|
+
}
|
|
1355
|
+
if (schema instanceof z.ZodDefault) {
|
|
1356
|
+
return getFinalType(schema._def.innerType)
|
|
1357
|
+
}
|
|
1358
|
+
return schema
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
export function interpolateFieldsInTemplate(
|
|
1362
|
+
templateContent: string,
|
|
1363
|
+
allFields: ParsedTemplateField[],
|
|
1364
|
+
opts?: { silent: boolean },
|
|
1365
|
+
): AssemblyInstructionsInput {
|
|
1366
|
+
const [templateError, template] = parseSafeTemplate(templateContent, opts)
|
|
1367
|
+
if (templateError) {
|
|
1368
|
+
return { steps: {} }
|
|
1369
|
+
}
|
|
1370
|
+
const { steps } = template
|
|
1371
|
+
const newSteps: StepsInput = {}
|
|
1372
|
+
|
|
1373
|
+
for (const [stepName, step] of entries(steps)) {
|
|
1374
|
+
const newStep = { ...step }
|
|
1375
|
+
|
|
1376
|
+
for (const [paramName, paramValue] of entries(step)) {
|
|
1377
|
+
if (typeof paramValue === 'string') {
|
|
1378
|
+
let newValue: string | number | boolean = paramValue
|
|
1379
|
+
|
|
1380
|
+
for (const field of allFields) {
|
|
1381
|
+
for (const occurrence of field.occurrences) {
|
|
1382
|
+
if (occurrence.stepName === stepName && occurrence.paramName === paramName) {
|
|
1383
|
+
const compiledValue = `${occurrence.leader}${field.value}${occurrence.trailer}`
|
|
1384
|
+
newValue = compiledValue
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
// Convert to number if expected
|
|
1390
|
+
const schema = getSchemaForRobot(step.robot)
|
|
1391
|
+
if (schema && isZodObject(schema)) {
|
|
1392
|
+
const paramSchema = schema.shape[paramName]
|
|
1393
|
+
const finalParamSchema = getFinalType(paramSchema)
|
|
1394
|
+
|
|
1395
|
+
if (finalParamSchema instanceof z.ZodNumber) {
|
|
1396
|
+
const num = Number(newValue)
|
|
1397
|
+
if (!Number.isNaN(num)) {
|
|
1398
|
+
newValue = num
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
Object.assign(newStep, { [paramName]: newValue })
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
newSteps[stepName] = newStep
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
return { ...template, steps: newSteps }
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
export function validateInterpolatedTemplate(
|
|
1414
|
+
template: AssemblyInstructionsInput,
|
|
1415
|
+
fieldsWithValues: ParsedTemplateField[],
|
|
1416
|
+
fieldNameToValidate?: string,
|
|
1417
|
+
): ValidationError[] {
|
|
1418
|
+
const errors: ValidationError[] = []
|
|
1419
|
+
|
|
1420
|
+
for (const [stepName, step] of entries(template.steps)) {
|
|
1421
|
+
const schema = getSchemaForRobot(step.robot)
|
|
1422
|
+
if (!schema) {
|
|
1423
|
+
console.error('No schema linked up for', step.robot)
|
|
1424
|
+
continue
|
|
1425
|
+
}
|
|
1426
|
+
|
|
1427
|
+
const zodRes = schema.safeParse(step)
|
|
1428
|
+
if (!zodRes.success) {
|
|
1429
|
+
for (const err of zodRes.error.errors) {
|
|
1430
|
+
if (err.path.length !== 1) {
|
|
1431
|
+
continue
|
|
1432
|
+
}
|
|
1433
|
+
if (typeof err.path[0] !== 'string') {
|
|
1434
|
+
continue
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
const fieldNames: string[] = []
|
|
1438
|
+
for (const field of fieldsWithValues) {
|
|
1439
|
+
if (field.fieldName === fieldNameToValidate || fieldNameToValidate === undefined) {
|
|
1440
|
+
for (const occurrence of field.occurrences) {
|
|
1441
|
+
if (occurrence.stepName === stepName && occurrence.paramName === err.path[0]) {
|
|
1442
|
+
fieldNames.push(field.fieldName)
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
if (fieldNames.length > 0) {
|
|
1449
|
+
const paramName = err.path[0]
|
|
1450
|
+
const value = Object.entries(step).find(([key]) => key === paramName)?.[1]
|
|
1451
|
+
errors.push({
|
|
1452
|
+
stepName,
|
|
1453
|
+
robotName: step.robot,
|
|
1454
|
+
paramName,
|
|
1455
|
+
value,
|
|
1456
|
+
fieldNames,
|
|
1457
|
+
message: err.message,
|
|
1458
|
+
})
|
|
1459
|
+
}
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
return errors
|
|
1465
|
+
}
|