import * as z from "zod" import type { JsonObject } from "type-fest" import { defineAction } from "../../../automation/actions" import { getSlackApi } from "../lib/api" const SLACK_LISTS_READ_SCOPE = "lists:read" const SLACK_LISTS_WRITE_SCOPE = "lists:write" const SLACK_OK_RESPONSE_SCHEMA = z.looseObject({ ok: z.literal(true) }) const SLACK_RICH_TEXT_BLOCK_SCHEMA = z.object({ block_id: z.string().optional(), elements: z.record(z.string(), z.json()).array(), type: z.literal("rich_text"), }) const SLACK_LIST_COLUMN_TYPE_SCHEMA = z.enum([ "attachment", "canvas", "channel", "checkbox", "created_by", "created_time", "date", "email", "last_edited_by", "last_edited_time", "link", "message", "number", "phone", "rating", "reference", "select", "text", "user", "vote", ]) const SLACK_LIST_COLUMN_SCHEMA = z.object({ /** Whether this is the List's single primary text column. */ isPrimaryColumn: z.boolean().optional(), /** Stable caller-selected key for the column. */ key: z.string().min(1), /** Human-readable column name. */ name: z.string().min(1), /** Type of values accepted by the column. */ type: SLACK_LIST_COLUMN_TYPE_SCHEMA, /** Type-specific display, selection, and default-value options. */ options: z .object({ choices: z .object({ color: z.enum([ "blue", "brown", "cyan", "gray", "green", "indigo", "orange", "pink", "purple", "red", "yellow", ]), label: z.string(), value: z.string(), }) .array() .max(100) .optional(), dateFormat: z .enum([ "DD MMMM YYYY", "DD/MM/YYYY", "MM/DD/YYYY", "MMMM DD, YYYY", "YYYY/MM/DD", "default", ]) .optional(), defaultValue: z .object({ channel: z.string().array().optional(), select: z.string().array().max(50).optional(), user: z.string().array().optional(), }) .optional(), emoji: z.string().optional(), emojiTeamId: z.string().optional(), format: z .enum([ "multi_entity", "multi_select", "single_entity", "single_select", ]) .optional(), max: z.number().int().positive().optional(), notifyUsers: z.boolean().optional(), precision: z.number().int().nonnegative().optional(), showMemberName: z.boolean().optional(), }) .optional(), }) const SLACK_LIST_SCHEMA = SLACK_LIST_COLUMN_SCHEMA.array().check((context) => { const primaryColumns = context.value.filter( (column) => column.isPrimaryColumn, ) if (primaryColumns.length > 1) { context.issues.push({ code: "custom", input: context.value, message: "A Slack List can have only one primary column.", }) } if (primaryColumns.some((column) => column.type !== "text")) { context.issues.push({ code: "custom", input: context.value, message: "A Slack List primary column must be a text column.", }) } }) const SLACK_LIST_ITEM_FIELD_SCHEMA = z.discriminatedUnion("type", [ itemField("attachment", z.string().min(1).array()), itemField("channel", z.string().min(1).array()), itemField("checkbox", z.boolean()), itemField("date", z.iso.date().array()), itemField("email", z.email().array()), itemField( "link", z .object({ displayAsUrl: z.boolean(), displayName: z.string(), originalUrl: z.url(), }) .array(), ), itemField("message", z.url().array()), itemField("number", z.number().array()), itemField("phone", z.string().array()), itemField("rating", z.number().int().nonnegative().array()), itemField( "reference", z.object({ file: z.object({ fileId: z.string().min(1) }) }).array(), ), itemField("richText", SLACK_RICH_TEXT_BLOCK_SCHEMA.array()), itemField("select", z.string().min(1).array()), itemField("timestamp", z.number().int().nonnegative().array()), itemField("user", z.string().min(1).array()), ]) const SLACK_LIST_ITEM_CELL_SCHEMA = z.intersection( SLACK_LIST_ITEM_FIELD_SCHEMA, z.object({ rowId: z.string().min(1) }), ) const SLACK_LIST_ACCESS_SUBJECT_SCHEMA = z.discriminatedUnion("type", [ z.object({ /** Slack channel IDs receiving or losing access. */ ids: z.string().min(1).array().min(1), type: z.literal("channels"), }), z.object({ /** Slack user IDs receiving or losing access. */ ids: z.string().min(1).array().min(1), type: z.literal("users"), }), ]) const SLACK_LIST_EXPORT_OPTIONS_SCHEMA = z .object({ /** Export format. JSON preserves hierarchy, threads, and attachments. */ format: z.enum(["csv", "json"]).optional(), /** Include attachment metadata and paths in a JSON export. */ includeAttachments: z.boolean().optional(), /** Include item conversation threads in a JSON export. */ includeThreads: z.boolean().optional(), }) .check((context) => { if ( context.value.format !== "json" && (context.value.includeAttachments !== undefined || context.value.includeThreads !== undefined) ) { context.issues.push({ code: "custom", input: context.value, message: "Slack List threads and attachments can only be included in JSON exports.", }) } }) const PROVIDER_LIST_COLUMN_SCHEMA = z.looseObject({ id: z.string().optional(), is_primary_column: z.boolean().optional(), key: z.string().optional(), name: z.string().optional(), options: z.record(z.string(), z.json()).optional(), type: z.string().optional(), }) const PROVIDER_LIST_METADATA_SCHEMA = z.looseObject({ integrations: z.string().array().optional(), schema: PROVIDER_LIST_COLUMN_SCHEMA.array().optional(), subtask_schema: PROVIDER_LIST_COLUMN_SCHEMA.array().optional(), views: z .looseObject({ id: z.string().optional(), is_locked: z.boolean().optional(), name: z.string().optional(), position: z.string().optional(), type: z.string().optional(), }) .array() .optional(), }) const PROVIDER_LIST_SCHEMA = z.looseObject({ access: z.string().optional(), created: z.number().int().optional(), editable: z.boolean().optional(), file_access: z.string().optional(), id: z.string(), is_public: z.boolean().optional(), list_limits: z .looseObject({ archived_row_count: z.number().int().nonnegative().optional(), column_count: z.number().int().nonnegative().optional(), column_count_limit: z.number().int().nonnegative().optional(), max_attachments_per_cell: z.number().int().nonnegative().optional(), over_column_maximum: z.boolean().optional(), over_row_maximum: z.boolean().optional(), over_view_maximum: z.boolean().optional(), row_count: z.number().int().nonnegative().optional(), row_count_limit: z.number().int().nonnegative().optional(), view_count: z.number().int().nonnegative().optional(), view_count_limit: z.number().int().nonnegative().optional(), }) .optional(), list_metadata: PROVIDER_LIST_METADATA_SCHEMA.optional(), name: z.string().optional(), permalink: z.url().optional(), title: z.string().optional(), updated: z.number().int().optional(), user: z.string().optional(), }) const CREATE_LIST_RESPONSE_SCHEMA = z.looseObject({ list_id: z.string(), list_metadata: PROVIDER_LIST_METADATA_SCHEMA.optional(), ok: z.literal(true), }) const PROVIDER_LIST_ITEM_FIELD_SCHEMA = z.looseObject({ attachment: z.string().array().optional(), channel: z.string().array().optional(), checkbox: z.union([z.boolean(), z.boolean().array()]).optional(), column_id: z.string().optional(), date: z.string().array().optional(), email: z.string().array().optional(), key: z.string(), link: z .looseObject({ attachment: z .union([z.record(z.string(), z.json()), z.null()]) .optional(), displayAsUrl: z.boolean().optional(), displayName: z.string().optional(), originalUrl: z.url(), }) .array() .optional(), message: z .looseObject({ channel_id: z.string(), thread_ts: z.string().optional(), ts: z.string(), value: z.url(), }) .array() .optional(), number: z.number().array().optional(), phone: z.string().array().optional(), rating: z.number().int().array().optional(), reference: z .looseObject({ canvas_section: z .looseObject({ file_id: z.string(), section_id: z.string() }) .optional(), file: z.looseObject({ file_id: z.string() }).optional(), list_record: z .looseObject({ list_id: z.string(), row_id: z.string() }) .optional(), message: z .looseObject({ channel_id: z.string(), thread_ts: z.string().optional(), ts: z.string(), }) .optional(), }) .array() .optional(), rich_text: SLACK_RICH_TEXT_BLOCK_SCHEMA.array().optional(), select: z.string().array().optional(), text: z.string().optional(), timestamp: z.number().int().array().optional(), user: z.string().array().optional(), value: z.union([z.boolean(), z.string(), z.number(), z.null()]), }) const PROVIDER_SAVED_ITEM_SCHEMA = z.looseObject({ date_completed: z.number().int().optional(), date_due: z.number().int().optional(), is_archived: z.boolean(), state: z.enum(["archived", "completed", "in_progress"]), }) const PROVIDER_LIST_ITEM_SCHEMA = z.looseObject({ archived: z.boolean().optional(), created_by: z.string().optional(), date_created: z.number().int(), fields: PROVIDER_LIST_ITEM_FIELD_SCHEMA.array(), id: z.string(), is_subscribed: z.boolean().optional(), list_id: z.string(), parent_record_id: z.string().optional(), saved: PROVIDER_SAVED_ITEM_SCHEMA.optional(), saved_fields: z.record(z.string(), PROVIDER_SAVED_ITEM_SCHEMA).optional(), updated_by: z.string().optional(), updated_timestamp: z.string().optional(), }) const LIST_ITEM_RESPONSE_SCHEMA = z.looseObject({ item: PROVIDER_LIST_ITEM_SCHEMA, ok: z.literal(true), }) const LIST_ITEMS_RESPONSE_SCHEMA = z.looseObject({ items: PROVIDER_LIST_ITEM_SCHEMA.array(), ok: z.literal(true), response_metadata: z .looseObject({ next_cursor: z.string().optional() }) .optional(), }) const LIST_ITEM_INFO_RESPONSE_SCHEMA = z.looseObject({ list: PROVIDER_LIST_SCHEMA, ok: z.literal(true), record: PROVIDER_LIST_ITEM_SCHEMA, subtasks: PROVIDER_LIST_ITEM_SCHEMA.array().optional(), }) const START_LIST_DOWNLOAD_RESPONSE_SCHEMA = z.looseObject({ job_id: z.string(), ok: z.literal(true), }) const GET_LIST_DOWNLOAD_RESPONSE_SCHEMA = z.discriminatedUnion("status", [ z.looseObject({ ok: z.literal(true), status: z.literal("IN_PROGRESS"), }), z.looseObject({ download_url: z.url(), ok: z.literal(true), status: z.literal("COMPLETED"), }), ]) const SLACK_LIST_ITEM_FIELD_OUTPUT_SCHEMA = z.object({ attachment: z.string().array().optional(), channel: z.string().array().optional(), checkbox: z.union([z.boolean(), z.boolean().array()]).optional(), columnId: z.string().optional(), date: z.string().array().optional(), email: z.string().array().optional(), key: z.string(), link: z .object({ attachment: z .union([z.record(z.string(), z.json()), z.null()]) .optional(), displayAsUrl: z.boolean().optional(), displayName: z.string().optional(), originalUrl: z.url(), }) .array() .optional(), message: z .object({ channelId: z.string(), threadTimestamp: z.string().optional(), timestamp: z.string(), url: z.url(), }) .array() .optional(), number: z.number().array().optional(), phone: z.string().array().optional(), rating: z.number().int().array().optional(), references: z .object({ canvasSection: z .object({ canvasId: z.string(), sectionId: z.string() }) .optional(), fileId: z.string().optional(), listItem: z.object({ itemId: z.string(), listId: z.string() }).optional(), message: z .object({ channelId: z.string(), threadTimestamp: z.string().optional(), timestamp: z.string(), }) .optional(), }) .array() .optional(), richText: SLACK_RICH_TEXT_BLOCK_SCHEMA.array().optional(), select: z.string().array().optional(), text: z.string().optional(), timestamp: z.number().int().array().optional(), user: z.string().array().optional(), value: z.union([z.boolean(), z.string(), z.number(), z.null()]), }) const SLACK_LIST_ITEM_OUTPUT_SCHEMA = z.object({ archived: z.boolean().optional(), createdAt: z.number().int(), createdBy: z.string().optional(), fields: SLACK_LIST_ITEM_FIELD_OUTPUT_SCHEMA.array(), id: z.string(), isSubscribed: z.boolean().optional(), listId: z.string(), parentItemId: z.string().optional(), saved: z .object({ completedAt: z.number().int().optional(), dueAt: z.number().int().optional(), isArchived: z.boolean(), state: z.enum(["archived", "completed", "in_progress"]), }) .optional(), savedFields: z .record( z.string(), z.object({ completedAt: z.number().int().optional(), dueAt: z.number().int().optional(), isArchived: z.boolean(), state: z.enum(["archived", "completed", "in_progress"]), }), ) .optional(), updatedAt: z.string().optional(), updatedBy: z.string().optional(), }) const SLACK_LIST_METADATA_OUTPUT_SCHEMA = z.object({ integrations: z.string().array().optional(), schema: z .object({ id: z.string().optional(), isPrimaryColumn: z.boolean().optional(), key: z.string().optional(), name: z.string().optional(), options: z.record(z.string(), z.json()).optional(), type: z.string().optional(), }) .array() .optional(), subtaskSchema: z .object({ id: z.string().optional(), isPrimaryColumn: z.boolean().optional(), key: z.string().optional(), name: z.string().optional(), options: z.record(z.string(), z.json()).optional(), type: z.string().optional(), }) .array() .optional(), views: z .object({ id: z.string().optional(), isLocked: z.boolean().optional(), name: z.string().optional(), position: z.string().optional(), type: z.string().optional(), }) .array() .optional(), }) const SLACK_LIST_OUTPUT_SCHEMA = z.object({ access: z.string().optional(), createdAt: z.number().int().optional(), editable: z.boolean().optional(), fileAccess: z.string().optional(), id: z.string(), isPublic: z.boolean().optional(), metadata: SLACK_LIST_METADATA_OUTPUT_SCHEMA.optional(), name: z.string().optional(), ownerId: z.string().optional(), permalink: z.url().optional(), title: z.string().optional(), updatedAt: z.number().int().optional(), }) /** Creates a standalone Slack List on a paid workspace. */ export const createSlackList = defineAction("Create Slack List") .describe("Creates a standalone Slack List on a paid workspace.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z .object({ /** Existing List whose structure, and optionally records, are copied. */ copyFromListId: z.string().min(1).optional(), /** Rich-text List description. */ descriptionBlocks: SLACK_RICH_TEXT_BLOCK_SCHEMA.array().optional(), /** Whether records are copied from `copyFromListId`. */ includeCopiedListRecords: z.boolean().optional(), /** Human-readable List name. */ name: z.string().min(1), /** Column definitions. Slack creates one text column when omitted. */ schema: SLACK_LIST_SCHEMA.optional(), /** Adds Slack's completed, assignee, and due-date task fields. */ todoMode: z.boolean().optional(), }) .check((context) => { if ( context.value.includeCopiedListRecords !== undefined && !context.value.copyFromListId ) { context.issues.push({ code: "custom", input: context.value, message: "includeCopiedListRecords requires a Slack List to copy.", }) } }), ) .output( z.object({ /** Stable ID of the created Slack List. */ listId: z.string(), /** Generated columns, subtask columns, and views, when Slack returns them. */ metadata: SLACK_LIST_METADATA_OUTPUT_SCHEMA.optional(), }), ) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call("slackLists.create", { body: { ...(input.copyFromListId && { copy_from_list_id: input.copyFromListId, }), ...(input.descriptionBlocks && { description_blocks: input.descriptionBlocks, }), ...(input.includeCopiedListRecords !== undefined && { include_copied_list_records: input.includeCopiedListRecords, }), name: input.name, ...(input.schema && { schema: input.schema.map(toProviderColumn) }), ...(input.todoMode !== undefined && { todo_mode: input.todoMode }), }, responseSchema: CREATE_LIST_RESPONSE_SCHEMA, }) return { listId: result.list_id, metadata: result.list_metadata && toSlackListMetadata(result.list_metadata), } }) /** Updates a Slack List's name, rich-text description, or task mode. */ export const updateSlackList = defineAction("Update Slack List") .describe("Updates a Slack List's name, description, or task mode.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z .object({ descriptionBlocks: SLACK_RICH_TEXT_BLOCK_SCHEMA.array().optional(), listId: z.string().min(1), name: z.string().min(1).optional(), todoMode: z.boolean().optional(), }) .check((context) => { if ( context.value.descriptionBlocks === undefined && context.value.name === undefined && context.value.todoMode === undefined ) { context.issues.push({ code: "custom", input: context.value, message: "Provide at least one Slack List property to update.", }) } }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.update", { body: { ...(input.descriptionBlocks && { description_blocks: input.descriptionBlocks, }), id: input.listId, ...(input.name && { name: input.name }), ...(input.todoMode !== undefined && { todo_mode: input.todoMode }), }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Creates an item, duplicate, or subtask in a Slack List. */ export const createSlackListItem = defineAction("Create Slack List item") .describe("Creates an item, duplicate, or subtask in a Slack List.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z.object({ duplicatedItemId: z.string().min(1).optional(), fields: SLACK_LIST_ITEM_FIELD_SCHEMA.array().optional(), listId: z.string().min(1), parentItemId: z.string().min(1).optional(), }), ) .output(SLACK_LIST_ITEM_OUTPUT_SCHEMA) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { return toSlackListItem( ( await getSlackApi(account.secret).call("slackLists.items.create", { body: { ...(input.duplicatedItemId && { duplicated_item_id: input.duplicatedItemId, }), ...(input.fields && { initial_fields: input.fields.map(toProviderItemField), }), list_id: input.listId, ...(input.parentItemId && { parent_item_id: input.parentItemId }), }, responseSchema: LIST_ITEM_RESPONSE_SCHEMA, }) ).item, ) }) /** Updates one or more cells across items in a Slack List. */ export const updateSlackListItems = defineAction("Update Slack List items") .describe("Updates one or more cells across items in a Slack List.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z.object({ cells: SLACK_LIST_ITEM_CELL_SCHEMA.array().min(1), listId: z.string().min(1), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.items.update", { body: { cells: input.cells.map((cell) => ({ ...toProviderItemField(cell), row_id: cell.rowId, })), list_id: input.listId, }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Deletes one Slack List item. */ export const deleteSlackListItem = defineAction("Delete Slack List item") .describe("Deletes one item from a Slack List.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input(z.object({ itemId: z.string().min(1), listId: z.string().min(1) })) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.items.delete", { body: { id: input.itemId, list_id: input.listId }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Deletes several Slack List items in one request. */ export const deleteSlackListItems = defineAction("Delete Slack List items") .describe("Deletes several items from a Slack List in one request.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z.object({ itemIds: z.string().min(1).array().min(1), listId: z.string().min(1), }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.items.deleteMultiple", { body: { ids: input.itemIds, list_id: input.listId }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Lists a cursor-paginated page of normal or archived Slack List items. */ export const listSlackListItems = defineAction("List Slack List items") .describe("Lists a cursor-paginated page of Slack List items.") .account("slack", SLACK_LISTS_READ_SCOPE) .input( z.object({ archived: z.boolean().optional(), cursor: z.string().min(1).optional(), limit: z.number().int().positive().optional(), listId: z.string().min(1), }), ) .output( z.object({ items: SLACK_LIST_ITEM_OUTPUT_SCHEMA.array(), nextCursor: z.string().optional(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "slackLists.items.list", { body: { ...(input.archived !== undefined && { archived: input.archived }), ...(input.cursor && { cursor: input.cursor }), ...(input.limit !== undefined && { limit: input.limit }), list_id: input.listId, }, responseSchema: LIST_ITEMS_RESPONSE_SCHEMA, }, ) return { items: result.items.map(toSlackListItem), nextCursor: result.response_metadata?.next_cursor || undefined, } }) /** Gets one Slack List item and its subtasks. */ export const getSlackListItem = defineAction("Get Slack List item") .describe("Gets one Slack List item and its subtasks.") .account("slack", SLACK_LISTS_READ_SCOPE) .input( z.object({ includeSubscription: z.boolean().optional(), itemId: z.string().min(1), listId: z.string().min(1), }), ) .output( z.object({ item: SLACK_LIST_ITEM_OUTPUT_SCHEMA, list: SLACK_LIST_OUTPUT_SCHEMA, subtasks: SLACK_LIST_ITEM_OUTPUT_SCHEMA.array(), }), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "slackLists.items.info", { body: { id: input.itemId, ...(input.includeSubscription !== undefined && { include_is_subscribed: input.includeSubscription, }), list_id: input.listId, }, responseSchema: LIST_ITEM_INFO_RESPONSE_SCHEMA, }, ) return { item: toSlackListItem(result.record), list: toSlackList(result.list), subtasks: (result.subtasks ?? []).map(toSlackListItem), } }) /** Grants channel or user access to a Slack List. */ export const setSlackListAccess = defineAction("Set Slack List access") .describe("Sets channel or user access to a Slack List.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z .object({ accessLevel: z.enum(["owner", "read", "write"]), listId: z.string().min(1), subject: SLACK_LIST_ACCESS_SUBJECT_SCHEMA, }) .check((context) => { if ( context.value.accessLevel === "owner" && context.value.subject.type === "channels" ) { context.issues.push({ code: "custom", input: context.value, message: "Slack List ownership can only be granted to users.", }) } }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.access.set", { body: { access_level: input.accessLevel, list_id: input.listId, [input.subject.type === "channels" ? "channel_ids" : "user_ids"]: input.subject.ids, }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Revokes channel or user access to a Slack List. */ export const deleteSlackListAccess = defineAction("Delete Slack List access") .describe("Revokes channel or user access to a Slack List.") .account("slack", SLACK_LISTS_WRITE_SCOPE) .input( z.object({ listId: z.string().min(1), subject: SLACK_LIST_ACCESS_SUBJECT_SCHEMA, }), ) .output(z.void()) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { await getSlackApi(account.secret).call("slackLists.access.delete", { body: { list_id: input.listId, [input.subject.type === "channels" ? "channel_ids" : "user_ids"]: input.subject.ids, }, responseSchema: SLACK_OK_RESPONSE_SCHEMA, }) }) /** Starts an asynchronous CSV or JSON export of a Slack List. */ export const startSlackListDownload = defineAction("Start Slack List download") .describe("Starts an asynchronous CSV or JSON export of a Slack List.") .account("slack", SLACK_LISTS_READ_SCOPE) .input( SLACK_LIST_EXPORT_OPTIONS_SCHEMA.safeExtend({ includeArchived: z.boolean().optional(), listId: z.string().min(1), }), ) .output(z.object({ jobId: z.string() })) .retry({ replaySafety: "unsafe" }) .handler(async ({ account, input }) => { return { jobId: ( await getSlackApi(account.secret).call("slackLists.download.start", { body: toProviderExportOptions(input), responseSchema: START_LIST_DOWNLOAD_RESPONSE_SCHEMA, }) ).job_id, } }) /** Gets the current status and completed URL for a Slack List export job. */ export const getSlackListDownload = defineAction("Get Slack List download") .describe("Gets the current status and URL for a Slack List export job.") .account("slack", SLACK_LISTS_READ_SCOPE) .input( SLACK_LIST_EXPORT_OPTIONS_SCHEMA.safeExtend({ jobId: z.string().min(1), listId: z.string().min(1), }), ) .output( z.discriminatedUnion("status", [ z.object({ status: z.literal("IN_PROGRESS") }), z.object({ downloadUrl: z.url(), status: z.literal("COMPLETED") }), ]), ) .retry({ replaySafety: "safe" }) .handler(async ({ account, input }) => { const result = await getSlackApi(account.secret).call( "slackLists.download.get", { body: toProviderExportOptions(input), responseSchema: GET_LIST_DOWNLOAD_RESPONSE_SCHEMA, }, ) return result.status === "IN_PROGRESS" ? { status: result.status } : { downloadUrl: result.download_url, status: result.status } }) /** * Builds one discriminated Slack List item-field input variant. * * @param type - Public field type discriminator. * @param schema - Schema for the field's typed value. */ function itemField( type: TKey, schema: TSchema, ) { return z.object({ columnId: z.string().min(1), type: z.literal(type), value: schema, }) } /** * Converts a public Slack List column to the provider request shape. * * @param column - Validated public column definition. */ function toProviderColumn(column: z.output) { return { ...(column.isPrimaryColumn !== undefined && { is_primary_column: column.isPrimaryColumn, }), key: column.key, name: column.name, ...(column.options && { options: { ...(column.options.choices && { choices: column.options.choices }), ...(column.options.dateFormat && { date_format: column.options.dateFormat, }), ...(column.options.defaultValue && { default_value_typed: column.options.defaultValue, }), ...(column.options.emoji && { emoji: column.options.emoji }), ...(column.options.emojiTeamId && { emoji_team_id: column.options.emojiTeamId, }), ...(column.options.format && { format: column.options.format }), ...(column.options.max !== undefined && { max: column.options.max }), ...(column.options.notifyUsers !== undefined && { notify_users: column.options.notifyUsers, }), ...(column.options.precision !== undefined && { precision: column.options.precision, }), ...(column.options.showMemberName !== undefined && { show_member_name: column.options.showMemberName, }), }, }), type: column.type, } } /** * Normalizes provider List metadata and generated column IDs. * * @param metadata - Validated provider List metadata. */ function toSlackListMetadata( metadata: z.output, ) { return { integrations: metadata.integrations, schema: metadata.schema?.map(toSlackListColumn), subtaskSchema: metadata.subtask_schema?.map(toSlackListColumn), views: metadata.views?.map((view) => ({ id: view.id, isLocked: view.is_locked, name: view.name, position: view.position, type: view.type, })), } } /** * Normalizes one provider List column and its generated ID. * * @param column - Validated provider List column. */ function toSlackListColumn( column: z.output, ) { return { id: column.id, isPrimaryColumn: column.is_primary_column, key: column.key, name: column.name, options: column.options, type: column.type, } } /** * Normalizes the List summary returned alongside item details. * * @param list - Validated provider List summary. */ function toSlackList(list: z.output) { return { access: list.access, createdAt: list.created, editable: list.editable, fileAccess: list.file_access, id: list.id, isPublic: list.is_public, metadata: list.list_metadata && toSlackListMetadata(list.list_metadata), name: list.name, ownerId: list.user, permalink: list.permalink, title: list.title, updatedAt: list.updated, } } /** * Converts one public typed List field to Slack's provider request shape. * * @param field - Validated public item field. */ function toProviderItemField( field: z.output, ): JsonObject { if (field.type === "attachment") return { attachment: field.value, column_id: field.columnId } if (field.type === "channel") return { channel: field.value, column_id: field.columnId } if (field.type === "checkbox") return { checkbox: field.value, column_id: field.columnId } if (field.type === "date") return { column_id: field.columnId, date: field.value } if (field.type === "email") return { column_id: field.columnId, email: field.value } if (field.type === "link") return { column_id: field.columnId, link: field.value.map((link) => ({ display_as_url: link.displayAsUrl, display_name: link.displayName, original_url: link.originalUrl, })), } if (field.type === "message") return { column_id: field.columnId, message: field.value } if (field.type === "number") return { column_id: field.columnId, number: field.value } if (field.type === "phone") return { column_id: field.columnId, phone: field.value } if (field.type === "rating") return { column_id: field.columnId, rating: field.value } if (field.type === "reference") return { column_id: field.columnId, reference: field.value.map((reference) => ({ file: { file_id: reference.file.fileId }, })), } if (field.type === "richText") return { column_id: field.columnId, rich_text: field.value } if (field.type === "select") return { column_id: field.columnId, select: field.value } if (field.type === "timestamp") return { column_id: field.columnId, timestamp: field.value } return { column_id: field.columnId, user: field.value } } /** * Normalizes one provider List record into the public action shape. * * @param item - Validated provider List record. */ function toSlackListItem(item: z.output) { return { archived: item.archived, createdAt: item.date_created, createdBy: item.created_by, fields: item.fields.map((field) => ({ attachment: field.attachment, channel: field.channel, checkbox: field.checkbox, columnId: field.column_id, date: field.date, email: field.email, key: field.key, link: field.link, message: field.message?.map((message) => ({ channelId: message.channel_id, threadTimestamp: message.thread_ts, timestamp: message.ts, url: message.value, })), number: field.number, phone: field.phone, rating: field.rating, references: field.reference?.map((reference) => ({ canvasSection: reference.canvas_section && { canvasId: reference.canvas_section.file_id, sectionId: reference.canvas_section.section_id, }, fileId: reference.file?.file_id, listItem: reference.list_record && { itemId: reference.list_record.row_id, listId: reference.list_record.list_id, }, message: reference.message && { channelId: reference.message.channel_id, threadTimestamp: reference.message.thread_ts, timestamp: reference.message.ts, }, })), richText: field.rich_text, select: field.select, text: field.text, timestamp: field.timestamp, user: field.user, value: field.value, })), id: item.id, isSubscribed: item.is_subscribed, listId: item.list_id, parentItemId: item.parent_record_id, saved: item.saved && toSlackSavedItem(item.saved), savedFields: item.saved_fields && Object.fromEntries( Object.entries(item.saved_fields).map(([columnId, saved]) => [ columnId, toSlackSavedItem(saved), ]), ), updatedAt: item.updated_timestamp, updatedBy: item.updated_by, } } /** * Normalizes Slack's saved reminder metadata for an item or cell. * * @param saved - Validated provider reminder metadata. */ function toSlackSavedItem(saved: z.output) { return { completedAt: saved.date_completed, dueAt: saved.date_due, isArchived: saved.is_archived, state: saved.state, } } /** * Converts shared public export options to Slack's provider request shape. * * @param input - Validated start or get-download input. * @param input.format - CSV or JSON export format. * @param input.includeArchived - Whether a start request includes archived * rows. * @param input.includeAttachments - Whether a JSON export includes attachments. * @param input.includeThreads - Whether a JSON export includes item threads. * @param input.jobId - Existing export job ID for a get request. * @param input.listId - Slack List being exported. */ function toProviderExportOptions(input: { format?: "csv" | "json" includeArchived?: boolean includeAttachments?: boolean includeThreads?: boolean jobId?: string listId: string }) { return { ...(input.format && { format: input.format }), ...(input.includeArchived !== undefined && { include_archived: input.includeArchived, }), ...(input.includeAttachments !== undefined && { include_attachments: input.includeAttachments, }), ...(input.includeThreads !== undefined && { include_threads: input.includeThreads, }), ...(input.jobId && { job_id: input.jobId }), list_id: input.listId, } }