import { JOB_NIMBUS_FILE_SCHEMA, JOB_NIMBUS_ID_SCHEMA, jobNimbusDataSchema, jobNimbusPageSchema, } from "@automate.ax/integration-contracts/jobnimbus" import * as z from "zod" import { defineAction } from "../../../automation/actions" import { downloadJobNimbusFile, JOB_NIMBUS_ACCOUNT, getJobNimbusApi, JOB_NIMBUS_LIST_FIELDS, jobNimbusListQuery, } from "./lib" const FILE_LIST_RESPONSE_SCHEMA = z.looseObject({ count: z.number().int().nonnegative(), files: JOB_NIMBUS_FILE_SCHEMA.array(), }) const FILE_UPLOAD_TICKET_SCHEMA = jobNimbusDataSchema( z.object({ id: JOB_NIMBUS_ID_SCHEMA, url: z.url(), }), ) const FILE_UPLOAD_COMPLETION_SCHEMA = jobNimbusDataSchema( z.object({ id: JOB_NIMBUS_ID_SCHEMA, thumbnailUrl: z.string().nullable().optional(), }), ) const FILE_UPLOAD_RESULT_SCHEMA = FILE_UPLOAD_COMPLETION_SCHEMA.shape.data.extend({ filename: z.string() }) /** Lists JobNimbus file attachments and their metadata. */ export const listJobNimbusFiles = defineAction("List JobNimbus files") .describe("Lists JobNimbus file attachments and their metadata.") .account(JOB_NIMBUS_ACCOUNT) .input(z.object(JOB_NIMBUS_LIST_FIELDS)) .output(jobNimbusPageSchema(JOB_NIMBUS_FILE_SCHEMA)) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const page = await getJobNimbusApi(account.secret, "public").request( "files", { query: jobNimbusListQuery(input), responseSchema: FILE_LIST_RESPONSE_SCHEMA, }, ) return { count: page.count, results: page.files } }) /** Downloads one JobNimbus file attachment. */ export const getJobNimbusFile = defineAction("Get JobNimbus file") .describe("Downloads one JobNimbus file attachment by ID.") .account(JOB_NIMBUS_ACCOUNT) .input(z.object({ fileId: JOB_NIMBUS_ID_SCHEMA })) .output(z.instanceof(Blob)) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => downloadJobNimbusFile(account.secret, input.fileId), ) /** Uploads one file through JobNimbus's current single-part upload flow. */ export const uploadJobNimbusFile = defineAction("Upload JobNimbus file") .describe("Uploads a file under 100 MB and relates it to JobNimbus records.") .account(JOB_NIMBUS_ACCOUNT) .input( z.object({ description: z.string().max(1_000).optional(), file: z .instanceof(File) .refine( ({ size }) => size < 100_000_000, "Single-part JobNimbus uploads must be smaller than 100 MB.", ), generateThumbnail: z.boolean().prefault(true), isPrivate: z.boolean().optional(), relatedIds: JOB_NIMBUS_ID_SCHEMA.array().min(1), type: z.number().int().positive(), }), ) .output(FILE_UPLOAD_RESULT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const files = getJobNimbusApi(account.secret, "files") const ticket = await files.request("uploads/url", { body: { description: input.description, filename: input.file.name, isPrivate: input.isPrivate, related: input.relatedIds, type: input.type, }, method: "POST", responseSchema: FILE_UPLOAD_TICKET_SCHEMA, }) const uploaded = await fetch(ticket.data.url, { body: input.file, headers: { "Content-Type": input.file.type || "application/octet-stream", }, method: "PUT", }) if (!uploaded.ok) { throw new Error( `JobNimbus file storage rejected the upload (${uploaded.status}).`, ) } return { ...( await files.request( `uploads/${encodeURIComponent(ticket.data.id)}/complete`, { method: "POST", query: { generateThumbnail: input.generateThumbnail }, responseSchema: FILE_UPLOAD_COMPLETION_SCHEMA, }, ) ).data, filename: input.file.name, } })