@automate.ax/catalog 0.153.2 → 0.154.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automate.ax/catalog",
3
- "version": "0.153.2",
3
+ "version": "0.154.2",
4
4
  "description": "Shared integration service and trigger definitions for Automate.ax.",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/catalog.ts CHANGED
@@ -207,7 +207,7 @@ export const cloudflareService = defineIntegrationService({
207
207
  input: "password",
208
208
  label: "API token",
209
209
  name: "apiKey",
210
- placeholder: "Cloudflare API token",
210
+ placeholder: "Token with Account Settings Read",
211
211
  required: true,
212
212
  },
213
213
  ],
@@ -216,7 +216,8 @@ export const cloudflareService = defineIntegrationService({
216
216
  type: "form",
217
217
  },
218
218
  ],
219
- description: "Connect a Cloudflare account with OAuth or an API token.",
219
+ description:
220
+ "Connect with OAuth or an API token that includes Account Settings Read.",
220
221
  id: "cloudflare",
221
222
  name: "Cloudflare",
222
223
  preferredConnectionMethodId: "oauth",
@@ -0,0 +1,513 @@
1
+ import * as z from "zod/mini"
2
+ import { sentenceCase } from "./triggers/name"
3
+
4
+ const EMAIL_SCHEMA = z.email()
5
+ const URL_SCHEMA = z.url()
6
+ const DATE_SCHEMA = z.iso.date()
7
+ const DATETIME_SCHEMA = z.iso.datetime({ local: true })
8
+ const FIELD_NAME_SCHEMA = z
9
+ .string()
10
+ .check(z.trim(), z.minLength(1), z.maxLength(100))
11
+ const FIELD_LABEL_SCHEMA = z
12
+ .string()
13
+ .check(z.trim(), z.minLength(1), z.maxLength(255))
14
+ const FIELD_DESCRIPTION_SCHEMA = z.string().check(z.maxLength(2_000))
15
+ const FIELD_LENGTH_SCHEMA = z.int().check(z.nonnegative())
16
+ const FORM_FIELD_BASE_SCHEMA = {
17
+ description: z.optional(FIELD_DESCRIPTION_SCHEMA),
18
+ label: z.optional(FIELD_LABEL_SCHEMA),
19
+ name: FIELD_NAME_SCHEMA,
20
+ }
21
+ const FORM_TEXT_FIELD_SCHEMA = z.object({
22
+ ...FORM_FIELD_BASE_SCHEMA,
23
+ defaultValue: z.optional(z.string()),
24
+ maxLength: z.optional(FIELD_LENGTH_SCHEMA),
25
+ minLength: z.optional(FIELD_LENGTH_SCHEMA),
26
+ placeholder: z.optional(z.string()),
27
+ required: z.prefault(z.boolean(), true),
28
+ type: z.enum(["email", "phone", "text", "textarea", "url"]),
29
+ })
30
+ const FORM_NUMBER_FIELD_SCHEMA = z.object({
31
+ ...FORM_FIELD_BASE_SCHEMA,
32
+ defaultValue: z.optional(z.number()),
33
+ max: z.optional(z.number()),
34
+ min: z.optional(z.number()),
35
+ placeholder: z.optional(z.string()),
36
+ required: z.prefault(z.boolean(), true),
37
+ step: z.prefault(z.number().check(z.positive()), 1),
38
+ type: z.literal("number"),
39
+ })
40
+ const FORM_DATE_FIELD_SCHEMA = z.object({
41
+ ...FORM_FIELD_BASE_SCHEMA,
42
+ defaultValue: z.optional(z.iso.date()),
43
+ max: z.optional(z.iso.date()),
44
+ min: z.optional(z.iso.date()),
45
+ required: z.prefault(z.boolean(), true),
46
+ step: z.prefault(z.int().check(z.positive()), 1),
47
+ type: z.literal("date"),
48
+ })
49
+ const FORM_DATETIME_FIELD_SCHEMA = z.object({
50
+ ...FORM_FIELD_BASE_SCHEMA,
51
+ defaultValue: z.optional(z.iso.datetime({ local: true })),
52
+ max: z.optional(z.iso.datetime({ local: true })),
53
+ min: z.optional(z.iso.datetime({ local: true })),
54
+ required: z.prefault(z.boolean(), true),
55
+ step: z.prefault(z.number().check(z.positive()), 60),
56
+ type: z.literal("datetime"),
57
+ })
58
+ const FORM_CHOICE_FIELD_SCHEMA = {
59
+ ...FORM_FIELD_BASE_SCHEMA,
60
+ defaultValue: z.optional(FIELD_NAME_SCHEMA),
61
+ options: z
62
+ .array(
63
+ z.object({
64
+ label: FIELD_LABEL_SCHEMA,
65
+ value: FIELD_NAME_SCHEMA,
66
+ }),
67
+ )
68
+ .check(z.minLength(1)),
69
+ placeholder: z.optional(z.string()),
70
+ required: z.prefault(z.boolean(), true),
71
+ }
72
+ const FORM_SELECT_FIELD_SCHEMA = z.object({
73
+ ...FORM_CHOICE_FIELD_SCHEMA,
74
+ type: z.literal("select"),
75
+ })
76
+ const FORM_RADIO_FIELD_SCHEMA = z.object({
77
+ ...FORM_CHOICE_FIELD_SCHEMA,
78
+ type: z.literal("radio"),
79
+ })
80
+ const FORM_MULTI_SELECT_FIELD_SCHEMA = z.object({
81
+ ...FORM_FIELD_BASE_SCHEMA,
82
+ defaultValue: z.optional(z.array(FIELD_NAME_SCHEMA)),
83
+ options: z
84
+ .array(
85
+ z.object({
86
+ label: FIELD_LABEL_SCHEMA,
87
+ value: FIELD_NAME_SCHEMA,
88
+ }),
89
+ )
90
+ .check(z.minLength(1)),
91
+ placeholder: z.optional(z.string()),
92
+ required: z.prefault(z.boolean(), true),
93
+ type: z.literal("multi-select"),
94
+ })
95
+ const FORM_CHECKBOX_FIELD_SCHEMA = z.object({
96
+ ...FORM_FIELD_BASE_SCHEMA,
97
+ defaultValue: z.prefault(z.boolean(), false),
98
+ required: z.prefault(z.boolean(), false),
99
+ type: z.literal("checkbox"),
100
+ })
101
+ const FORM_FILE_FIELD_SCHEMA = z.object({
102
+ ...FORM_FIELD_BASE_SCHEMA,
103
+ required: z.prefault(z.boolean(), true),
104
+ type: z.literal("file"),
105
+ })
106
+ const FORM_FIELD_SCHEMA = z.pipe(
107
+ z.discriminatedUnion("type", [
108
+ FORM_TEXT_FIELD_SCHEMA,
109
+ FORM_NUMBER_FIELD_SCHEMA,
110
+ FORM_DATE_FIELD_SCHEMA,
111
+ FORM_DATETIME_FIELD_SCHEMA,
112
+ FORM_SELECT_FIELD_SCHEMA,
113
+ FORM_RADIO_FIELD_SCHEMA,
114
+ FORM_MULTI_SELECT_FIELD_SCHEMA,
115
+ FORM_CHECKBOX_FIELD_SCHEMA,
116
+ FORM_FILE_FIELD_SCHEMA,
117
+ ]),
118
+ z.transform((field) => ({
119
+ ...field,
120
+ label:
121
+ field.label ??
122
+ `${sentenceCase(field.name)}${field.type === "checkbox" ? "?" : ""}`,
123
+ })),
124
+ )
125
+ export const FORM_FIELDS_SCHEMA = z.array(FORM_FIELD_SCHEMA).check(
126
+ z.refine(
127
+ (fields) =>
128
+ new Set(fields.map((field) => field.name)).size === fields.length,
129
+ "Form field names must be unique.",
130
+ ),
131
+ z.refine(
132
+ (fields) => fields.every(isFormFieldDefaultValid),
133
+ "Form field defaults must satisfy their field constraints.",
134
+ ),
135
+ )
136
+
137
+ export type FormFieldValue = File | boolean | number | string | string[] | null
138
+
139
+ /**
140
+ * Returns the configured fallback for a field without an assigned value.
141
+ *
142
+ * @param field - Field whose fallback should be resolved.
143
+ */
144
+ export function getFormFieldDefaultValue(field: FormField) {
145
+ if (field.type === "file") return field.required === false ? null : undefined
146
+ if (field.defaultValue !== undefined) return field.defaultValue
147
+ if (field.type === "checkbox") return false
148
+ if (field.type === "multi-select") return []
149
+ if (field.type === "number")
150
+ return field.required === false ? null : undefined
151
+ return field.required === false ? "" : undefined
152
+ }
153
+
154
+ /**
155
+ * Checks one decoded value against a dashboard field declaration.
156
+ *
157
+ * @param field - Field declaration whose constraints apply.
158
+ * @param value - Decoded value to validate.
159
+ */
160
+ export function isFormFieldValueValid(
161
+ field: FormField,
162
+ value: unknown,
163
+ ): value is FormFieldValue {
164
+ return getFormFieldValueSchema(field).safeParse(value).success
165
+ }
166
+
167
+ /**
168
+ * Builds the shared validator for decoded field values.
169
+ *
170
+ * @param field - Field declaration whose constraints apply.
171
+ */
172
+ export function getFormFieldValueSchema(field: FormField) {
173
+ return z.custom<FormFieldValue>(
174
+ (value) => getFormFieldError(field, value) === undefined,
175
+ { error: (issue) => getFormFieldError(field, issue.input) },
176
+ )
177
+ }
178
+
179
+ /**
180
+ * Normalizes controlled or prompted values before validation.
181
+ *
182
+ * @param field - Field declaration.
183
+ * @param value - Raw control value.
184
+ */
185
+ export function normalizeFormFieldValue(field: FormField, value: unknown) {
186
+ if (field.type === "file") {
187
+ // Browsers submit an unnamed empty File when no file was selected.
188
+ return value instanceof File && !value.name && value.size === 0
189
+ ? null
190
+ : value
191
+ }
192
+ if (typeof value === "string") {
193
+ const text = value.trim()
194
+ return field.type === "number" ? (text ? Number(text) : null) : text
195
+ }
196
+ if (field.type === "multi-select" && Array.isArray(value)) {
197
+ return value
198
+ .map((entry: unknown) =>
199
+ typeof entry === "string" ? entry.trim() : entry,
200
+ )
201
+ .filter((entry) => entry !== "")
202
+ }
203
+ return value
204
+ }
205
+
206
+ /**
207
+ * Reads and validates one submitted field, preserving uploaded File values.
208
+ *
209
+ * @param field - Field declaration.
210
+ * @param data - Submitted form data.
211
+ * @param name - Form name, including any UI namespace.
212
+ */
213
+ export function readFormFieldValue(
214
+ field: FormField,
215
+ data: FormData,
216
+ name = field.name,
217
+ ) {
218
+ // Preserve the transport value before applying field-specific normalization.
219
+ const raw =
220
+ field.type === "checkbox"
221
+ ? data.has(name)
222
+ : field.type === "multi-select"
223
+ ? data.getAll(name)
224
+ : (data.get(name) ?? (field.type === "file" ? null : ""))
225
+ return getFormFieldValueSchema(field).safeParse(
226
+ normalizeFormFieldValue(field, raw),
227
+ )
228
+ }
229
+
230
+ /**
231
+ * Validates configured form fields and ignores unconfigured entries.
232
+ *
233
+ * @param fields - Ordered field declarations.
234
+ * @param data - Submitted form data.
235
+ */
236
+ export function parseFormFields(fields: readonly FormField[], data: FormData) {
237
+ const results = fields.map((field) => ({
238
+ field,
239
+ result: readFormFieldValue(field, data),
240
+ }))
241
+ const issues = results.flatMap(({ field, result }) =>
242
+ result.success
243
+ ? []
244
+ : [
245
+ {
246
+ field: field.name,
247
+ message: result.error.issues[0]!.message,
248
+ },
249
+ ],
250
+ )
251
+ if (issues.length > 0) return { issues, success: false as const }
252
+ return {
253
+ data: Object.fromEntries(
254
+ results.flatMap(({ field, result }) =>
255
+ result.success ? [[field.name, result.data]] : [],
256
+ ),
257
+ ),
258
+ success: true as const,
259
+ }
260
+ }
261
+
262
+ /**
263
+ * Reports the first violated field constraint for every input surface.
264
+ *
265
+ * @param field - Field declaration.
266
+ * @param value - Decoded value.
267
+ */
268
+ function getFormFieldError(
269
+ field: FormField,
270
+ value: unknown,
271
+ ): string | undefined {
272
+ const label = field.label ?? sentenceCase(field.name)
273
+ const required = `${label} is required.`
274
+ const invalid = `${label} is invalid.`
275
+ if (field.type === "file") {
276
+ if (value === null) return field.required === false ? undefined : required
277
+ return value instanceof File ? undefined : `${label} must be a file.`
278
+ }
279
+ if (field.type === "checkbox") {
280
+ if (typeof value !== "boolean") return invalid
281
+ return field.required === true && !value ? required : undefined
282
+ }
283
+ if (field.type === "multi-select") {
284
+ if (!Array.isArray(value)) return invalid
285
+ if (field.required !== false && value.length === 0) return required
286
+ return value.every(
287
+ (entry) =>
288
+ typeof entry === "string" &&
289
+ field.options.some((option) => option.value === entry),
290
+ )
291
+ ? undefined
292
+ : invalid
293
+ }
294
+ if (field.type === "number") {
295
+ if (value === null) return field.required === false ? undefined : required
296
+ if (typeof value !== "number" || !Number.isFinite(value))
297
+ return `${label} must be a number.`
298
+ if (field.min !== undefined && value < field.min)
299
+ return `${label} must be at least ${field.min}.`
300
+ if (field.max !== undefined && value > field.max)
301
+ return `${label} must be at most ${field.max}.`
302
+ const step = field.step ?? 1
303
+ return isFormStepMismatch(value, field.min ?? field.defaultValue ?? 0, step)
304
+ ? `${label} must use increments of ${step}.`
305
+ : undefined
306
+ }
307
+ if (typeof value !== "string") return invalid
308
+ const text =
309
+ field.type === "radio" ||
310
+ field.type === "select" ||
311
+ field.type === "date" ||
312
+ field.type === "datetime"
313
+ ? value
314
+ : value.trim()
315
+ if (text === "") return field.required === false ? undefined : required
316
+ if (field.type === "radio" || field.type === "select") {
317
+ return field.options.some((option) => option.value === text)
318
+ ? undefined
319
+ : invalid
320
+ }
321
+ if (field.type === "date" || field.type === "datetime") {
322
+ if (
323
+ !(field.type === "date" ? DATE_SCHEMA : DATETIME_SCHEMA).safeParse(text)
324
+ .success
325
+ )
326
+ return invalid
327
+ const date = parseFormDateValue(field.type, text)
328
+ if (!Number.isFinite(date)) return invalid
329
+ if (
330
+ field.min !== undefined &&
331
+ date < parseFormDateValue(field.type, field.min)
332
+ )
333
+ return `${label} must be on or after ${field.min}.`
334
+ if (
335
+ field.max !== undefined &&
336
+ date > parseFormDateValue(field.type, field.max)
337
+ )
338
+ return `${label} must be on or before ${field.max}.`
339
+ // HTML temporal steps are measured from this configured origin.
340
+ const base = parseFormDateValue(
341
+ field.type,
342
+ field.min ??
343
+ field.defaultValue ??
344
+ (field.type === "date" ? "1970-01-01" : "1970-01-01T00:00"),
345
+ )
346
+ // Compare timestamps using millisecond intervals.
347
+ const step =
348
+ (field.step ?? (field.type === "date" ? 1 : 60)) *
349
+ (field.type === "date" ? 86_400_000 : 1_000)
350
+ return isFormStepMismatch(date, base, step)
351
+ ? `${label} does not match the configured step.`
352
+ : undefined
353
+ }
354
+ if (field.minLength !== undefined && text.length < field.minLength)
355
+ return `${label} must contain at least ${field.minLength} characters.`
356
+ if (field.maxLength !== undefined && text.length > field.maxLength)
357
+ return `${label} must contain at most ${field.maxLength} characters.`
358
+ if (field.type === "email" && !EMAIL_SCHEMA.safeParse(text).success)
359
+ return `${label} must be an email address.`
360
+ if (field.type === "url" && !URL_SCHEMA.safeParse(text).success)
361
+ return `${label} must be a URL.`
362
+ }
363
+
364
+ interface FormFieldBase {
365
+ /** Supporting text shown below the field. */
366
+ description?: string
367
+
368
+ /** Visible field label. Inferred from `name` when omitted. */
369
+ label?: string
370
+
371
+ /** Property name used in submitted trigger data. */
372
+ name: string
373
+
374
+ /** Whether the form must contain a value. Defaults to true. */
375
+ required?: boolean
376
+ }
377
+
378
+ export type FormField =
379
+ | (FormFieldBase & { type: "file" })
380
+ | (FormFieldBase & {
381
+ defaultValue?: string
382
+ maxLength?: number
383
+ minLength?: number
384
+ placeholder?: string
385
+ type: "email" | "phone" | "text" | "textarea" | "url"
386
+ })
387
+ | (FormFieldBase & {
388
+ defaultValue?: number
389
+ max?: number
390
+ min?: number
391
+ placeholder?: string
392
+
393
+ /** Increment used by the number input. Defaults to 1. */
394
+ step?: number
395
+ type: "number"
396
+ })
397
+ | (FormFieldBase & {
398
+ /** Initial local date in `YYYY-MM-DD` format. */
399
+ defaultValue?: string
400
+ max?: string
401
+ min?: string
402
+
403
+ /** Allowed increment in days. Defaults to 1. */
404
+ step?: number
405
+ type: "date"
406
+ })
407
+ | (FormFieldBase & {
408
+ /** Initial local date and time in `YYYY-MM-DDTHH:mm` format. */
409
+ defaultValue?: string
410
+ max?: string
411
+ min?: string
412
+
413
+ /** Allowed increment in seconds. Defaults to 60. */
414
+ step?: number
415
+ type: "datetime"
416
+ })
417
+ | (FormFieldBase & {
418
+ defaultValue?: string
419
+ options: readonly { label: string; value: string }[]
420
+ placeholder?: string
421
+ type: "select"
422
+ })
423
+ | (FormFieldBase & {
424
+ defaultValue?: string
425
+ options: readonly { label: string; value: string }[]
426
+ type: "radio"
427
+ })
428
+ | (FormFieldBase & {
429
+ defaultValue?: readonly string[]
430
+ options: readonly { label: string; value: string }[]
431
+ placeholder?: string
432
+ type: "multi-select"
433
+ })
434
+ | (FormFieldBase & {
435
+ /** Initial checked state. Defaults to false. */
436
+ defaultValue?: boolean
437
+
438
+ /** Whether the checkbox must be checked. Defaults to false. */
439
+ required?: boolean
440
+ type: "checkbox"
441
+ })
442
+
443
+ /**
444
+ * Checks a configured default against the same constraints as submitted data.
445
+ *
446
+ * @param field - Field definition to validate.
447
+ */
448
+ function isFormFieldDefaultValid(field: FormField) {
449
+ if (
450
+ field.type === "file" ||
451
+ field.type === "checkbox" ||
452
+ field.defaultValue === undefined
453
+ )
454
+ return true
455
+ return isFormFieldValueValid(field, field.defaultValue)
456
+ }
457
+
458
+ /**
459
+ * Checks whether a value falls outside an allowed step interval.
460
+ *
461
+ * @param value - Configured default value.
462
+ * @param base - Value from which step intervals begin.
463
+ * @param step - Allowed interval size.
464
+ */
465
+ function isFormStepMismatch(value: number, base: number, step: number) {
466
+ const intervals = (value - base) / step
467
+ return Math.abs(intervals - Math.round(intervals)) > 1e-9
468
+ }
469
+
470
+ /**
471
+ * Converts a validated local date or date-time string to a UTC number.
472
+ *
473
+ * @param type - Temporal field type.
474
+ * @param value - Local ISO value to convert.
475
+ */
476
+ function parseFormDateValue(type: "date" | "datetime", value: string) {
477
+ return Date.parse(type === "date" ? `${value}T00:00:00Z` : `${value}Z`)
478
+ }
479
+
480
+ /** Resolves one field declaration to its author-facing value type. */
481
+ type FormFieldValueFor<TField extends FormField> = TField extends {
482
+ type: "file"
483
+ }
484
+ ? TField extends { required: false }
485
+ ? File | null
486
+ : File
487
+ : TField extends { type: "checkbox" }
488
+ ? boolean
489
+ : TField extends {
490
+ options: readonly (infer TOption)[]
491
+ type: "multi-select"
492
+ }
493
+ ? [TOption] extends [{ value: infer TValue extends string }]
494
+ ? TValue[]
495
+ : string[]
496
+ : TField extends { required: false; type: "number" }
497
+ ? number | null
498
+ : TField extends { type: "number" }
499
+ ? number
500
+ : TField extends {
501
+ options: readonly (infer TOption)[]
502
+ type: "radio" | "select"
503
+ }
504
+ ? TOption extends { value: infer TValue extends string }
505
+ ? TField extends { required: false }
506
+ ? TValue | ""
507
+ : TValue
508
+ : string
509
+ : string
510
+
511
+ export type FormFieldValues<TFields extends readonly FormField[]> = {
512
+ readonly [TField in TFields[number] as TField["name"]]: FormFieldValueFor<TField>
513
+ }
package/src/index.ts CHANGED
@@ -92,9 +92,6 @@ export {
92
92
  getMailhookAddress,
93
93
  getMailhookAddressKey,
94
94
  DASHBOARD_RUN_CONFIG_SCHEMA,
95
- DASHBOARD_RUN_FIELDS_SCHEMA,
96
- getDashboardRunFieldDefaultValue,
97
- isDashboardRunFieldValueValid,
98
95
  getTriggerDefinition,
99
96
  getTriggerName,
100
97
  getTriggerPresentation,
@@ -104,11 +101,22 @@ export {
104
101
  triggerDefinitions,
105
102
  type DashboardRunConfig,
106
103
  type DashboardRunEndpointOptions,
107
- type DashboardRunField,
108
- type DashboardRunFieldValue,
109
104
  type HttpTriggerEndpointOptions,
110
105
  type HttpTriggerScope,
111
106
  type MailhookAddressOptions,
112
107
  type MailhookScope,
113
108
  type TriggerType,
114
109
  } from "./triggers"
110
+
111
+ export {
112
+ FORM_FIELDS_SCHEMA,
113
+ getFormFieldDefaultValue,
114
+ getFormFieldValueSchema,
115
+ isFormFieldValueValid,
116
+ normalizeFormFieldValue,
117
+ readFormFieldValue,
118
+ parseFormFields,
119
+ type FormField,
120
+ type FormFieldValue,
121
+ type FormFieldValues,
122
+ } from "./form-fields"