From a5838a8a1b5936e799627e483bb484f95d713f92 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 3 Mar 2026 13:48:47 -0300 Subject: [PATCH 01/23] feat: workflows --- .../parse-workflow-to-api-options.spec.ts | 219 ++++++++ .../utils/parse-workflow-to-api-options.ts | 100 ++++ src/index.ts | 1 + src/resend.ts | 2 + .../create-workflow-event.interface.ts | 24 + .../create-workflow-options.interface.ts | 22 + .../interfaces/get-workflow.interface.ts | 13 + src/workflows/interfaces/index.ts | 7 + .../interfaces/list-workflows.interface.ts | 13 + .../interfaces/remove-workflow.interface.ts | 9 + .../interfaces/workflow-step.interface.ts | 90 ++++ src/workflows/interfaces/workflow.ts | 8 + src/workflows/workflows.spec.ts | 494 ++++++++++++++++++ src/workflows/workflows.ts | 77 +++ 14 files changed, 1079 insertions(+) create mode 100644 src/common/utils/parse-workflow-to-api-options.spec.ts create mode 100644 src/common/utils/parse-workflow-to-api-options.ts create mode 100644 src/workflows/interfaces/create-workflow-event.interface.ts create mode 100644 src/workflows/interfaces/create-workflow-options.interface.ts create mode 100644 src/workflows/interfaces/get-workflow.interface.ts create mode 100644 src/workflows/interfaces/index.ts create mode 100644 src/workflows/interfaces/list-workflows.interface.ts create mode 100644 src/workflows/interfaces/remove-workflow.interface.ts create mode 100644 src/workflows/interfaces/workflow-step.interface.ts create mode 100644 src/workflows/interfaces/workflow.ts create mode 100644 src/workflows/workflows.spec.ts create mode 100644 src/workflows/workflows.ts diff --git a/src/common/utils/parse-workflow-to-api-options.spec.ts b/src/common/utils/parse-workflow-to-api-options.spec.ts new file mode 100644 index 00000000..ffdeeae1 --- /dev/null +++ b/src/common/utils/parse-workflow-to-api-options.spec.ts @@ -0,0 +1,219 @@ +import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; +import type { CreateWorkflowEventOptions } from '../../workflows/interfaces/create-workflow-event.interface'; +import { + parseWorkflowToApiOptions, + parseWorkflowEventToApiOptions, +} from './parse-workflow-to-api-options'; + +describe('parseWorkflowToApiOptions', () => { + it('converts full payload with all step types from camelCase to snake_case', () => { + const workflow: CreateWorkflowOptions = { + name: 'Welcome Series', + status: 'enabled', + steps: [ + { + ref: 'trigger_1', + type: 'trigger', + config: { eventName: 'user.signed_up' }, + }, + { + ref: 'delay_1', + type: 'delay', + config: { seconds: 3600 }, + }, + { + ref: 'send_1', + type: 'send_email', + config: { + templateId: 'tmpl_123', + subject: 'Welcome!', + from: 'hello@example.com', + replyTo: 'support@example.com', + variables: { userName: { var: 'contact.name' } }, + }, + }, + { + ref: 'wait_1', + type: 'wait_for_event', + config: { + eventName: 'user.confirmed', + timeoutSeconds: 86400, + filterRule: { + type: 'rule', + field: 'status', + operator: 'eq', + value: 'confirmed', + }, + }, + }, + { + ref: 'cond_1', + type: 'condition', + config: { + type: 'rule', + field: 'plan', + operator: 'eq', + value: 'pro', + }, + }, + ], + edges: [ + { from: 'trigger_1', to: 'delay_1' }, + { from: 'delay_1', to: 'send_1', edgeType: 'default' }, + { from: 'send_1', to: 'wait_1' }, + { from: 'wait_1', to: 'cond_1', edgeType: 'event_received' }, + ], + }; + + const apiOptions = parseWorkflowToApiOptions(workflow); + + expect(apiOptions).toEqual({ + name: 'Welcome Series', + status: 'enabled', + steps: [ + { + ref: 'trigger_1', + type: 'trigger', + config: { event_name: 'user.signed_up' }, + }, + { + ref: 'delay_1', + type: 'delay', + config: { seconds: 3600 }, + }, + { + ref: 'send_1', + type: 'send_email', + config: { + template_id: 'tmpl_123', + subject: 'Welcome!', + from: 'hello@example.com', + reply_to: 'support@example.com', + variables: { userName: { var: 'contact.name' } }, + }, + }, + { + ref: 'wait_1', + type: 'wait_for_event', + config: { + event_name: 'user.confirmed', + timeout_seconds: 86400, + filter_rule: { + type: 'rule', + field: 'status', + operator: 'eq', + value: 'confirmed', + }, + }, + }, + { + ref: 'cond_1', + type: 'condition', + config: { + type: 'rule', + field: 'plan', + operator: 'eq', + value: 'pro', + }, + }, + ], + edges: [ + { from: 'trigger_1', to: 'delay_1', edge_type: undefined }, + { from: 'delay_1', to: 'send_1', edge_type: 'default' }, + { from: 'send_1', to: 'wait_1', edge_type: undefined }, + { from: 'wait_1', to: 'cond_1', edge_type: 'event_received' }, + ], + }); + }); + + it('converts edge edgeType to edge_type', () => { + const workflow: CreateWorkflowOptions = { + name: 'Edge Test', + steps: [ + { + ref: 'trigger_1', + type: 'trigger', + config: { eventName: 'test.event' }, + }, + ], + edges: [ + { from: 'trigger_1', to: 'step_2', edgeType: 'condition_met' }, + { from: 'trigger_1', to: 'step_3', edgeType: 'condition_not_met' }, + { from: 'step_2', to: 'step_4', edgeType: 'timeout' }, + ], + }; + + const apiOptions = parseWorkflowToApiOptions(workflow); + + expect(apiOptions.edges).toEqual([ + { from: 'trigger_1', to: 'step_2', edge_type: 'condition_met' }, + { from: 'trigger_1', to: 'step_3', edge_type: 'condition_not_met' }, + { from: 'step_2', to: 'step_4', edge_type: 'timeout' }, + ]); + }); + + it('handles minimal payload with only required fields', () => { + const workflow: CreateWorkflowOptions = { + name: 'Minimal Workflow', + steps: [ + { + ref: 'trigger_1', + type: 'trigger', + config: { eventName: 'test.event' }, + }, + ], + edges: [{ from: 'trigger_1', to: 'step_2' }], + }; + + const apiOptions = parseWorkflowToApiOptions(workflow); + + expect(apiOptions).toEqual({ + name: 'Minimal Workflow', + status: undefined, + steps: [ + { + ref: 'trigger_1', + type: 'trigger', + config: { event_name: 'test.event' }, + }, + ], + edges: [{ from: 'trigger_1', to: 'step_2', edge_type: undefined }], + }); + }); +}); + +describe('parseWorkflowEventToApiOptions', () => { + it('converts contactId to contact_id', () => { + const event: CreateWorkflowEventOptions = { + event: 'user.signed_up', + contactId: 'contact_abc123', + payload: { plan: 'pro' }, + }; + + const apiOptions = parseWorkflowEventToApiOptions(event); + + expect(apiOptions).toEqual({ + event: 'user.signed_up', + contact_id: 'contact_abc123', + email: undefined, + payload: { plan: 'pro' }, + }); + }); + + it('passes email through without conversion', () => { + const event: CreateWorkflowEventOptions = { + event: 'user.signed_up', + email: 'user@example.com', + payload: { source: 'website' }, + }; + + const apiOptions = parseWorkflowEventToApiOptions(event); + + expect(apiOptions).toEqual({ + event: 'user.signed_up', + contact_id: undefined, + email: 'user@example.com', + payload: { source: 'website' }, + }); + }); +}); diff --git a/src/common/utils/parse-workflow-to-api-options.ts b/src/common/utils/parse-workflow-to-api-options.ts new file mode 100644 index 00000000..d5f25766 --- /dev/null +++ b/src/common/utils/parse-workflow-to-api-options.ts @@ -0,0 +1,100 @@ +import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; +import type { CreateWorkflowEventOptions } from '../../workflows/interfaces/create-workflow-event.interface'; +import type { + WorkflowEdge, + WorkflowEdgeType, + WorkflowStep, +} from '../../workflows/interfaces/workflow-step.interface'; + +interface WorkflowStepApiOptions { + ref: string; + type: string; + config: unknown; +} + +interface WorkflowEdgeApiOptions { + from: string; + to: string; + edge_type?: WorkflowEdgeType; +} + +interface WorkflowApiOptions { + name: string; + status?: 'enabled' | 'disabled'; + steps?: WorkflowStepApiOptions[]; + edges?: WorkflowEdgeApiOptions[]; +} + +interface WorkflowEventApiOptions { + event: string; + contact_id?: string; + email?: string; + payload?: Record; +} + +function parseStepConfig(step: WorkflowStep): WorkflowStepApiOptions { + switch (step.type) { + case 'trigger': + return { + ref: step.ref, + type: step.type, + config: { event_name: step.config.eventName }, + }; + case 'delay': + return { ref: step.ref, type: step.type, config: step.config }; + case 'send_email': + return { + ref: step.ref, + type: step.type, + config: { + template_id: step.config.templateId, + subject: step.config.subject, + from: step.config.from, + reply_to: step.config.replyTo, + variables: step.config.variables, + }, + }; + case 'wait_for_event': + return { + ref: step.ref, + type: step.type, + config: { + event_name: step.config.eventName, + timeout_seconds: step.config.timeoutSeconds, + filter_rule: step.config.filterRule, + }, + }; + case 'condition': + return { ref: step.ref, type: step.type, config: step.config }; + } +} + +function parseEdge(edge: WorkflowEdge): WorkflowEdgeApiOptions { + return { + from: edge.from, + to: edge.to, + edge_type: edge.edgeType, + }; +} + +export function parseWorkflowToApiOptions( + workflow: CreateWorkflowOptions, +): WorkflowApiOptions { + return { + name: workflow.name, + status: workflow.status, + steps: workflow.steps?.map(parseStepConfig), + edges: workflow.edges?.map(parseEdge), + }; +} + +export function parseWorkflowEventToApiOptions( + event: CreateWorkflowEventOptions, +): WorkflowEventApiOptions { + return { + event: event.event, + contact_id: event.contactId, + email: event.email, + payload: event.payload, + }; +} diff --git a/src/index.ts b/src/index.ts index d3e97719..9cbc7bbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,3 +16,4 @@ export * from './segments/interfaces'; export * from './templates/interfaces'; export * from './topics/interfaces'; export * from './webhooks/interfaces'; +export * from './workflows/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index a19f3754..ba1aaff6 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -14,6 +14,7 @@ import { Segments } from './segments/segments'; import { Templates } from './templates/templates'; import { Topics } from './topics/topics'; import { Webhooks } from './webhooks/webhooks'; +import { Workflows } from './workflows/workflows'; const defaultBaseUrl = 'https://api.resend.com'; const defaultUserAgent = `resend-node:${version}`; @@ -44,6 +45,7 @@ export class Resend { readonly webhooks = new Webhooks(this); readonly templates = new Templates(this); readonly topics = new Topics(this); + readonly workflows = new Workflows(this); constructor(readonly key?: string) { if (!key) { diff --git a/src/workflows/interfaces/create-workflow-event.interface.ts b/src/workflows/interfaces/create-workflow-event.interface.ts new file mode 100644 index 00000000..91ab5a14 --- /dev/null +++ b/src/workflows/interfaces/create-workflow-event.interface.ts @@ -0,0 +1,24 @@ +import type { Response } from '../../interfaces'; + +export type CreateWorkflowEventOptions = + | { + event: string; + contactId: string; + email?: never; + payload?: Record; + } + | { + event: string; + email: string; + contactId?: never; + payload?: Record; + }; + +export interface CreateWorkflowEventResponseSuccess { + object: 'workflow_event'; + event: string; + event_instance_id: string; +} + +export type CreateWorkflowEventResponse = + Response; diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts new file mode 100644 index 00000000..d23e1adf --- /dev/null +++ b/src/workflows/interfaces/create-workflow-options.interface.ts @@ -0,0 +1,22 @@ +import type { Response } from '../../interfaces'; +import type { Workflow } from './workflow'; +import type { + WorkflowEdge, + WorkflowResponseEdge, + WorkflowResponseStep, + WorkflowStep, +} from './workflow-step.interface'; + +export interface CreateWorkflowOptions { + name: string; + status?: 'enabled' | 'disabled'; + steps: WorkflowStep[]; + edges: WorkflowEdge[]; +} + +export interface CreateWorkflowResponseSuccess extends Workflow { + steps: WorkflowResponseStep[]; + edges: WorkflowResponseEdge[]; +} + +export type CreateWorkflowResponse = Response; diff --git a/src/workflows/interfaces/get-workflow.interface.ts b/src/workflows/interfaces/get-workflow.interface.ts new file mode 100644 index 00000000..f8afad85 --- /dev/null +++ b/src/workflows/interfaces/get-workflow.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; +import type { Workflow } from './workflow'; +import type { + WorkflowResponseEdge, + WorkflowResponseStep, +} from './workflow-step.interface'; + +export interface GetWorkflowResponseSuccess extends Workflow { + steps: WorkflowResponseStep[]; + edges: WorkflowResponseEdge[]; +} + +export type GetWorkflowResponse = Response; diff --git a/src/workflows/interfaces/index.ts b/src/workflows/interfaces/index.ts new file mode 100644 index 00000000..dd93f00a --- /dev/null +++ b/src/workflows/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from './workflow'; +export * from './workflow-step.interface'; +export * from './create-workflow-event.interface'; +export * from './create-workflow-options.interface'; +export * from './get-workflow.interface'; +export * from './list-workflows.interface'; +export * from './remove-workflow.interface'; diff --git a/src/workflows/interfaces/list-workflows.interface.ts b/src/workflows/interfaces/list-workflows.interface.ts new file mode 100644 index 00000000..d8cedabc --- /dev/null +++ b/src/workflows/interfaces/list-workflows.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; +import type { PaginationOptions } from '../../common/interfaces'; +import type { Workflow } from './workflow'; + +export type ListWorkflowsOptions = PaginationOptions; + +export interface ListWorkflowsResponseSuccess { + object: 'list'; + has_more: boolean; + data: Workflow[]; +} + +export type ListWorkflowsResponse = Response; diff --git a/src/workflows/interfaces/remove-workflow.interface.ts b/src/workflows/interfaces/remove-workflow.interface.ts new file mode 100644 index 00000000..bdf0100c --- /dev/null +++ b/src/workflows/interfaces/remove-workflow.interface.ts @@ -0,0 +1,9 @@ +import type { Response } from '../../interfaces'; +import type { Workflow } from './workflow'; + +export interface RemoveWorkflowResponseSuccess extends Pick { + object: 'workflow'; + deleted: boolean; +} + +export type RemoveWorkflowResponse = Response; diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts new file mode 100644 index 00000000..ad4aa9ff --- /dev/null +++ b/src/workflows/interfaces/workflow-step.interface.ts @@ -0,0 +1,90 @@ +export type ConditionRule = + | { + type: 'rule'; + field: string; + operator: 'eq' | 'neq'; + value: string | number | boolean | null; + } + | { + type: 'rule'; + field: string; + operator: 'gt' | 'gte' | 'lt' | 'lte'; + value: number; + } + | { + type: 'rule'; + field: string; + operator: 'contains' | 'starts_with' | 'ends_with'; + value: string; + } + | { + type: 'rule'; + field: string; + operator: 'exists' | 'is_empty'; + }; + +export type TemplateVariableValue = + | string + | number + | boolean + | { var: string } + | Record + | Array>; + +export interface TriggerStepConfig { + eventName: string; +} + +export interface DelayStepConfig { + seconds: number; +} + +export interface SendEmailStepConfig { + templateId: string; + subject?: string; + from?: string; + replyTo?: string; + variables?: Record; +} + +export interface WaitForEventStepConfig { + eventName: string; + timeoutSeconds?: number; + filterRule?: ConditionRule; +} + +export type ConditionStepConfig = ConditionRule; + +export type WorkflowStep = + | { ref: string; type: 'trigger'; config: TriggerStepConfig } + | { ref: string; type: 'delay'; config: DelayStepConfig } + | { ref: string; type: 'send_email'; config: SendEmailStepConfig } + | { ref: string; type: 'wait_for_event'; config: WaitForEventStepConfig } + | { ref: string; type: 'condition'; config: ConditionStepConfig }; + +export type WorkflowEdgeType = + | 'default' + | 'condition_met' + | 'condition_not_met' + | 'timeout' + | 'event_received'; + +export interface WorkflowEdge { + from: string; + to: string; + edgeType?: WorkflowEdgeType; +} + +export interface WorkflowResponseStep { + id: string; + type: string; + config: unknown; + ref?: string; +} + +export interface WorkflowResponseEdge { + id: string; + from_step_id: string; + to_step_id: string; + edge_type: string; +} diff --git a/src/workflows/interfaces/workflow.ts b/src/workflows/interfaces/workflow.ts new file mode 100644 index 00000000..3fe7437c --- /dev/null +++ b/src/workflows/interfaces/workflow.ts @@ -0,0 +1,8 @@ +export interface Workflow { + object: 'workflow'; + id: string; + name: string; + status: 'enabled' | 'disabled'; + created_at: string; + updated_at: string; +} diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts new file mode 100644 index 00000000..1b7b72fe --- /dev/null +++ b/src/workflows/workflows.spec.ts @@ -0,0 +1,494 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { + CreateWorkflowOptions, + CreateWorkflowResponseSuccess, +} from './interfaces/create-workflow-options.interface'; +import type { CreateWorkflowEventResponseSuccess } from './interfaces/create-workflow-event.interface'; +import type { GetWorkflowResponseSuccess } from './interfaces/get-workflow.interface'; +import type { ListWorkflowsResponseSuccess } from './interfaces/list-workflows.interface'; +import type { RemoveWorkflowResponseSuccess } from './interfaces/remove-workflow.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Workflows', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('create', () => { + it('creates a workflow', async () => { + const response: CreateWorkflowResponseSuccess = { + object: 'workflow', + id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', + name: 'Welcome Flow', + status: 'enabled', + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + steps: [ + { + id: 'step-1', + type: 'trigger', + config: { event_name: 'user.created' }, + ref: 'trigger', + }, + { + id: 'step-2', + type: 'send_email', + config: { template_id: 'tpl-123' }, + ref: 'welcome_email', + }, + ], + edges: [ + { + id: 'edge-1', + from_step_id: 'step-1', + to_step_id: 'step-2', + edge_type: 'default', + }, + ], + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const payload: CreateWorkflowOptions = { + name: 'Welcome Flow', + status: 'enabled', + steps: [ + { + ref: 'trigger', + type: 'trigger', + config: { eventName: 'user.created' }, + }, + { + ref: 'welcome_email', + type: 'send_email', + config: { templateId: 'tpl-123' }, + }, + ], + edges: [ + { from: 'trigger', to: 'welcome_email', edgeType: 'default' }, + ], + }; + + const data = await resend.workflows.create(payload); + expect(data).toMatchInlineSnapshot(` + { + "data": { + "created_at": "2025-01-01T00:00:00.000Z", + "edges": [ + { + "edge_type": "default", + "from_step_id": "step-1", + "id": "edge-1", + "to_step_id": "step-2", + }, + ], + "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", + "name": "Welcome Flow", + "object": "workflow", + "status": "enabled", + "steps": [ + { + "config": { + "event_name": "user.created", + }, + "id": "step-1", + "ref": "trigger", + "type": "trigger", + }, + { + "config": { + "template_id": "tpl-123", + }, + "id": "step-2", + "ref": "welcome_email", + "type": "send_email", + }, + ], + "updated_at": "2025-01-01T00:00:00.000Z", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('throws an error when an ErrorResponse is returned', async () => { + const response: ErrorResponse = { + name: 'missing_required_field', + statusCode: 422, + message: 'Missing `name` field.', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.workflows.create({} as CreateWorkflowOptions); + expect(data).toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Missing \`name\` field.", + "name": "missing_required_field", + "statusCode": 422, + }, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + describe('list', () => { + const response: ListWorkflowsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + object: 'workflow', + id: '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794', + name: 'Welcome Flow', + status: 'enabled', + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + }, + { + object: 'workflow', + id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + name: 'Onboarding Flow', + status: 'disabled', + created_at: '2025-02-01T00:00:00.000Z', + updated_at: '2025-02-01T00:00:00.000Z', + }, + ], + }; + + describe('when no pagination options are provided', () => { + it('lists workflows', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.workflows.list(); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/workflows', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('when pagination options are provided', () => { + it('passes limit param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.list({ limit: 1 }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/workflows?limit=1', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes after param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.list({ + limit: 1, + after: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/workflows?limit=1&after=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes before param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.list({ + limit: 1, + before: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/workflows?limit=1&before=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + }); + + describe('get', () => { + describe('when workflow not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + statusCode: 404, + message: 'Workflow not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.workflows.get( + '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + ); + + await expect(result).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Workflow not found", + "name": "not_found", + "statusCode": 404, + }, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + it('gets a workflow', async () => { + const response: GetWorkflowResponseSuccess = { + object: 'workflow', + id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + name: 'Welcome Flow', + status: 'enabled', + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + steps: [ + { + id: 'step-1', + type: 'trigger', + config: { event_name: 'user.created' }, + ref: 'trigger', + }, + ], + edges: [], + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.workflows.get('559ac32e-9ef5-46fb-82a1-b76b840c0f7b'), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "created_at": "2025-01-01T00:00:00.000Z", + "edges": [], + "id": "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + "name": "Welcome Flow", + "object": "workflow", + "status": "enabled", + "steps": [ + { + "config": { + "event_name": "user.created", + }, + "id": "step-1", + "ref": "trigger", + "type": "trigger", + }, + ], + "updated_at": "2025-01-01T00:00:00.000Z", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + describe('remove', () => { + it('removes a workflow', async () => { + const id = 'b01e0de9-7c27-4a53-bf38-2e3f98389a65'; + const response: RemoveWorkflowResponseSuccess = { + object: 'workflow', + id, + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.workflows.remove(id), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "deleted": true, + "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", + "object": "workflow", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + describe('createEvent', () => { + it('creates a workflow event with contactId', async () => { + const response: CreateWorkflowEventResponseSuccess = { + object: 'workflow_event', + event: 'user.created', + event_instance_id: 'evt-inst-123', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.workflows.createEvent({ + event: 'user.created', + contactId: 'contact-123', + payload: { name: 'John' }, + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "event": "user.created", + "event_instance_id": "evt-inst-123", + "object": "workflow_event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('creates a workflow event with email', async () => { + const response: CreateWorkflowEventResponseSuccess = { + object: 'workflow_event', + event: 'user.created', + event_instance_id: 'evt-inst-456', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.workflows.createEvent({ + event: 'user.created', + email: 'john@example.com', + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "event": "user.created", + "event_instance_id": "evt-inst-456", + "object": "workflow_event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); +}); diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts new file mode 100644 index 00000000..9e99319a --- /dev/null +++ b/src/workflows/workflows.ts @@ -0,0 +1,77 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import { + parseWorkflowEventToApiOptions, + parseWorkflowToApiOptions, +} from '../common/utils/parse-workflow-to-api-options'; +import type { Resend } from '../resend'; +import type { + CreateWorkflowEventOptions, + CreateWorkflowEventResponse, + CreateWorkflowEventResponseSuccess, +} from './interfaces/create-workflow-event.interface'; +import type { + CreateWorkflowOptions, + CreateWorkflowResponse, + CreateWorkflowResponseSuccess, +} from './interfaces/create-workflow-options.interface'; +import type { + GetWorkflowResponse, + GetWorkflowResponseSuccess, +} from './interfaces/get-workflow.interface'; +import type { + ListWorkflowsOptions, + ListWorkflowsResponse, + ListWorkflowsResponseSuccess, +} from './interfaces/list-workflows.interface'; +import type { + RemoveWorkflowResponse, + RemoveWorkflowResponseSuccess, +} from './interfaces/remove-workflow.interface'; + +export class Workflows { + constructor(private readonly resend: Resend) {} + + async create(payload: CreateWorkflowOptions): Promise { + const data = await this.resend.post( + '/workflows', + parseWorkflowToApiOptions(payload), + ); + + return data; + } + + async list( + options: ListWorkflowsOptions = {}, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/workflows?${queryString}` : '/workflows'; + + const data = await this.resend.get(url); + return data; + } + + async get(id: string): Promise { + const data = await this.resend.get( + `/workflows/${id}`, + ); + return data; + } + + async remove(id: string): Promise { + const data = await this.resend.delete( + `/workflows/${id}`, + ); + return data; + } + + async createEvent( + payload: CreateWorkflowEventOptions, + ): Promise { + const data = await this.resend.post( + '/workflows/events', + parseWorkflowEventToApiOptions(payload), + ); + + return data; + } +} From d774651d546539c025f9e10b8afaa9f5528778e6 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 3 Mar 2026 13:48:59 -0300 Subject: [PATCH 02/23] chore: bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 03d5772e..95a450c4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.9.3", + "version": "6.10.0-preview-workflows.0", "description": "Node.js library for the Resend API", "main": "./dist/index.cjs", "module": "./dist/index.mjs", From 63f557f11049ae6c511fb5ac933025c003e28071 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 3 Mar 2026 16:59:48 -0300 Subject: [PATCH 03/23] feat: add events and patch workflow --- .../parse-workflow-to-api-options.spec.ts | 6 +- .../utils/parse-workflow-to-api-options.ts | 4 +- src/events/events.spec.ts | 424 ++++++++++++++++++ src/events/events.ts | 86 ++++ .../interfaces/create-event.interface.ts | 13 + src/events/interfaces/event.ts | 8 + src/events/interfaces/get-event.interface.ts | 6 + src/events/interfaces/index.ts | 7 + .../interfaces/list-events.interface.ts | 13 + .../interfaces/remove-event.interface.ts | 9 + .../interfaces/send-event.interface.ts} | 7 +- .../interfaces/update-event.interface.ts | 12 + src/index.ts | 1 + src/resend.ts | 2 + src/workflows/interfaces/index.ts | 2 +- .../interfaces/update-workflow.interface.ts | 13 + src/workflows/interfaces/workflow.ts | 2 +- src/workflows/workflows.spec.ts | 69 +-- src/workflows/workflows.ts | 29 +- 19 files changed, 637 insertions(+), 76 deletions(-) create mode 100644 src/events/events.spec.ts create mode 100644 src/events/events.ts create mode 100644 src/events/interfaces/create-event.interface.ts create mode 100644 src/events/interfaces/event.ts create mode 100644 src/events/interfaces/get-event.interface.ts create mode 100644 src/events/interfaces/index.ts create mode 100644 src/events/interfaces/list-events.interface.ts create mode 100644 src/events/interfaces/remove-event.interface.ts rename src/{workflows/interfaces/create-workflow-event.interface.ts => events/interfaces/send-event.interface.ts} (66%) create mode 100644 src/events/interfaces/update-event.interface.ts create mode 100644 src/workflows/interfaces/update-workflow.interface.ts diff --git a/src/common/utils/parse-workflow-to-api-options.spec.ts b/src/common/utils/parse-workflow-to-api-options.spec.ts index ffdeeae1..3e5a75d5 100644 --- a/src/common/utils/parse-workflow-to-api-options.spec.ts +++ b/src/common/utils/parse-workflow-to-api-options.spec.ts @@ -1,5 +1,5 @@ import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; -import type { CreateWorkflowEventOptions } from '../../workflows/interfaces/create-workflow-event.interface'; +import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; import { parseWorkflowToApiOptions, parseWorkflowEventToApiOptions, @@ -184,7 +184,7 @@ describe('parseWorkflowToApiOptions', () => { describe('parseWorkflowEventToApiOptions', () => { it('converts contactId to contact_id', () => { - const event: CreateWorkflowEventOptions = { + const event: SendEventOptions = { event: 'user.signed_up', contactId: 'contact_abc123', payload: { plan: 'pro' }, @@ -201,7 +201,7 @@ describe('parseWorkflowEventToApiOptions', () => { }); it('passes email through without conversion', () => { - const event: CreateWorkflowEventOptions = { + const event: SendEventOptions = { event: 'user.signed_up', email: 'user@example.com', payload: { source: 'website' }, diff --git a/src/common/utils/parse-workflow-to-api-options.ts b/src/common/utils/parse-workflow-to-api-options.ts index d5f25766..33fb2526 100644 --- a/src/common/utils/parse-workflow-to-api-options.ts +++ b/src/common/utils/parse-workflow-to-api-options.ts @@ -1,5 +1,5 @@ import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; -import type { CreateWorkflowEventOptions } from '../../workflows/interfaces/create-workflow-event.interface'; +import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; import type { WorkflowEdge, WorkflowEdgeType, @@ -89,7 +89,7 @@ export function parseWorkflowToApiOptions( } export function parseWorkflowEventToApiOptions( - event: CreateWorkflowEventOptions, + event: SendEventOptions, ): WorkflowEventApiOptions { return { event: event.event, diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts new file mode 100644 index 00000000..cd223db0 --- /dev/null +++ b/src/events/events.spec.ts @@ -0,0 +1,424 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { SendEventResponseSuccess } from './interfaces/send-event.interface'; +import type { CreateEventResponseSuccess } from './interfaces/create-event.interface'; +import type { GetEventResponseSuccess } from './interfaces/get-event.interface'; +import type { ListEventsResponseSuccess } from './interfaces/list-events.interface'; +import type { UpdateEventResponseSuccess } from './interfaces/update-event.interface'; +import type { RemoveEventResponseSuccess } from './interfaces/remove-event.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +describe('Events', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('send', () => { + it('sends an event with contactId', async () => { + const response: SendEventResponseSuccess = { + object: 'workflow_event', + event: 'user.created', + event_instance_id: 'evt-inst-123', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.send({ + event: 'user.created', + contactId: 'contact-123', + payload: { name: 'John' }, + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "event": "user.created", + "event_instance_id": "evt-inst-123", + "object": "workflow_event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/send', + expect.objectContaining({ + method: 'POST', + headers: expect.any(Headers), + body: JSON.stringify({ + event: 'user.created', + contact_id: 'contact-123', + payload: { name: 'John' }, + }), + }), + ); + }); + + it('sends an event with email', async () => { + const response: SendEventResponseSuccess = { + object: 'workflow_event', + event: 'user.created', + event_instance_id: 'evt-inst-456', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.send({ + event: 'user.created', + email: 'john@example.com', + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "event": "user.created", + "event_instance_id": "evt-inst-456", + "object": "workflow_event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/send', + expect.objectContaining({ + method: 'POST', + headers: expect.any(Headers), + body: JSON.stringify({ + event: 'user.created', + email: 'john@example.com', + }), + }), + ); + }); + }); + + describe('create', () => { + it('creates an event', async () => { + const response: CreateEventResponseSuccess = { + object: 'event', + id: 'evt-123', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.create({ + name: 'user.created', + schema: { type: 'object' }, + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "id": "evt-123", + "object": "event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events', + expect.objectContaining({ + method: 'POST', + headers: expect.any(Headers), + body: JSON.stringify({ + name: 'user.created', + schema: { type: 'object' }, + }), + }), + ); + }); + + it('returns error on failure', async () => { + const response: ErrorResponse = { + name: 'missing_required_field', + statusCode: 422, + message: 'Missing `name` field.', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.create({ name: '' }); + expect(data).toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Missing \`name\` field.", + "name": "missing_required_field", + "statusCode": 422, + }, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + describe('get', () => { + it('gets an event by id', async () => { + const response: GetEventResponseSuccess = { + object: 'event', + id: 'evt-123', + name: 'user.created', + schema: { type: 'object' }, + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.get('evt-123'); + expect(data).toMatchInlineSnapshot(` + { + "data": { + "created_at": "2025-01-01T00:00:00.000Z", + "id": "evt-123", + "name": "user.created", + "object": "event", + "schema": { + "type": "object", + }, + "updated_at": "2025-01-01T00:00:00.000Z", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/evt-123', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('gets an event by name', async () => { + const response: GetEventResponseSuccess = { + object: 'event', + id: 'evt-123', + name: 'user.created', + schema: null, + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.get('user.created'); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/user.created', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('list', () => { + const response: ListEventsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: 'evt-123', + name: 'user.created', + schema: null, + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + }, + { + id: 'evt-456', + name: 'user.updated', + schema: null, + created_at: '2025-02-01T00:00:00.000Z', + updated_at: '2025-02-01T00:00:00.000Z', + }, + ], + }; + + it('lists events without pagination', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const data = await resend.events.list(); + expect(data).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('lists events with pagination', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const data = await resend.events.list({ limit: 10, after: 'cursor-abc' }); + expect(data).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events?limit=10&after=cursor-abc', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('update', () => { + it('updates an event', async () => { + const response: UpdateEventResponseSuccess = { + object: 'event', + id: 'evt-123', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.update('evt-123', { + schema: { type: 'object', properties: { name: { type: 'string' } } }, + }); + + expect(data).toMatchInlineSnapshot(` + { + "data": { + "id": "evt-123", + "object": "event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/evt-123', + expect.objectContaining({ + method: 'PATCH', + headers: expect.any(Headers), + body: JSON.stringify({ + schema: { + type: 'object', + properties: { name: { type: 'string' } }, + }, + }), + }), + ); + }); + }); + + describe('remove', () => { + it('removes an event', async () => { + const response: RemoveEventResponseSuccess = { + object: 'event', + id: 'evt-123', + deleted: true, + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.events.remove('evt-123'); + expect(data).toMatchInlineSnapshot(` + { + "data": { + "deleted": true, + "id": "evt-123", + "object": "event", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/events/evt-123', + expect.objectContaining({ + method: 'DELETE', + headers: expect.any(Headers), + }), + ); + }); + }); +}); diff --git a/src/events/events.ts b/src/events/events.ts new file mode 100644 index 00000000..f2c1c3ce --- /dev/null +++ b/src/events/events.ts @@ -0,0 +1,86 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import { parseWorkflowEventToApiOptions } from '../common/utils/parse-workflow-to-api-options'; +import type { Resend } from '../resend'; +import type { + CreateEventOptions, + CreateEventResponse, + CreateEventResponseSuccess, +} from './interfaces/create-event.interface'; +import type { + GetEventResponse, + GetEventResponseSuccess, +} from './interfaces/get-event.interface'; +import type { + ListEventsOptions, + ListEventsResponse, + ListEventsResponseSuccess, +} from './interfaces/list-events.interface'; +import type { + RemoveEventResponse, + RemoveEventResponseSuccess, +} from './interfaces/remove-event.interface'; +import type { + SendEventOptions, + SendEventResponse, + SendEventResponseSuccess, +} from './interfaces/send-event.interface'; +import type { + UpdateEventOptions, + UpdateEventResponse, + UpdateEventResponseSuccess, +} from './interfaces/update-event.interface'; + +export class Events { + constructor(private readonly resend: Resend) {} + + async send(payload: SendEventOptions): Promise { + const data = await this.resend.post( + '/events/send', + parseWorkflowEventToApiOptions(payload), + ); + + return data; + } + + async create(payload: CreateEventOptions): Promise { + const data = await this.resend.post( + '/events', + payload, + ); + + return data; + } + + async get(identifier: string): Promise { + const data = await this.resend.get( + `/events/${encodeURIComponent(identifier)}`, + ); + return data; + } + + async list(options: ListEventsOptions = {}): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/events?${queryString}` : '/events'; + + const data = await this.resend.get(url); + return data; + } + + async update( + identifier: string, + payload: UpdateEventOptions, + ): Promise { + const data = await this.resend.patch( + `/events/${encodeURIComponent(identifier)}`, + payload, + ); + return data; + } + + async remove(identifier: string): Promise { + const data = await this.resend.delete( + `/events/${encodeURIComponent(identifier)}`, + ); + return data; + } +} diff --git a/src/events/interfaces/create-event.interface.ts b/src/events/interfaces/create-event.interface.ts new file mode 100644 index 00000000..bf6866c0 --- /dev/null +++ b/src/events/interfaces/create-event.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; + +export interface CreateEventOptions { + name: string; + schema?: unknown | null; +} + +export interface CreateEventResponseSuccess { + object: 'event'; + id: string; +} + +export type CreateEventResponse = Response; diff --git a/src/events/interfaces/event.ts b/src/events/interfaces/event.ts new file mode 100644 index 00000000..ad4ec128 --- /dev/null +++ b/src/events/interfaces/event.ts @@ -0,0 +1,8 @@ +export interface Event { + object: 'event'; + id: string; + name: string; + schema: unknown | null; + created_at: string; + updated_at: string | null; +} diff --git a/src/events/interfaces/get-event.interface.ts b/src/events/interfaces/get-event.interface.ts new file mode 100644 index 00000000..e0c54ff0 --- /dev/null +++ b/src/events/interfaces/get-event.interface.ts @@ -0,0 +1,6 @@ +import type { Response } from '../../interfaces'; +import type { Event } from './event'; + +export type GetEventResponseSuccess = Event; + +export type GetEventResponse = Response; diff --git a/src/events/interfaces/index.ts b/src/events/interfaces/index.ts new file mode 100644 index 00000000..0487459c --- /dev/null +++ b/src/events/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from './event'; +export * from './send-event.interface'; +export * from './create-event.interface'; +export * from './get-event.interface'; +export * from './list-events.interface'; +export * from './update-event.interface'; +export * from './remove-event.interface'; diff --git a/src/events/interfaces/list-events.interface.ts b/src/events/interfaces/list-events.interface.ts new file mode 100644 index 00000000..8ed9d731 --- /dev/null +++ b/src/events/interfaces/list-events.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; +import type { PaginationOptions } from '../../common/interfaces'; +import type { Event } from './event'; + +export type ListEventsOptions = PaginationOptions; + +export interface ListEventsResponseSuccess { + object: 'list'; + has_more: boolean; + data: Omit[]; +} + +export type ListEventsResponse = Response; diff --git a/src/events/interfaces/remove-event.interface.ts b/src/events/interfaces/remove-event.interface.ts new file mode 100644 index 00000000..be545e8f --- /dev/null +++ b/src/events/interfaces/remove-event.interface.ts @@ -0,0 +1,9 @@ +import type { Response } from '../../interfaces'; + +export interface RemoveEventResponseSuccess { + object: 'event'; + id: string; + deleted: true; +} + +export type RemoveEventResponse = Response; diff --git a/src/workflows/interfaces/create-workflow-event.interface.ts b/src/events/interfaces/send-event.interface.ts similarity index 66% rename from src/workflows/interfaces/create-workflow-event.interface.ts rename to src/events/interfaces/send-event.interface.ts index 91ab5a14..f937bb40 100644 --- a/src/workflows/interfaces/create-workflow-event.interface.ts +++ b/src/events/interfaces/send-event.interface.ts @@ -1,6 +1,6 @@ import type { Response } from '../../interfaces'; -export type CreateWorkflowEventOptions = +export type SendEventOptions = | { event: string; contactId: string; @@ -14,11 +14,10 @@ export type CreateWorkflowEventOptions = payload?: Record; }; -export interface CreateWorkflowEventResponseSuccess { +export interface SendEventResponseSuccess { object: 'workflow_event'; event: string; event_instance_id: string; } -export type CreateWorkflowEventResponse = - Response; +export type SendEventResponse = Response; diff --git a/src/events/interfaces/update-event.interface.ts b/src/events/interfaces/update-event.interface.ts new file mode 100644 index 00000000..d17bb9c7 --- /dev/null +++ b/src/events/interfaces/update-event.interface.ts @@ -0,0 +1,12 @@ +import type { Response } from '../../interfaces'; + +export interface UpdateEventOptions { + schema: unknown; +} + +export interface UpdateEventResponseSuccess { + object: 'event'; + id: string; +} + +export type UpdateEventResponse = Response; diff --git a/src/index.ts b/src/index.ts index 9cbc7bbc..d2d80e18 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export * from './contacts/topics/interfaces'; export * from './domains/interfaces'; export * from './emails/attachments/interfaces'; export * from './emails/interfaces'; +export * from './events/interfaces'; export * from './emails/receiving/interfaces'; export type { ErrorResponse, Response } from './interfaces'; export { Resend } from './resend'; diff --git a/src/resend.ts b/src/resend.ts index ba1aaff6..8f0e0bb5 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -13,6 +13,7 @@ import type { ErrorResponse, Response } from './interfaces'; import { Segments } from './segments/segments'; import { Templates } from './templates/templates'; import { Topics } from './topics/topics'; +import { Events } from './events/events'; import { Webhooks } from './webhooks/webhooks'; import { Workflows } from './workflows/workflows'; @@ -42,6 +43,7 @@ export class Resend { readonly contactProperties = new ContactProperties(this); readonly domains = new Domains(this); readonly emails = new Emails(this); + readonly events = new Events(this); readonly webhooks = new Webhooks(this); readonly templates = new Templates(this); readonly topics = new Topics(this); diff --git a/src/workflows/interfaces/index.ts b/src/workflows/interfaces/index.ts index dd93f00a..ae251a32 100644 --- a/src/workflows/interfaces/index.ts +++ b/src/workflows/interfaces/index.ts @@ -1,7 +1,7 @@ export * from './workflow'; export * from './workflow-step.interface'; -export * from './create-workflow-event.interface'; export * from './create-workflow-options.interface'; export * from './get-workflow.interface'; export * from './list-workflows.interface'; export * from './remove-workflow.interface'; +export * from './update-workflow.interface'; diff --git a/src/workflows/interfaces/update-workflow.interface.ts b/src/workflows/interfaces/update-workflow.interface.ts new file mode 100644 index 00000000..4d3572ea --- /dev/null +++ b/src/workflows/interfaces/update-workflow.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; + +export interface UpdateWorkflowOptions { + status: 'enabled' | 'disabled'; +} + +export interface UpdateWorkflowResponseSuccess { + object: 'workflow'; + id: string; + status: 'enabled' | 'disabled'; +} + +export type UpdateWorkflowResponse = Response; diff --git a/src/workflows/interfaces/workflow.ts b/src/workflows/interfaces/workflow.ts index 3fe7437c..862425dd 100644 --- a/src/workflows/interfaces/workflow.ts +++ b/src/workflows/interfaces/workflow.ts @@ -4,5 +4,5 @@ export interface Workflow { name: string; status: 'enabled' | 'disabled'; created_at: string; - updated_at: string; + updated_at: string | null; } diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index 1b7b72fe..beea3071 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -6,10 +6,10 @@ import type { CreateWorkflowOptions, CreateWorkflowResponseSuccess, } from './interfaces/create-workflow-options.interface'; -import type { CreateWorkflowEventResponseSuccess } from './interfaces/create-workflow-event.interface'; import type { GetWorkflowResponseSuccess } from './interfaces/get-workflow.interface'; import type { ListWorkflowsResponseSuccess } from './interfaces/list-workflows.interface'; import type { RemoveWorkflowResponseSuccess } from './interfaces/remove-workflow.interface'; +import type { UpdateWorkflowResponseSuccess } from './interfaces/update-workflow.interface'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); @@ -421,12 +421,13 @@ describe('Workflows', () => { }); }); - describe('createEvent', () => { - it('creates a workflow event with contactId', async () => { - const response: CreateWorkflowEventResponseSuccess = { - object: 'workflow_event', - event: 'user.created', - event_instance_id: 'evt-inst-123', + describe('update', () => { + it('updates a workflow', async () => { + const id = '71cdfe68-cf79-473a-a9d7-21f91db6a526'; + const response: UpdateWorkflowResponseSuccess = { + object: 'workflow', + id, + status: 'disabled', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -436,18 +437,13 @@ describe('Workflows', () => { }, }); - const data = await resend.workflows.createEvent({ - event: 'user.created', - contactId: 'contact-123', - payload: { name: 'John' }, - }); - + const data = await resend.workflows.update(id, { status: 'disabled' }); expect(data).toMatchInlineSnapshot(` { "data": { - "event": "user.created", - "event_instance_id": "evt-inst-123", - "object": "workflow_event", + "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", + "object": "workflow", + "status": "disabled", }, "error": null, "headers": { @@ -455,40 +451,15 @@ describe('Workflows', () => { }, } `); - }); - - it('creates a workflow event with email', async () => { - const response: CreateWorkflowEventResponseSuccess = { - object: 'workflow_event', - event: 'user.created', - event_instance_id: 'evt-inst-456', - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }); - - const data = await resend.workflows.createEvent({ - event: 'user.created', - email: 'john@example.com', - }); - expect(data).toMatchInlineSnapshot(` - { - "data": { - "event": "user.created", - "event_instance_id": "evt-inst-456", - "object": "workflow_event", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); + expect(fetchMock).toHaveBeenCalledWith( + `https://api.resend.com/workflows/${id}`, + expect.objectContaining({ + method: 'PATCH', + headers: expect.any(Headers), + body: JSON.stringify({ status: 'disabled' }), + }), + ); }); }); }); diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index 9e99319a..6cb872f3 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -1,14 +1,6 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import { - parseWorkflowEventToApiOptions, - parseWorkflowToApiOptions, -} from '../common/utils/parse-workflow-to-api-options'; +import { parseWorkflowToApiOptions } from '../common/utils/parse-workflow-to-api-options'; import type { Resend } from '../resend'; -import type { - CreateWorkflowEventOptions, - CreateWorkflowEventResponse, - CreateWorkflowEventResponseSuccess, -} from './interfaces/create-workflow-event.interface'; import type { CreateWorkflowOptions, CreateWorkflowResponse, @@ -27,6 +19,11 @@ import type { RemoveWorkflowResponse, RemoveWorkflowResponseSuccess, } from './interfaces/remove-workflow.interface'; +import type { + UpdateWorkflowOptions, + UpdateWorkflowResponse, + UpdateWorkflowResponseSuccess, +} from './interfaces/update-workflow.interface'; export class Workflows { constructor(private readonly resend: Resend) {} @@ -64,14 +61,14 @@ export class Workflows { return data; } - async createEvent( - payload: CreateWorkflowEventOptions, - ): Promise { - const data = await this.resend.post( - '/workflows/events', - parseWorkflowEventToApiOptions(payload), + async update( + id: string, + payload: UpdateWorkflowOptions, + ): Promise { + const data = await this.resend.patch( + `/workflows/${id}`, + payload, ); - return data; } } From 02d278137245bd581473666aa96b66c1d1851e78 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Tue, 3 Mar 2026 18:15:19 -0300 Subject: [PATCH 04/23] fix: update workflow create response --- .../create-workflow-options.interface.ts | 9 +-- src/workflows/workflows.spec.ts | 56 ------------------- 2 files changed, 3 insertions(+), 62 deletions(-) diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts index d23e1adf..30117452 100644 --- a/src/workflows/interfaces/create-workflow-options.interface.ts +++ b/src/workflows/interfaces/create-workflow-options.interface.ts @@ -1,9 +1,6 @@ import type { Response } from '../../interfaces'; -import type { Workflow } from './workflow'; import type { WorkflowEdge, - WorkflowResponseEdge, - WorkflowResponseStep, WorkflowStep, } from './workflow-step.interface'; @@ -14,9 +11,9 @@ export interface CreateWorkflowOptions { edges: WorkflowEdge[]; } -export interface CreateWorkflowResponseSuccess extends Workflow { - steps: WorkflowResponseStep[]; - edges: WorkflowResponseEdge[]; +export interface CreateWorkflowResponseSuccess { + object: 'workflow'; + id: string; } export type CreateWorkflowResponse = Response; diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index beea3071..304111b5 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -25,32 +25,6 @@ describe('Workflows', () => { const response: CreateWorkflowResponseSuccess = { object: 'workflow', id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', - name: 'Welcome Flow', - status: 'enabled', - created_at: '2025-01-01T00:00:00.000Z', - updated_at: '2025-01-01T00:00:00.000Z', - steps: [ - { - id: 'step-1', - type: 'trigger', - config: { event_name: 'user.created' }, - ref: 'trigger', - }, - { - id: 'step-2', - type: 'send_email', - config: { template_id: 'tpl-123' }, - ref: 'welcome_email', - }, - ], - edges: [ - { - id: 'edge-1', - from_step_id: 'step-1', - to_step_id: 'step-2', - edge_type: 'default', - }, - ], }; fetchMock.mockOnce(JSON.stringify(response), { @@ -84,38 +58,8 @@ describe('Workflows', () => { expect(data).toMatchInlineSnapshot(` { "data": { - "created_at": "2025-01-01T00:00:00.000Z", - "edges": [ - { - "edge_type": "default", - "from_step_id": "step-1", - "id": "edge-1", - "to_step_id": "step-2", - }, - ], "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", - "name": "Welcome Flow", "object": "workflow", - "status": "enabled", - "steps": [ - { - "config": { - "event_name": "user.created", - }, - "id": "step-1", - "ref": "trigger", - "type": "trigger", - }, - { - "config": { - "template_id": "tpl-123", - }, - "id": "step-2", - "ref": "welcome_email", - "type": "send_email", - }, - ], - "updated_at": "2025-01-01T00:00:00.000Z", }, "error": null, "headers": { From 92f7bf6b2a97071bda05a4a176888cd2c8b9b621 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 4 Mar 2026 10:42:01 -0300 Subject: [PATCH 05/23] fix: update event schema validation --- src/events/events.spec.ts | 15 ++++++--------- src/events/interfaces/create-event.interface.ts | 3 ++- src/events/interfaces/event.ts | 6 +++++- src/events/interfaces/update-event.interface.ts | 3 ++- 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index cd223db0..b6041bb7 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -130,7 +130,7 @@ describe('Events', () => { const data = await resend.events.create({ name: 'user.created', - schema: { type: 'object' }, + schema: { name: 'string', age: 'number' }, }); expect(data).toMatchInlineSnapshot(` @@ -153,7 +153,7 @@ describe('Events', () => { headers: expect.any(Headers), body: JSON.stringify({ name: 'user.created', - schema: { type: 'object' }, + schema: { name: 'string', age: 'number' }, }), }), ); @@ -196,7 +196,7 @@ describe('Events', () => { object: 'event', id: 'evt-123', name: 'user.created', - schema: { type: 'object' }, + schema: { name: 'string' }, created_at: '2025-01-01T00:00:00.000Z', updated_at: '2025-01-01T00:00:00.000Z', }; @@ -217,7 +217,7 @@ describe('Events', () => { "name": "user.created", "object": "event", "schema": { - "type": "object", + "name": "string", }, "updated_at": "2025-01-01T00:00:00.000Z", }, @@ -350,7 +350,7 @@ describe('Events', () => { }); const data = await resend.events.update('evt-123', { - schema: { type: 'object', properties: { name: { type: 'string' } } }, + schema: { name: 'string', active: 'boolean' }, }); expect(data).toMatchInlineSnapshot(` @@ -372,10 +372,7 @@ describe('Events', () => { method: 'PATCH', headers: expect.any(Headers), body: JSON.stringify({ - schema: { - type: 'object', - properties: { name: { type: 'string' } }, - }, + schema: { name: 'string', active: 'boolean' }, }), }), ); diff --git a/src/events/interfaces/create-event.interface.ts b/src/events/interfaces/create-event.interface.ts index bf6866c0..a85d2d22 100644 --- a/src/events/interfaces/create-event.interface.ts +++ b/src/events/interfaces/create-event.interface.ts @@ -1,8 +1,9 @@ import type { Response } from '../../interfaces'; +import type { EventSchemaMap } from './event'; export interface CreateEventOptions { name: string; - schema?: unknown | null; + schema?: EventSchemaMap | null; } export interface CreateEventResponseSuccess { diff --git a/src/events/interfaces/event.ts b/src/events/interfaces/event.ts index ad4ec128..b07cb618 100644 --- a/src/events/interfaces/event.ts +++ b/src/events/interfaces/event.ts @@ -1,8 +1,12 @@ +export type EventSchemaType = 'string' | 'number' | 'boolean' | 'date'; + +export type EventSchemaMap = Record; + export interface Event { object: 'event'; id: string; name: string; - schema: unknown | null; + schema: EventSchemaMap | null; created_at: string; updated_at: string | null; } diff --git a/src/events/interfaces/update-event.interface.ts b/src/events/interfaces/update-event.interface.ts index d17bb9c7..a7ebe7f8 100644 --- a/src/events/interfaces/update-event.interface.ts +++ b/src/events/interfaces/update-event.interface.ts @@ -1,7 +1,8 @@ import type { Response } from '../../interfaces'; +import type { EventSchemaMap } from './event'; export interface UpdateEventOptions { - schema: unknown; + schema: EventSchemaMap | null; } export interface UpdateEventResponseSuccess { From 03165b8ed0b7fd66ffe4a6e6aa0ac00cc6640ab6 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 4 Mar 2026 14:17:39 -0300 Subject: [PATCH 06/23] fix: linter --- src/common/utils/parse-workflow-to-api-options.spec.ts | 4 ++-- src/common/utils/parse-workflow-to-api-options.ts | 2 +- src/events/events.spec.ts | 6 +++--- src/events/interfaces/index.ts | 6 +++--- src/events/interfaces/list-events.interface.ts | 2 +- src/index.ts | 2 +- src/resend.ts | 2 +- .../interfaces/create-workflow-options.interface.ts | 5 +---- src/workflows/interfaces/index.ts | 4 ++-- src/workflows/interfaces/list-workflows.interface.ts | 2 +- src/workflows/interfaces/workflow-step.interface.ts | 4 +++- src/workflows/workflows.spec.ts | 8 ++------ src/workflows/workflows.ts | 4 +++- 13 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/common/utils/parse-workflow-to-api-options.spec.ts b/src/common/utils/parse-workflow-to-api-options.spec.ts index 3e5a75d5..68b7280b 100644 --- a/src/common/utils/parse-workflow-to-api-options.spec.ts +++ b/src/common/utils/parse-workflow-to-api-options.spec.ts @@ -1,8 +1,8 @@ -import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; +import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import { - parseWorkflowToApiOptions, parseWorkflowEventToApiOptions, + parseWorkflowToApiOptions, } from './parse-workflow-to-api-options'; describe('parseWorkflowToApiOptions', () => { diff --git a/src/common/utils/parse-workflow-to-api-options.ts b/src/common/utils/parse-workflow-to-api-options.ts index 33fb2526..bf0aa409 100644 --- a/src/common/utils/parse-workflow-to-api-options.ts +++ b/src/common/utils/parse-workflow-to-api-options.ts @@ -1,5 +1,5 @@ -import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; +import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import type { WorkflowEdge, WorkflowEdgeType, diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index b6041bb7..a9cc9a93 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -2,12 +2,12 @@ import createFetchMock from 'vitest-fetch-mock'; import type { ErrorResponse } from '../interfaces'; import { Resend } from '../resend'; import { mockSuccessResponse } from '../test-utils/mock-fetch'; -import type { SendEventResponseSuccess } from './interfaces/send-event.interface'; import type { CreateEventResponseSuccess } from './interfaces/create-event.interface'; import type { GetEventResponseSuccess } from './interfaces/get-event.interface'; import type { ListEventsResponseSuccess } from './interfaces/list-events.interface'; -import type { UpdateEventResponseSuccess } from './interfaces/update-event.interface'; import type { RemoveEventResponseSuccess } from './interfaces/remove-event.interface'; +import type { SendEventResponseSuccess } from './interfaces/send-event.interface'; +import type { UpdateEventResponseSuccess } from './interfaces/update-event.interface'; const fetchMocker = createFetchMock(vi); fetchMocker.enableMocks(); @@ -254,7 +254,7 @@ describe('Events', () => { }, }); - const data = await resend.events.get('user.created'); + await resend.events.get('user.created'); expect(fetchMock).toHaveBeenCalledWith( 'https://api.resend.com/events/user.created', diff --git a/src/events/interfaces/index.ts b/src/events/interfaces/index.ts index 0487459c..bb9950d2 100644 --- a/src/events/interfaces/index.ts +++ b/src/events/interfaces/index.ts @@ -1,7 +1,7 @@ -export * from './event'; -export * from './send-event.interface'; export * from './create-event.interface'; +export * from './event'; export * from './get-event.interface'; export * from './list-events.interface'; -export * from './update-event.interface'; export * from './remove-event.interface'; +export * from './send-event.interface'; +export * from './update-event.interface'; diff --git a/src/events/interfaces/list-events.interface.ts b/src/events/interfaces/list-events.interface.ts index 8ed9d731..2e72351a 100644 --- a/src/events/interfaces/list-events.interface.ts +++ b/src/events/interfaces/list-events.interface.ts @@ -1,5 +1,5 @@ -import type { Response } from '../../interfaces'; import type { PaginationOptions } from '../../common/interfaces'; +import type { Response } from '../../interfaces'; import type { Event } from './event'; export type ListEventsOptions = PaginationOptions; diff --git a/src/index.ts b/src/index.ts index d2d80e18..953cc26f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,8 +9,8 @@ export * from './contacts/topics/interfaces'; export * from './domains/interfaces'; export * from './emails/attachments/interfaces'; export * from './emails/interfaces'; -export * from './events/interfaces'; export * from './emails/receiving/interfaces'; +export * from './events/interfaces'; export type { ErrorResponse, Response } from './interfaces'; export { Resend } from './resend'; export * from './segments/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index 8f0e0bb5..3bd4fb6e 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -9,11 +9,11 @@ import { ContactProperties } from './contact-properties/contact-properties'; import { Contacts } from './contacts/contacts'; import { Domains } from './domains/domains'; import { Emails } from './emails/emails'; +import { Events } from './events/events'; import type { ErrorResponse, Response } from './interfaces'; import { Segments } from './segments/segments'; import { Templates } from './templates/templates'; import { Topics } from './topics/topics'; -import { Events } from './events/events'; import { Webhooks } from './webhooks/webhooks'; import { Workflows } from './workflows/workflows'; diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts index 30117452..66416728 100644 --- a/src/workflows/interfaces/create-workflow-options.interface.ts +++ b/src/workflows/interfaces/create-workflow-options.interface.ts @@ -1,8 +1,5 @@ import type { Response } from '../../interfaces'; -import type { - WorkflowEdge, - WorkflowStep, -} from './workflow-step.interface'; +import type { WorkflowEdge, WorkflowStep } from './workflow-step.interface'; export interface CreateWorkflowOptions { name: string; diff --git a/src/workflows/interfaces/index.ts b/src/workflows/interfaces/index.ts index ae251a32..9a0d3c0a 100644 --- a/src/workflows/interfaces/index.ts +++ b/src/workflows/interfaces/index.ts @@ -1,7 +1,7 @@ -export * from './workflow'; -export * from './workflow-step.interface'; export * from './create-workflow-options.interface'; export * from './get-workflow.interface'; export * from './list-workflows.interface'; export * from './remove-workflow.interface'; export * from './update-workflow.interface'; +export * from './workflow'; +export * from './workflow-step.interface'; diff --git a/src/workflows/interfaces/list-workflows.interface.ts b/src/workflows/interfaces/list-workflows.interface.ts index d8cedabc..c5396f1b 100644 --- a/src/workflows/interfaces/list-workflows.interface.ts +++ b/src/workflows/interfaces/list-workflows.interface.ts @@ -1,5 +1,5 @@ -import type { Response } from '../../interfaces'; import type { PaginationOptions } from '../../common/interfaces'; +import type { Response } from '../../interfaces'; import type { Workflow } from './workflow'; export type ListWorkflowsOptions = PaginationOptions; diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts index ad4aa9ff..d48991ce 100644 --- a/src/workflows/interfaces/workflow-step.interface.ts +++ b/src/workflows/interfaces/workflow-step.interface.ts @@ -29,7 +29,9 @@ export type TemplateVariableValue = | boolean | { var: string } | Record - | Array>; + | Array< + string | number | boolean | Record + >; export interface TriggerStepConfig { eventName: string; diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index 304111b5..20cc481b 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -49,9 +49,7 @@ describe('Workflows', () => { config: { templateId: 'tpl-123' }, }, ], - edges: [ - { from: 'trigger', to: 'welcome_email', edgeType: 'default' }, - ], + edges: [{ from: 'trigger', to: 'welcome_email', edgeType: 'default' }], }; const data = await resend.workflows.create(payload); @@ -347,9 +345,7 @@ describe('Workflows', () => { const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.remove(id), - ).resolves.toMatchInlineSnapshot(` + await expect(resend.workflows.remove(id)).resolves.toMatchInlineSnapshot(` { "data": { "deleted": true, diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index 6cb872f3..b761f770 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -28,7 +28,9 @@ import type { export class Workflows { constructor(private readonly resend: Resend) {} - async create(payload: CreateWorkflowOptions): Promise { + async create( + payload: CreateWorkflowOptions, + ): Promise { const data = await this.resend.post( '/workflows', parseWorkflowToApiOptions(payload), From 0ff3c507df2fe5f7e5429dcc02ac2c8eceedf1e9 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Wed, 4 Mar 2026 15:49:38 -0300 Subject: [PATCH 07/23] fix: update types --- src/common/utils/parse-workflow-to-api-options.ts | 4 ++-- src/events/interfaces/event.ts | 1 - src/events/interfaces/get-event.interface.ts | 4 +++- src/events/interfaces/list-events.interface.ts | 2 +- src/events/interfaces/remove-event.interface.ts | 6 +++--- src/workflows/interfaces/get-workflow.interface.ts | 1 + src/workflows/interfaces/workflow.ts | 1 - src/workflows/workflows.spec.ts | 8 +++++--- 8 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/common/utils/parse-workflow-to-api-options.ts b/src/common/utils/parse-workflow-to-api-options.ts index bf0aa409..5c8e6f6a 100644 --- a/src/common/utils/parse-workflow-to-api-options.ts +++ b/src/common/utils/parse-workflow-to-api-options.ts @@ -83,8 +83,8 @@ export function parseWorkflowToApiOptions( return { name: workflow.name, status: workflow.status, - steps: workflow.steps?.map(parseStepConfig), - edges: workflow.edges?.map(parseEdge), + steps: workflow.steps.map(parseStepConfig), + edges: workflow.edges.map(parseEdge), }; } diff --git a/src/events/interfaces/event.ts b/src/events/interfaces/event.ts index b07cb618..a15d2387 100644 --- a/src/events/interfaces/event.ts +++ b/src/events/interfaces/event.ts @@ -3,7 +3,6 @@ export type EventSchemaType = 'string' | 'number' | 'boolean' | 'date'; export type EventSchemaMap = Record; export interface Event { - object: 'event'; id: string; name: string; schema: EventSchemaMap | null; diff --git a/src/events/interfaces/get-event.interface.ts b/src/events/interfaces/get-event.interface.ts index e0c54ff0..85c0ac6d 100644 --- a/src/events/interfaces/get-event.interface.ts +++ b/src/events/interfaces/get-event.interface.ts @@ -1,6 +1,8 @@ import type { Response } from '../../interfaces'; import type { Event } from './event'; -export type GetEventResponseSuccess = Event; +export interface GetEventResponseSuccess extends Event { + object: 'event'; +} export type GetEventResponse = Response; diff --git a/src/events/interfaces/list-events.interface.ts b/src/events/interfaces/list-events.interface.ts index 2e72351a..c995e0bf 100644 --- a/src/events/interfaces/list-events.interface.ts +++ b/src/events/interfaces/list-events.interface.ts @@ -7,7 +7,7 @@ export type ListEventsOptions = PaginationOptions; export interface ListEventsResponseSuccess { object: 'list'; has_more: boolean; - data: Omit[]; + data: Event[]; } export type ListEventsResponse = Response; diff --git a/src/events/interfaces/remove-event.interface.ts b/src/events/interfaces/remove-event.interface.ts index be545e8f..b5c35b4e 100644 --- a/src/events/interfaces/remove-event.interface.ts +++ b/src/events/interfaces/remove-event.interface.ts @@ -1,9 +1,9 @@ import type { Response } from '../../interfaces'; +import type { Event } from './event'; -export interface RemoveEventResponseSuccess { +export interface RemoveEventResponseSuccess extends Pick { object: 'event'; - id: string; - deleted: true; + deleted: boolean; } export type RemoveEventResponse = Response; diff --git a/src/workflows/interfaces/get-workflow.interface.ts b/src/workflows/interfaces/get-workflow.interface.ts index f8afad85..8fd66443 100644 --- a/src/workflows/interfaces/get-workflow.interface.ts +++ b/src/workflows/interfaces/get-workflow.interface.ts @@ -6,6 +6,7 @@ import type { } from './workflow-step.interface'; export interface GetWorkflowResponseSuccess extends Workflow { + object: 'workflow'; steps: WorkflowResponseStep[]; edges: WorkflowResponseEdge[]; } diff --git a/src/workflows/interfaces/workflow.ts b/src/workflows/interfaces/workflow.ts index 862425dd..c07865ff 100644 --- a/src/workflows/interfaces/workflow.ts +++ b/src/workflows/interfaces/workflow.ts @@ -1,5 +1,4 @@ export interface Workflow { - object: 'workflow'; id: string; name: string; status: 'enabled' | 'disabled'; diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index 20cc481b..28885313 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -81,7 +81,11 @@ describe('Workflows', () => { }, }); - const data = await resend.workflows.create({} as CreateWorkflowOptions); + const data = await resend.workflows.create({ + name: '', + steps: [], + edges: [], + }); expect(data).toMatchInlineSnapshot(` { "data": null, @@ -104,7 +108,6 @@ describe('Workflows', () => { has_more: false, data: [ { - object: 'workflow', id: '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794', name: 'Welcome Flow', status: 'enabled', @@ -112,7 +115,6 @@ describe('Workflows', () => { updated_at: '2025-01-01T00:00:00.000Z', }, { - object: 'workflow', id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', name: 'Onboarding Flow', status: 'disabled', From 5aef44a3d914c4b54ca5c80aba9ab6ec964b9eec Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Thu, 5 Mar 2026 16:50:46 -0300 Subject: [PATCH 08/23] feat: add workflow runs list and details --- src/index.ts | 1 + .../interfaces/get-workflow-run.interface.ts | 11 + src/workflow-runs/interfaces/index.ts | 3 + .../list-workflow-runs.interface.ts | 15 ++ src/workflow-runs/interfaces/workflow-run.ts | 14 ++ src/workflow-runs/workflow-runs.spec.ts | 190 ++++++++++++++++++ src/workflow-runs/workflow-runs.ts | 35 ++++ src/workflows/workflows.ts | 7 +- 8 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/workflow-runs/interfaces/get-workflow-run.interface.ts create mode 100644 src/workflow-runs/interfaces/index.ts create mode 100644 src/workflow-runs/interfaces/list-workflow-runs.interface.ts create mode 100644 src/workflow-runs/interfaces/workflow-run.ts create mode 100644 src/workflow-runs/workflow-runs.spec.ts create mode 100644 src/workflow-runs/workflow-runs.ts diff --git a/src/index.ts b/src/index.ts index 953cc26f..9d9ddb22 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,4 +17,5 @@ export * from './segments/interfaces'; export * from './templates/interfaces'; export * from './topics/interfaces'; export * from './webhooks/interfaces'; +export * from './workflow-runs/interfaces'; export * from './workflows/interfaces'; diff --git a/src/workflow-runs/interfaces/get-workflow-run.interface.ts b/src/workflow-runs/interfaces/get-workflow-run.interface.ts new file mode 100644 index 00000000..c0708c35 --- /dev/null +++ b/src/workflow-runs/interfaces/get-workflow-run.interface.ts @@ -0,0 +1,11 @@ +import type { Response } from '../../interfaces'; +import type { WorkflowRun } from './workflow-run'; + +export interface GetWorkflowRunOptions { + workflowId: string; + runId: string; +} + +export type GetWorkflowRunResponseSuccess = WorkflowRun; + +export type GetWorkflowRunResponse = Response; diff --git a/src/workflow-runs/interfaces/index.ts b/src/workflow-runs/interfaces/index.ts new file mode 100644 index 00000000..98a50425 --- /dev/null +++ b/src/workflow-runs/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './get-workflow-run.interface'; +export * from './list-workflow-runs.interface'; +export * from './workflow-run'; diff --git a/src/workflow-runs/interfaces/list-workflow-runs.interface.ts b/src/workflow-runs/interfaces/list-workflow-runs.interface.ts new file mode 100644 index 00000000..33050aae --- /dev/null +++ b/src/workflow-runs/interfaces/list-workflow-runs.interface.ts @@ -0,0 +1,15 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../interfaces'; +import type { WorkflowRunItem } from './workflow-run'; + +export type ListWorkflowRunsOptions = PaginationOptions & { + workflowId: string; +}; + +export type ListWorkflowRunsResponseSuccess = PaginatedData; + +export type ListWorkflowRunsResponse = + Response; diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts new file mode 100644 index 00000000..788d83c0 --- /dev/null +++ b/src/workflow-runs/interfaces/workflow-run.ts @@ -0,0 +1,14 @@ +export interface WorkflowRun { + object: 'workflow_run'; + id: string; + status: string; + durable_execution_name: string | null; + started_at: string | null; + completed_at: string | null; + created_at: string; +} + +export type WorkflowRunItem = Pick< + WorkflowRun, + 'id' | 'status' | 'started_at' | 'completed_at' | 'created_at' +>; diff --git a/src/workflow-runs/workflow-runs.spec.ts b/src/workflow-runs/workflow-runs.spec.ts new file mode 100644 index 00000000..1318ce3e --- /dev/null +++ b/src/workflow-runs/workflow-runs.spec.ts @@ -0,0 +1,190 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + GetWorkflowRunOptions, + GetWorkflowRunResponseSuccess, +} from './interfaces/get-workflow-run.interface'; +import type { + ListWorkflowRunsOptions, + ListWorkflowRunsResponseSuccess, +} from './interfaces/list-workflow-runs.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('WorkflowRuns', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('get', () => { + it('gets a workflow run', async () => { + const options: GetWorkflowRunOptions = { + workflowId: 'wf_123', + runId: 'wr_456', + }; + const response: GetWorkflowRunResponseSuccess = { + object: 'workflow_run', + id: 'wr_456', + status: 'completed', + durable_execution_name: null, + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.get(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "durable_execution_name": null, + "id": "wr_456", + "object": "workflow_run", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: GetWorkflowRunOptions = { + workflowId: 'wf_123', + runId: 'wr_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Workflow run not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.runs.get(options); + expect(result.error).not.toBeNull(); + }); + }); + + describe('list', () => { + it('lists workflow runs', async () => { + const options: ListWorkflowRunsOptions = { + workflowId: 'wf_123', + }; + const response: ListWorkflowRunsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wr_456', + status: 'completed', + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wr_456", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('lists workflow runs with pagination', async () => { + const options: ListWorkflowRunsOptions = { + workflowId: 'wf_123', + limit: 1, + after: 'wr_cursor', + }; + const response: ListWorkflowRunsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wr_789', + status: 'running', + started_at: '2024-01-02T00:00:00.000Z', + completed_at: null, + created_at: '2024-01-02T00:00:00.000Z', + }, + ], + has_more: true, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": null, + "created_at": "2024-01-02T00:00:00.000Z", + "id": "wr_789", + "started_at": "2024-01-02T00:00:00.000Z", + "status": "running", + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: ListWorkflowRunsOptions = { + workflowId: 'wf_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Workflow not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.runs.list(options); + expect(result.error).not.toBeNull(); + }); + }); +}); diff --git a/src/workflow-runs/workflow-runs.ts b/src/workflow-runs/workflow-runs.ts new file mode 100644 index 00000000..ebe6beb9 --- /dev/null +++ b/src/workflow-runs/workflow-runs.ts @@ -0,0 +1,35 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + GetWorkflowRunOptions, + GetWorkflowRunResponse, + GetWorkflowRunResponseSuccess, +} from './interfaces/get-workflow-run.interface'; +import type { + ListWorkflowRunsOptions, + ListWorkflowRunsResponse, + ListWorkflowRunsResponseSuccess, +} from './interfaces/list-workflow-runs.interface'; + +export class WorkflowRuns { + constructor(private readonly resend: Resend) {} + + async get(options: GetWorkflowRunOptions): Promise { + const data = await this.resend.get( + `/workflows/${options.workflowId}/runs/${options.runId}`, + ); + return data; + } + + async list( + options: ListWorkflowRunsOptions, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/workflows/${options.workflowId}/runs?${queryString}` + : `/workflows/${options.workflowId}/runs`; + + const data = await this.resend.get(url); + return data; + } +} diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index b761f770..afc116d3 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -1,6 +1,7 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; import { parseWorkflowToApiOptions } from '../common/utils/parse-workflow-to-api-options'; import type { Resend } from '../resend'; +import { WorkflowRuns } from '../workflow-runs/workflow-runs'; import type { CreateWorkflowOptions, CreateWorkflowResponse, @@ -26,7 +27,11 @@ import type { } from './interfaces/update-workflow.interface'; export class Workflows { - constructor(private readonly resend: Resend) {} + readonly runs: WorkflowRuns; + + constructor(private readonly resend: Resend) { + this.runs = new WorkflowRuns(this.resend); + } async create( payload: CreateWorkflowOptions, From fcd1f7e60e168b8c1b037862dbe18d102c49df1d Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Thu, 5 Mar 2026 17:21:17 -0300 Subject: [PATCH 09/23] feat: add workflow run steps --- src/index.ts | 1 + .../get-workflow-run-step.interface.ts | 13 ++ src/workflow-run-steps/interfaces/index.ts | 3 + .../list-workflow-run-steps.interface.ts | 17 ++ .../interfaces/workflow-run-step.ts | 18 ++ .../workflow-run-steps.spec.ts | 207 ++++++++++++++++++ src/workflow-run-steps/workflow-run-steps.ts | 38 ++++ src/workflow-runs/interfaces/workflow-run.ts | 1 - src/workflow-runs/workflow-runs.spec.ts | 2 - src/workflow-runs/workflow-runs.ts | 7 +- 10 files changed, 303 insertions(+), 4 deletions(-) create mode 100644 src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts create mode 100644 src/workflow-run-steps/interfaces/index.ts create mode 100644 src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts create mode 100644 src/workflow-run-steps/interfaces/workflow-run-step.ts create mode 100644 src/workflow-run-steps/workflow-run-steps.spec.ts create mode 100644 src/workflow-run-steps/workflow-run-steps.ts diff --git a/src/index.ts b/src/index.ts index 9d9ddb22..0f1a7ef3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,5 +17,6 @@ export * from './segments/interfaces'; export * from './templates/interfaces'; export * from './topics/interfaces'; export * from './webhooks/interfaces'; +export * from './workflow-run-steps/interfaces'; export * from './workflow-runs/interfaces'; export * from './workflows/interfaces'; diff --git a/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts b/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts new file mode 100644 index 00000000..a506f4fe --- /dev/null +++ b/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; +import type { WorkflowRunStep } from './workflow-run-step'; + +export interface GetWorkflowRunStepOptions { + workflowId: string; + runId: string; + stepId: string; +} + +export type GetWorkflowRunStepResponseSuccess = WorkflowRunStep; + +export type GetWorkflowRunStepResponse = + Response; diff --git a/src/workflow-run-steps/interfaces/index.ts b/src/workflow-run-steps/interfaces/index.ts new file mode 100644 index 00000000..3a6724ad --- /dev/null +++ b/src/workflow-run-steps/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './get-workflow-run-step.interface'; +export * from './list-workflow-run-steps.interface'; +export * from './workflow-run-step'; diff --git a/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts b/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts new file mode 100644 index 00000000..179e2bfd --- /dev/null +++ b/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts @@ -0,0 +1,17 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../interfaces'; +import type { WorkflowRunStepItem } from './workflow-run-step'; + +export type ListWorkflowRunStepsOptions = PaginationOptions & { + workflowId: string; + runId: string; +}; + +export type ListWorkflowRunStepsResponseSuccess = + PaginatedData; + +export type ListWorkflowRunStepsResponse = + Response; diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts new file mode 100644 index 00000000..357a688b --- /dev/null +++ b/src/workflow-run-steps/interfaces/workflow-run-step.ts @@ -0,0 +1,18 @@ +export interface WorkflowRunStep { + object: 'workflow_run_step'; + id: string; + step_id: string; + status: string; + started_at: string | null; + completed_at: string | null; + created_at: string; + output: unknown; + error: string | null; + callback_id: string | null; + waiting_for_event_name: string | null; +} + +export type WorkflowRunStepItem = Pick< + WorkflowRunStep, + 'id' | 'step_id' | 'status' | 'started_at' | 'completed_at' | 'created_at' +>; diff --git a/src/workflow-run-steps/workflow-run-steps.spec.ts b/src/workflow-run-steps/workflow-run-steps.spec.ts new file mode 100644 index 00000000..b953a973 --- /dev/null +++ b/src/workflow-run-steps/workflow-run-steps.spec.ts @@ -0,0 +1,207 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + GetWorkflowRunStepOptions, + GetWorkflowRunStepResponseSuccess, +} from './interfaces/get-workflow-run-step.interface'; +import type { + ListWorkflowRunStepsOptions, + ListWorkflowRunStepsResponseSuccess, +} from './interfaces/list-workflow-run-steps.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +describe('WorkflowRunSteps', () => { + afterEach(() => fetchMock.resetMocks()); + afterAll(() => fetchMocker.disableMocks()); + + describe('get', () => { + it('gets a workflow run step', async () => { + const options: GetWorkflowRunStepOptions = { + workflowId: 'wf_123', + runId: 'wr_456', + stepId: 'wrs_789', + }; + const response: GetWorkflowRunStepResponseSuccess = { + object: 'workflow_run_step', + id: 'wrs_789', + step_id: 'step_1', + status: 'completed', + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + output: null, + error: null, + callback_id: null, + waiting_for_event_name: null, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.steps.get(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "callback_id": null, + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "error": null, + "id": "wrs_789", + "object": "workflow_run_step", + "output": null, + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "step_id": "step_1", + "waiting_for_event_name": null, + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: GetWorkflowRunStepOptions = { + workflowId: 'wf_123', + runId: 'wr_456', + stepId: 'wrs_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Workflow run step not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.runs.steps.get(options); + expect(result.error).not.toBeNull(); + }); + }); + + describe('list', () => { + it('lists workflow run steps', async () => { + const options: ListWorkflowRunStepsOptions = { + workflowId: 'wf_123', + runId: 'wr_456', + }; + const response: ListWorkflowRunStepsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wrs_789', + step_id: 'step_1', + status: 'completed', + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.steps.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wrs_789", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "step_id": "step_1", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('lists workflow run steps with pagination', async () => { + const options: ListWorkflowRunStepsOptions = { + workflowId: 'wf_123', + runId: 'wr_456', + limit: 1, + after: 'wrs_cursor', + }; + const response: ListWorkflowRunStepsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wrs_101', + step_id: 'step_2', + status: 'running', + started_at: '2024-01-02T00:00:00.000Z', + completed_at: null, + created_at: '2024-01-02T00:00:00.000Z', + }, + ], + has_more: true, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.workflows.runs.steps.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": null, + "created_at": "2024-01-02T00:00:00.000Z", + "id": "wrs_101", + "started_at": "2024-01-02T00:00:00.000Z", + "status": "running", + "step_id": "step_2", + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: ListWorkflowRunStepsOptions = { + workflowId: 'wf_invalid', + runId: 'wr_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Workflow not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.workflows.runs.steps.list(options); + expect(result.error).not.toBeNull(); + }); + }); +}); diff --git a/src/workflow-run-steps/workflow-run-steps.ts b/src/workflow-run-steps/workflow-run-steps.ts new file mode 100644 index 00000000..6ca9db29 --- /dev/null +++ b/src/workflow-run-steps/workflow-run-steps.ts @@ -0,0 +1,38 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + GetWorkflowRunStepOptions, + GetWorkflowRunStepResponse, + GetWorkflowRunStepResponseSuccess, +} from './interfaces/get-workflow-run-step.interface'; +import type { + ListWorkflowRunStepsOptions, + ListWorkflowRunStepsResponse, + ListWorkflowRunStepsResponseSuccess, +} from './interfaces/list-workflow-run-steps.interface'; + +export class WorkflowRunSteps { + constructor(private readonly resend: Resend) {} + + async get( + options: GetWorkflowRunStepOptions, + ): Promise { + const data = await this.resend.get( + `/workflows/${options.workflowId}/runs/${options.runId}/steps/${options.stepId}`, + ); + return data; + } + + async list( + options: ListWorkflowRunStepsOptions, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/workflows/${options.workflowId}/runs/${options.runId}/steps?${queryString}` + : `/workflows/${options.workflowId}/runs/${options.runId}/steps`; + + const data = + await this.resend.get(url); + return data; + } +} diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts index 788d83c0..a778bd09 100644 --- a/src/workflow-runs/interfaces/workflow-run.ts +++ b/src/workflow-runs/interfaces/workflow-run.ts @@ -2,7 +2,6 @@ export interface WorkflowRun { object: 'workflow_run'; id: string; status: string; - durable_execution_name: string | null; started_at: string | null; completed_at: string | null; created_at: string; diff --git a/src/workflow-runs/workflow-runs.spec.ts b/src/workflow-runs/workflow-runs.spec.ts index 1318ce3e..2ae0e1c4 100644 --- a/src/workflow-runs/workflow-runs.spec.ts +++ b/src/workflow-runs/workflow-runs.spec.ts @@ -30,7 +30,6 @@ describe('WorkflowRuns', () => { object: 'workflow_run', id: 'wr_456', status: 'completed', - durable_execution_name: null, started_at: '2024-01-01T00:00:00.000Z', completed_at: '2024-01-01T00:01:00.000Z', created_at: '2024-01-01T00:00:00.000Z', @@ -46,7 +45,6 @@ describe('WorkflowRuns', () => { "data": { "completed_at": "2024-01-01T00:01:00.000Z", "created_at": "2024-01-01T00:00:00.000Z", - "durable_execution_name": null, "id": "wr_456", "object": "workflow_run", "started_at": "2024-01-01T00:00:00.000Z", diff --git a/src/workflow-runs/workflow-runs.ts b/src/workflow-runs/workflow-runs.ts index ebe6beb9..2c8045ac 100644 --- a/src/workflow-runs/workflow-runs.ts +++ b/src/workflow-runs/workflow-runs.ts @@ -1,5 +1,6 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; import type { Resend } from '../resend'; +import { WorkflowRunSteps } from '../workflow-run-steps/workflow-run-steps'; import type { GetWorkflowRunOptions, GetWorkflowRunResponse, @@ -12,7 +13,11 @@ import type { } from './interfaces/list-workflow-runs.interface'; export class WorkflowRuns { - constructor(private readonly resend: Resend) {} + readonly steps: WorkflowRunSteps; + + constructor(private readonly resend: Resend) { + this.steps = new WorkflowRunSteps(resend); + } async get(options: GetWorkflowRunOptions): Promise { const data = await this.resend.get( From 1d6e316a1f5cb9e01bb092b637dbc421ce4bf627 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Thu, 5 Mar 2026 17:40:08 -0300 Subject: [PATCH 10/23] fix: remove implementation details --- src/workflow-run-steps/interfaces/workflow-run-step.ts | 2 -- src/workflow-run-steps/workflow-run-steps.spec.ts | 4 ---- 2 files changed, 6 deletions(-) diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts index 357a688b..14366757 100644 --- a/src/workflow-run-steps/interfaces/workflow-run-step.ts +++ b/src/workflow-run-steps/interfaces/workflow-run-step.ts @@ -8,8 +8,6 @@ export interface WorkflowRunStep { created_at: string; output: unknown; error: string | null; - callback_id: string | null; - waiting_for_event_name: string | null; } export type WorkflowRunStepItem = Pick< diff --git a/src/workflow-run-steps/workflow-run-steps.spec.ts b/src/workflow-run-steps/workflow-run-steps.spec.ts index b953a973..4119b96e 100644 --- a/src/workflow-run-steps/workflow-run-steps.spec.ts +++ b/src/workflow-run-steps/workflow-run-steps.spec.ts @@ -37,8 +37,6 @@ describe('WorkflowRunSteps', () => { created_at: '2024-01-01T00:00:00.000Z', output: null, error: null, - callback_id: null, - waiting_for_event_name: null, }; mockSuccessResponse(response, {}); @@ -49,7 +47,6 @@ describe('WorkflowRunSteps', () => { ).resolves.toMatchInlineSnapshot(` { "data": { - "callback_id": null, "completed_at": "2024-01-01T00:01:00.000Z", "created_at": "2024-01-01T00:00:00.000Z", "error": null, @@ -59,7 +56,6 @@ describe('WorkflowRunSteps', () => { "started_at": "2024-01-01T00:00:00.000Z", "status": "completed", "step_id": "step_1", - "waiting_for_event_name": null, }, "error": null, "headers": { From 46f35a66d290244cc5d2a37ce5917931ccf1218e Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 15:41:58 -0300 Subject: [PATCH 11/23] feat: add steps payload --- .../list-workflow-run-steps.interface.ts | 5 +++-- .../interfaces/workflow-run-step.ts | 12 +++++++++--- .../workflow-run-steps.spec.ts | 14 ++++++++++---- src/workflow-runs/interfaces/workflow-run.ts | 8 +++++++- src/workflow-runs/workflow-runs.spec.ts | 16 ++++++++++++++++ 5 files changed, 45 insertions(+), 10 deletions(-) diff --git a/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts b/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts index 179e2bfd..ef24d5df 100644 --- a/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts +++ b/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts @@ -10,8 +10,9 @@ export type ListWorkflowRunStepsOptions = PaginationOptions & { runId: string; }; -export type ListWorkflowRunStepsResponseSuccess = - PaginatedData; +export type ListWorkflowRunStepsResponseSuccess = PaginatedData< + WorkflowRunStepItem[] +>; export type ListWorkflowRunStepsResponse = Response; diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts index 14366757..132cbbdb 100644 --- a/src/workflow-run-steps/interfaces/workflow-run-step.ts +++ b/src/workflow-run-steps/interfaces/workflow-run-step.ts @@ -2,15 +2,21 @@ export interface WorkflowRunStep { object: 'workflow_run_step'; id: string; step_id: string; + type: string; + config: unknown; status: string; started_at: string | null; completed_at: string | null; created_at: string; - output: unknown; - error: string | null; } export type WorkflowRunStepItem = Pick< WorkflowRunStep, - 'id' | 'step_id' | 'status' | 'started_at' | 'completed_at' | 'created_at' + | 'id' + | 'step_id' + | 'type' + | 'status' + | 'started_at' + | 'completed_at' + | 'created_at' >; diff --git a/src/workflow-run-steps/workflow-run-steps.spec.ts b/src/workflow-run-steps/workflow-run-steps.spec.ts index 4119b96e..60f4d031 100644 --- a/src/workflow-run-steps/workflow-run-steps.spec.ts +++ b/src/workflow-run-steps/workflow-run-steps.spec.ts @@ -31,12 +31,12 @@ describe('WorkflowRunSteps', () => { object: 'workflow_run_step', id: 'wrs_789', step_id: 'step_1', + type: 'trigger', + config: { event_name: 'user.created' }, status: 'completed', started_at: '2024-01-01T00:00:00.000Z', completed_at: '2024-01-01T00:01:00.000Z', created_at: '2024-01-01T00:00:00.000Z', - output: null, - error: null, }; mockSuccessResponse(response, {}); @@ -48,14 +48,16 @@ describe('WorkflowRunSteps', () => { { "data": { "completed_at": "2024-01-01T00:01:00.000Z", + "config": { + "event_name": "user.created", + }, "created_at": "2024-01-01T00:00:00.000Z", - "error": null, "id": "wrs_789", "object": "workflow_run_step", - "output": null, "started_at": "2024-01-01T00:00:00.000Z", "status": "completed", "step_id": "step_1", + "type": "trigger", }, "error": null, "headers": { @@ -95,6 +97,7 @@ describe('WorkflowRunSteps', () => { { id: 'wrs_789', step_id: 'step_1', + type: 'trigger', status: 'completed', started_at: '2024-01-01T00:00:00.000Z', completed_at: '2024-01-01T00:01:00.000Z', @@ -120,6 +123,7 @@ describe('WorkflowRunSteps', () => { "started_at": "2024-01-01T00:00:00.000Z", "status": "completed", "step_id": "step_1", + "type": "trigger", }, ], "has_more": false, @@ -146,6 +150,7 @@ describe('WorkflowRunSteps', () => { { id: 'wrs_101', step_id: 'step_2', + type: 'send_email', status: 'running', started_at: '2024-01-02T00:00:00.000Z', completed_at: null, @@ -171,6 +176,7 @@ describe('WorkflowRunSteps', () => { "started_at": "2024-01-02T00:00:00.000Z", "status": "running", "step_id": "step_2", + "type": "send_email", }, ], "has_more": true, diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts index a778bd09..e54c2a76 100644 --- a/src/workflow-runs/interfaces/workflow-run.ts +++ b/src/workflow-runs/interfaces/workflow-run.ts @@ -1,7 +1,13 @@ +export interface WorkflowRunTrigger { + event_name: string; + payload?: Record; +} + export interface WorkflowRun { object: 'workflow_run'; id: string; status: string; + trigger: WorkflowRunTrigger | null; started_at: string | null; completed_at: string | null; created_at: string; @@ -9,5 +15,5 @@ export interface WorkflowRun { export type WorkflowRunItem = Pick< WorkflowRun, - 'id' | 'status' | 'started_at' | 'completed_at' | 'created_at' + 'id' | 'status' | 'trigger' | 'started_at' | 'completed_at' | 'created_at' >; diff --git a/src/workflow-runs/workflow-runs.spec.ts b/src/workflow-runs/workflow-runs.spec.ts index 2ae0e1c4..65e6a624 100644 --- a/src/workflow-runs/workflow-runs.spec.ts +++ b/src/workflow-runs/workflow-runs.spec.ts @@ -30,6 +30,10 @@ describe('WorkflowRuns', () => { object: 'workflow_run', id: 'wr_456', status: 'completed', + trigger: { + event_name: 'user.created', + payload: { email: 'jane@example.com' }, + }, started_at: '2024-01-01T00:00:00.000Z', completed_at: '2024-01-01T00:01:00.000Z', created_at: '2024-01-01T00:00:00.000Z', @@ -49,6 +53,12 @@ describe('WorkflowRuns', () => { "object": "workflow_run", "started_at": "2024-01-01T00:00:00.000Z", "status": "completed", + "trigger": { + "event_name": "user.created", + "payload": { + "email": "jane@example.com", + }, + }, }, "error": null, "headers": { @@ -86,6 +96,7 @@ describe('WorkflowRuns', () => { { id: 'wr_456', status: 'completed', + trigger: { event_name: 'user.created' }, started_at: '2024-01-01T00:00:00.000Z', completed_at: '2024-01-01T00:01:00.000Z', created_at: '2024-01-01T00:00:00.000Z', @@ -109,6 +120,9 @@ describe('WorkflowRuns', () => { "id": "wr_456", "started_at": "2024-01-01T00:00:00.000Z", "status": "completed", + "trigger": { + "event_name": "user.created", + }, }, ], "has_more": false, @@ -134,6 +148,7 @@ describe('WorkflowRuns', () => { { id: 'wr_789', status: 'running', + trigger: null, started_at: '2024-01-02T00:00:00.000Z', completed_at: null, created_at: '2024-01-02T00:00:00.000Z', @@ -157,6 +172,7 @@ describe('WorkflowRuns', () => { "id": "wr_789", "started_at": "2024-01-02T00:00:00.000Z", "status": "running", + "trigger": null, }, ], "has_more": true, From 216d6651ba629e2445742a5541fbec77b34fcfcc Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 16:20:21 -0300 Subject: [PATCH 12/23] feat: improve workflow types --- .../interfaces/workflow-run-step.ts | 6 ++++-- src/workflow-runs/interfaces/workflow-run.ts | 4 +++- .../interfaces/workflow-step.interface.ts | 18 +++++++++++++----- 3 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts index 132cbbdb..7d0415fe 100644 --- a/src/workflow-run-steps/interfaces/workflow-run-step.ts +++ b/src/workflow-run-steps/interfaces/workflow-run-step.ts @@ -1,9 +1,11 @@ +import type { WorkflowStepType } from '../../workflows/interfaces/workflow-step.interface'; + export interface WorkflowRunStep { object: 'workflow_run_step'; id: string; step_id: string; - type: string; - config: unknown; + type: WorkflowStepType; + config: Record; status: string; started_at: string | null; completed_at: string | null; diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts index e54c2a76..d5153389 100644 --- a/src/workflow-runs/interfaces/workflow-run.ts +++ b/src/workflow-runs/interfaces/workflow-run.ts @@ -3,10 +3,12 @@ export interface WorkflowRunTrigger { payload?: Record; } +export type WorkflowRunStatus = 'running' | 'completed' | 'failed' | 'cancelled'; + export interface WorkflowRun { object: 'workflow_run'; id: string; - status: string; + status: WorkflowRunStatus; trigger: WorkflowRunTrigger | null; started_at: string | null; completed_at: string | null; diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts index d48991ce..7988b592 100644 --- a/src/workflows/interfaces/workflow-step.interface.ts +++ b/src/workflows/interfaces/workflow-step.interface.ts @@ -21,7 +21,9 @@ export type ConditionRule = type: 'rule'; field: string; operator: 'exists' | 'is_empty'; - }; + } + | { type: 'and'; rules: ConditionRule[] } + | { type: 'or'; rules: ConditionRule[] }; export type TemplateVariableValue = | string @@ -77,16 +79,22 @@ export interface WorkflowEdge { edgeType?: WorkflowEdgeType; } +export type WorkflowStepType = + | 'trigger' + | 'delay' + | 'send_email' + | 'wait_for_event' + | 'condition'; + export interface WorkflowResponseStep { id: string; - type: string; - config: unknown; - ref?: string; + type: WorkflowStepType; + config: Record; } export interface WorkflowResponseEdge { id: string; from_step_id: string; to_step_id: string; - edge_type: string; + edge_type: WorkflowEdgeType; } From c09435ef5adeb7dcee1058a00899be2f938265a3 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 16:45:24 -0300 Subject: [PATCH 13/23] chore: bump package --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 95a450c4..b9e66e2a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.10.0-preview-workflows.0", + "version": "6.10.0-preview-workflows.1", "description": "Node.js library for the Resend API", "main": "./dist/index.cjs", "module": "./dist/index.mjs", From e9640e4b60d73aa480f2531a3a6472043de45c44 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 16:49:27 -0300 Subject: [PATCH 14/23] fix:lint --- src/workflow-runs/interfaces/workflow-run.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts index d5153389..46ba8a8f 100644 --- a/src/workflow-runs/interfaces/workflow-run.ts +++ b/src/workflow-runs/interfaces/workflow-run.ts @@ -3,7 +3,11 @@ export interface WorkflowRunTrigger { payload?: Record; } -export type WorkflowRunStatus = 'running' | 'completed' | 'failed' | 'cancelled'; +export type WorkflowRunStatus = + | 'running' + | 'completed' + | 'failed' + | 'cancelled'; export interface WorkflowRun { object: 'workflow_run'; From 50076598052d47ec0e458b1a77cb52b4ac547eab Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 16:53:28 -0300 Subject: [PATCH 15/23] fix: types --- src/workflow-run-steps/interfaces/workflow-run-step.ts | 10 +++++++++- src/workflows/interfaces/workflow-step.interface.ts | 7 +------ 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts index 7d0415fe..4313ea3a 100644 --- a/src/workflow-run-steps/interfaces/workflow-run-step.ts +++ b/src/workflow-run-steps/interfaces/workflow-run-step.ts @@ -1,12 +1,20 @@ import type { WorkflowStepType } from '../../workflows/interfaces/workflow-step.interface'; +export type WorkflowRunStepStatus = + | 'pending' + | 'running' + | 'completed' + | 'failed' + | 'skipped' + | 'waiting'; + export interface WorkflowRunStep { object: 'workflow_run_step'; id: string; step_id: string; type: WorkflowStepType; config: Record; - status: string; + status: WorkflowRunStepStatus; started_at: string | null; completed_at: string | null; created_at: string; diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts index 7988b592..1ddf97ce 100644 --- a/src/workflows/interfaces/workflow-step.interface.ts +++ b/src/workflows/interfaces/workflow-step.interface.ts @@ -79,12 +79,7 @@ export interface WorkflowEdge { edgeType?: WorkflowEdgeType; } -export type WorkflowStepType = - | 'trigger' - | 'delay' - | 'send_email' - | 'wait_for_event' - | 'condition'; +export type WorkflowStepType = WorkflowStep['type']; export interface WorkflowResponseStep { id: string; From 557f8bd17d12aeb3d8962abbb226c59b89b0169e Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 16:59:20 -0300 Subject: [PATCH 16/23] fix: remove type repetition --- .../interfaces/create-workflow-options.interface.ts | 3 ++- src/workflows/interfaces/list-workflows.interface.ts | 11 +++++------ src/workflows/interfaces/update-workflow.interface.ts | 8 ++++---- src/workflows/interfaces/workflow.ts | 2 ++ 4 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts index 66416728..ce256ea4 100644 --- a/src/workflows/interfaces/create-workflow-options.interface.ts +++ b/src/workflows/interfaces/create-workflow-options.interface.ts @@ -1,9 +1,10 @@ import type { Response } from '../../interfaces'; import type { WorkflowEdge, WorkflowStep } from './workflow-step.interface'; +import type { WorkflowStatus } from './workflow'; export interface CreateWorkflowOptions { name: string; - status?: 'enabled' | 'disabled'; + status?: WorkflowStatus; steps: WorkflowStep[]; edges: WorkflowEdge[]; } diff --git a/src/workflows/interfaces/list-workflows.interface.ts b/src/workflows/interfaces/list-workflows.interface.ts index c5396f1b..19efa6a4 100644 --- a/src/workflows/interfaces/list-workflows.interface.ts +++ b/src/workflows/interfaces/list-workflows.interface.ts @@ -1,13 +1,12 @@ -import type { PaginationOptions } from '../../common/interfaces'; +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; import type { Response } from '../../interfaces'; import type { Workflow } from './workflow'; export type ListWorkflowsOptions = PaginationOptions; -export interface ListWorkflowsResponseSuccess { - object: 'list'; - has_more: boolean; - data: Workflow[]; -} +export type ListWorkflowsResponseSuccess = PaginatedData; export type ListWorkflowsResponse = Response; diff --git a/src/workflows/interfaces/update-workflow.interface.ts b/src/workflows/interfaces/update-workflow.interface.ts index 4d3572ea..ea855e4d 100644 --- a/src/workflows/interfaces/update-workflow.interface.ts +++ b/src/workflows/interfaces/update-workflow.interface.ts @@ -1,13 +1,13 @@ import type { Response } from '../../interfaces'; +import type { Workflow, WorkflowStatus } from './workflow'; export interface UpdateWorkflowOptions { - status: 'enabled' | 'disabled'; + status: WorkflowStatus; } -export interface UpdateWorkflowResponseSuccess { +export interface UpdateWorkflowResponseSuccess + extends Pick { object: 'workflow'; - id: string; - status: 'enabled' | 'disabled'; } export type UpdateWorkflowResponse = Response; diff --git a/src/workflows/interfaces/workflow.ts b/src/workflows/interfaces/workflow.ts index c07865ff..f361d16e 100644 --- a/src/workflows/interfaces/workflow.ts +++ b/src/workflows/interfaces/workflow.ts @@ -5,3 +5,5 @@ export interface Workflow { created_at: string; updated_at: string | null; } + +export type WorkflowStatus = Workflow['status']; From fe596d9a7a8355e78a212c5f86f7c65082034dc3 Mon Sep 17 00:00:00 2001 From: Felipe Freitag Date: Fri, 6 Mar 2026 17:03:54 -0300 Subject: [PATCH 17/23] fix: lint --- src/workflows/interfaces/create-workflow-options.interface.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts index ce256ea4..12685632 100644 --- a/src/workflows/interfaces/create-workflow-options.interface.ts +++ b/src/workflows/interfaces/create-workflow-options.interface.ts @@ -1,6 +1,6 @@ import type { Response } from '../../interfaces'; -import type { WorkflowEdge, WorkflowStep } from './workflow-step.interface'; import type { WorkflowStatus } from './workflow'; +import type { WorkflowEdge, WorkflowStep } from './workflow-step.interface'; export interface CreateWorkflowOptions { name: string; From 33a856a3ff3b61c5cd68526174bb39756f3e10a9 Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Wed, 25 Mar 2026 13:01:37 -0700 Subject: [PATCH 18/23] move to automations --- .../automation-run-steps.spec.ts | 207 +++++++++ .../automation-run-steps.ts | 38 ++ .../interfaces/automation-run-step.ts | 32 ++ .../get-automation-run-step.interface.ts | 13 + src/automation-run-steps/interfaces/index.ts | 3 + .../list-automation-run-steps.interface.ts | 18 + src/automation-runs/automation-runs.spec.ts | 202 +++++++++ src/automation-runs/automation-runs.ts | 42 ++ .../interfaces/automation-run.ts | 25 ++ .../get-automation-run.interface.ts | 12 + src/automation-runs/interfaces/index.ts | 3 + .../list-automation-runs.interface.ts | 17 + src/automations/automations.spec.ts | 403 ++++++++++++++++++ src/automations/automations.ts | 81 ++++ .../interfaces/automation-step.interface.ts | 95 +++++ src/automations/interfaces/automation.ts | 9 + .../create-automation-options.interface.ts | 21 + .../interfaces/get-automation.interface.ts | 14 + src/automations/interfaces/index.ts | 7 + .../interfaces/list-automation.interface.ts | 12 + .../interfaces/remove-automation.interface.ts | 11 + .../interfaces/update-automation.interface.ts | 14 + ...> parse-automation-to-api-options.spec.ts} | 28 +- ....ts => parse-automation-to-api-options.ts} | 48 +-- src/events/events.ts | 4 +- src/index.ts | 3 + src/resend.ts | 2 + .../interfaces/workflow-step.interface.ts | 14 +- src/workflows/workflows.spec.ts | 2 - src/workflows/workflows.ts | 4 +- 30 files changed, 1333 insertions(+), 51 deletions(-) create mode 100644 src/automation-run-steps/automation-run-steps.spec.ts create mode 100644 src/automation-run-steps/automation-run-steps.ts create mode 100644 src/automation-run-steps/interfaces/automation-run-step.ts create mode 100644 src/automation-run-steps/interfaces/get-automation-run-step.interface.ts create mode 100644 src/automation-run-steps/interfaces/index.ts create mode 100644 src/automation-run-steps/interfaces/list-automation-run-steps.interface.ts create mode 100644 src/automation-runs/automation-runs.spec.ts create mode 100644 src/automation-runs/automation-runs.ts create mode 100644 src/automation-runs/interfaces/automation-run.ts create mode 100644 src/automation-runs/interfaces/get-automation-run.interface.ts create mode 100644 src/automation-runs/interfaces/index.ts create mode 100644 src/automation-runs/interfaces/list-automation-runs.interface.ts create mode 100644 src/automations/automations.spec.ts create mode 100644 src/automations/automations.ts create mode 100644 src/automations/interfaces/automation-step.interface.ts create mode 100644 src/automations/interfaces/automation.ts create mode 100644 src/automations/interfaces/create-automation-options.interface.ts create mode 100644 src/automations/interfaces/get-automation.interface.ts create mode 100644 src/automations/interfaces/index.ts create mode 100644 src/automations/interfaces/list-automation.interface.ts create mode 100644 src/automations/interfaces/remove-automation.interface.ts create mode 100644 src/automations/interfaces/update-automation.interface.ts rename src/common/utils/{parse-workflow-to-api-options.spec.ts => parse-automation-to-api-options.spec.ts} (87%) rename src/common/utils/{parse-workflow-to-api-options.ts => parse-automation-to-api-options.ts} (62%) diff --git a/src/automation-run-steps/automation-run-steps.spec.ts b/src/automation-run-steps/automation-run-steps.spec.ts new file mode 100644 index 00000000..dfa6bf89 --- /dev/null +++ b/src/automation-run-steps/automation-run-steps.spec.ts @@ -0,0 +1,207 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + GetAutomationRunStepOptions, + GetAutomationRunStepResponseSuccess, +} from './interfaces/get-automation-run-step.interface'; +import type { + ListAutomationRunStepsOptions, + ListAutomationRunStepsResponseSuccess, +} from './interfaces/list-automation-run-steps.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +afterEach(() => fetchMock.resetMocks()); +afterAll(() => fetchMocker.disableMocks()); + +describe('get', () => { + it('gets a automation run step', async () => { + const options: GetAutomationRunStepOptions = { + automationId: 'wf_123', + runId: 'wr_456', + stepId: 'wrs_789', + }; + const response: GetAutomationRunStepResponseSuccess = { + object: 'automation_run_step', + id: 'wrs_789', + step_id: 'step_1', + type: 'trigger', + config: { event_name: 'user.created' }, + status: 'completed', + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.steps.get(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "completed_at": "2024-01-01T00:01:00.000Z", + "config": { + "event_name": "user.created", + }, + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wrs_789", + "object": "automation_run_step", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "step_id": "step_1", + "type": "trigger", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: GetAutomationRunStepOptions = { + automationId: 'wf_123', + runId: 'wr_456', + stepId: 'wrs_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Automation run step not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.runs.steps.get(options); + expect(result.error).not.toBeNull(); + }); +}); + +describe('list', () => { + it('lists automation run steps', async () => { + const options: ListAutomationRunStepsOptions = { + automationId: 'wf_123', + runId: 'wr_456', + }; + const response: ListAutomationRunStepsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wrs_789', + step_id: 'step_1', + type: 'trigger', + status: 'completed', + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.steps.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wrs_789", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "step_id": "step_1", + "type": "trigger", + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('lists automation run steps with pagination', async () => { + const options: ListAutomationRunStepsOptions = { + automationId: 'wf_123', + runId: 'wr_456', + limit: 1, + after: 'wrs_cursor', + }; + const response: ListAutomationRunStepsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wrs_101', + step_id: 'step_2', + type: 'send_email', + status: 'running', + started_at: '2024-01-02T00:00:00.000Z', + completed_at: null, + created_at: '2024-01-02T00:00:00.000Z', + }, + ], + has_more: true, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.steps.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": null, + "created_at": "2024-01-02T00:00:00.000Z", + "id": "wrs_101", + "started_at": "2024-01-02T00:00:00.000Z", + "status": "running", + "step_id": "step_2", + "type": "send_email", + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: ListAutomationRunStepsOptions = { + automationId: 'wf_invalid', + runId: 'wr_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Automation not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.runs.steps.list(options); + expect(result.error).not.toBeNull(); + }); +}); diff --git a/src/automation-run-steps/automation-run-steps.ts b/src/automation-run-steps/automation-run-steps.ts new file mode 100644 index 00000000..5b2b33d1 --- /dev/null +++ b/src/automation-run-steps/automation-run-steps.ts @@ -0,0 +1,38 @@ +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + GetAutomationRunStepOptions, + GetAutomationRunStepResponse, + GetAutomationRunStepResponseSuccess, +} from './interfaces/get-automation-run-step.interface'; +import type { + ListAutomationRunStepsOptions, + ListAutomationRunStepsResponse, + ListAutomationRunStepsResponseSuccess, +} from './interfaces/list-automation-run-steps.interface'; + +export class AutomationRunSteps { + constructor(private readonly resend: Resend) {} + + async get( + options: GetAutomationRunStepOptions, + ): Promise { + const data = await this.resend.get( + `/automations/${options.automationId}/runs/${options.runId}/steps/${options.stepId}`, + ); + return data; + } + + async list( + options: ListAutomationRunStepsOptions, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/automations/${options.automationId}/runs/${options.runId}/steps?${queryString}` + : `/automations/${options.automationId}/runs/${options.runId}/steps`; + + const data = + await this.resend.get(url); + return data; + } +} diff --git a/src/automation-run-steps/interfaces/automation-run-step.ts b/src/automation-run-steps/interfaces/automation-run-step.ts new file mode 100644 index 00000000..5fa5dcaf --- /dev/null +++ b/src/automation-run-steps/interfaces/automation-run-step.ts @@ -0,0 +1,32 @@ +import type { AutomationStepType } from '../../automations/interfaces/automation-step.interface'; + +export type AutomationRunStepStatus = + | 'pending' + | 'running' + | 'completed' + | 'failed' + | 'skipped' + | 'waiting'; + +export interface AutomationRunStep { + object: 'automation_run_step'; + id: string; + step_id: string; + type: AutomationStepType; + config: Record; + status: AutomationRunStepStatus; + started_at: string | null; + completed_at: string | null; + created_at: string; +} + +export type AutomationRunStepItem = Pick< + AutomationRunStep, + | 'id' + | 'step_id' + | 'type' + | 'status' + | 'started_at' + | 'completed_at' + | 'created_at' +>; diff --git a/src/automation-run-steps/interfaces/get-automation-run-step.interface.ts b/src/automation-run-steps/interfaces/get-automation-run-step.interface.ts new file mode 100644 index 00000000..fbf9e44d --- /dev/null +++ b/src/automation-run-steps/interfaces/get-automation-run-step.interface.ts @@ -0,0 +1,13 @@ +import type { Response } from '../../interfaces'; +import type { AutomationRunStep } from './automation-run-step'; + +export interface GetAutomationRunStepOptions { + automationId: string; + runId: string; + stepId: string; +} + +export type GetAutomationRunStepResponseSuccess = AutomationRunStep; + +export type GetAutomationRunStepResponse = + Response; diff --git a/src/automation-run-steps/interfaces/index.ts b/src/automation-run-steps/interfaces/index.ts new file mode 100644 index 00000000..c7cd82ad --- /dev/null +++ b/src/automation-run-steps/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './automation-run-step'; +export * from './get-automation-run-step.interface'; +export * from './list-automation-run-steps.interface'; diff --git a/src/automation-run-steps/interfaces/list-automation-run-steps.interface.ts b/src/automation-run-steps/interfaces/list-automation-run-steps.interface.ts new file mode 100644 index 00000000..7e1c49da --- /dev/null +++ b/src/automation-run-steps/interfaces/list-automation-run-steps.interface.ts @@ -0,0 +1,18 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../interfaces'; +import type { AutomationRunStepItem } from './automation-run-step'; + +export type ListAutomationRunStepsOptions = PaginationOptions & { + automationId: string; + runId: string; +}; + +export type ListAutomationRunStepsResponseSuccess = PaginatedData< + AutomationRunStepItem[] +>; + +export type ListAutomationRunStepsResponse = + Response; diff --git a/src/automation-runs/automation-runs.spec.ts b/src/automation-runs/automation-runs.spec.ts new file mode 100644 index 00000000..766d0194 --- /dev/null +++ b/src/automation-runs/automation-runs.spec.ts @@ -0,0 +1,202 @@ +import createFetchMock from 'vitest-fetch-mock'; +import { Resend } from '../resend'; +import { + mockErrorResponse, + mockSuccessResponse, +} from '../test-utils/mock-fetch'; +import type { + GetAutomationRunOptions, + GetAutomationRunResponseSuccess, +} from './interfaces/get-automation-run.interface'; +import type { + ListAutomationRunsOptions, + ListAutomationRunsResponseSuccess, +} from './interfaces/list-automation-runs.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +afterEach(() => fetchMock.resetMocks()); +afterAll(() => fetchMocker.disableMocks()); + +describe('get', () => { + it('gets an automation run', async () => { + const options: GetAutomationRunOptions = { + automationId: 'wf_123', + runId: 'wr_456', + }; + const response: GetAutomationRunResponseSuccess = { + object: 'automation_run', + id: 'wr_456', + status: 'completed', + trigger: { + event_name: 'user.created', + payload: { email: 'jane@example.com' }, + }, + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.get(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wr_456", + "object": "automation_run", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "trigger": { + "event_name": "user.created", + "payload": { + "email": "jane@example.com", + }, + }, + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: GetAutomationRunOptions = { + automationId: 'wf_123', + runId: 'wr_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Automation run not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.runs.get(options); + expect(result.error).not.toBeNull(); + }); +}); + +describe('list', () => { + it('lists automation runs', async () => { + const options: ListAutomationRunsOptions = { + automationId: 'wf_123', + }; + const response: ListAutomationRunsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wr_456', + status: 'completed', + trigger: { event_name: 'user.created' }, + started_at: '2024-01-01T00:00:00.000Z', + completed_at: '2024-01-01T00:01:00.000Z', + created_at: '2024-01-01T00:00:00.000Z', + }, + ], + has_more: false, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": "2024-01-01T00:01:00.000Z", + "created_at": "2024-01-01T00:00:00.000Z", + "id": "wr_456", + "started_at": "2024-01-01T00:00:00.000Z", + "status": "completed", + "trigger": { + "event_name": "user.created", + }, + }, + ], + "has_more": false, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('lists automation runs with pagination', async () => { + const options: ListAutomationRunsOptions = { + automationId: 'wf_123', + limit: 1, + after: 'wr_cursor', + }; + const response: ListAutomationRunsResponseSuccess = { + object: 'list', + data: [ + { + id: 'wr_789', + status: 'running', + trigger: null, + started_at: '2024-01-02T00:00:00.000Z', + completed_at: null, + created_at: '2024-01-02T00:00:00.000Z', + }, + ], + has_more: true, + }; + + mockSuccessResponse(response, {}); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + await expect( + resend.automations.runs.list(options), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "data": [ + { + "completed_at": null, + "created_at": "2024-01-02T00:00:00.000Z", + "id": "wr_789", + "started_at": "2024-01-02T00:00:00.000Z", + "status": "running", + "trigger": null, + }, + ], + "has_more": true, + "object": "list", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('returns error', async () => { + const options: ListAutomationRunsOptions = { + automationId: 'wf_invalid', + }; + + mockErrorResponse( + { name: 'not_found', message: 'Automation not found' }, + {}, + ); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.runs.list(options); + expect(result.error).not.toBeNull(); + }); +}); diff --git a/src/automation-runs/automation-runs.ts b/src/automation-runs/automation-runs.ts new file mode 100644 index 00000000..e3094870 --- /dev/null +++ b/src/automation-runs/automation-runs.ts @@ -0,0 +1,42 @@ +import { AutomationRunSteps } from '../automation-run-steps/automation-run-steps'; +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import type { Resend } from '../resend'; +import type { + GetAutomationRunOptions, + GetAutomationRunResponse, + GetAutomationRunResponseSuccess, +} from './interfaces/get-automation-run.interface'; +import type { + ListAutomationRunsOptions, + ListAutomationRunsResponse, + ListAutomationRunsResponseSuccess, +} from './interfaces/list-automation-runs.interface'; + +export class AutomationRuns { + readonly steps: AutomationRunSteps; + + constructor(private readonly resend: Resend) { + this.steps = new AutomationRunSteps(resend); + } + + async get( + options: GetAutomationRunOptions, + ): Promise { + const data = await this.resend.get( + `/automations/${options.automationId}/runs/${options.runId}`, + ); + return data; + } + + async list( + options: ListAutomationRunsOptions, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString + ? `/automations/${options.automationId}/runs?${queryString}` + : `/automations/${options.automationId}/runs`; + + const data = await this.resend.get(url); + return data; + } +} diff --git a/src/automation-runs/interfaces/automation-run.ts b/src/automation-runs/interfaces/automation-run.ts new file mode 100644 index 00000000..b2edf8a6 --- /dev/null +++ b/src/automation-runs/interfaces/automation-run.ts @@ -0,0 +1,25 @@ +export interface AutomationRunTrigger { + event_name: string; + payload?: Record; +} + +export type AutomationRunStatus = + | 'running' + | 'completed' + | 'failed' + | 'cancelled'; + +export interface AutomationRun { + object: 'automation_run'; + id: string; + status: AutomationRunStatus; + trigger: AutomationRunTrigger | null; + started_at: string | null; + completed_at: string | null; + created_at: string; +} + +export type AutomationRunItem = Pick< + AutomationRun, + 'id' | 'status' | 'trigger' | 'started_at' | 'completed_at' | 'created_at' +>; diff --git a/src/automation-runs/interfaces/get-automation-run.interface.ts b/src/automation-runs/interfaces/get-automation-run.interface.ts new file mode 100644 index 00000000..4a981840 --- /dev/null +++ b/src/automation-runs/interfaces/get-automation-run.interface.ts @@ -0,0 +1,12 @@ +import type { Response } from '../../interfaces'; +import type { AutomationRun } from './automation-run'; + +export interface GetAutomationRunOptions { + automationId: string; + runId: string; +} + +export type GetAutomationRunResponseSuccess = AutomationRun; + +export type GetAutomationRunResponse = + Response; diff --git a/src/automation-runs/interfaces/index.ts b/src/automation-runs/interfaces/index.ts new file mode 100644 index 00000000..6bc5e2cd --- /dev/null +++ b/src/automation-runs/interfaces/index.ts @@ -0,0 +1,3 @@ +export * from './automation-run'; +export * from './get-automation-run.interface'; +export * from './list-automation-runs.interface'; diff --git a/src/automation-runs/interfaces/list-automation-runs.interface.ts b/src/automation-runs/interfaces/list-automation-runs.interface.ts new file mode 100644 index 00000000..c07637e1 --- /dev/null +++ b/src/automation-runs/interfaces/list-automation-runs.interface.ts @@ -0,0 +1,17 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../interfaces'; +import type { AutomationRunItem } from './automation-run'; + +export type ListAutomationRunsOptions = PaginationOptions & { + automationId: string; +}; + +export type ListAutomationRunsResponseSuccess = PaginatedData< + AutomationRunItem[] +>; + +export type ListAutomationRunsResponse = + Response; diff --git a/src/automations/automations.spec.ts b/src/automations/automations.spec.ts new file mode 100644 index 00000000..dfe778da --- /dev/null +++ b/src/automations/automations.spec.ts @@ -0,0 +1,403 @@ +import createFetchMock from 'vitest-fetch-mock'; +import type { ErrorResponse } from '../interfaces'; +import { Resend } from '../resend'; +import { mockSuccessResponse } from '../test-utils/mock-fetch'; +import type { + CreateAutomationOptions, + CreateAutomationResponseSuccess, +} from './interfaces/create-automation-options.interface'; +import type { GetAutomationResponseSuccess } from './interfaces/get-automation.interface'; +import type { ListAutomationsResponseSuccess } from './interfaces/list-automation.interface'; +import type { RemoveAutomationResponseSuccess } from './interfaces/remove-automation.interface'; +import type { UpdateAutomationResponseSuccess } from './interfaces/update-automation.interface'; + +const fetchMocker = createFetchMock(vi); +fetchMocker.enableMocks(); + +const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + +afterEach(() => fetchMock.resetMocks()); +afterAll(() => fetchMocker.disableMocks()); + +describe('create', () => { + it('creates an automation', async () => { + const response: CreateAutomationResponseSuccess = { + object: 'automation', + id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const payload: CreateAutomationOptions = { + name: 'Welcome Flow', + status: 'enabled', + steps: [ + { + ref: 'trigger', + type: 'trigger', + config: { eventName: 'user.created' }, + }, + { + ref: 'welcome_email', + type: 'send_email', + config: { templateId: 'tpl-123' }, + }, + ], + edges: [{ from: 'trigger', to: 'welcome_email', edgeType: 'default' }], + }; + + const data = await resend.automations.create(payload); + expect(data).toMatchInlineSnapshot(` + { + "data": { + "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", + "object": "automation", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + + it('throws an error when an ErrorResponse is returned', async () => { + const response: ErrorResponse = { + name: 'missing_required_field', + statusCode: 422, + message: 'Missing `name` field.', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 422, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.automations.create({ + name: '', + steps: [], + edges: [], + }); + expect(data).toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Missing \`name\` field.", + "name": "missing_required_field", + "statusCode": 422, + }, + "headers": { + "content-type": "application/json", + }, + } + `); + }); +}); + +describe('list', () => { + const response: ListAutomationsResponseSuccess = { + object: 'list', + has_more: false, + data: [ + { + id: '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794', + name: 'Welcome Flow', + status: 'enabled', + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + }, + { + id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + name: 'Onboarding Flow', + status: 'disabled', + created_at: '2025-02-01T00:00:00.000Z', + updated_at: '2025-02-01T00:00:00.000Z', + }, + ], + }; + + describe('when no pagination options are provided', () => { + it('lists automations', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = await resend.automations.list(); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/automations', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); + + describe('when pagination options are provided', () => { + it('passes limit param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.list({ limit: 1 }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/automations?limit=1', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes after param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.list({ + limit: 1, + after: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/automations?limit=1&after=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + + it('passes before param and returns a response', async () => { + mockSuccessResponse(response, { + headers: {}, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + const result = await resend.automations.list({ + limit: 1, + before: 'cursor-value', + }); + expect(result).toEqual({ + data: response, + error: null, + headers: { + 'content-type': 'application/json', + }, + }); + + expect(fetchMock).toHaveBeenCalledWith( + 'https://api.resend.com/automations?limit=1&before=cursor-value', + expect.objectContaining({ + method: 'GET', + headers: expect.any(Headers), + }), + ); + }); + }); +}); + +describe('get', () => { + describe('when automation not found', () => { + it('returns error', async () => { + const response: ErrorResponse = { + name: 'not_found', + statusCode: 404, + message: 'Automation not found', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 404, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + const result = resend.automations.get( + '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + ); + + await expect(result).resolves.toMatchInlineSnapshot(` + { + "data": null, + "error": { + "message": "Automation not found", + "name": "not_found", + "statusCode": 404, + }, + "headers": { + "content-type": "application/json", + }, + } + `); + }); + }); + + it('gets an automation', async () => { + const response: GetAutomationResponseSuccess = { + object: 'automation', + id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', + name: 'Welcome Flow', + status: 'enabled', + created_at: '2025-01-01T00:00:00.000Z', + updated_at: '2025-01-01T00:00:00.000Z', + steps: [ + { + id: 'step-1', + type: 'trigger', + config: { event_name: 'user.created' }, + }, + ], + edges: [], + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect( + resend.automations.get('559ac32e-9ef5-46fb-82a1-b76b840c0f7b'), + ).resolves.toMatchInlineSnapshot(` + { + "data": { + "created_at": "2025-01-01T00:00:00.000Z", + "edges": [], + "id": "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", + "name": "Welcome Flow", + "object": "automation", + "status": "enabled", + "steps": [ + { + "config": { + "event_name": "user.created", + }, + "id": "step-1", + "type": "trigger", + }, + ], + "updated_at": "2025-01-01T00:00:00.000Z", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); +}); + +describe('remove', () => { + it('removes an automation', async () => { + const id = 'b01e0de9-7c27-4a53-bf38-2e3f98389a65'; + const response: RemoveAutomationResponseSuccess = { + object: 'automation', + id, + deleted: true, + }; + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); + + await expect(resend.automations.remove(id)).resolves.toMatchInlineSnapshot(` + { + "data": { + "deleted": true, + "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", + "object": "automation", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + }); +}); + +describe('update', () => { + it('updates an automation', async () => { + const id = '71cdfe68-cf79-473a-a9d7-21f91db6a526'; + const response: UpdateAutomationResponseSuccess = { + object: 'automation', + id, + status: 'disabled', + }; + + fetchMock.mockOnce(JSON.stringify(response), { + status: 200, + headers: { + 'content-type': 'application/json', + }, + }); + + const data = await resend.automations.update(id, { status: 'disabled' }); + expect(data).toMatchInlineSnapshot(` + { + "data": { + "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", + "object": "automation", + "status": "disabled", + }, + "error": null, + "headers": { + "content-type": "application/json", + }, + } + `); + + expect(fetchMock).toHaveBeenCalledWith( + `https://api.resend.com/automations/${id}`, + expect.objectContaining({ + method: 'PATCH', + headers: expect.any(Headers), + body: JSON.stringify({ status: 'disabled' }), + }), + ); + }); +}); diff --git a/src/automations/automations.ts b/src/automations/automations.ts new file mode 100644 index 00000000..2ef88ff5 --- /dev/null +++ b/src/automations/automations.ts @@ -0,0 +1,81 @@ +import { AutomationRuns } from '../automation-runs/automation-runs'; +import { buildPaginationQuery } from '../common/utils/build-pagination-query'; +import { parseAutomationToApiOptions } from '../common/utils/parse-automation-to-api-options'; +import type { Resend } from '../resend'; +import type { + CreateAutomationOptions, + CreateAutomationResponse, + CreateAutomationResponseSuccess, +} from './interfaces/create-automation-options.interface'; +import type { + GetAutomationResponse, + GetAutomationResponseSuccess, +} from './interfaces/get-automation.interface'; +import type { + ListAutomationsOptions, + ListAutomationsResponse, + ListAutomationsResponseSuccess, +} from './interfaces/list-automation.interface'; +import type { + RemoveAutomationResponse, + RemoveAutomationResponseSuccess, +} from './interfaces/remove-automation.interface'; +import type { + UpdateAutomationOptions, + UpdateAutomationResponse, + UpdateAutomationResponseSuccess, +} from './interfaces/update-automation.interface'; + +export class Automations { + readonly runs: AutomationRuns; + + constructor(private readonly resend: Resend) { + this.runs = new AutomationRuns(this.resend); + } + + async create( + payload: CreateAutomationOptions, + ): Promise { + const data = await this.resend.post( + '/automations', + parseAutomationToApiOptions(payload), + ); + + return data; + } + + async list( + options: ListAutomationsOptions = {}, + ): Promise { + const queryString = buildPaginationQuery(options); + const url = queryString ? `/automations?${queryString}` : '/automations'; + + const data = await this.resend.get(url); + return data; + } + + async get(id: string): Promise { + const data = await this.resend.get( + `/automations/${id}`, + ); + return data; + } + + async remove(id: string): Promise { + const data = await this.resend.delete( + `/automations/${id}`, + ); + return data; + } + + async update( + id: string, + payload: UpdateAutomationOptions, + ): Promise { + const data = await this.resend.patch( + `/automations/${id}`, + payload, + ); + return data; + } +} diff --git a/src/automations/interfaces/automation-step.interface.ts b/src/automations/interfaces/automation-step.interface.ts new file mode 100644 index 00000000..8894d49b --- /dev/null +++ b/src/automations/interfaces/automation-step.interface.ts @@ -0,0 +1,95 @@ +export type ConditionRule = + | { + type: 'rule'; + field: string; + operator: 'eq' | 'neq'; + value: string | number | boolean | null; + } + | { + type: 'rule'; + field: string; + operator: 'gt' | 'gte' | 'lt' | 'lte'; + value: number; + } + | { + type: 'rule'; + field: string; + operator: 'contains' | 'starts_with' | 'ends_with'; + value: string; + } + | { + type: 'rule'; + field: string; + operator: 'exists' | 'is_empty'; + } + | { type: 'and'; rules: ConditionRule[] } + | { type: 'or'; rules: ConditionRule[] }; + +export type TemplateVariableValue = + | string + | number + | boolean + | { var: string } + | Record + | Array< + string | number | boolean | Record + >; + +export interface TriggerStepConfig { + eventName: string; +} + +export interface DelayStepConfig { + seconds: number; +} + +export interface SendEmailStepConfig { + templateId: string; + subject?: string; + from?: string; + replyTo?: string; + variables?: Record; +} + +export interface WaitForEventStepConfig { + eventName: string; + timeoutSeconds?: number; + filterRule?: ConditionRule; +} + +export type ConditionStepConfig = ConditionRule; + +export type AutomationStep = + | { ref: string; type: 'trigger'; config: TriggerStepConfig } + | { ref: string; type: 'delay'; config: DelayStepConfig } + | { ref: string; type: 'send_email'; config: SendEmailStepConfig } + | { ref: string; type: 'wait_for_event'; config: WaitForEventStepConfig } + | { ref: string; type: 'condition'; config: ConditionStepConfig }; + +export type AutomationEdgeType = + | 'default' + | 'condition_met' + | 'condition_not_met' + | 'timeout' + | 'event_received'; + +export interface AutomationEdge { + from: string; + to: string; + edgeType?: AutomationEdgeType; +} + +export type AutomationStepType = AutomationStep['type']; + +export interface AutomationResponseStep { + id: string; + type: AutomationStepType; + config: Record; +} + +export interface AutomationResponseEdge { + id: string; + from_step_id: string; + to_step_id: string; + edge_type: AutomationEdgeType; +} diff --git a/src/automations/interfaces/automation.ts b/src/automations/interfaces/automation.ts new file mode 100644 index 00000000..cc766ca5 --- /dev/null +++ b/src/automations/interfaces/automation.ts @@ -0,0 +1,9 @@ +export interface Automation { + id: string; + name: string; + status: 'enabled' | 'disabled'; + created_at: string; + updated_at: string | null; +} + +export type AutomationStatus = Automation['status']; diff --git a/src/automations/interfaces/create-automation-options.interface.ts b/src/automations/interfaces/create-automation-options.interface.ts new file mode 100644 index 00000000..e4335e0e --- /dev/null +++ b/src/automations/interfaces/create-automation-options.interface.ts @@ -0,0 +1,21 @@ +import type { Response } from '../../interfaces'; +import type { AutomationStatus } from './automation'; +import type { + AutomationEdge, + AutomationStep, +} from './automation-step.interface'; + +export interface CreateAutomationOptions { + name: string; + status?: AutomationStatus; + steps: AutomationStep[]; + edges: AutomationEdge[]; +} + +export interface CreateAutomationResponseSuccess { + object: 'automation'; + id: string; +} + +export type CreateAutomationResponse = + Response; diff --git a/src/automations/interfaces/get-automation.interface.ts b/src/automations/interfaces/get-automation.interface.ts new file mode 100644 index 00000000..a2243223 --- /dev/null +++ b/src/automations/interfaces/get-automation.interface.ts @@ -0,0 +1,14 @@ +import type { Response } from '../../interfaces'; +import type { Automation } from './automation'; +import type { + AutomationResponseEdge, + AutomationResponseStep, +} from './automation-step.interface'; + +export interface GetAutomationResponseSuccess extends Automation { + object: 'automation'; + steps: AutomationResponseStep[]; + edges: AutomationResponseEdge[]; +} + +export type GetAutomationResponse = Response; diff --git a/src/automations/interfaces/index.ts b/src/automations/interfaces/index.ts new file mode 100644 index 00000000..c5f183aa --- /dev/null +++ b/src/automations/interfaces/index.ts @@ -0,0 +1,7 @@ +export * from './automation'; +export * from './automation-step.interface'; +export * from './create-automation-options.interface'; +export * from './get-automation.interface'; +export * from './list-automation.interface'; +export * from './remove-automation.interface'; +export * from './update-automation.interface'; diff --git a/src/automations/interfaces/list-automation.interface.ts b/src/automations/interfaces/list-automation.interface.ts new file mode 100644 index 00000000..de2ee49d --- /dev/null +++ b/src/automations/interfaces/list-automation.interface.ts @@ -0,0 +1,12 @@ +import type { + PaginatedData, + PaginationOptions, +} from '../../common/interfaces/pagination-options.interface'; +import type { Response } from '../../interfaces'; +import type { Automation } from './automation'; + +export type ListAutomationsOptions = PaginationOptions; + +export type ListAutomationsResponseSuccess = PaginatedData; + +export type ListAutomationsResponse = Response; diff --git a/src/automations/interfaces/remove-automation.interface.ts b/src/automations/interfaces/remove-automation.interface.ts new file mode 100644 index 00000000..dc23e9ad --- /dev/null +++ b/src/automations/interfaces/remove-automation.interface.ts @@ -0,0 +1,11 @@ +import type { Response } from '../../interfaces'; +import type { Automation } from './automation'; + +export interface RemoveAutomationResponseSuccess + extends Pick { + object: 'automation'; + deleted: boolean; +} + +export type RemoveAutomationResponse = + Response; diff --git a/src/automations/interfaces/update-automation.interface.ts b/src/automations/interfaces/update-automation.interface.ts new file mode 100644 index 00000000..53c231cb --- /dev/null +++ b/src/automations/interfaces/update-automation.interface.ts @@ -0,0 +1,14 @@ +import type { Response } from '../../interfaces'; +import type { Automation, AutomationStatus } from './automation'; + +export interface UpdateAutomationOptions { + status: AutomationStatus; +} + +export interface UpdateAutomationResponseSuccess + extends Pick { + object: 'automation'; +} + +export type UpdateAutomationResponse = + Response; diff --git a/src/common/utils/parse-workflow-to-api-options.spec.ts b/src/common/utils/parse-automation-to-api-options.spec.ts similarity index 87% rename from src/common/utils/parse-workflow-to-api-options.spec.ts rename to src/common/utils/parse-automation-to-api-options.spec.ts index 68b7280b..ddf7c74c 100644 --- a/src/common/utils/parse-workflow-to-api-options.spec.ts +++ b/src/common/utils/parse-automation-to-api-options.spec.ts @@ -1,14 +1,14 @@ +import type { CreateAutomationOptions } from '../../automations/interfaces/create-automation-options.interface'; import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; -import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import { - parseWorkflowEventToApiOptions, - parseWorkflowToApiOptions, -} from './parse-workflow-to-api-options'; + parseAutomationToApiOptions, + parseEventToApiOptions, +} from './parse-automation-to-api-options'; -describe('parseWorkflowToApiOptions', () => { +describe('parseAutomationToApiOptions', () => { it('converts full payload with all step types from camelCase to snake_case', () => { - const workflow: CreateWorkflowOptions = { - name: 'Welcome Series', + const automation: CreateAutomationOptions = { + name: 'Welcome Automation', status: 'enabled', steps: [ { @@ -65,7 +65,7 @@ describe('parseWorkflowToApiOptions', () => { ], }; - const apiOptions = parseWorkflowToApiOptions(workflow); + const apiOptions = parseAutomationToApiOptions(automation); expect(apiOptions).toEqual({ name: 'Welcome Series', @@ -127,7 +127,7 @@ describe('parseWorkflowToApiOptions', () => { }); it('converts edge edgeType to edge_type', () => { - const workflow: CreateWorkflowOptions = { + const automation: CreateAutomationOptions = { name: 'Edge Test', steps: [ { @@ -143,7 +143,7 @@ describe('parseWorkflowToApiOptions', () => { ], }; - const apiOptions = parseWorkflowToApiOptions(workflow); + const apiOptions = parseAutomationToApiOptions(automation); expect(apiOptions.edges).toEqual([ { from: 'trigger_1', to: 'step_2', edge_type: 'condition_met' }, @@ -153,7 +153,7 @@ describe('parseWorkflowToApiOptions', () => { }); it('handles minimal payload with only required fields', () => { - const workflow: CreateWorkflowOptions = { + const automation: CreateAutomationOptions = { name: 'Minimal Workflow', steps: [ { @@ -165,7 +165,7 @@ describe('parseWorkflowToApiOptions', () => { edges: [{ from: 'trigger_1', to: 'step_2' }], }; - const apiOptions = parseWorkflowToApiOptions(workflow); + const apiOptions = parseAutomationToApiOptions(automation); expect(apiOptions).toEqual({ name: 'Minimal Workflow', @@ -190,7 +190,7 @@ describe('parseWorkflowEventToApiOptions', () => { payload: { plan: 'pro' }, }; - const apiOptions = parseWorkflowEventToApiOptions(event); + const apiOptions = parseEventToApiOptions(event); expect(apiOptions).toEqual({ event: 'user.signed_up', @@ -207,7 +207,7 @@ describe('parseWorkflowEventToApiOptions', () => { payload: { source: 'website' }, }; - const apiOptions = parseWorkflowEventToApiOptions(event); + const apiOptions = parseEventToApiOptions(event); expect(apiOptions).toEqual({ event: 'user.signed_up', diff --git a/src/common/utils/parse-workflow-to-api-options.ts b/src/common/utils/parse-automation-to-api-options.ts similarity index 62% rename from src/common/utils/parse-workflow-to-api-options.ts rename to src/common/utils/parse-automation-to-api-options.ts index 5c8e6f6a..af794668 100644 --- a/src/common/utils/parse-workflow-to-api-options.ts +++ b/src/common/utils/parse-automation-to-api-options.ts @@ -1,38 +1,38 @@ -import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; -import type { CreateWorkflowOptions } from '../../workflows/interfaces/create-workflow-options.interface'; import type { - WorkflowEdge, - WorkflowEdgeType, - WorkflowStep, -} from '../../workflows/interfaces/workflow-step.interface'; + AutomationEdge, + AutomationEdgeType, + AutomationStep, +} from '../../automations/interfaces/automation-step.interface'; +import type { CreateAutomationOptions } from '../../automations/interfaces/create-automation-options.interface'; +import type { SendEventOptions } from '../../events/interfaces/send-event.interface'; -interface WorkflowStepApiOptions { +interface AutomationStepApiOptions { ref: string; type: string; config: unknown; } -interface WorkflowEdgeApiOptions { +interface AutomationEdgeApiOptions { from: string; to: string; - edge_type?: WorkflowEdgeType; + edge_type?: AutomationEdgeType; } -interface WorkflowApiOptions { +interface AutomationApiOptions { name: string; status?: 'enabled' | 'disabled'; - steps?: WorkflowStepApiOptions[]; - edges?: WorkflowEdgeApiOptions[]; + steps?: AutomationStepApiOptions[]; + edges?: AutomationEdgeApiOptions[]; } -interface WorkflowEventApiOptions { +interface EventApiOptions { event: string; contact_id?: string; email?: string; payload?: Record; } -function parseStepConfig(step: WorkflowStep): WorkflowStepApiOptions { +function parseStepConfig(step: AutomationStep): AutomationStepApiOptions { switch (step.type) { case 'trigger': return { @@ -69,7 +69,7 @@ function parseStepConfig(step: WorkflowStep): WorkflowStepApiOptions { } } -function parseEdge(edge: WorkflowEdge): WorkflowEdgeApiOptions { +function parseEdge(edge: AutomationEdge): AutomationEdgeApiOptions { return { from: edge.from, to: edge.to, @@ -77,20 +77,20 @@ function parseEdge(edge: WorkflowEdge): WorkflowEdgeApiOptions { }; } -export function parseWorkflowToApiOptions( - workflow: CreateWorkflowOptions, -): WorkflowApiOptions { +export function parseAutomationToApiOptions( + automation: CreateAutomationOptions, +): AutomationApiOptions { return { - name: workflow.name, - status: workflow.status, - steps: workflow.steps.map(parseStepConfig), - edges: workflow.edges.map(parseEdge), + name: automation.name, + status: automation.status, + steps: automation.steps.map(parseStepConfig), + edges: automation.edges.map(parseEdge), }; } -export function parseWorkflowEventToApiOptions( +export function parseEventToApiOptions( event: SendEventOptions, -): WorkflowEventApiOptions { +): EventApiOptions { return { event: event.event, contact_id: event.contactId, diff --git a/src/events/events.ts b/src/events/events.ts index f2c1c3ce..a71a769b 100644 --- a/src/events/events.ts +++ b/src/events/events.ts @@ -1,5 +1,5 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import { parseWorkflowEventToApiOptions } from '../common/utils/parse-workflow-to-api-options'; +import { parseEventToApiOptions } from '../common/utils/parse-automation-to-api-options'; import type { Resend } from '../resend'; import type { CreateEventOptions, @@ -36,7 +36,7 @@ export class Events { async send(payload: SendEventOptions): Promise { const data = await this.resend.post( '/events/send', - parseWorkflowEventToApiOptions(payload), + parseEventToApiOptions(payload), ); return data; diff --git a/src/index.ts b/src/index.ts index 0f1a7ef3..296ccd5f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,7 @@ export * from './api-keys/interfaces'; +export * from './automation-run-steps/interfaces'; +export * from './automation-runs/interfaces'; +export * from './automations/interfaces'; export * from './batch/interfaces'; export * from './broadcasts/interfaces'; export * from './common/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index 3bd4fb6e..e37ee4be 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -1,5 +1,6 @@ import { version } from '../package.json'; import { ApiKeys } from './api-keys/api-keys'; +import { Automations } from './automations/automations'; import { Batch } from './batch/batch'; import { Broadcasts } from './broadcasts/broadcasts'; import type { GetOptions, PostOptions, PutOptions } from './common/interfaces'; @@ -37,6 +38,7 @@ export class Resend { * @deprecated Use segments instead */ readonly audiences = this.segments; + readonly automations = new Automations(this); readonly batch = new Batch(this); readonly broadcasts = new Broadcasts(this); readonly contacts = new Contacts(this); diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts index 1ddf97ce..93c2f53c 100644 --- a/src/workflows/interfaces/workflow-step.interface.ts +++ b/src/workflows/interfaces/workflow-step.interface.ts @@ -1,4 +1,4 @@ -export type ConditionRule = +type ConditionRule = | { type: 'rule'; field: string; @@ -25,7 +25,7 @@ export type ConditionRule = | { type: 'and'; rules: ConditionRule[] } | { type: 'or'; rules: ConditionRule[] }; -export type TemplateVariableValue = +type TemplateVariableValue = | string | number | boolean @@ -35,15 +35,15 @@ export type TemplateVariableValue = string | number | boolean | Record >; -export interface TriggerStepConfig { +interface TriggerStepConfig { eventName: string; } -export interface DelayStepConfig { +interface DelayStepConfig { seconds: number; } -export interface SendEmailStepConfig { +interface SendEmailStepConfig { templateId: string; subject?: string; from?: string; @@ -51,13 +51,13 @@ export interface SendEmailStepConfig { variables?: Record; } -export interface WaitForEventStepConfig { +interface WaitForEventStepConfig { eventName: string; timeoutSeconds?: number; filterRule?: ConditionRule; } -export type ConditionStepConfig = ConditionRule; +type ConditionStepConfig = ConditionRule; export type WorkflowStep = | { ref: string; type: 'trigger'; config: TriggerStepConfig } diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index 28885313..0e3eec24 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -283,7 +283,6 @@ describe('Workflows', () => { id: 'step-1', type: 'trigger', config: { event_name: 'user.created' }, - ref: 'trigger', }, ], edges: [], @@ -315,7 +314,6 @@ describe('Workflows', () => { "event_name": "user.created", }, "id": "step-1", - "ref": "trigger", "type": "trigger", }, ], diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index afc116d3..b98259ae 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -1,5 +1,5 @@ import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import { parseWorkflowToApiOptions } from '../common/utils/parse-workflow-to-api-options'; +import { parseAutomationToApiOptions } from '../common/utils/parse-automation-to-api-options'; import type { Resend } from '../resend'; import { WorkflowRuns } from '../workflow-runs/workflow-runs'; import type { @@ -38,7 +38,7 @@ export class Workflows { ): Promise { const data = await this.resend.post( '/workflows', - parseWorkflowToApiOptions(payload), + parseAutomationToApiOptions(payload), ); return data; From 1b5b2d6185ec9b0ff19c4e87d656e510ba0a7657 Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Wed, 25 Mar 2026 13:58:16 -0700 Subject: [PATCH 19/23] use automations endpoint --- src/workflow-run-steps/workflow-run-steps.ts | 6 +++--- src/workflow-runs/workflow-runs.ts | 6 +++--- src/workflows/workflows.spec.ts | 10 +++++----- src/workflows/workflows.ts | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/workflow-run-steps/workflow-run-steps.ts b/src/workflow-run-steps/workflow-run-steps.ts index 6ca9db29..2ee43bd6 100644 --- a/src/workflow-run-steps/workflow-run-steps.ts +++ b/src/workflow-run-steps/workflow-run-steps.ts @@ -18,7 +18,7 @@ export class WorkflowRunSteps { options: GetWorkflowRunStepOptions, ): Promise { const data = await this.resend.get( - `/workflows/${options.workflowId}/runs/${options.runId}/steps/${options.stepId}`, + `/automations/${options.workflowId}/runs/${options.runId}/steps/${options.stepId}`, ); return data; } @@ -28,8 +28,8 @@ export class WorkflowRunSteps { ): Promise { const queryString = buildPaginationQuery(options); const url = queryString - ? `/workflows/${options.workflowId}/runs/${options.runId}/steps?${queryString}` - : `/workflows/${options.workflowId}/runs/${options.runId}/steps`; + ? `/automations/${options.workflowId}/runs/${options.runId}/steps?${queryString}` + : `/automations/${options.workflowId}/runs/${options.runId}/steps`; const data = await this.resend.get(url); diff --git a/src/workflow-runs/workflow-runs.ts b/src/workflow-runs/workflow-runs.ts index 2c8045ac..ebc1e01a 100644 --- a/src/workflow-runs/workflow-runs.ts +++ b/src/workflow-runs/workflow-runs.ts @@ -21,7 +21,7 @@ export class WorkflowRuns { async get(options: GetWorkflowRunOptions): Promise { const data = await this.resend.get( - `/workflows/${options.workflowId}/runs/${options.runId}`, + `/automations/${options.workflowId}/runs/${options.runId}`, ); return data; } @@ -31,8 +31,8 @@ export class WorkflowRuns { ): Promise { const queryString = buildPaginationQuery(options); const url = queryString - ? `/workflows/${options.workflowId}/runs?${queryString}` - : `/workflows/${options.workflowId}/runs`; + ? `/automations/${options.workflowId}/runs?${queryString}` + : `/automations/${options.workflowId}/runs`; const data = await this.resend.get(url); return data; diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts index 0e3eec24..e1d8a75c 100644 --- a/src/workflows/workflows.spec.ts +++ b/src/workflows/workflows.spec.ts @@ -142,7 +142,7 @@ describe('Workflows', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/workflows', + 'https://api.resend.com/automations', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -168,7 +168,7 @@ describe('Workflows', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/workflows?limit=1', + 'https://api.resend.com/automations?limit=1', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -195,7 +195,7 @@ describe('Workflows', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/workflows?limit=1&after=cursor-value', + 'https://api.resend.com/automations?limit=1&after=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -222,7 +222,7 @@ describe('Workflows', () => { }); expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/workflows?limit=1&before=cursor-value', + 'https://api.resend.com/automations?limit=1&before=cursor-value', expect.objectContaining({ method: 'GET', headers: expect.any(Headers), @@ -393,7 +393,7 @@ describe('Workflows', () => { `); expect(fetchMock).toHaveBeenCalledWith( - `https://api.resend.com/workflows/${id}`, + `https://api.resend.com/automations/${id}`, expect.objectContaining({ method: 'PATCH', headers: expect.any(Headers), diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts index b98259ae..f710471a 100644 --- a/src/workflows/workflows.ts +++ b/src/workflows/workflows.ts @@ -37,7 +37,7 @@ export class Workflows { payload: CreateWorkflowOptions, ): Promise { const data = await this.resend.post( - '/workflows', + '/automations', parseAutomationToApiOptions(payload), ); @@ -48,7 +48,7 @@ export class Workflows { options: ListWorkflowsOptions = {}, ): Promise { const queryString = buildPaginationQuery(options); - const url = queryString ? `/workflows?${queryString}` : '/workflows'; + const url = queryString ? `/automations?${queryString}` : '/automations'; const data = await this.resend.get(url); return data; @@ -56,14 +56,14 @@ export class Workflows { async get(id: string): Promise { const data = await this.resend.get( - `/workflows/${id}`, + `/automations/${id}`, ); return data; } async remove(id: string): Promise { const data = await this.resend.delete( - `/workflows/${id}`, + `/automations/${id}`, ); return data; } @@ -73,7 +73,7 @@ export class Workflows { payload: UpdateWorkflowOptions, ): Promise { const data = await this.resend.patch( - `/workflows/${id}`, + `/automations/${id}`, payload, ); return data; From 84f40da20fe3419b43136df1c4d5743a2281bfc3 Mon Sep 17 00:00:00 2001 From: Carolina de Moraes Josephik Date: Wed, 25 Mar 2026 18:07:58 -0300 Subject: [PATCH 20/23] chore: bump package version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b9e66e2a..cd5e0ab6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.10.0-preview-workflows.1", + "version": "6.10.0-preview-workflows.2", "description": "Node.js library for the Resend API", "main": "./dist/index.cjs", "module": "./dist/index.mjs", From 7e25f6dba60921feec21b8f6db02eeda1c089ff5 Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Mon, 30 Mar 2026 10:07:09 -0400 Subject: [PATCH 21/23] feat: remove workflows namespace from SDK (#898) Co-authored-by: Cursor Agent --- .../parse-automation-to-api-options.spec.ts | 8 +- src/index.ts | 3 - src/resend.ts | 2 - .../get-workflow-run-step.interface.ts | 13 - src/workflow-run-steps/interfaces/index.ts | 3 - .../list-workflow-run-steps.interface.ts | 18 - .../interfaces/workflow-run-step.ts | 32 -- .../workflow-run-steps.spec.ts | 209 --------- src/workflow-run-steps/workflow-run-steps.ts | 38 -- .../interfaces/get-workflow-run.interface.ts | 11 - src/workflow-runs/interfaces/index.ts | 3 - .../list-workflow-runs.interface.ts | 15 - src/workflow-runs/interfaces/workflow-run.ts | 25 -- src/workflow-runs/workflow-runs.spec.ts | 204 --------- src/workflow-runs/workflow-runs.ts | 40 -- .../create-workflow-options.interface.ts | 17 - .../interfaces/get-workflow.interface.ts | 14 - src/workflows/interfaces/index.ts | 7 - .../interfaces/list-workflows.interface.ts | 12 - .../interfaces/remove-workflow.interface.ts | 9 - .../interfaces/update-workflow.interface.ts | 13 - .../interfaces/workflow-step.interface.ts | 95 ---- src/workflows/interfaces/workflow.ts | 9 - src/workflows/workflows.spec.ts | 405 ------------------ src/workflows/workflows.ts | 81 ---- 25 files changed, 4 insertions(+), 1282 deletions(-) delete mode 100644 src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts delete mode 100644 src/workflow-run-steps/interfaces/index.ts delete mode 100644 src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts delete mode 100644 src/workflow-run-steps/interfaces/workflow-run-step.ts delete mode 100644 src/workflow-run-steps/workflow-run-steps.spec.ts delete mode 100644 src/workflow-run-steps/workflow-run-steps.ts delete mode 100644 src/workflow-runs/interfaces/get-workflow-run.interface.ts delete mode 100644 src/workflow-runs/interfaces/index.ts delete mode 100644 src/workflow-runs/interfaces/list-workflow-runs.interface.ts delete mode 100644 src/workflow-runs/interfaces/workflow-run.ts delete mode 100644 src/workflow-runs/workflow-runs.spec.ts delete mode 100644 src/workflow-runs/workflow-runs.ts delete mode 100644 src/workflows/interfaces/create-workflow-options.interface.ts delete mode 100644 src/workflows/interfaces/get-workflow.interface.ts delete mode 100644 src/workflows/interfaces/index.ts delete mode 100644 src/workflows/interfaces/list-workflows.interface.ts delete mode 100644 src/workflows/interfaces/remove-workflow.interface.ts delete mode 100644 src/workflows/interfaces/update-workflow.interface.ts delete mode 100644 src/workflows/interfaces/workflow-step.interface.ts delete mode 100644 src/workflows/interfaces/workflow.ts delete mode 100644 src/workflows/workflows.spec.ts delete mode 100644 src/workflows/workflows.ts diff --git a/src/common/utils/parse-automation-to-api-options.spec.ts b/src/common/utils/parse-automation-to-api-options.spec.ts index ddf7c74c..3e451331 100644 --- a/src/common/utils/parse-automation-to-api-options.spec.ts +++ b/src/common/utils/parse-automation-to-api-options.spec.ts @@ -68,7 +68,7 @@ describe('parseAutomationToApiOptions', () => { const apiOptions = parseAutomationToApiOptions(automation); expect(apiOptions).toEqual({ - name: 'Welcome Series', + name: 'Welcome Automation', status: 'enabled', steps: [ { @@ -154,7 +154,7 @@ describe('parseAutomationToApiOptions', () => { it('handles minimal payload with only required fields', () => { const automation: CreateAutomationOptions = { - name: 'Minimal Workflow', + name: 'Minimal Automation', steps: [ { ref: 'trigger_1', @@ -168,7 +168,7 @@ describe('parseAutomationToApiOptions', () => { const apiOptions = parseAutomationToApiOptions(automation); expect(apiOptions).toEqual({ - name: 'Minimal Workflow', + name: 'Minimal Automation', status: undefined, steps: [ { @@ -182,7 +182,7 @@ describe('parseAutomationToApiOptions', () => { }); }); -describe('parseWorkflowEventToApiOptions', () => { +describe('parseEventToApiOptions', () => { it('converts contactId to contact_id', () => { const event: SendEventOptions = { event: 'user.signed_up', diff --git a/src/index.ts b/src/index.ts index 296ccd5f..7ff7d073 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,6 +20,3 @@ export * from './segments/interfaces'; export * from './templates/interfaces'; export * from './topics/interfaces'; export * from './webhooks/interfaces'; -export * from './workflow-run-steps/interfaces'; -export * from './workflow-runs/interfaces'; -export * from './workflows/interfaces'; diff --git a/src/resend.ts b/src/resend.ts index e37ee4be..3986d7f4 100644 --- a/src/resend.ts +++ b/src/resend.ts @@ -16,7 +16,6 @@ import { Segments } from './segments/segments'; import { Templates } from './templates/templates'; import { Topics } from './topics/topics'; import { Webhooks } from './webhooks/webhooks'; -import { Workflows } from './workflows/workflows'; const defaultBaseUrl = 'https://api.resend.com'; const defaultUserAgent = `resend-node:${version}`; @@ -49,7 +48,6 @@ export class Resend { readonly webhooks = new Webhooks(this); readonly templates = new Templates(this); readonly topics = new Topics(this); - readonly workflows = new Workflows(this); constructor(readonly key?: string) { if (!key) { diff --git a/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts b/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts deleted file mode 100644 index a506f4fe..00000000 --- a/src/workflow-run-steps/interfaces/get-workflow-run-step.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { WorkflowRunStep } from './workflow-run-step'; - -export interface GetWorkflowRunStepOptions { - workflowId: string; - runId: string; - stepId: string; -} - -export type GetWorkflowRunStepResponseSuccess = WorkflowRunStep; - -export type GetWorkflowRunStepResponse = - Response; diff --git a/src/workflow-run-steps/interfaces/index.ts b/src/workflow-run-steps/interfaces/index.ts deleted file mode 100644 index 3a6724ad..00000000 --- a/src/workflow-run-steps/interfaces/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-workflow-run-step.interface'; -export * from './list-workflow-run-steps.interface'; -export * from './workflow-run-step'; diff --git a/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts b/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts deleted file mode 100644 index ef24d5df..00000000 --- a/src/workflow-run-steps/interfaces/list-workflow-run-steps.interface.ts +++ /dev/null @@ -1,18 +0,0 @@ -import type { - PaginatedData, - PaginationOptions, -} from '../../common/interfaces/pagination-options.interface'; -import type { Response } from '../../interfaces'; -import type { WorkflowRunStepItem } from './workflow-run-step'; - -export type ListWorkflowRunStepsOptions = PaginationOptions & { - workflowId: string; - runId: string; -}; - -export type ListWorkflowRunStepsResponseSuccess = PaginatedData< - WorkflowRunStepItem[] ->; - -export type ListWorkflowRunStepsResponse = - Response; diff --git a/src/workflow-run-steps/interfaces/workflow-run-step.ts b/src/workflow-run-steps/interfaces/workflow-run-step.ts deleted file mode 100644 index 4313ea3a..00000000 --- a/src/workflow-run-steps/interfaces/workflow-run-step.ts +++ /dev/null @@ -1,32 +0,0 @@ -import type { WorkflowStepType } from '../../workflows/interfaces/workflow-step.interface'; - -export type WorkflowRunStepStatus = - | 'pending' - | 'running' - | 'completed' - | 'failed' - | 'skipped' - | 'waiting'; - -export interface WorkflowRunStep { - object: 'workflow_run_step'; - id: string; - step_id: string; - type: WorkflowStepType; - config: Record; - status: WorkflowRunStepStatus; - started_at: string | null; - completed_at: string | null; - created_at: string; -} - -export type WorkflowRunStepItem = Pick< - WorkflowRunStep, - | 'id' - | 'step_id' - | 'type' - | 'status' - | 'started_at' - | 'completed_at' - | 'created_at' ->; diff --git a/src/workflow-run-steps/workflow-run-steps.spec.ts b/src/workflow-run-steps/workflow-run-steps.spec.ts deleted file mode 100644 index 60f4d031..00000000 --- a/src/workflow-run-steps/workflow-run-steps.spec.ts +++ /dev/null @@ -1,209 +0,0 @@ -import createFetchMock from 'vitest-fetch-mock'; -import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; -import type { - GetWorkflowRunStepOptions, - GetWorkflowRunStepResponseSuccess, -} from './interfaces/get-workflow-run-step.interface'; -import type { - ListWorkflowRunStepsOptions, - ListWorkflowRunStepsResponseSuccess, -} from './interfaces/list-workflow-run-steps.interface'; - -const fetchMocker = createFetchMock(vi); -fetchMocker.enableMocks(); - -describe('WorkflowRunSteps', () => { - afterEach(() => fetchMock.resetMocks()); - afterAll(() => fetchMocker.disableMocks()); - - describe('get', () => { - it('gets a workflow run step', async () => { - const options: GetWorkflowRunStepOptions = { - workflowId: 'wf_123', - runId: 'wr_456', - stepId: 'wrs_789', - }; - const response: GetWorkflowRunStepResponseSuccess = { - object: 'workflow_run_step', - id: 'wrs_789', - step_id: 'step_1', - type: 'trigger', - config: { event_name: 'user.created' }, - status: 'completed', - started_at: '2024-01-01T00:00:00.000Z', - completed_at: '2024-01-01T00:01:00.000Z', - created_at: '2024-01-01T00:00:00.000Z', - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.steps.get(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "completed_at": "2024-01-01T00:01:00.000Z", - "config": { - "event_name": "user.created", - }, - "created_at": "2024-01-01T00:00:00.000Z", - "id": "wrs_789", - "object": "workflow_run_step", - "started_at": "2024-01-01T00:00:00.000Z", - "status": "completed", - "step_id": "step_1", - "type": "trigger", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('returns error', async () => { - const options: GetWorkflowRunStepOptions = { - workflowId: 'wf_123', - runId: 'wr_456', - stepId: 'wrs_invalid', - }; - - mockErrorResponse( - { name: 'not_found', message: 'Workflow run step not found' }, - {}, - ); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.runs.steps.get(options); - expect(result.error).not.toBeNull(); - }); - }); - - describe('list', () => { - it('lists workflow run steps', async () => { - const options: ListWorkflowRunStepsOptions = { - workflowId: 'wf_123', - runId: 'wr_456', - }; - const response: ListWorkflowRunStepsResponseSuccess = { - object: 'list', - data: [ - { - id: 'wrs_789', - step_id: 'step_1', - type: 'trigger', - status: 'completed', - started_at: '2024-01-01T00:00:00.000Z', - completed_at: '2024-01-01T00:01:00.000Z', - created_at: '2024-01-01T00:00:00.000Z', - }, - ], - has_more: false, - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.steps.list(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "data": [ - { - "completed_at": "2024-01-01T00:01:00.000Z", - "created_at": "2024-01-01T00:00:00.000Z", - "id": "wrs_789", - "started_at": "2024-01-01T00:00:00.000Z", - "status": "completed", - "step_id": "step_1", - "type": "trigger", - }, - ], - "has_more": false, - "object": "list", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('lists workflow run steps with pagination', async () => { - const options: ListWorkflowRunStepsOptions = { - workflowId: 'wf_123', - runId: 'wr_456', - limit: 1, - after: 'wrs_cursor', - }; - const response: ListWorkflowRunStepsResponseSuccess = { - object: 'list', - data: [ - { - id: 'wrs_101', - step_id: 'step_2', - type: 'send_email', - status: 'running', - started_at: '2024-01-02T00:00:00.000Z', - completed_at: null, - created_at: '2024-01-02T00:00:00.000Z', - }, - ], - has_more: true, - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.steps.list(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "data": [ - { - "completed_at": null, - "created_at": "2024-01-02T00:00:00.000Z", - "id": "wrs_101", - "started_at": "2024-01-02T00:00:00.000Z", - "status": "running", - "step_id": "step_2", - "type": "send_email", - }, - ], - "has_more": true, - "object": "list", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('returns error', async () => { - const options: ListWorkflowRunStepsOptions = { - workflowId: 'wf_invalid', - runId: 'wr_invalid', - }; - - mockErrorResponse( - { name: 'not_found', message: 'Workflow not found' }, - {}, - ); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.runs.steps.list(options); - expect(result.error).not.toBeNull(); - }); - }); -}); diff --git a/src/workflow-run-steps/workflow-run-steps.ts b/src/workflow-run-steps/workflow-run-steps.ts deleted file mode 100644 index 2ee43bd6..00000000 --- a/src/workflow-run-steps/workflow-run-steps.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import type { Resend } from '../resend'; -import type { - GetWorkflowRunStepOptions, - GetWorkflowRunStepResponse, - GetWorkflowRunStepResponseSuccess, -} from './interfaces/get-workflow-run-step.interface'; -import type { - ListWorkflowRunStepsOptions, - ListWorkflowRunStepsResponse, - ListWorkflowRunStepsResponseSuccess, -} from './interfaces/list-workflow-run-steps.interface'; - -export class WorkflowRunSteps { - constructor(private readonly resend: Resend) {} - - async get( - options: GetWorkflowRunStepOptions, - ): Promise { - const data = await this.resend.get( - `/automations/${options.workflowId}/runs/${options.runId}/steps/${options.stepId}`, - ); - return data; - } - - async list( - options: ListWorkflowRunStepsOptions, - ): Promise { - const queryString = buildPaginationQuery(options); - const url = queryString - ? `/automations/${options.workflowId}/runs/${options.runId}/steps?${queryString}` - : `/automations/${options.workflowId}/runs/${options.runId}/steps`; - - const data = - await this.resend.get(url); - return data; - } -} diff --git a/src/workflow-runs/interfaces/get-workflow-run.interface.ts b/src/workflow-runs/interfaces/get-workflow-run.interface.ts deleted file mode 100644 index c0708c35..00000000 --- a/src/workflow-runs/interfaces/get-workflow-run.interface.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { WorkflowRun } from './workflow-run'; - -export interface GetWorkflowRunOptions { - workflowId: string; - runId: string; -} - -export type GetWorkflowRunResponseSuccess = WorkflowRun; - -export type GetWorkflowRunResponse = Response; diff --git a/src/workflow-runs/interfaces/index.ts b/src/workflow-runs/interfaces/index.ts deleted file mode 100644 index 98a50425..00000000 --- a/src/workflow-runs/interfaces/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './get-workflow-run.interface'; -export * from './list-workflow-runs.interface'; -export * from './workflow-run'; diff --git a/src/workflow-runs/interfaces/list-workflow-runs.interface.ts b/src/workflow-runs/interfaces/list-workflow-runs.interface.ts deleted file mode 100644 index 33050aae..00000000 --- a/src/workflow-runs/interfaces/list-workflow-runs.interface.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { - PaginatedData, - PaginationOptions, -} from '../../common/interfaces/pagination-options.interface'; -import type { Response } from '../../interfaces'; -import type { WorkflowRunItem } from './workflow-run'; - -export type ListWorkflowRunsOptions = PaginationOptions & { - workflowId: string; -}; - -export type ListWorkflowRunsResponseSuccess = PaginatedData; - -export type ListWorkflowRunsResponse = - Response; diff --git a/src/workflow-runs/interfaces/workflow-run.ts b/src/workflow-runs/interfaces/workflow-run.ts deleted file mode 100644 index 46ba8a8f..00000000 --- a/src/workflow-runs/interfaces/workflow-run.ts +++ /dev/null @@ -1,25 +0,0 @@ -export interface WorkflowRunTrigger { - event_name: string; - payload?: Record; -} - -export type WorkflowRunStatus = - | 'running' - | 'completed' - | 'failed' - | 'cancelled'; - -export interface WorkflowRun { - object: 'workflow_run'; - id: string; - status: WorkflowRunStatus; - trigger: WorkflowRunTrigger | null; - started_at: string | null; - completed_at: string | null; - created_at: string; -} - -export type WorkflowRunItem = Pick< - WorkflowRun, - 'id' | 'status' | 'trigger' | 'started_at' | 'completed_at' | 'created_at' ->; diff --git a/src/workflow-runs/workflow-runs.spec.ts b/src/workflow-runs/workflow-runs.spec.ts deleted file mode 100644 index 65e6a624..00000000 --- a/src/workflow-runs/workflow-runs.spec.ts +++ /dev/null @@ -1,204 +0,0 @@ -import createFetchMock from 'vitest-fetch-mock'; -import { Resend } from '../resend'; -import { - mockErrorResponse, - mockSuccessResponse, -} from '../test-utils/mock-fetch'; -import type { - GetWorkflowRunOptions, - GetWorkflowRunResponseSuccess, -} from './interfaces/get-workflow-run.interface'; -import type { - ListWorkflowRunsOptions, - ListWorkflowRunsResponseSuccess, -} from './interfaces/list-workflow-runs.interface'; - -const fetchMocker = createFetchMock(vi); -fetchMocker.enableMocks(); - -describe('WorkflowRuns', () => { - afterEach(() => fetchMock.resetMocks()); - afterAll(() => fetchMocker.disableMocks()); - - describe('get', () => { - it('gets a workflow run', async () => { - const options: GetWorkflowRunOptions = { - workflowId: 'wf_123', - runId: 'wr_456', - }; - const response: GetWorkflowRunResponseSuccess = { - object: 'workflow_run', - id: 'wr_456', - status: 'completed', - trigger: { - event_name: 'user.created', - payload: { email: 'jane@example.com' }, - }, - started_at: '2024-01-01T00:00:00.000Z', - completed_at: '2024-01-01T00:01:00.000Z', - created_at: '2024-01-01T00:00:00.000Z', - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.get(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "completed_at": "2024-01-01T00:01:00.000Z", - "created_at": "2024-01-01T00:00:00.000Z", - "id": "wr_456", - "object": "workflow_run", - "started_at": "2024-01-01T00:00:00.000Z", - "status": "completed", - "trigger": { - "event_name": "user.created", - "payload": { - "email": "jane@example.com", - }, - }, - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('returns error', async () => { - const options: GetWorkflowRunOptions = { - workflowId: 'wf_123', - runId: 'wr_invalid', - }; - - mockErrorResponse( - { name: 'not_found', message: 'Workflow run not found' }, - {}, - ); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.runs.get(options); - expect(result.error).not.toBeNull(); - }); - }); - - describe('list', () => { - it('lists workflow runs', async () => { - const options: ListWorkflowRunsOptions = { - workflowId: 'wf_123', - }; - const response: ListWorkflowRunsResponseSuccess = { - object: 'list', - data: [ - { - id: 'wr_456', - status: 'completed', - trigger: { event_name: 'user.created' }, - started_at: '2024-01-01T00:00:00.000Z', - completed_at: '2024-01-01T00:01:00.000Z', - created_at: '2024-01-01T00:00:00.000Z', - }, - ], - has_more: false, - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.list(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "data": [ - { - "completed_at": "2024-01-01T00:01:00.000Z", - "created_at": "2024-01-01T00:00:00.000Z", - "id": "wr_456", - "started_at": "2024-01-01T00:00:00.000Z", - "status": "completed", - "trigger": { - "event_name": "user.created", - }, - }, - ], - "has_more": false, - "object": "list", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('lists workflow runs with pagination', async () => { - const options: ListWorkflowRunsOptions = { - workflowId: 'wf_123', - limit: 1, - after: 'wr_cursor', - }; - const response: ListWorkflowRunsResponseSuccess = { - object: 'list', - data: [ - { - id: 'wr_789', - status: 'running', - trigger: null, - started_at: '2024-01-02T00:00:00.000Z', - completed_at: null, - created_at: '2024-01-02T00:00:00.000Z', - }, - ], - has_more: true, - }; - - mockSuccessResponse(response, {}); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - await expect( - resend.workflows.runs.list(options), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "data": [ - { - "completed_at": null, - "created_at": "2024-01-02T00:00:00.000Z", - "id": "wr_789", - "started_at": "2024-01-02T00:00:00.000Z", - "status": "running", - "trigger": null, - }, - ], - "has_more": true, - "object": "list", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('returns error', async () => { - const options: ListWorkflowRunsOptions = { - workflowId: 'wf_invalid', - }; - - mockErrorResponse( - { name: 'not_found', message: 'Workflow not found' }, - {}, - ); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.runs.list(options); - expect(result.error).not.toBeNull(); - }); - }); -}); diff --git a/src/workflow-runs/workflow-runs.ts b/src/workflow-runs/workflow-runs.ts deleted file mode 100644 index ebc1e01a..00000000 --- a/src/workflow-runs/workflow-runs.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import type { Resend } from '../resend'; -import { WorkflowRunSteps } from '../workflow-run-steps/workflow-run-steps'; -import type { - GetWorkflowRunOptions, - GetWorkflowRunResponse, - GetWorkflowRunResponseSuccess, -} from './interfaces/get-workflow-run.interface'; -import type { - ListWorkflowRunsOptions, - ListWorkflowRunsResponse, - ListWorkflowRunsResponseSuccess, -} from './interfaces/list-workflow-runs.interface'; - -export class WorkflowRuns { - readonly steps: WorkflowRunSteps; - - constructor(private readonly resend: Resend) { - this.steps = new WorkflowRunSteps(resend); - } - - async get(options: GetWorkflowRunOptions): Promise { - const data = await this.resend.get( - `/automations/${options.workflowId}/runs/${options.runId}`, - ); - return data; - } - - async list( - options: ListWorkflowRunsOptions, - ): Promise { - const queryString = buildPaginationQuery(options); - const url = queryString - ? `/automations/${options.workflowId}/runs?${queryString}` - : `/automations/${options.workflowId}/runs`; - - const data = await this.resend.get(url); - return data; - } -} diff --git a/src/workflows/interfaces/create-workflow-options.interface.ts b/src/workflows/interfaces/create-workflow-options.interface.ts deleted file mode 100644 index 12685632..00000000 --- a/src/workflows/interfaces/create-workflow-options.interface.ts +++ /dev/null @@ -1,17 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { WorkflowStatus } from './workflow'; -import type { WorkflowEdge, WorkflowStep } from './workflow-step.interface'; - -export interface CreateWorkflowOptions { - name: string; - status?: WorkflowStatus; - steps: WorkflowStep[]; - edges: WorkflowEdge[]; -} - -export interface CreateWorkflowResponseSuccess { - object: 'workflow'; - id: string; -} - -export type CreateWorkflowResponse = Response; diff --git a/src/workflows/interfaces/get-workflow.interface.ts b/src/workflows/interfaces/get-workflow.interface.ts deleted file mode 100644 index 8fd66443..00000000 --- a/src/workflows/interfaces/get-workflow.interface.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { Workflow } from './workflow'; -import type { - WorkflowResponseEdge, - WorkflowResponseStep, -} from './workflow-step.interface'; - -export interface GetWorkflowResponseSuccess extends Workflow { - object: 'workflow'; - steps: WorkflowResponseStep[]; - edges: WorkflowResponseEdge[]; -} - -export type GetWorkflowResponse = Response; diff --git a/src/workflows/interfaces/index.ts b/src/workflows/interfaces/index.ts deleted file mode 100644 index 9a0d3c0a..00000000 --- a/src/workflows/interfaces/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -export * from './create-workflow-options.interface'; -export * from './get-workflow.interface'; -export * from './list-workflows.interface'; -export * from './remove-workflow.interface'; -export * from './update-workflow.interface'; -export * from './workflow'; -export * from './workflow-step.interface'; diff --git a/src/workflows/interfaces/list-workflows.interface.ts b/src/workflows/interfaces/list-workflows.interface.ts deleted file mode 100644 index 19efa6a4..00000000 --- a/src/workflows/interfaces/list-workflows.interface.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { - PaginatedData, - PaginationOptions, -} from '../../common/interfaces/pagination-options.interface'; -import type { Response } from '../../interfaces'; -import type { Workflow } from './workflow'; - -export type ListWorkflowsOptions = PaginationOptions; - -export type ListWorkflowsResponseSuccess = PaginatedData; - -export type ListWorkflowsResponse = Response; diff --git a/src/workflows/interfaces/remove-workflow.interface.ts b/src/workflows/interfaces/remove-workflow.interface.ts deleted file mode 100644 index bdf0100c..00000000 --- a/src/workflows/interfaces/remove-workflow.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { Workflow } from './workflow'; - -export interface RemoveWorkflowResponseSuccess extends Pick { - object: 'workflow'; - deleted: boolean; -} - -export type RemoveWorkflowResponse = Response; diff --git a/src/workflows/interfaces/update-workflow.interface.ts b/src/workflows/interfaces/update-workflow.interface.ts deleted file mode 100644 index ea855e4d..00000000 --- a/src/workflows/interfaces/update-workflow.interface.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Response } from '../../interfaces'; -import type { Workflow, WorkflowStatus } from './workflow'; - -export interface UpdateWorkflowOptions { - status: WorkflowStatus; -} - -export interface UpdateWorkflowResponseSuccess - extends Pick { - object: 'workflow'; -} - -export type UpdateWorkflowResponse = Response; diff --git a/src/workflows/interfaces/workflow-step.interface.ts b/src/workflows/interfaces/workflow-step.interface.ts deleted file mode 100644 index 93c2f53c..00000000 --- a/src/workflows/interfaces/workflow-step.interface.ts +++ /dev/null @@ -1,95 +0,0 @@ -type ConditionRule = - | { - type: 'rule'; - field: string; - operator: 'eq' | 'neq'; - value: string | number | boolean | null; - } - | { - type: 'rule'; - field: string; - operator: 'gt' | 'gte' | 'lt' | 'lte'; - value: number; - } - | { - type: 'rule'; - field: string; - operator: 'contains' | 'starts_with' | 'ends_with'; - value: string; - } - | { - type: 'rule'; - field: string; - operator: 'exists' | 'is_empty'; - } - | { type: 'and'; rules: ConditionRule[] } - | { type: 'or'; rules: ConditionRule[] }; - -type TemplateVariableValue = - | string - | number - | boolean - | { var: string } - | Record - | Array< - string | number | boolean | Record - >; - -interface TriggerStepConfig { - eventName: string; -} - -interface DelayStepConfig { - seconds: number; -} - -interface SendEmailStepConfig { - templateId: string; - subject?: string; - from?: string; - replyTo?: string; - variables?: Record; -} - -interface WaitForEventStepConfig { - eventName: string; - timeoutSeconds?: number; - filterRule?: ConditionRule; -} - -type ConditionStepConfig = ConditionRule; - -export type WorkflowStep = - | { ref: string; type: 'trigger'; config: TriggerStepConfig } - | { ref: string; type: 'delay'; config: DelayStepConfig } - | { ref: string; type: 'send_email'; config: SendEmailStepConfig } - | { ref: string; type: 'wait_for_event'; config: WaitForEventStepConfig } - | { ref: string; type: 'condition'; config: ConditionStepConfig }; - -export type WorkflowEdgeType = - | 'default' - | 'condition_met' - | 'condition_not_met' - | 'timeout' - | 'event_received'; - -export interface WorkflowEdge { - from: string; - to: string; - edgeType?: WorkflowEdgeType; -} - -export type WorkflowStepType = WorkflowStep['type']; - -export interface WorkflowResponseStep { - id: string; - type: WorkflowStepType; - config: Record; -} - -export interface WorkflowResponseEdge { - id: string; - from_step_id: string; - to_step_id: string; - edge_type: WorkflowEdgeType; -} diff --git a/src/workflows/interfaces/workflow.ts b/src/workflows/interfaces/workflow.ts deleted file mode 100644 index f361d16e..00000000 --- a/src/workflows/interfaces/workflow.ts +++ /dev/null @@ -1,9 +0,0 @@ -export interface Workflow { - id: string; - name: string; - status: 'enabled' | 'disabled'; - created_at: string; - updated_at: string | null; -} - -export type WorkflowStatus = Workflow['status']; diff --git a/src/workflows/workflows.spec.ts b/src/workflows/workflows.spec.ts deleted file mode 100644 index e1d8a75c..00000000 --- a/src/workflows/workflows.spec.ts +++ /dev/null @@ -1,405 +0,0 @@ -import createFetchMock from 'vitest-fetch-mock'; -import type { ErrorResponse } from '../interfaces'; -import { Resend } from '../resend'; -import { mockSuccessResponse } from '../test-utils/mock-fetch'; -import type { - CreateWorkflowOptions, - CreateWorkflowResponseSuccess, -} from './interfaces/create-workflow-options.interface'; -import type { GetWorkflowResponseSuccess } from './interfaces/get-workflow.interface'; -import type { ListWorkflowsResponseSuccess } from './interfaces/list-workflows.interface'; -import type { RemoveWorkflowResponseSuccess } from './interfaces/remove-workflow.interface'; -import type { UpdateWorkflowResponseSuccess } from './interfaces/update-workflow.interface'; - -const fetchMocker = createFetchMock(vi); -fetchMocker.enableMocks(); - -const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - -describe('Workflows', () => { - afterEach(() => fetchMock.resetMocks()); - afterAll(() => fetchMocker.disableMocks()); - - describe('create', () => { - it('creates a workflow', async () => { - const response: CreateWorkflowResponseSuccess = { - object: 'workflow', - id: '71cdfe68-cf79-473a-a9d7-21f91db6a526', - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }); - - const payload: CreateWorkflowOptions = { - name: 'Welcome Flow', - status: 'enabled', - steps: [ - { - ref: 'trigger', - type: 'trigger', - config: { eventName: 'user.created' }, - }, - { - ref: 'welcome_email', - type: 'send_email', - config: { templateId: 'tpl-123' }, - }, - ], - edges: [{ from: 'trigger', to: 'welcome_email', edgeType: 'default' }], - }; - - const data = await resend.workflows.create(payload); - expect(data).toMatchInlineSnapshot(` - { - "data": { - "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", - "object": "workflow", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - - it('throws an error when an ErrorResponse is returned', async () => { - const response: ErrorResponse = { - name: 'missing_required_field', - statusCode: 422, - message: 'Missing `name` field.', - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 422, - headers: { - 'content-type': 'application/json', - }, - }); - - const data = await resend.workflows.create({ - name: '', - steps: [], - edges: [], - }); - expect(data).toMatchInlineSnapshot(` - { - "data": null, - "error": { - "message": "Missing \`name\` field.", - "name": "missing_required_field", - "statusCode": 422, - }, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - }); - - describe('list', () => { - const response: ListWorkflowsResponseSuccess = { - object: 'list', - has_more: false, - data: [ - { - id: '49a3999c-0ce1-4ea6-ab68-afcd6dc2e794', - name: 'Welcome Flow', - status: 'enabled', - created_at: '2025-01-01T00:00:00.000Z', - updated_at: '2025-01-01T00:00:00.000Z', - }, - { - id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', - name: 'Onboarding Flow', - status: 'disabled', - created_at: '2025-02-01T00:00:00.000Z', - updated_at: '2025-02-01T00:00:00.000Z', - }, - ], - }; - - describe('when no pagination options are provided', () => { - it('lists workflows', async () => { - mockSuccessResponse(response, { - headers: {}, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - - const result = await resend.workflows.list(); - expect(result).toEqual({ - data: response, - error: null, - headers: { - 'content-type': 'application/json', - }, - }); - - expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/automations', - expect.objectContaining({ - method: 'GET', - headers: expect.any(Headers), - }), - ); - }); - }); - - describe('when pagination options are provided', () => { - it('passes limit param and returns a response', async () => { - mockSuccessResponse(response, { - headers: {}, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.list({ limit: 1 }); - expect(result).toEqual({ - data: response, - error: null, - headers: { - 'content-type': 'application/json', - }, - }); - - expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/automations?limit=1', - expect.objectContaining({ - method: 'GET', - headers: expect.any(Headers), - }), - ); - }); - - it('passes after param and returns a response', async () => { - mockSuccessResponse(response, { - headers: {}, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.list({ - limit: 1, - after: 'cursor-value', - }); - expect(result).toEqual({ - data: response, - error: null, - headers: { - 'content-type': 'application/json', - }, - }); - - expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/automations?limit=1&after=cursor-value', - expect.objectContaining({ - method: 'GET', - headers: expect.any(Headers), - }), - ); - }); - - it('passes before param and returns a response', async () => { - mockSuccessResponse(response, { - headers: {}, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - const result = await resend.workflows.list({ - limit: 1, - before: 'cursor-value', - }); - expect(result).toEqual({ - data: response, - error: null, - headers: { - 'content-type': 'application/json', - }, - }); - - expect(fetchMock).toHaveBeenCalledWith( - 'https://api.resend.com/automations?limit=1&before=cursor-value', - expect.objectContaining({ - method: 'GET', - headers: expect.any(Headers), - }), - ); - }); - }); - }); - - describe('get', () => { - describe('when workflow not found', () => { - it('returns error', async () => { - const response: ErrorResponse = { - name: 'not_found', - statusCode: 404, - message: 'Workflow not found', - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 404, - headers: { - 'content-type': 'application/json', - }, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - - const result = resend.workflows.get( - '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', - ); - - await expect(result).resolves.toMatchInlineSnapshot(` - { - "data": null, - "error": { - "message": "Workflow not found", - "name": "not_found", - "statusCode": 404, - }, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - }); - - it('gets a workflow', async () => { - const response: GetWorkflowResponseSuccess = { - object: 'workflow', - id: '559ac32e-9ef5-46fb-82a1-b76b840c0f7b', - name: 'Welcome Flow', - status: 'enabled', - created_at: '2025-01-01T00:00:00.000Z', - updated_at: '2025-01-01T00:00:00.000Z', - steps: [ - { - id: 'step-1', - type: 'trigger', - config: { event_name: 'user.created' }, - }, - ], - edges: [], - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - - await expect( - resend.workflows.get('559ac32e-9ef5-46fb-82a1-b76b840c0f7b'), - ).resolves.toMatchInlineSnapshot(` - { - "data": { - "created_at": "2025-01-01T00:00:00.000Z", - "edges": [], - "id": "559ac32e-9ef5-46fb-82a1-b76b840c0f7b", - "name": "Welcome Flow", - "object": "workflow", - "status": "enabled", - "steps": [ - { - "config": { - "event_name": "user.created", - }, - "id": "step-1", - "type": "trigger", - }, - ], - "updated_at": "2025-01-01T00:00:00.000Z", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - }); - - describe('remove', () => { - it('removes a workflow', async () => { - const id = 'b01e0de9-7c27-4a53-bf38-2e3f98389a65'; - const response: RemoveWorkflowResponseSuccess = { - object: 'workflow', - id, - deleted: true, - }; - fetchMock.mockOnce(JSON.stringify(response), { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }); - - const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); - - await expect(resend.workflows.remove(id)).resolves.toMatchInlineSnapshot(` - { - "data": { - "deleted": true, - "id": "b01e0de9-7c27-4a53-bf38-2e3f98389a65", - "object": "workflow", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - }); - }); - - describe('update', () => { - it('updates a workflow', async () => { - const id = '71cdfe68-cf79-473a-a9d7-21f91db6a526'; - const response: UpdateWorkflowResponseSuccess = { - object: 'workflow', - id, - status: 'disabled', - }; - - fetchMock.mockOnce(JSON.stringify(response), { - status: 200, - headers: { - 'content-type': 'application/json', - }, - }); - - const data = await resend.workflows.update(id, { status: 'disabled' }); - expect(data).toMatchInlineSnapshot(` - { - "data": { - "id": "71cdfe68-cf79-473a-a9d7-21f91db6a526", - "object": "workflow", - "status": "disabled", - }, - "error": null, - "headers": { - "content-type": "application/json", - }, - } - `); - - expect(fetchMock).toHaveBeenCalledWith( - `https://api.resend.com/automations/${id}`, - expect.objectContaining({ - method: 'PATCH', - headers: expect.any(Headers), - body: JSON.stringify({ status: 'disabled' }), - }), - ); - }); - }); -}); diff --git a/src/workflows/workflows.ts b/src/workflows/workflows.ts deleted file mode 100644 index f710471a..00000000 --- a/src/workflows/workflows.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { buildPaginationQuery } from '../common/utils/build-pagination-query'; -import { parseAutomationToApiOptions } from '../common/utils/parse-automation-to-api-options'; -import type { Resend } from '../resend'; -import { WorkflowRuns } from '../workflow-runs/workflow-runs'; -import type { - CreateWorkflowOptions, - CreateWorkflowResponse, - CreateWorkflowResponseSuccess, -} from './interfaces/create-workflow-options.interface'; -import type { - GetWorkflowResponse, - GetWorkflowResponseSuccess, -} from './interfaces/get-workflow.interface'; -import type { - ListWorkflowsOptions, - ListWorkflowsResponse, - ListWorkflowsResponseSuccess, -} from './interfaces/list-workflows.interface'; -import type { - RemoveWorkflowResponse, - RemoveWorkflowResponseSuccess, -} from './interfaces/remove-workflow.interface'; -import type { - UpdateWorkflowOptions, - UpdateWorkflowResponse, - UpdateWorkflowResponseSuccess, -} from './interfaces/update-workflow.interface'; - -export class Workflows { - readonly runs: WorkflowRuns; - - constructor(private readonly resend: Resend) { - this.runs = new WorkflowRuns(this.resend); - } - - async create( - payload: CreateWorkflowOptions, - ): Promise { - const data = await this.resend.post( - '/automations', - parseAutomationToApiOptions(payload), - ); - - return data; - } - - async list( - options: ListWorkflowsOptions = {}, - ): Promise { - const queryString = buildPaginationQuery(options); - const url = queryString ? `/automations?${queryString}` : '/automations'; - - const data = await this.resend.get(url); - return data; - } - - async get(id: string): Promise { - const data = await this.resend.get( - `/automations/${id}`, - ); - return data; - } - - async remove(id: string): Promise { - const data = await this.resend.delete( - `/automations/${id}`, - ); - return data; - } - - async update( - id: string, - payload: UpdateWorkflowOptions, - ): Promise { - const data = await this.resend.patch( - `/automations/${id}`, - payload, - ); - return data; - } -} From 646480bfb4aaa4dbc9cff89c045b4a2807a3ec3a Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Mon, 30 Mar 2026 12:26:50 -0300 Subject: [PATCH 22/23] fix send event --- src/events/events.spec.ts | 12 ++++-------- src/events/interfaces/send-event.interface.ts | 3 +-- 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/events/events.spec.ts b/src/events/events.spec.ts index a9cc9a93..7d546893 100644 --- a/src/events/events.spec.ts +++ b/src/events/events.spec.ts @@ -21,9 +21,8 @@ describe('Events', () => { describe('send', () => { it('sends an event with contactId', async () => { const response: SendEventResponseSuccess = { - object: 'workflow_event', + object: 'event', event: 'user.created', - event_instance_id: 'evt-inst-123', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -43,8 +42,7 @@ describe('Events', () => { { "data": { "event": "user.created", - "event_instance_id": "evt-inst-123", - "object": "workflow_event", + "object": "event", }, "error": null, "headers": { @@ -69,9 +67,8 @@ describe('Events', () => { it('sends an event with email', async () => { const response: SendEventResponseSuccess = { - object: 'workflow_event', + object: 'event', event: 'user.created', - event_instance_id: 'evt-inst-456', }; fetchMock.mockOnce(JSON.stringify(response), { @@ -90,8 +87,7 @@ describe('Events', () => { { "data": { "event": "user.created", - "event_instance_id": "evt-inst-456", - "object": "workflow_event", + "object": "event", }, "error": null, "headers": { diff --git a/src/events/interfaces/send-event.interface.ts b/src/events/interfaces/send-event.interface.ts index f937bb40..d7cf8091 100644 --- a/src/events/interfaces/send-event.interface.ts +++ b/src/events/interfaces/send-event.interface.ts @@ -15,9 +15,8 @@ export type SendEventOptions = }; export interface SendEventResponseSuccess { - object: 'workflow_event'; + object: 'event'; event: string; - event_instance_id: string; } export type SendEventResponse = Response; From 3e2d00f126ff8b28248a02cbc58dabccdefebc6a Mon Sep 17 00:00:00 2001 From: Vitor Capretz Date: Mon, 30 Mar 2026 12:32:44 -0300 Subject: [PATCH 23/23] new version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cd5e0ab6..3bb50fbc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resend", - "version": "6.10.0-preview-workflows.2", + "version": "6.10.0-preview-workflows.3", "description": "Node.js library for the Resend API", "main": "./dist/index.cjs", "module": "./dist/index.mjs",