-
Notifications
You must be signed in to change notification settings - Fork 81
feat: add automations and events #866
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
945c9b4
959561e
610ca1f
41ea962
51494bd
dfcf123
c35348c
c77997b
ebfd3d9
2a52217
3c18f09
37bdf58
c95c1cd
6044ed9
14f96ea
c02d756
74abc72
8ee5942
ae6d889
2b845bd
19e5642
a1799f9
1e57f4f
2096c66
b21dd33
027ee94
3ab7349
2455ebf
d8fbb20
ab25d1f
e145638
7ef5d29
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,235 @@ | ||
| 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', | ||
| started_at: '2024-01-01T00:00:00.000Z', | ||
| completed_at: '2024-01-01T00:01:00.000Z', | ||
| created_at: '2024-01-01T00:00:00.000Z', | ||
| steps: [ | ||
| { | ||
| key: 'trigger_1', | ||
| type: 'trigger', | ||
| status: 'completed', | ||
| output: null, | ||
| error: null, | ||
| started_at: '2024-01-01T00:00:00.000Z', | ||
| completed_at: '2024-01-01T00:00:01.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), | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Custom agent: API Key Permission Check SDK Methods The new Resend SDK methods for automation run get/list require confirming production API keys include the necessary permissions for automation run read/list operations, per the API Key Permission Check rule. Prompt for AI agents |
||
| ).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", | ||
| "steps": [ | ||
| { | ||
| "completed_at": "2024-01-01T00:00:01.000Z", | ||
| "created_at": "2024-01-01T00:00:00.000Z", | ||
| "error": null, | ||
| "key": "trigger_1", | ||
| "output": null, | ||
| "started_at": "2024-01-01T00:00:00.000Z", | ||
| "status": "completed", | ||
| "type": "trigger", | ||
| }, | ||
| ], | ||
| }, | ||
| "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', | ||
| 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", | ||
| }, | ||
| ], | ||
| "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', | ||
| 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", | ||
| }, | ||
| ], | ||
| "has_more": true, | ||
| "object": "list", | ||
| }, | ||
| "error": null, | ||
| "headers": { | ||
| "content-type": "application/json", | ||
| }, | ||
| } | ||
| `); | ||
| }); | ||
|
|
||
| it('lists automation runs with status filter', async () => { | ||
| const options: ListAutomationRunsOptions = { | ||
| automationId: 'wf_123', | ||
| status: ['running', 'failed'], | ||
| }; | ||
| const response: ListAutomationRunsResponseSuccess = { | ||
| object: 'list', | ||
| data: [], | ||
| has_more: false, | ||
| }; | ||
|
|
||
| mockSuccessResponse(response, {}); | ||
|
|
||
| const resend = new Resend('re_zKa4RCko_Lhm9ost2YjNCctnPjbLw8Nop'); | ||
| await resend.automations.runs.list(options); | ||
|
|
||
| expect(fetchMock).toHaveBeenCalledWith( | ||
| 'https://api.resend.com/automations/wf_123/runs?status=running%2Cfailed', | ||
| expect.objectContaining({ | ||
| method: 'GET', | ||
| headers: expect.any(Headers), | ||
| }), | ||
| ); | ||
| }); | ||
|
|
||
| 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(); | ||
| }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,47 @@ | ||||||||||||||||||||||||||||||||
| 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 { | ||||||||||||||||||||||||||||||||
| constructor(private readonly resend: Resend) {} | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async get( | ||||||||||||||||||||||||||||||||
| options: GetAutomationRunOptions, | ||||||||||||||||||||||||||||||||
| ): Promise<GetAutomationRunResponse> { | ||||||||||||||||||||||||||||||||
| const data = await this.resend.get<GetAutomationRunResponseSuccess>( | ||||||||||||||||||||||||||||||||
| `/automations/${options.automationId}/runs/${options.runId}`, | ||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| async list( | ||||||||||||||||||||||||||||||||
| options: ListAutomationRunsOptions, | ||||||||||||||||||||||||||||||||
| ): Promise<ListAutomationRunsResponse> { | ||||||||||||||||||||||||||||||||
| const queryString = buildPaginationQuery(options); | ||||||||||||||||||||||||||||||||
| const searchParams = new URLSearchParams(queryString); | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| if (options.status) { | ||||||||||||||||||||||||||||||||
| const statusValue = Array.isArray(options.status) | ||||||||||||||||||||||||||||||||
| ? options.status.join(',') | ||||||||||||||||||||||||||||||||
| : options.status; | ||||||||||||||||||||||||||||||||
| searchParams.set('status', statusValue); | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+37
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Avoid setting Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const qs = searchParams.toString(); | ||||||||||||||||||||||||||||||||
| const url = qs | ||||||||||||||||||||||||||||||||
| ? `/automations/${options.automationId}/runs?${qs}` | ||||||||||||||||||||||||||||||||
| : `/automations/${options.automationId}/runs`; | ||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||
| const data = await this.resend.get<ListAutomationRunsResponseSuccess>(url); | ||||||||||||||||||||||||||||||||
| return data; | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| import type { AutomationStepType } from '../../automations/interfaces/automation-step.interface'; | ||
|
|
||
| export type AutomationRunStatus = | ||
| | 'running' | ||
| | 'completed' | ||
| | 'failed' | ||
| | 'cancelled'; | ||
|
|
||
| export type AutomationRunStepStatus = | ||
| | 'pending' | ||
| | 'running' | ||
| | 'completed' | ||
| | 'failed' | ||
| | 'skipped' | ||
| | 'waiting'; | ||
|
|
||
| export interface AutomationRunStep { | ||
| key: string; | ||
| type: AutomationStepType; | ||
| status: AutomationRunStepStatus; | ||
| output: Record<string, unknown> | null; | ||
| error: Record<string, unknown> | null; | ||
| started_at: string | null; | ||
| completed_at: string | null; | ||
| created_at: string; | ||
| } | ||
|
|
||
| export interface AutomationRun { | ||
| object: 'automation_run'; | ||
| id: string; | ||
| status: AutomationRunStatus; | ||
| started_at: string | null; | ||
| completed_at: string | null; | ||
| created_at: string; | ||
| steps: AutomationRunStep[]; | ||
| } | ||
|
|
||
| export type AutomationRunItem = Pick< | ||
| AutomationRun, | ||
| 'id' | 'status' | 'started_at' | 'completed_at' | 'created_at' | ||
| >; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<GetAutomationRunResponseSuccess>; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| export * from './automation-run'; | ||
| export * from './get-automation-run.interface'; | ||
| export * from './list-automation-runs.interface'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| import type { | ||
| PaginatedData, | ||
| PaginationOptions, | ||
| } from '../../common/interfaces/pagination-options.interface'; | ||
| import type { Response } from '../../interfaces'; | ||
| import type { AutomationRunItem, AutomationRunStatus } from './automation-run'; | ||
|
|
||
| export type ListAutomationRunsOptions = PaginationOptions & { | ||
| automationId: string; | ||
| status?: AutomationRunStatus | AutomationRunStatus[]; | ||
| }; | ||
|
|
||
| export type ListAutomationRunsResponseSuccess = PaginatedData< | ||
| AutomationRunItem[] | ||
| >; | ||
|
|
||
| export type ListAutomationRunsResponse = | ||
| Response<ListAutomationRunsResponseSuccess>; |
Uh oh!
There was an error while loading. Please reload this page.