From f6a2fce83abaa712e60d89fe255b13fbe9b905e8 Mon Sep 17 00:00:00 2001 From: Kyle Mistele Date: Mon, 15 Jun 2026 15:53:15 -0700 Subject: [PATCH] feat(resend): add Resend SDK package - Add new @distilled.cloud/resend package with full API coverage - Includes operations for emails, domains, contacts, templates, webhooks, and more - Add CI workflow for testing - Add publication infrastructure to release.yml and pr-package.yml - Add nuke workflow job for test resource cleanup - Configure resend-openapi as git submodule for spec tracking --- .github/workflows/nuke.yml | 26 +- .github/workflows/pr-package.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/test.yml | 24 + .gitmodules | 5 + bun.lock | 60 +- packages/resend/README.md | 71 ++ packages/resend/package.json | 89 +++ .../001-add-error-responses.patch.json | 649 +++++++++++++++++ packages/resend/scripts/generate.ts | 164 +++++ packages/resend/scripts/nuke.ts | 684 ++++++++++++++++++ packages/resend/specs/resend-openapi | 1 + packages/resend/src/category.ts | 4 + packages/resend/src/client.ts | 106 +++ packages/resend/src/credentials.ts | 35 + packages/resend/src/errors.ts | 68 ++ packages/resend/src/index.ts | 16 + packages/resend/src/operations/batchEmail.ts | 46 ++ packages/resend/src/operations/cancelEmail.ts | 54 ++ .../resend/src/operations/createApiKey.ts | 31 + .../resend/src/operations/createAutomation.ts | 61 ++ .../resend/src/operations/createBroadcast.ts | 38 + .../resend/src/operations/createContact.ts | 41 ++ .../src/operations/createContactImport.ts | 39 + .../src/operations/createContactProperty.ts | 34 + .../src/operations/createContactSegment.ts | 41 ++ .../resend/src/operations/createDomain.ts | 91 +++ packages/resend/src/operations/createEmail.ts | 77 ++ packages/resend/src/operations/createEvent.ts | 28 + .../resend/src/operations/createSegment.ts | 29 + .../resend/src/operations/createTemplate.ts | 48 ++ packages/resend/src/operations/createTopic.ts | 30 + .../resend/src/operations/createWebhook.ts | 30 + .../resend/src/operations/deleteApiKey.ts | 30 + .../resend/src/operations/deleteAutomation.ts | 32 + .../resend/src/operations/deleteBroadcast.ts | 30 + .../resend/src/operations/deleteContact.ts | 30 + .../src/operations/deleteContactProperty.ts | 35 + .../src/operations/deleteContactSegment.ts | 42 ++ .../resend/src/operations/deleteDomain.ts | 30 + packages/resend/src/operations/deleteEvent.ts | 28 + .../resend/src/operations/deleteSegment.ts | 30 + .../resend/src/operations/deleteTemplate.ts | 30 + packages/resend/src/operations/deleteTopic.ts | 30 + .../resend/src/operations/deleteWebhook.ts | 30 + .../src/operations/duplicateTemplate.ts | 32 + .../resend/src/operations/getAutomation.ts | 70 ++ .../resend/src/operations/getAutomationRun.ts | 66 ++ .../resend/src/operations/getBroadcast.ts | 42 ++ packages/resend/src/operations/getContact.ts | 35 + .../resend/src/operations/getContactImport.ts | 45 ++ .../src/operations/getContactProperty.ts | 35 + packages/resend/src/operations/getDomain.ts | 80 ++ packages/resend/src/operations/getEmail.ts | 54 ++ .../src/operations/getEmailAttachment.ts | 47 ++ .../src/operations/getEmailReceiving.ts | 57 ++ .../operations/getEmailReceivingAttachment.ts | 51 ++ packages/resend/src/operations/getEvent.ts | 33 + packages/resend/src/operations/getLog.ts | 38 + packages/resend/src/operations/getSegment.ts | 33 + packages/resend/src/operations/getTemplate.ts | 60 ++ packages/resend/src/operations/getTopic.ts | 34 + packages/resend/src/operations/getWebhook.ts | 35 + packages/resend/src/operations/index.ts | 82 +++ packages/resend/src/operations/listApiKeys.ts | 41 ++ .../src/operations/listAutomationRuns.ts | 52 ++ .../resend/src/operations/listAutomations.ts | 44 ++ .../resend/src/operations/listBroadcasts.ts | 46 ++ .../src/operations/listContactImports.ts | 59 ++ .../src/operations/listContactProperties.ts | 47 ++ .../src/operations/listContactSegments.ts | 44 ++ .../src/operations/listContactTopics.ts | 46 ++ .../resend/src/operations/listContacts.ts | 44 ++ packages/resend/src/operations/listDomains.ts | 59 ++ .../src/operations/listEmailAttachments.ts | 55 ++ .../src/operations/listEmailReceiving.ts | 62 ++ .../listEmailReceivingAttachments.ts | 58 ++ packages/resend/src/operations/listEmails.ts | 64 ++ packages/resend/src/operations/listEvents.ts | 42 ++ packages/resend/src/operations/listLogs.ts | 45 ++ .../resend/src/operations/listSegments.ts | 41 ++ .../resend/src/operations/listTemplates.ts | 44 ++ packages/resend/src/operations/listTopics.ts | 45 ++ .../resend/src/operations/listWebhooks.ts | 42 ++ .../resend/src/operations/publishTemplate.ts | 29 + .../resend/src/operations/sendBroadcast.ts | 29 + packages/resend/src/operations/sendEvent.ts | 27 + .../resend/src/operations/stopAutomation.ts | 30 + .../resend/src/operations/updateAutomation.ts | 68 ++ .../resend/src/operations/updateBroadcast.ts | 39 + .../resend/src/operations/updateContact.ts | 34 + .../src/operations/updateContactProperty.ts | 35 + .../src/operations/updateContactTopic.ts | 45 ++ .../resend/src/operations/updateDomain.ts | 39 + packages/resend/src/operations/updateEmail.ts | 28 + packages/resend/src/operations/updateEvent.ts | 28 + .../resend/src/operations/updateTemplate.ts | 51 ++ packages/resend/src/operations/updateTopic.ts | 32 + .../resend/src/operations/updateWebhook.ts | 32 + .../resend/src/operations/verifyDomain.ts | 31 + packages/resend/src/retry.ts | 47 ++ packages/resend/src/sensitive.ts | 4 + packages/resend/src/traits.ts | 4 + packages/resend/test/batchEmail.test.ts | 73 ++ packages/resend/test/cancelEmail.test.ts | 69 ++ packages/resend/test/createApiKey.test.ts | 72 ++ packages/resend/test/createAutomation.test.ts | 86 +++ packages/resend/test/createBroadcast.test.ts | 88 +++ packages/resend/test/createContact.test.ts | 81 +++ .../resend/test/createContactImport.test.ts | 28 + .../resend/test/createContactProperty.test.ts | 100 +++ .../resend/test/createContactSegment.test.ts | 99 +++ packages/resend/test/createDomain.test.ts | 84 +++ packages/resend/test/createEmail.test.ts | 129 ++++ packages/resend/test/createEvent.test.ts | 73 ++ packages/resend/test/createSegment.test.ts | 39 + packages/resend/test/createTemplate.test.ts | 80 ++ packages/resend/test/createTopic.test.ts | 73 ++ packages/resend/test/createWebhook.test.ts | 73 ++ packages/resend/test/deleteApiKey.test.ts | 33 + packages/resend/test/deleteAutomation.test.ts | 57 ++ packages/resend/test/deleteBroadcast.test.ts | 60 ++ packages/resend/test/deleteContact.test.ts | 36 + .../resend/test/deleteContactProperty.test.ts | 41 ++ .../resend/test/deleteContactSegment.test.ts | 94 +++ packages/resend/test/deleteDomain.test.ts | 45 ++ packages/resend/test/deleteEvent.test.ts | 49 ++ packages/resend/test/deleteSegment.test.ts | 36 + packages/resend/test/deleteTemplate.test.ts | 39 + packages/resend/test/deleteTopic.test.ts | 39 + packages/resend/test/deleteWebhook.test.ts | 39 + .../resend/test/duplicateTemplate.test.ts | 59 ++ packages/resend/test/getAutomation.test.ts | 69 ++ packages/resend/test/getAutomationRun.test.ts | 138 ++++ packages/resend/test/getBroadcast.test.ts | 71 ++ packages/resend/test/getContact.test.ts | 52 ++ packages/resend/test/getContactImport.test.ts | 42 ++ .../resend/test/getContactProperty.test.ts | 52 ++ packages/resend/test/getDomain.test.ts | 44 ++ packages/resend/test/getEmail.test.ts | 66 ++ .../resend/test/getEmailAttachment.test.ts | 106 +++ .../resend/test/getEmailReceiving.test.ts | 36 + .../test/getEmailReceivingAttachment.test.ts | 70 ++ packages/resend/test/getEvent.test.ts | 70 ++ packages/resend/test/getLog.test.ts | 39 + packages/resend/test/getSegment.test.ts | 46 ++ packages/resend/test/getTemplate.test.ts | 52 ++ packages/resend/test/getTopic.test.ts | 50 ++ packages/resend/test/getWebhook.test.ts | 52 ++ packages/resend/test/listApiKeys.test.ts | 33 + .../resend/test/listAutomationRuns.test.ts | 124 ++++ packages/resend/test/listAutomations.test.ts | 47 ++ packages/resend/test/listBroadcasts.test.ts | 38 + .../resend/test/listContactImports.test.ts | 50 ++ .../resend/test/listContactProperties.test.ts | 43 ++ .../resend/test/listContactSegments.test.ts | 82 +++ .../resend/test/listContactTopics.test.ts | 82 +++ packages/resend/test/listContacts.test.ts | 43 ++ packages/resend/test/listDomains.test.ts | 33 + .../resend/test/listEmailAttachments.test.ts | 118 +++ .../resend/test/listEmailReceiving.test.ts | 33 + .../listEmailReceivingAttachments.test.ts | 56 ++ packages/resend/test/listEmails.test.ts | 33 + packages/resend/test/listEvents.test.ts | 33 + packages/resend/test/listLogs.test.ts | 33 + packages/resend/test/listSegments.test.ts | 38 + packages/resend/test/listTemplates.test.ts | 41 ++ packages/resend/test/listTopics.test.ts | 47 ++ packages/resend/test/listWebhooks.test.ts | 44 ++ packages/resend/test/publishTemplate.test.ts | 50 ++ packages/resend/test/sendBroadcast.test.ts | 130 ++++ packages/resend/test/sendEvent.test.ts | 62 ++ packages/resend/test/setup.ts | 28 + packages/resend/test/stopAutomation.test.ts | 68 ++ packages/resend/test/updateAutomation.test.ts | 123 ++++ packages/resend/test/updateBroadcast.test.ts | 134 ++++ packages/resend/test/updateContact.test.ts | 94 +++ .../resend/test/updateContactProperty.test.ts | 98 +++ .../resend/test/updateContactTopic.test.ts | 123 ++++ packages/resend/test/updateDomain.test.ts | 84 +++ packages/resend/test/updateEmail.test.ts | 92 +++ packages/resend/test/updateEvent.test.ts | 93 +++ packages/resend/test/updateTemplate.test.ts | 96 +++ packages/resend/test/updateTopic.test.ts | 97 +++ packages/resend/test/updateWebhook.test.ts | 95 +++ packages/resend/test/verifyDomain.test.ts | 43 ++ packages/resend/tsconfig.json | 20 + packages/resend/tsconfig.test.json | 16 + packages/resend/vitest.config.ts | 17 + 189 files changed, 11162 insertions(+), 21 deletions(-) create mode 100644 packages/resend/README.md create mode 100644 packages/resend/package.json create mode 100644 packages/resend/patches/001-add-error-responses.patch.json create mode 100644 packages/resend/scripts/generate.ts create mode 100644 packages/resend/scripts/nuke.ts create mode 160000 packages/resend/specs/resend-openapi create mode 100644 packages/resend/src/category.ts create mode 100644 packages/resend/src/client.ts create mode 100644 packages/resend/src/credentials.ts create mode 100644 packages/resend/src/errors.ts create mode 100644 packages/resend/src/index.ts create mode 100644 packages/resend/src/operations/batchEmail.ts create mode 100644 packages/resend/src/operations/cancelEmail.ts create mode 100644 packages/resend/src/operations/createApiKey.ts create mode 100644 packages/resend/src/operations/createAutomation.ts create mode 100644 packages/resend/src/operations/createBroadcast.ts create mode 100644 packages/resend/src/operations/createContact.ts create mode 100644 packages/resend/src/operations/createContactImport.ts create mode 100644 packages/resend/src/operations/createContactProperty.ts create mode 100644 packages/resend/src/operations/createContactSegment.ts create mode 100644 packages/resend/src/operations/createDomain.ts create mode 100644 packages/resend/src/operations/createEmail.ts create mode 100644 packages/resend/src/operations/createEvent.ts create mode 100644 packages/resend/src/operations/createSegment.ts create mode 100644 packages/resend/src/operations/createTemplate.ts create mode 100644 packages/resend/src/operations/createTopic.ts create mode 100644 packages/resend/src/operations/createWebhook.ts create mode 100644 packages/resend/src/operations/deleteApiKey.ts create mode 100644 packages/resend/src/operations/deleteAutomation.ts create mode 100644 packages/resend/src/operations/deleteBroadcast.ts create mode 100644 packages/resend/src/operations/deleteContact.ts create mode 100644 packages/resend/src/operations/deleteContactProperty.ts create mode 100644 packages/resend/src/operations/deleteContactSegment.ts create mode 100644 packages/resend/src/operations/deleteDomain.ts create mode 100644 packages/resend/src/operations/deleteEvent.ts create mode 100644 packages/resend/src/operations/deleteSegment.ts create mode 100644 packages/resend/src/operations/deleteTemplate.ts create mode 100644 packages/resend/src/operations/deleteTopic.ts create mode 100644 packages/resend/src/operations/deleteWebhook.ts create mode 100644 packages/resend/src/operations/duplicateTemplate.ts create mode 100644 packages/resend/src/operations/getAutomation.ts create mode 100644 packages/resend/src/operations/getAutomationRun.ts create mode 100644 packages/resend/src/operations/getBroadcast.ts create mode 100644 packages/resend/src/operations/getContact.ts create mode 100644 packages/resend/src/operations/getContactImport.ts create mode 100644 packages/resend/src/operations/getContactProperty.ts create mode 100644 packages/resend/src/operations/getDomain.ts create mode 100644 packages/resend/src/operations/getEmail.ts create mode 100644 packages/resend/src/operations/getEmailAttachment.ts create mode 100644 packages/resend/src/operations/getEmailReceiving.ts create mode 100644 packages/resend/src/operations/getEmailReceivingAttachment.ts create mode 100644 packages/resend/src/operations/getEvent.ts create mode 100644 packages/resend/src/operations/getLog.ts create mode 100644 packages/resend/src/operations/getSegment.ts create mode 100644 packages/resend/src/operations/getTemplate.ts create mode 100644 packages/resend/src/operations/getTopic.ts create mode 100644 packages/resend/src/operations/getWebhook.ts create mode 100644 packages/resend/src/operations/index.ts create mode 100644 packages/resend/src/operations/listApiKeys.ts create mode 100644 packages/resend/src/operations/listAutomationRuns.ts create mode 100644 packages/resend/src/operations/listAutomations.ts create mode 100644 packages/resend/src/operations/listBroadcasts.ts create mode 100644 packages/resend/src/operations/listContactImports.ts create mode 100644 packages/resend/src/operations/listContactProperties.ts create mode 100644 packages/resend/src/operations/listContactSegments.ts create mode 100644 packages/resend/src/operations/listContactTopics.ts create mode 100644 packages/resend/src/operations/listContacts.ts create mode 100644 packages/resend/src/operations/listDomains.ts create mode 100644 packages/resend/src/operations/listEmailAttachments.ts create mode 100644 packages/resend/src/operations/listEmailReceiving.ts create mode 100644 packages/resend/src/operations/listEmailReceivingAttachments.ts create mode 100644 packages/resend/src/operations/listEmails.ts create mode 100644 packages/resend/src/operations/listEvents.ts create mode 100644 packages/resend/src/operations/listLogs.ts create mode 100644 packages/resend/src/operations/listSegments.ts create mode 100644 packages/resend/src/operations/listTemplates.ts create mode 100644 packages/resend/src/operations/listTopics.ts create mode 100644 packages/resend/src/operations/listWebhooks.ts create mode 100644 packages/resend/src/operations/publishTemplate.ts create mode 100644 packages/resend/src/operations/sendBroadcast.ts create mode 100644 packages/resend/src/operations/sendEvent.ts create mode 100644 packages/resend/src/operations/stopAutomation.ts create mode 100644 packages/resend/src/operations/updateAutomation.ts create mode 100644 packages/resend/src/operations/updateBroadcast.ts create mode 100644 packages/resend/src/operations/updateContact.ts create mode 100644 packages/resend/src/operations/updateContactProperty.ts create mode 100644 packages/resend/src/operations/updateContactTopic.ts create mode 100644 packages/resend/src/operations/updateDomain.ts create mode 100644 packages/resend/src/operations/updateEmail.ts create mode 100644 packages/resend/src/operations/updateEvent.ts create mode 100644 packages/resend/src/operations/updateTemplate.ts create mode 100644 packages/resend/src/operations/updateTopic.ts create mode 100644 packages/resend/src/operations/updateWebhook.ts create mode 100644 packages/resend/src/operations/verifyDomain.ts create mode 100644 packages/resend/src/retry.ts create mode 100644 packages/resend/src/sensitive.ts create mode 100644 packages/resend/src/traits.ts create mode 100644 packages/resend/test/batchEmail.test.ts create mode 100644 packages/resend/test/cancelEmail.test.ts create mode 100644 packages/resend/test/createApiKey.test.ts create mode 100644 packages/resend/test/createAutomation.test.ts create mode 100644 packages/resend/test/createBroadcast.test.ts create mode 100644 packages/resend/test/createContact.test.ts create mode 100644 packages/resend/test/createContactImport.test.ts create mode 100644 packages/resend/test/createContactProperty.test.ts create mode 100644 packages/resend/test/createContactSegment.test.ts create mode 100644 packages/resend/test/createDomain.test.ts create mode 100644 packages/resend/test/createEmail.test.ts create mode 100644 packages/resend/test/createEvent.test.ts create mode 100644 packages/resend/test/createSegment.test.ts create mode 100644 packages/resend/test/createTemplate.test.ts create mode 100644 packages/resend/test/createTopic.test.ts create mode 100644 packages/resend/test/createWebhook.test.ts create mode 100644 packages/resend/test/deleteApiKey.test.ts create mode 100644 packages/resend/test/deleteAutomation.test.ts create mode 100644 packages/resend/test/deleteBroadcast.test.ts create mode 100644 packages/resend/test/deleteContact.test.ts create mode 100644 packages/resend/test/deleteContactProperty.test.ts create mode 100644 packages/resend/test/deleteContactSegment.test.ts create mode 100644 packages/resend/test/deleteDomain.test.ts create mode 100644 packages/resend/test/deleteEvent.test.ts create mode 100644 packages/resend/test/deleteSegment.test.ts create mode 100644 packages/resend/test/deleteTemplate.test.ts create mode 100644 packages/resend/test/deleteTopic.test.ts create mode 100644 packages/resend/test/deleteWebhook.test.ts create mode 100644 packages/resend/test/duplicateTemplate.test.ts create mode 100644 packages/resend/test/getAutomation.test.ts create mode 100644 packages/resend/test/getAutomationRun.test.ts create mode 100644 packages/resend/test/getBroadcast.test.ts create mode 100644 packages/resend/test/getContact.test.ts create mode 100644 packages/resend/test/getContactImport.test.ts create mode 100644 packages/resend/test/getContactProperty.test.ts create mode 100644 packages/resend/test/getDomain.test.ts create mode 100644 packages/resend/test/getEmail.test.ts create mode 100644 packages/resend/test/getEmailAttachment.test.ts create mode 100644 packages/resend/test/getEmailReceiving.test.ts create mode 100644 packages/resend/test/getEmailReceivingAttachment.test.ts create mode 100644 packages/resend/test/getEvent.test.ts create mode 100644 packages/resend/test/getLog.test.ts create mode 100644 packages/resend/test/getSegment.test.ts create mode 100644 packages/resend/test/getTemplate.test.ts create mode 100644 packages/resend/test/getTopic.test.ts create mode 100644 packages/resend/test/getWebhook.test.ts create mode 100644 packages/resend/test/listApiKeys.test.ts create mode 100644 packages/resend/test/listAutomationRuns.test.ts create mode 100644 packages/resend/test/listAutomations.test.ts create mode 100644 packages/resend/test/listBroadcasts.test.ts create mode 100644 packages/resend/test/listContactImports.test.ts create mode 100644 packages/resend/test/listContactProperties.test.ts create mode 100644 packages/resend/test/listContactSegments.test.ts create mode 100644 packages/resend/test/listContactTopics.test.ts create mode 100644 packages/resend/test/listContacts.test.ts create mode 100644 packages/resend/test/listDomains.test.ts create mode 100644 packages/resend/test/listEmailAttachments.test.ts create mode 100644 packages/resend/test/listEmailReceiving.test.ts create mode 100644 packages/resend/test/listEmailReceivingAttachments.test.ts create mode 100644 packages/resend/test/listEmails.test.ts create mode 100644 packages/resend/test/listEvents.test.ts create mode 100644 packages/resend/test/listLogs.test.ts create mode 100644 packages/resend/test/listSegments.test.ts create mode 100644 packages/resend/test/listTemplates.test.ts create mode 100644 packages/resend/test/listTopics.test.ts create mode 100644 packages/resend/test/listWebhooks.test.ts create mode 100644 packages/resend/test/publishTemplate.test.ts create mode 100644 packages/resend/test/sendBroadcast.test.ts create mode 100644 packages/resend/test/sendEvent.test.ts create mode 100644 packages/resend/test/setup.ts create mode 100644 packages/resend/test/stopAutomation.test.ts create mode 100644 packages/resend/test/updateAutomation.test.ts create mode 100644 packages/resend/test/updateBroadcast.test.ts create mode 100644 packages/resend/test/updateContact.test.ts create mode 100644 packages/resend/test/updateContactProperty.test.ts create mode 100644 packages/resend/test/updateContactTopic.test.ts create mode 100644 packages/resend/test/updateDomain.test.ts create mode 100644 packages/resend/test/updateEmail.test.ts create mode 100644 packages/resend/test/updateEvent.test.ts create mode 100644 packages/resend/test/updateTemplate.test.ts create mode 100644 packages/resend/test/updateTopic.test.ts create mode 100644 packages/resend/test/updateWebhook.test.ts create mode 100644 packages/resend/test/verifyDomain.test.ts create mode 100644 packages/resend/tsconfig.json create mode 100644 packages/resend/tsconfig.test.json create mode 100644 packages/resend/vitest.config.ts diff --git a/.github/workflows/nuke.yml b/.github/workflows/nuke.yml index 9a26d96d84..d345f6886b 100644 --- a/.github/workflows/nuke.yml +++ b/.github/workflows/nuke.yml @@ -70,6 +70,10 @@ on: description: "Eas" type: boolean default: false + resend: + description: "Resend" + type: boolean + default: false env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" @@ -98,7 +102,8 @@ jobs: && "${{ inputs.stripe }}" != "true" \ && "${{ inputs.supabase }}" != "true" \ && "${{ inputs.turso }}" != "true" \ - && "${{ inputs.workos }}" != "true" ]]; then + && "${{ inputs.workos }}" != "true" \ + && "${{ inputs.resend }}" != "true" ]]; then echo "::error::You must select at least one SDK to nuke (or choose 'all')" exit 1 fi @@ -321,3 +326,22 @@ jobs: # runs-on: ubuntu-latest # steps: # - TODO: no nuke script yet — run `bun run generate-nuke turso` + + # ── Resend ── + nuke-resend: + needs: gate + if: needs.gate.outputs.run == 'true' && (github.event_name == 'merge_group' || inputs.all || inputs.resend) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install --frozen-lockfile + - run: bun run build + working-directory: packages/core + - name: Run nuke script + run: bun scripts/nuke.ts + working-directory: packages/resend + env: + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} diff --git a/.github/workflows/pr-package.yml b/.github/workflows/pr-package.yml index 80b69a59f7..f51a14b010 100644 --- a/.github/workflows/pr-package.yml +++ b/.github/workflows/pr-package.yml @@ -37,6 +37,7 @@ jobs: { "dir": "packages/planetscale", "name": "@distilled.cloud/planetscale", "install": "planetscale" }, { "dir": "packages/posthog", "name": "@distilled.cloud/posthog", "install": "posthog" }, { "dir": "packages/prisma-postgres", "name": "@distilled.cloud/prisma-postgres", "install": "prisma-postgres" }, + { "dir": "packages/resend", "name": "@distilled.cloud/resend", "install": "resend" }, { "dir": "packages/stripe", "name": "@distilled.cloud/stripe", "install": "stripe" }, { "dir": "packages/supabase", "name": "@distilled.cloud/supabase", "install": "supabase" }, { "dir": "packages/turso", "name": "@distilled.cloud/turso", "install": "turso" }, diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2d865f6a3a..9a4e8d92ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -48,6 +48,7 @@ jobs: { "dir": "packages/planetscale", "name": "@distilled.cloud/planetscale" }, { "dir": "packages/posthog", "name": "@distilled.cloud/posthog" }, { "dir": "packages/prisma-postgres", "name": "@distilled.cloud/prisma-postgres" }, + { "dir": "packages/resend", "name": "@distilled.cloud/resend" }, { "dir": "packages/stripe", "name": "@distilled.cloud/stripe" }, { "dir": "packages/supabase", "name": "@distilled.cloud/supabase" }, { "dir": "packages/turso", "name": "@distilled.cloud/turso" }, diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 30c3bc584b..841871bbf9 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,6 +42,7 @@ jobs: typesense: ${{ steps.force.outputs.all || steps.changes.outputs.typesense }} workos: ${{ steps.force.outputs.all || steps.changes.outputs.workos }} expo-eas: ${{ steps.force.outputs.all || steps.changes.outputs.expo-eas }} + resend: ${{ steps.force.outputs.all || steps.changes.outputs.resend }} steps: - id: force if: contains(github.event.pull_request.labels.*.name, 'force-ci') @@ -110,6 +111,9 @@ jobs: expo-eas: - 'packages/expo-eas/**' - 'packages/core/**' + resend: + - 'packages/resend/**' + - 'packages/core/**' ci-core: needs: detect-changes @@ -494,3 +498,23 @@ jobs: working-directory: packages/expo-eas env: EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }} + + ci-resend: + needs: detect-changes + if: needs.detect-changes.outputs.resend == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + - run: bun install + - run: bun run build + working-directory: packages/core + - run: bun run check + working-directory: packages/resend + - run: bun run test + working-directory: packages/resend + env: + RESEND_API_BASE_URL: ${{ secrets.RESEND_API_BASE_URL }} + RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }} diff --git a/.gitmodules b/.gitmodules index e326dc56a9..612cd8cc90 100644 --- a/.gitmodules +++ b/.gitmodules @@ -107,3 +107,8 @@ shallow = true ignore = dirty fetchRecurseSubmodules = false +[submodule "packages/resend/specs/resend-openapi"] + path = packages/resend/specs/resend-openapi + url = https://github.com/resend/resend-openapi.git + shallow = true + ignore = dirty diff --git a/bun.lock b/bun.lock index b4c18219a9..5afa9c9d9d 100644 --- a/bun.lock +++ b/bun.lock @@ -23,7 +23,7 @@ }, "packages/aws": { "name": "@distilled.cloud/aws", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@aws-crypto/crc32": "catalog:", "@aws-crypto/util": "catalog:", @@ -54,7 +54,7 @@ }, "packages/axiom": { "name": "@distilled.cloud/axiom", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -70,7 +70,7 @@ }, "packages/azure": { "name": "@distilled.cloud/azure", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -86,7 +86,7 @@ }, "packages/cloudflare": { "name": "@distilled.cloud/cloudflare", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -105,7 +105,7 @@ }, "packages/coinbase": { "name": "@distilled.cloud/coinbase", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -122,7 +122,7 @@ }, "packages/core": { "name": "@distilled.cloud/core", - "version": "0.21.3", + "version": "0.24.9", "devDependencies": { "@types/bun": "catalog:", "@types/node": "catalog:", @@ -135,7 +135,7 @@ }, "packages/expo-eas": { "name": "@distilled.cloud/expo-eas", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -151,7 +151,7 @@ }, "packages/fly-io": { "name": "@distilled.cloud/fly-io", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -167,7 +167,7 @@ }, "packages/gcp": { "name": "@distilled.cloud/gcp", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -183,7 +183,7 @@ }, "packages/kubernetes": { "name": "@distilled.cloud/kubernetes", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -199,7 +199,7 @@ }, "packages/mongodb-atlas": { "name": "@distilled.cloud/mongodb-atlas", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -215,7 +215,7 @@ }, "packages/neon": { "name": "@distilled.cloud/neon", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -231,7 +231,7 @@ }, "packages/planetscale": { "name": "@distilled.cloud/planetscale", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -247,7 +247,7 @@ }, "packages/posthog": { "name": "@distilled.cloud/posthog", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -265,7 +265,7 @@ }, "packages/prisma-postgres": { "name": "@distilled.cloud/prisma-postgres", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -279,9 +279,27 @@ "effect": "catalog:", }, }, + "packages/resend": { + "name": "@distilled.cloud/resend", + "version": "0.2.0-alpha", + "dependencies": { + "@distilled.cloud/core": "workspace:*", + "effect": "catalog:", + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "dotenv": "catalog:", + "vitest": "catalog:", + "yaml": "catalog:", + }, + "peerDependencies": { + "effect": "catalog:", + }, + }, "packages/stripe": { "name": "@distilled.cloud/stripe", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -297,7 +315,7 @@ }, "packages/supabase": { "name": "@distilled.cloud/supabase", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -313,7 +331,7 @@ }, "packages/turso": { "name": "@distilled.cloud/turso", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -329,7 +347,7 @@ }, "packages/typesense": { "name": "@distilled.cloud/typesense", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -346,7 +364,7 @@ }, "packages/workos": { "name": "@distilled.cloud/workos", - "version": "0.21.3", + "version": "0.24.9", "dependencies": { "@distilled.cloud/core": "workspace:*", }, @@ -498,6 +516,8 @@ "@distilled.cloud/prisma-postgres": ["@distilled.cloud/prisma-postgres@workspace:packages/prisma-postgres"], + "@distilled.cloud/resend": ["@distilled.cloud/resend@workspace:packages/resend"], + "@distilled.cloud/stripe": ["@distilled.cloud/stripe@workspace:packages/stripe"], "@distilled.cloud/supabase": ["@distilled.cloud/supabase@workspace:packages/supabase"], diff --git a/packages/resend/README.md b/packages/resend/README.md new file mode 100644 index 0000000000..18adf9896f --- /dev/null +++ b/packages/resend/README.md @@ -0,0 +1,71 @@ +# @distilled.cloud/resend + +Effect-native Resend SDK generated from the [Resend OpenAPI specification](https://github.com/resend/resend-openapi). Send transactional and broadcast email, manage domains, audiences, contacts, templates, webhooks, and more — with exhaustive error typing. + +## Installation + +```bash +npm install @distilled.cloud/resend effect +``` + +## Quick Start + +```typescript +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { listEmails } from "@distilled.cloud/resend/Operations"; +import { CredentialsFromEnv } from "@distilled.cloud/resend"; + +const program = Effect.gen(function* () { + const emails = yield* listEmails({ limit: 20 }); + return emails.data ?? []; +}); + +const ResendLive = Layer.mergeAll(FetchHttpClient.layer, CredentialsFromEnv); + +program.pipe(Effect.provide(ResendLive), Effect.runPromise); +``` + +## Configuration + +Set the following environment variable: + +```bash +RESEND_API_KEY=re_your_api_key +``` + +Create an API key in the [Resend dashboard](https://resend.com/api-keys) under **API Keys**. + +`RESEND_API_BASE_URL` is also recognized and overrides the default `https://api.resend.com` for self-hosted or proxied deployments. + +## Error Handling + +```typescript +import { Effect } from "effect"; +import { getEmail } from "@distilled.cloud/resend/Operations"; + +getEmail({ email_id: "missing" }).pipe( + Effect.catchTags({ + NotFound: () => Effect.succeed(null), + UnknownResendError: (e) => Effect.fail(new Error(`Unknown: ${e.message}`)), + }), +); +``` + +## Services + +- **Emails** — `createEmail`, `listEmails`, `getEmail`, `updateEmail`, `cancelEmail`, `batchEmail`, plus receiving / attachment operations +- **Domains** — `createDomain`, `listDomains`, `getDomain`, `updateDomain`, `deleteDomain`, `verifyDomain` +- **API Keys** — `createApiKey`, `listApiKeys`, `deleteApiKey` +- **Audiences / Segments / Topics** — list / create / get / update / delete operations for each +- **Contacts** — `createContact`, `listContacts`, `getContact`, `updateContact`, `deleteContact`, contact imports and contact-property operations +- **Templates** — `createTemplate`, `listTemplates`, `getTemplate`, `updateTemplate`, `deleteTemplate`, `publishTemplate`, `duplicateTemplate` +- **Broadcasts** — `createBroadcast`, `listBroadcasts`, `getBroadcast`, `updateBroadcast`, `deleteBroadcast`, `sendBroadcast` +- **Webhooks** — `createWebhook`, `listWebhooks`, `getWebhook`, `updateWebhook`, `deleteWebhook` +- **Automations** — `createAutomation`, `listAutomations`, `getAutomation`, `updateAutomation`, `deleteAutomation`, `stopAutomation`, `listAutomationRuns`, `getAutomationRun` +- **Events** — `createEvent`, `sendEvent`, `listEvents`, `getEvent`, `updateEvent`, `deleteEvent` +- **Logs** — `listLogs`, `getLog` + +## License + +MIT diff --git a/packages/resend/package.json b/packages/resend/package.json new file mode 100644 index 0000000000..883d143eba --- /dev/null +++ b/packages/resend/package.json @@ -0,0 +1,89 @@ +{ + "name": "@distilled.cloud/resend", + "version": "0.2.0-alpha", + "repository": { + "type": "git", + "url": "https://github.com/alchemy-run/distilled", + "directory": "packages/resend" + }, + "type": "module", + "sideEffects": false, + "module": "src/index.ts", + "files": [ + "lib", + "src" + ], + "exports": { + ".": { + "types": "./lib/index.d.ts", + "bun": "./src/index.ts", + "default": "./lib/index.js" + }, + "./Category": { + "types": "./lib/category.d.ts", + "bun": "./src/category.ts", + "default": "./lib/category.js" + }, + "./Client": { + "types": "./lib/client.d.ts", + "bun": "./src/client.ts", + "default": "./lib/client.js" + }, + "./Credentials": { + "types": "./lib/credentials.d.ts", + "bun": "./src/credentials.ts", + "default": "./lib/credentials.js" + }, + "./Errors": { + "types": "./lib/errors.d.ts", + "bun": "./src/errors.ts", + "default": "./lib/errors.js" + }, + "./Operations": { + "types": "./lib/operations/index.d.ts", + "bun": "./src/operations/index.ts", + "default": "./lib/operations/index.js" + }, + "./Retry": { + "types": "./lib/retry.d.ts", + "bun": "./src/retry.ts", + "default": "./lib/retry.js" + }, + "./Sensitive": { + "types": "./lib/sensitive.d.ts", + "bun": "./src/sensitive.ts", + "default": "./lib/sensitive.js" + }, + "./Traits": { + "types": "./lib/traits.d.ts", + "bun": "./src/traits.ts", + "default": "./lib/traits.js" + } + }, + "scripts": { + "typecheck": "tsgo", + "build": "tsgo -b", + "fmt": "oxfmt --write src", + "lint": "oxlint --fix src", + "check": "tsgo && oxlint src && oxfmt --check src", + "test": "bunx vitest run test --passWithNoTests", + "publish:npm": "bun run build && bun publish --access public", + "generate": "bun run scripts/generate.ts && oxlint --fix src && oxfmt --write src && oxfmt --write src", + "specs:fetch": "git submodule update --init --recursive specs", + "specs:update": "git submodule update --remote --merge specs" + }, + "dependencies": { + "@distilled.cloud/core": "workspace:*", + "effect": "catalog:" + }, + "devDependencies": { + "@types/bun": "catalog:", + "@types/node": "catalog:", + "dotenv": "catalog:", + "vitest": "catalog:", + "yaml": "catalog:" + }, + "peerDependencies": { + "effect": "catalog:" + } +} \ No newline at end of file diff --git a/packages/resend/patches/001-add-error-responses.patch.json b/packages/resend/patches/001-add-error-responses.patch.json new file mode 100644 index 0000000000..6c814032a1 --- /dev/null +++ b/packages/resend/patches/001-add-error-responses.patch.json @@ -0,0 +1,649 @@ +{ + "description": "Document error responses for Resend operations. The upstream OpenAPI spec only declares 200 responses; this patch annotates the additional status codes that Resend's API actually returns, so the code generator can emit typed error classes per operation. Codes sourced from https://resend.com/docs/api-reference/errors and confirmed against live API probes.", + "patches": [ + { + "op": "add", + "path": "/paths/~1emails/post/responses/400", + "value": { + "description": "validation_error / invalid_idempotency_key: malformed request body or idempotency key." + } + }, + { + "op": "add", + "path": "/paths/~1emails/post/responses/403", + "value": { + "description": "invalid_api_key, or validation_error when the from domain is not verified or you are sending to non-personal addresses without a verified domain." + } + }, + { + "op": "add", + "path": "/paths/~1emails/post/responses/405", + "value": { + "description": "method_not_allowed." + } + }, + { + "op": "add", + "path": "/paths/~1emails/post/responses/409", + "value": { + "description": "invalid_idempotent_request when the same Idempotency-Key is reused with a different payload, or concurrent_idempotent_requests when the same key is reused while the original request is still in progress." + } + }, + { + "op": "add", + "path": "/paths/~1emails/post/responses/422", + "value": { + "description": "invalid_attachment, invalid_from_address, invalid_parameter, missing_required_field." + } + }, + { + "op": "add", + "path": "/paths/~1emails/post/responses/451", + "value": { + "description": "security_error: the request was rejected for security reasons." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1batch/post/responses/400", + "value": { + "description": "validation_error / invalid_idempotency_key." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1batch/post/responses/403", + "value": { + "description": "invalid_api_key or validation_error (unverified sending domain)." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1batch/post/responses/409", + "value": { + "description": "invalid_idempotent_request / concurrent_idempotent_requests." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1batch/post/responses/422", + "value": { + "description": "missing_required_field, invalid_attachment, invalid_from_address." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1batch/post/responses/451", + "value": { + "description": "security_error." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}/get/responses/403", + "value": { + "description": "invalid_api_key." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}/get/responses/404", + "value": { + "description": "not_found: the email id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}/patch/responses/403", + "value": { + "description": "invalid_api_key." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}/patch/responses/404", + "value": { + "description": "not_found: the email id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}/patch/responses/422", + "value": { + "description": "validation_error / invalid_parameter (e.g. malformed scheduled_at)." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}~1cancel/post/responses/403", + "value": { + "description": "invalid_api_key." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}~1cancel/post/responses/404", + "value": { + "description": "not_found: email already sent, cancelled, or never existed." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}~1attachments/get/responses/404", + "value": { + "description": "not_found: email id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1{email_id}~1attachments~1{attachment_id}/get/responses/404", + "value": { + "description": "not_found: email or attachment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1receiving~1{email_id}/get/responses/404", + "value": { + "description": "not_found: received email id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1receiving~1{email_id}~1attachments/get/responses/404", + "value": { + "description": "not_found: received email id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1emails~1receiving~1{email_id}~1attachments~1{attachment_id}/get/responses/404", + "value": { + "description": "not_found: received email or attachment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1domains/post/responses/403", + "value": { + "description": "validation_error: domain has already been registered in your account." + } + }, + { + "op": "add", + "path": "/paths/~1domains/post/responses/422", + "value": { + "description": "invalid_region, missing_required_field." + } + }, + { + "op": "add", + "path": "/paths/~1domains~1{domain_id}/get/responses/404", + "value": { + "description": "not_found: domain id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1domains~1{domain_id}/patch/responses/404", + "value": { + "description": "not_found: domain id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1domains~1{domain_id}/patch/responses/422", + "value": { + "description": "validation_error / invalid_parameter." + } + }, + { + "op": "add", + "path": "/paths/~1domains~1{domain_id}/delete/responses/404", + "value": { + "description": "not_found: domain id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1domains~1{domain_id}~1verify/post/responses/404", + "value": { + "description": "not_found: domain id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1api-keys/post/responses/422", + "value": { + "description": "invalid_access (permission is not full_access or sending_access), missing_required_field." + } + }, + { + "op": "add", + "path": "/paths/~1api-keys~1{api_key_id}/delete/responses/404", + "value": { + "description": "not_found: api key id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1templates/post/responses/422", + "value": { + "description": "missing_required_field, invalid_parameter." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}/get/responses/404", + "value": { + "description": "not_found: template id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}/patch/responses/404", + "value": { + "description": "not_found: template id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}/delete/responses/404", + "value": { + "description": "not_found: template id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}~1publish/post/responses/404", + "value": { + "description": "not_found: template id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1templates~1{id}~1duplicate/post/responses/404", + "value": { + "description": "not_found: template id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1audiences/post/responses/422", + "value": { + "description": "missing_required_field." + } + }, + { + "op": "add", + "path": "/paths/~1audiences~1{id}/get/responses/404", + "value": { + "description": "not_found: audience id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1audiences~1{id}/delete/responses/404", + "value": { + "description": "not_found: audience id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts/post/responses/404", + "value": { + "description": "not_found: audience id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1imports/post/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1imports~1{id}/get/responses/404", + "value": { + "description": "not_found: import id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{id}/get/responses/404", + "value": { + "description": "not_found: contact id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{id}/patch/responses/404", + "value": { + "description": "not_found: contact id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{id}/delete/responses/404", + "value": { + "description": "not_found: contact id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}/get/responses/404", + "value": { + "description": "not_found: broadcast id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}/patch/responses/404", + "value": { + "description": "not_found: broadcast id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}/delete/responses/404", + "value": { + "description": "not_found: broadcast id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}~1send/post/responses/404", + "value": { + "description": "not_found: broadcast id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1broadcasts~1{id}~1send/post/responses/422", + "value": { + "description": "validation_error: broadcast cannot be sent in its current state." + } + }, + { + "op": "add", + "path": "/paths/~1webhooks/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/get/responses/404", + "value": { + "description": "not_found: webhook id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/patch/responses/404", + "value": { + "description": "not_found: webhook id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1webhooks~1{webhook_id}/delete/responses/404", + "value": { + "description": "not_found: webhook id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1segments/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1segments~1{id}/get/responses/404", + "value": { + "description": "not_found: segment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1segments~1{id}/delete/responses/404", + "value": { + "description": "not_found: segment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1topics/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1topics~1{id}/get/responses/404", + "value": { + "description": "not_found: topic id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1topics~1{id}/patch/responses/404", + "value": { + "description": "not_found: topic id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1topics~1{id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1topics~1{id}/delete/responses/404", + "value": { + "description": "not_found: topic id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties/post/responses/409", + "value": { + "description": "validation_error: property with that name already exists." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties~1{id}/get/responses/404", + "value": { + "description": "not_found: contact property id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties~1{id}/patch/responses/404", + "value": { + "description": "not_found: contact property id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties~1{id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1contact-properties~1{id}/delete/responses/404", + "value": { + "description": "not_found: contact property id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{contact_id}~1segments~1{segment_id}/post/responses/404", + "value": { + "description": "not_found: contact or segment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{contact_id}~1segments~1{segment_id}/delete/responses/404", + "value": { + "description": "not_found: contact or segment id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{contact_id}~1topics/patch/responses/404", + "value": { + "description": "not_found: contact id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1contacts~1{contact_id}~1topics/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1logs~1{log_id}/get/responses/404", + "value": { + "description": "not_found: log id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}/get/responses/404", + "value": { + "description": "not_found: automation id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}/patch/responses/404", + "value": { + "description": "not_found: automation id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}/patch/responses/422", + "value": { + "description": "validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}/delete/responses/404", + "value": { + "description": "not_found: automation id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}~1stop/post/responses/404", + "value": { + "description": "not_found: automation id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}~1runs/get/responses/404", + "value": { + "description": "not_found: automation id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1automations~1{automation_id}~1runs~1{run_id}/get/responses/404", + "value": { + "description": "not_found: automation or run id does not exist." + } + }, + { + "op": "add", + "path": "/paths/~1events/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1events~1send/post/responses/422", + "value": { + "description": "missing_required_field, validation_error." + } + }, + { + "op": "add", + "path": "/paths/~1events~1{identifier}/get/responses/404", + "value": { + "description": "not_found: event id does not exist." + } + } + ] +} diff --git a/packages/resend/scripts/generate.ts b/packages/resend/scripts/generate.ts new file mode 100644 index 0000000000..32cced11ab --- /dev/null +++ b/packages/resend/scripts/generate.ts @@ -0,0 +1,164 @@ +/** + * Resend SDK Code Generator + * + * The Resend API spec is published as `resend.yaml` (the bundled `resend.json` + * is generated from it and treated as non-authoritative per the upstream repo). + * We parse the YAML and hand it to the shared OpenAPI generator from sdk-core. + * + * The Resend OpenAPI document omits `operationId` on every path/method, so this + * script synthesizes one for each operation before passing the spec to the + * generator. Naming follows REST conventions: + * - GET /resources -> listResources + * - POST /resources -> createResource + * - GET /resources/{id} -> getResource + * - PATCH /resources/{id} -> updateResource + * - DELETE /resources/{id} -> deleteResource + * - POST /resources/{id}/verb -> verbResource (for known action verbs) + */ +import * as fs from "fs"; +import * as path from "path"; +import YAML from "yaml"; +import { generateFromOpenAPI } from "@distilled.cloud/core/openapi/generate"; + +const rootDir = path.join(import.meta.dir, ".."); + +const yamlPath = path.join(rootDir, "specs/resend-openapi/resend.yaml"); +const jsonPath = path.join(rootDir, "specs/resend-openapi/resend.generated.json"); + +const yamlContent = fs.readFileSync(yamlPath, "utf-8"); +const spec = YAML.parse(yamlContent); + +// --- operationId synthesis --------------------------------------------------- + +const ACTION_VERBS = new Set([ + "cancel", + "verify", + "publish", + "duplicate", + "send", + "stop", + "start", + "run", + "batch", + "import", + "export", + "refresh", + "reset", +]); + +const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const; +type HttpMethod = (typeof HTTP_METHODS)[number]; + +function isParamSeg(seg: string): boolean { + return seg.startsWith("{") && seg.endsWith("}"); +} + +function singularize(word: string): string { + if (word.endsWith("ies")) return word.slice(0, -3) + "y"; + if (word.endsWith("ses")) return word.slice(0, -2); + if (word.endsWith("s") && !word.endsWith("ss")) return word.slice(0, -1); + return word; +} + +function camelCase(parts: readonly string[]): string { + const joined = parts + .filter(Boolean) + .join("_") + .replace(/[-]+/g, "_") + .toLowerCase(); + return joined.replace(/_+([a-z0-9])/g, (_, c) => c.toUpperCase()); +} + +function makeOperationId(method: HttpMethod, urlPath: string): string { + const segments = urlPath.split("/").filter(Boolean); + const nameSegs = segments.filter((s) => !isParamSeg(s)); + const last = segments[segments.length - 1] ?? ""; + const endsWithParam = isParamSeg(last); + const lastIsAction = + !endsWithParam && ACTION_VERBS.has(last.toLowerCase()) && nameSegs.length > 1; + + if (lastIsAction) { + const action = nameSegs[nameSegs.length - 1]!; + const resourceSegs = nameSegs.slice(0, -1).map(singularize); + return camelCase([action, ...resourceSegs]); + } + + if (endsWithParam) { + const verbMap: Record = { + get: "get", + put: "update", + patch: "update", + delete: "delete", + post: "create", + }; + const verb = verbMap[method]; + const base = singularize(nameSegs[nameSegs.length - 1] ?? ""); + const prefix = nameSegs.slice(0, -1).map(singularize); + return camelCase([verb, ...prefix, base]); + } + + // Collection access. + const verbMap: Record = { + get: "list", + post: "create", + put: "update", + patch: "update", + delete: "delete", + }; + const verb = verbMap[method]; + const last_ = nameSegs[nameSegs.length - 1] ?? ""; + const base = verb === "list" ? last_ : singularize(last_); + const prefix = nameSegs.slice(0, -1).map(singularize); + return camelCase([verb, ...prefix, base]); +} + +const seen = new Map(); +for (const [urlPath, pathItem] of Object.entries(spec.paths ?? {})) { + for (const method of HTTP_METHODS) { + const op = (pathItem as Record)[method] as + | { operationId?: string } + | undefined; + if (!op || op.operationId) continue; + let id = makeOperationId(method, urlPath); + if (seen.has(id)) { + // Disambiguate collisions by appending the method. + id = `${id}_${method}`; + } + seen.set(id, `${method.toUpperCase()} ${urlPath}`); + op.operationId = id; + } +} + +fs.writeFileSync(jsonPath, JSON.stringify(spec, null, 2)); + +try { + generateFromOpenAPI({ + specPath: jsonPath, + patchDir: path.join(rootDir, "patches"), + outputDir: path.join(rootDir, "src/operations"), + importPrefix: "..", + clientImport: "../client", + traitsImport: "../traits", + sensitiveImport: "../sensitive", + errorsImport: "../errors", + includeOperationErrors: true, + skipDeprecated: true, + // Resend-specific statuses beyond the shared defaults: + // - 405 method_not_allowed + // - 451 security_error + statusToErrorClass: { + "400": "BadRequest", + "403": "Forbidden", + "404": "NotFound", + "405": "MethodNotAllowed", + "409": "Conflict", + "422": "UnprocessableEntity", + "451": "UnavailableForLegalReasons", + }, + // Global error statuses handled by `matchError` (and excluded from + // per-operation typed errors). + defaultErrorStatuses: new Set(["401", "429", "500", "502", "503", "504"]), + }); +} finally { + fs.unlinkSync(jsonPath); +} diff --git a/packages/resend/scripts/nuke.ts b/packages/resend/scripts/nuke.ts new file mode 100644 index 0000000000..39ce2dcea5 --- /dev/null +++ b/packages/resend/scripts/nuke.ts @@ -0,0 +1,684 @@ +#!/usr/bin/env bun +/** + * Resend Nuke Script + * + * Lists and deletes all resources in a Resend account. + * Supports --dry-run to preview without deleting. + * + * Usage: + * bun packages/resend/scripts/nuke.ts --dry-run + * bun packages/resend/scripts/nuke.ts + * + * Exclusions may be configured via packages/resend/nuke-config.json. + * + * NOTES: + * - API keys are deleted last so that auth survives the rest of the run. + * - The currently-used API key cannot be reliably identified from the env + * (the env var holds the raw `re_...` secret while listApiKeys returns a + * UUID). Use nuke-config.json to protect specific key IDs/names. + * - Read-only listings are emitted for Emails, Email Receiving, Logs and + * Contact Imports — these resources cannot be deleted via the API. + */ +import { config } from "dotenv"; +import * as fs from "node:fs"; +import * as nodePath from "node:path"; + +// Load .env from package dir, then repo root, then CWD. +const PKG_DIR = nodePath.resolve(import.meta.dir, ".."); +config({ path: nodePath.join(PKG_DIR, ".env") }); +config({ path: nodePath.resolve(PKG_DIR, "../../.env") }); +if (!process.env.RESEND_API_KEY) config(); + +import { BunRuntime, BunServices } from "@effect/platform-bun"; +import { Console, Effect } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { Command, Flag } from "effect/unstable/cli"; +import { CredentialsFromEnv } from "../src/credentials.ts"; + +// List operations +import { listDomains } from "../src/operations/listDomains.ts"; +import { listApiKeys } from "../src/operations/listApiKeys.ts"; +import { listTemplates } from "../src/operations/listTemplates.ts"; +import { listContacts } from "../src/operations/listContacts.ts"; +import { listContactImports } from "../src/operations/listContactImports.ts"; +import { listBroadcasts } from "../src/operations/listBroadcasts.ts"; +import { listWebhooks } from "../src/operations/listWebhooks.ts"; +import { listSegments } from "../src/operations/listSegments.ts"; +import { listTopics } from "../src/operations/listTopics.ts"; +import { listContactProperties } from "../src/operations/listContactProperties.ts"; +import { listAutomations } from "../src/operations/listAutomations.ts"; +import { listEvents } from "../src/operations/listEvents.ts"; +import { listEmails } from "../src/operations/listEmails.ts"; +import { listEmailReceiving } from "../src/operations/listEmailReceiving.ts"; +import { listLogs } from "../src/operations/listLogs.ts"; + +// Delete operations +import { deleteDomain } from "../src/operations/deleteDomain.ts"; +import { deleteApiKey } from "../src/operations/deleteApiKey.ts"; +import { deleteTemplate } from "../src/operations/deleteTemplate.ts"; +import { deleteContact } from "../src/operations/deleteContact.ts"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast.ts"; +import { deleteWebhook } from "../src/operations/deleteWebhook.ts"; +import { deleteSegment } from "../src/operations/deleteSegment.ts"; +import { deleteTopic } from "../src/operations/deleteTopic.ts"; +import { deleteContactProperty } from "../src/operations/deleteContactProperty.ts"; +import { deleteAutomation } from "../src/operations/deleteAutomation.ts"; +import { deleteEvent } from "../src/operations/deleteEvent.ts"; + +// ============================================================================ +// ANSI colors +// ============================================================================ + +const RED = "\x1b[31m"; +const GREEN = "\x1b[32m"; +const YELLOW = "\x1b[33m"; +const CYAN = "\x1b[36m"; +const BOLD = "\x1b[1m"; +const DIM = "\x1b[2m"; +const RESET = "\x1b[0m"; + +// ============================================================================ +// Counters +// ============================================================================ + +let totalFound = 0; +let totalSkipped = 0; +let totalDeleted = 0; +let totalFailed = 0; + +// ============================================================================ +// Nuke Config +// ============================================================================ + +interface ExcludeRule { + type: string; + ids?: string[]; + namePatterns?: string[]; + reason?: string; +} + +interface NukeConfig { + exclude?: ExcludeRule[]; +} + +function loadNukeConfig(): NukeConfig { + const p = nodePath.join(PKG_DIR, "nuke-config.json"); + if (!fs.existsSync(p)) return {}; + return JSON.parse(fs.readFileSync(p, "utf-8")) as NukeConfig; +} + +function matchGlob(pattern: string, value: string): boolean { + return new RegExp( + "^" + pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*") + "$", + ).test(value); +} + +function isExcluded( + config: NukeConfig, + type: string, + id: string, + name?: string, +): ExcludeRule | undefined { + return config.exclude?.find((rule) => { + if (rule.type !== type) return false; + if (rule.ids?.includes(id)) return true; + if (name && rule.namePatterns?.some((p) => matchGlob(p, name))) return true; + return false; + }); +} + +// ============================================================================ +// Helpers +// ============================================================================ + +type DeleteEffect = Effect.Effect; + +const deleteOne = ( + dryRun: boolean, + type: string, + label: string, + id: string, + name: string | undefined, + nukeConfig: NukeConfig, + del: () => DeleteEffect, + indent = " ", +) => + Effect.gen(function* () { + totalFound++; + const excluded = isExcluded(nukeConfig, type, id, name); + if (excluded) { + totalSkipped++; + yield* Console.log( + `${indent}${YELLOW}[SKIP]${RESET} ${type}: ${label} ${DIM}(${id})${RESET} — ${excluded.reason ?? "excluded"}`, + ); + return; + } + + yield* Console.log( + `${indent}${RED}[DELETE]${RESET} ${type}: ${label} ${DIM}(${id})${RESET}`, + ); + + if (dryRun) return; + + yield* del().pipe( + Effect.andThen(() => { + totalDeleted++; + }), + Effect.catch((e) => { + totalFailed++; + return Console.log( + `${indent} ${RED}Failed to delete ${type} ${id}: ${String(e)}${RESET}`, + ); + }), + ); + }); + +// Paginate a list operation that returns { data?, has_more? }, using the last +// item's id as the `after` cursor. Hard cap at 1000 pages to avoid infinite +// loops if the API ever lies about has_more. +const paginate = ( + list: (input: { + limit?: number; + after?: string; + }) => Effect.Effect< + { data?: readonly T[] | undefined; has_more?: boolean | undefined }, + unknown, + never + >, +): Effect.Effect => + Effect.gen(function* () { + const all: T[] = []; + let after: string | undefined; + for (let i = 0; i < 1000; i++) { + const res = yield* list({ limit: 100, after }); + const data = res.data ?? []; + for (const item of data) all.push(item); + if (!res.has_more || data.length === 0) break; + const last = data[data.length - 1]; + if (!last?.id) break; + after = last.id; + } + return all; + }); + +const safeListMessage = (resource: string) => (e: unknown) => + Console.log(` ${RED}Failed to list ${resource}: ${String(e)}${RESET}`); + +// ============================================================================ +// Per-resource nukers +// ============================================================================ + +const nukeWebhooks = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Webhooks${RESET}`); + const items = yield* paginate(listWebhooks).pipe( + Effect.catch((e) => + safeListMessage("webhooks")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No webhooks found${RESET}`); + return; + } + for (const w of items) { + if (!w.id) continue; + yield* deleteOne( + dryRun, + "Webhook", + w.endpoint ?? w.id, + w.id, + w.endpoint, + nukeConfig, + () => deleteWebhook({ webhook_id: w.id! }), + ); + } + }); + +const nukeBroadcasts = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Broadcasts${RESET}`); + const items = yield* paginate(listBroadcasts).pipe( + Effect.catch((e) => + safeListMessage("broadcasts")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No broadcasts found${RESET}`); + return; + } + for (const b of items) { + if (!b.id) continue; + // Resend won't let you delete a broadcast that has already been sent. + // We still attempt and let the API return the error. + yield* deleteOne( + dryRun, + "Broadcast", + b.name ?? b.id, + b.id, + b.name, + nukeConfig, + () => deleteBroadcast({ id: b.id! }), + ); + } + }); + +const nukeAutomations = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Automations${RESET}`); + const items = yield* paginate(listAutomations).pipe( + Effect.catch((e) => + safeListMessage("automations")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No automations found${RESET}`); + return; + } + for (const a of items) { + if (!a.id) continue; + yield* deleteOne( + dryRun, + "Automation", + a.name ?? a.id, + a.id, + a.name, + nukeConfig, + () => deleteAutomation({ automation_id: a.id! }), + ); + } + }); + +const nukeContacts = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Contacts${RESET}`); + // listContacts has no `has_more` field in the schema, so paginate may stop + // after one page. That's still fine — Resend caps page size at 100 and we + // re-run pages by id when has_more is reported. + const items = yield* paginate(listContacts as any).pipe( + Effect.catch((e) => + safeListMessage("contacts")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No contacts found${RESET}`); + return; + } + for (const c of items) { + if (!c.id) continue; + yield* deleteOne( + dryRun, + "Contact", + c.email ?? c.id, + c.id, + c.email, + nukeConfig, + () => deleteContact({ id: c.id! }), + ); + } + }); + +const nukeSegments = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Segments${RESET}`); + const items = yield* paginate(listSegments).pipe( + Effect.catch((e) => + safeListMessage("segments")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No segments found${RESET}`); + return; + } + for (const s of items) { + if (!s.id) continue; + yield* deleteOne( + dryRun, + "Segment", + s.name ?? s.id, + s.id, + s.name, + nukeConfig, + () => deleteSegment({ id: s.id! }), + ); + } + }); + +const nukeTopics = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Topics${RESET}`); + const items = yield* paginate(listTopics).pipe( + Effect.catch((e) => + safeListMessage("topics")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No topics found${RESET}`); + return; + } + for (const t of items) { + if (!t.id) continue; + yield* deleteOne( + dryRun, + "Topic", + t.name ?? t.id, + t.id, + t.name, + nukeConfig, + () => deleteTopic({ id: t.id! }), + ); + } + }); + +const nukeContactProperties = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Contact Properties${RESET}`); + const items = yield* paginate(listContactProperties).pipe( + Effect.catch((e) => + safeListMessage("contact properties")(e).pipe( + Effect.map(() => [] as any[]), + ), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No contact properties found${RESET}`); + return; + } + for (const p of items) { + if (!p.id) continue; + yield* deleteOne( + dryRun, + "ContactProperty", + p.key ?? p.id, + p.id, + p.key, + nukeConfig, + () => deleteContactProperty({ id: p.id! }), + ); + } + }); + +const nukeTemplates = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Templates${RESET}`); + const items = yield* paginate(listTemplates as any).pipe( + Effect.catch((e) => + safeListMessage("templates")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No templates found${RESET}`); + return; + } + for (const t of items) { + if (!t.id) continue; + yield* deleteOne( + dryRun, + "Template", + t.name ?? t.alias ?? t.id, + t.id, + t.name ?? t.alias, + nukeConfig, + () => deleteTemplate({ id: t.id! }), + ); + } + }); + +const nukeEvents = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Events${RESET}`); + const items = yield* paginate(listEvents).pipe( + Effect.catch((e) => + safeListMessage("events")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No events found${RESET}`); + return; + } + for (const e of items) { + if (!e.id) continue; + yield* deleteOne( + dryRun, + "Event", + e.name ?? e.id, + e.id, + e.name, + nukeConfig, + // The deleteEvent op accepts identifier = UUID or name. + () => deleteEvent({ identifier: e.id! }), + ); + } + }); + +const nukeDomains = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}Domains${RESET}`); + const items = yield* paginate(listDomains).pipe( + Effect.catch((e) => + safeListMessage("domains")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No domains found${RESET}`); + return; + } + for (const d of items) { + if (!d.id) continue; + yield* deleteOne( + dryRun, + "Domain", + d.name ?? d.id, + d.id, + d.name, + nukeConfig, + () => deleteDomain({ domain_id: d.id! }), + ); + } + }); + +const nukeApiKeys = (dryRun: boolean, nukeConfig: NukeConfig) => + Effect.gen(function* () { + yield* Console.log(`\n${BOLD}${CYAN}API Keys${RESET}`); + yield* Console.log( + ` ${DIM}Note: the active key cannot be auto-detected. Use nuke-config.json to protect specific keys.${RESET}`, + ); + const items = yield* paginate(listApiKeys as any).pipe( + Effect.catch((e) => + safeListMessage("api keys")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No API keys found${RESET}`); + return; + } + for (const k of items) { + if (!k.id) continue; + yield* deleteOne( + dryRun, + "ApiKey", + k.name ?? k.id, + k.id, + k.name, + nukeConfig, + () => deleteApiKey({ api_key_id: k.id! }), + ); + } + }); + +// ============================================================================ +// Read-only listings (no delete API) +// ============================================================================ + +const listEmailsReadOnly = () => + Effect.gen(function* () { + yield* Console.log( + `\n${BOLD}${CYAN}Emails${RESET} ${DIM}(list-only; historical emails cannot be deleted)${RESET}`, + ); + const items = yield* paginate(listEmails).pipe( + Effect.catch((e) => + safeListMessage("emails")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No emails found${RESET}`); + return; + } + for (const e of items.slice(0, 25)) { + yield* Console.log( + ` ${DIM}- ${e.subject ?? "(no subject)"} → ${(e.to ?? []).join(", ")} ${DIM}(${e.id})${RESET}`, + ); + } + if (items.length > 25) { + yield* Console.log( + ` ${DIM}... and ${items.length - 25} more${RESET}`, + ); + } + }); + +const listEmailReceivingReadOnly = () => + Effect.gen(function* () { + yield* Console.log( + `\n${BOLD}${CYAN}Email Receiving${RESET} ${DIM}(list-only; received emails cannot be deleted via API)${RESET}`, + ); + const items = yield* paginate(listEmailReceiving as any).pipe( + Effect.catch((e) => + safeListMessage("email receiving")(e).pipe( + Effect.map(() => [] as any[]), + ), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No received emails found${RESET}`); + return; + } + yield* Console.log(` ${DIM}${items.length} received email(s)${RESET}`); + }); + +const listContactImportsReadOnly = () => + Effect.gen(function* () { + yield* Console.log( + `\n${BOLD}${CYAN}Contact Imports${RESET} ${DIM}(list-only; no delete API)${RESET}`, + ); + const items = yield* paginate(listContactImports).pipe( + Effect.catch((e) => + safeListMessage("contact imports")(e).pipe( + Effect.map(() => [] as any[]), + ), + ), + ); + if (items.length === 0) { + yield* Console.log(` ${DIM}No contact imports found${RESET}`); + return; + } + for (const ci of items) { + yield* Console.log( + ` ${DIM}- ${ci.id} status=${ci.status}${RESET}`, + ); + } + }); + +const listLogsReadOnly = () => + Effect.gen(function* () { + yield* Console.log( + `\n${BOLD}${CYAN}Logs${RESET} ${DIM}(list-only; no delete API)${RESET}`, + ); + const items = yield* paginate(listLogs as any).pipe( + Effect.catch((e) => + safeListMessage("logs")(e).pipe(Effect.map(() => [] as any[])), + ), + ); + yield* Console.log(` ${DIM}${items.length} log entr(ies)${RESET}`); + }); + +// ============================================================================ +// Main command +// ============================================================================ + +const nuke = Command.make( + "nuke", + { + dryRun: Flag.boolean("dry-run").pipe( + Flag.withDescription("Only list resources without deleting them"), + Flag.withDefault(false), + ), + }, + (cfg) => + Effect.gen(function* () { + const nukeConfig = loadNukeConfig(); + const mode = cfg.dryRun ? `${YELLOW}DRY RUN${RESET}` : `${RED}LIVE${RESET}`; + yield* Console.log( + `\n${BOLD}Resend Nuke${RESET} ${DIM}(${mode}${DIM})${RESET}`, + ); + + if (!cfg.dryRun) { + yield* Console.log( + `${RED}${BOLD}WARNING: This will DELETE all resources in the Resend account!${RESET}`, + ); + yield* Console.log( + `${DIM}Past emails, received emails, contact imports and logs are listed only — they have no delete API.${RESET}`, + ); + } + + if (nukeConfig.exclude && nukeConfig.exclude.length > 0) { + yield* Console.log( + `${DIM}Loaded ${nukeConfig.exclude.length} exclusion rule(s) from nuke-config.json${RESET}`, + ); + } + + // Deletion order (leaves → roots): + // 1. Webhooks (independent, ephemeral integrations) + yield* nukeWebhooks(cfg.dryRun, nukeConfig); + // 2. Broadcasts (reference templates / segments / topics / audiences) + yield* nukeBroadcasts(cfg.dryRun, nukeConfig); + // 3. Automations (reference templates / topics) + yield* nukeAutomations(cfg.dryRun, nukeConfig); + // 4. Contacts (members of segments/topics) + yield* nukeContacts(cfg.dryRun, nukeConfig); + // 5. Segments (after broadcasts & contacts that reference them) + yield* nukeSegments(cfg.dryRun, nukeConfig); + // 6. Topics (after broadcasts/automations/contacts that reference them) + yield* nukeTopics(cfg.dryRun, nukeConfig); + // 7. Contact Properties (after contacts) + yield* nukeContactProperties(cfg.dryRun, nukeConfig); + // 8. Templates (after broadcasts/automations that reference them) + yield* nukeTemplates(cfg.dryRun, nukeConfig); + // 9. Events (event schema registry) + yield* nukeEvents(cfg.dryRun, nukeConfig); + // 10. Domains (after broadcasts/emails that reference them) + yield* nukeDomains(cfg.dryRun, nukeConfig); + // 11. API Keys LAST so auth survives the run + yield* nukeApiKeys(cfg.dryRun, nukeConfig); + + // Read-only listings — never deleted, just shown for visibility. + yield* listEmailsReadOnly(); + yield* listEmailReceivingReadOnly(); + yield* listContactImportsReadOnly(); + yield* listLogsReadOnly(); + + // Summary + yield* Console.log(`\n${BOLD}Summary${RESET}`); + yield* Console.log(` Total found: ${totalFound}`); + yield* Console.log(` ${YELLOW}Skipped: ${totalSkipped}${RESET}`); + if (!cfg.dryRun) { + yield* Console.log(` ${GREEN}Deleted: ${totalDeleted}${RESET}`); + if (totalFailed > 0) { + yield* Console.log(` ${RED}Failed: ${totalFailed}${RESET}`); + } + } else { + yield* Console.log( + ` ${DIM}(dry-run: no deletions performed)${RESET}`, + ); + } + }).pipe( + Effect.provide(CredentialsFromEnv), + Effect.provide(FetchHttpClient.layer), + ), +).pipe(Command.withDescription("List and delete all Resend resources")); + +// ============================================================================ +// Entry Point +// ============================================================================ + +BunRuntime.runMain( + Effect.provide(Command.run(nuke, { version: "1.0.0" }), BunServices.layer), +); diff --git a/packages/resend/specs/resend-openapi b/packages/resend/specs/resend-openapi new file mode 160000 index 0000000000..615da33adc --- /dev/null +++ b/packages/resend/specs/resend-openapi @@ -0,0 +1 @@ +Subproject commit 615da33adca235d53865b84e4b3e7d0a94d99401 diff --git a/packages/resend/src/category.ts b/packages/resend/src/category.ts new file mode 100644 index 0000000000..a09ac2ecca --- /dev/null +++ b/packages/resend/src/category.ts @@ -0,0 +1,4 @@ +/** + * Re-export the shared category system from sdk-core. + */ +export * from "@distilled.cloud/core/category"; diff --git a/packages/resend/src/client.ts b/packages/resend/src/client.ts new file mode 100644 index 0000000000..6f16d3980c --- /dev/null +++ b/packages/resend/src/client.ts @@ -0,0 +1,106 @@ +/** + * Resend API Client. + * + * Wraps the shared REST client from sdk-core with Resend-specific + * error matching and credential handling. + */ +import * as Effect from "effect/Effect"; +import * as Redacted from "effect/Redacted"; +import * as Schema from "effect/Schema"; +import { makeAPI } from "@distilled.cloud/core/client"; +import { parseRetryAfterForStatus } from "@distilled.cloud/core/retry-after"; +import { Retry } from "./retry.ts"; +import { + HTTP_STATUS_MAP, + UnknownResendError, + ResendParseError, + MethodNotAllowed, + UnavailableForLegalReasons, +} from "./errors.ts"; + +/** + * HTTP status codes whose corresponding error class declares an optional + * `retryAfter` field. `matchError` only forwards a `Retry-After` hint into + * the constructor for these — passing it on non-retryable classes would + * silently retain it as a stale field on the instance. + */ +const RETRYABLE_STATUSES = new Set([423, 429, 500, 502, 503, 504]); + +/** + * Resend status codes not covered by the shared `HTTP_STATUS_MAP`: + * + * - 405 `method_not_allowed` + * - 451 `security_error` + */ +const RESEND_STATUS_MAP: Record< + number, + new (args: { message: string }) => unknown +> = { + 405: MethodNotAllowed, + 451: UnavailableForLegalReasons, +}; + +// Re-export for backwards compatibility +export { UnknownResendError } from "./errors.ts"; +import { Credentials } from "./credentials.ts"; + +/** + * Resend API error response shape: + * { statusCode: number, message: string, name: string } + * + * Example: + * { "statusCode": 422, "message": "...", "name": "validation_error" } + */ +const ApiErrorResponse = Schema.Struct({ + name: Schema.optional(Schema.String), + message: Schema.String, + statusCode: Schema.optional(Schema.Number), +}); + +/** + * Match a Resend API error response to the appropriate error class based on HTTP status. + */ +const matchError = ( + status: number, + errorBody: unknown, + _errors?: readonly unknown[], + headers?: Record, +): Effect.Effect => { + try { + const parsed = Schema.decodeUnknownSync(ApiErrorResponse)(errorBody); + const ErrorClass = + (HTTP_STATUS_MAP as any)[status] ?? RESEND_STATUS_MAP[status]; + if (ErrorClass) { + const args: { message: string; retryAfter?: unknown } = { + message: parsed.message ?? "", + }; + if (RETRYABLE_STATUSES.has(status)) { + args.retryAfter = parseRetryAfterForStatus(status, headers); + } + return Effect.fail(new ErrorClass(args)); + } + return Effect.fail( + new UnknownResendError({ + code: parsed.name, + message: parsed.message, + body: errorBody, + }), + ); + } catch { + return Effect.fail(new UnknownResendError({ body: errorBody })); + } +}; + +/** + * Resend API client. Authenticates with `Authorization: Bearer `. + */ +export const API = makeAPI({ + credentials: Credentials as any, + getBaseUrl: (creds: any) => creds.apiBaseUrl, + getAuthHeaders: (creds: any) => ({ + Authorization: `Bearer ${Redacted.value(creds.apiKey)}`, + }), + matchError, + ParseError: ResendParseError as any, + retry: Retry as any, +}); diff --git a/packages/resend/src/credentials.ts b/packages/resend/src/credentials.ts new file mode 100644 index 0000000000..f1f95cc117 --- /dev/null +++ b/packages/resend/src/credentials.ts @@ -0,0 +1,35 @@ +import { ConfigError } from "@distilled.cloud/core/errors"; +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Redacted from "effect/Redacted"; + +export const DEFAULT_API_BASE_URL = "https://api.resend.com"; + +export interface Config { + readonly apiKey: Redacted.Redacted; + readonly apiBaseUrl: string; +} + +export class Credentials extends Context.Service< + Credentials, + Effect.Effect +>()("ResendCredentials") {} + +export const CredentialsFromEnv = Layer.succeed( + Credentials, + Effect.gen(function* () { + const apiKey = process.env.RESEND_API_KEY; + + if (!apiKey) { + return yield* new ConfigError({ + message: "RESEND_API_KEY environment variable is required", + }); + } + + return { + apiKey: Redacted.make(apiKey), + apiBaseUrl: process.env.RESEND_API_BASE_URL ?? DEFAULT_API_BASE_URL, + }; + }).pipe(Effect.orDie), +); diff --git a/packages/resend/src/errors.ts b/packages/resend/src/errors.ts new file mode 100644 index 0000000000..9004bfe7e9 --- /dev/null +++ b/packages/resend/src/errors.ts @@ -0,0 +1,68 @@ +/** + * Resend-specific error types. + * + * Re-exports common HTTP errors from sdk-core and adds Resend-specific + * error matching and API error types. + */ +export { + BadGateway, + BadRequest, + Conflict, + ConfigError, + Forbidden, + GatewayTimeout, + InternalServerError, + Locked, + NotFound, + ServiceUnavailable, + TooManyRequests, + Unauthorized, + UnprocessableEntity, + HTTP_STATUS_MAP, + DEFAULT_ERRORS, + API_ERRORS, +} from "@distilled.cloud/core/errors"; +export type { DefaultErrors } from "@distilled.cloud/core/errors"; + +import * as Schema from "effect/Schema"; +import * as Category from "@distilled.cloud/core/category"; + +// Unknown Resend error - returned when an error code is not recognized +export class UnknownResendError extends Schema.TaggedErrorClass()( + "UnknownResendError", + { + code: Schema.optional(Schema.String), + message: Schema.optional(Schema.String), + body: Schema.Unknown, + }, +).pipe(Category.withServerError) {} + +/** + * MethodNotAllowed (405) - The HTTP method is not allowed for the requested path. + * + * Resend returns this with `name: "method_not_allowed"`. + */ +export class MethodNotAllowed extends Schema.TaggedErrorClass()( + "MethodNotAllowed", + { message: Schema.String }, +).pipe(Category.withBadRequestError) {} + +/** + * UnavailableForLegalReasons (451) - The request was rejected for legal/security reasons. + * + * Resend returns this with `name: "security_error"` when a security issue + * is detected with the request. + */ +export class UnavailableForLegalReasons extends Schema.TaggedErrorClass()( + "UnavailableForLegalReasons", + { message: Schema.String }, +).pipe(Category.withBadRequestError) {} + +// Schema parse error wrapper +export class ResendParseError extends Schema.TaggedErrorClass()( + "ResendParseError", + { + body: Schema.Unknown, + cause: Schema.Unknown, + }, +).pipe(Category.withParseError) {} diff --git a/packages/resend/src/index.ts b/packages/resend/src/index.ts new file mode 100644 index 0000000000..8270c29da8 --- /dev/null +++ b/packages/resend/src/index.ts @@ -0,0 +1,16 @@ +/** + * Resend SDK for Effect + * + * @example + * \`\`\`ts + * import * as Resend from "@distilled.cloud/resend"; + * \`\`\` + */ +export * from "./credentials.ts"; +export * as Category from "./category.ts"; +export * as T from "./traits.ts"; +export * as Retry from "./retry.ts"; +export { API } from "./client.ts"; +export * from "./errors.ts"; +export * from "./operations/index.ts"; +export { SensitiveString, SensitiveNullableString } from "./sensitive.ts"; diff --git a/packages/resend/src/operations/batchEmail.ts b/packages/resend/src/operations/batchEmail.ts new file mode 100644 index 0000000000..2f148378ae --- /dev/null +++ b/packages/resend/src/operations/batchEmail.ts @@ -0,0 +1,46 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { + BadRequest, + Forbidden, + Conflict, + UnprocessableEntity, + UnavailableForLegalReasons, +} from "../errors.ts"; + +// Input Schema +export const BatchEmailInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + {}, +).pipe(T.Http({ method: "POST", path: "/emails/batch" })); +export type BatchEmailInput = typeof BatchEmailInput.Type; + +// Output Schema +export const BatchEmailOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + }), + ), + ), +}); +export type BatchEmailOutput = typeof BatchEmailOutput.Type; + +// The operation +/** + * Trigger up to 100 batch emails at once. + * + * @param Idempotency-Key - A unique identifier for the request to ensure emails are only sent once. [Learn more](https://resend.com/docs/dashboard/emails/idempotency-keys) + */ +export const batchEmail = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: BatchEmailInput, + outputSchema: BatchEmailOutput, + errors: [ + BadRequest, + Forbidden, + Conflict, + UnprocessableEntity, + UnavailableForLegalReasons, + ] as const, +})); diff --git a/packages/resend/src/operations/cancelEmail.ts b/packages/resend/src/operations/cancelEmail.ts new file mode 100644 index 0000000000..2ca0a1d307 --- /dev/null +++ b/packages/resend/src/operations/cancelEmail.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const CancelEmailInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "POST", path: "/emails/{email_id}/cancel" })); +export type CancelEmailInput = typeof CancelEmailInput.Type; + +// Output Schema +export const CancelEmailOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + to: Schema.optional(Schema.Array(Schema.String)), + from: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + bcc: Schema.optional(Schema.Array(Schema.String)), + cc: Schema.optional(Schema.Array(Schema.String)), + reply_to: Schema.optional(Schema.Array(Schema.String)), + last_event: Schema.optional( + Schema.Literals([ + "bounced", + "canceled", + "clicked", + "complained", + "delivered", + "delivery_delayed", + "failed", + "opened", + "queued", + "scheduled", + "sent", + "suppressed", + ]), + ), +}); +export type CancelEmailOutput = typeof CancelEmailOutput.Type; + +// The operation +/** + * Cancel the schedule of the e-mail. + * + * @param email_id - The ID of the email. + */ +export const cancelEmail = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CancelEmailInput, + outputSchema: CancelEmailOutput, + errors: [Forbidden, NotFound] as const, +})); diff --git a/packages/resend/src/operations/createApiKey.ts b/packages/resend/src/operations/createApiKey.ts new file mode 100644 index 0000000000..d075c45b91 --- /dev/null +++ b/packages/resend/src/operations/createApiKey.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateApiKeyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + permission: Schema.optional( + Schema.Literals(["full_access", "sending_access"]), + ), + domain_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/api-keys" })); +export type CreateApiKeyInput = typeof CreateApiKeyInput.Type; + +// Output Schema +export const CreateApiKeyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + token: Schema.optional(Schema.String), +}); +export type CreateApiKeyOutput = typeof CreateApiKeyOutput.Type; + +// The operation +/** + * Create a new API key + */ +export const createApiKey = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateApiKeyInput, + outputSchema: CreateApiKeyOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createAutomation.ts b/packages/resend/src/operations/createAutomation.ts new file mode 100644 index 0000000000..11ce0cb902 --- /dev/null +++ b/packages/resend/src/operations/createAutomation.ts @@ -0,0 +1,61 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateAutomationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), + steps: Schema.Array( + Schema.Struct({ + key: Schema.String, + type: Schema.Literals([ + "trigger", + "send_email", + "delay", + "wait_for_event", + "condition", + "contact_update", + "contact_delete", + "add_to_segment", + ]), + config: Schema.Unknown, + }), + ), + connections: Schema.Array( + Schema.Struct({ + from: Schema.String, + to: Schema.String, + type: Schema.optional( + Schema.Literals([ + "default", + "condition_met", + "condition_not_met", + "timeout", + "event_received", + ]), + ), + }), + ), +}).pipe(T.Http({ method: "POST", path: "/automations" })); +export type CreateAutomationInput = typeof CreateAutomationInput.Type; + +// Output Schema +export const CreateAutomationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + }, +); +export type CreateAutomationOutput = typeof CreateAutomationOutput.Type; + +// The operation +/** + * Create an automation + */ +export const createAutomation = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateAutomationInput, + outputSchema: CreateAutomationOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createBroadcast.ts b/packages/resend/src/operations/createBroadcast.ts new file mode 100644 index 0000000000..5cf18eda06 --- /dev/null +++ b/packages/resend/src/operations/createBroadcast.ts @@ -0,0 +1,38 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateBroadcastInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.optional(Schema.String), + segment_id: Schema.String, + audience_id: Schema.optional(Schema.String), + from: Schema.String, + subject: Schema.String, + reply_to: Schema.optional(Schema.Array(Schema.String)), + preview_text: Schema.optional(Schema.String), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + topic_id: Schema.optional(Schema.String), + send: Schema.optional(Schema.Boolean), + scheduled_at: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/broadcasts" })); +export type CreateBroadcastInput = typeof CreateBroadcastInput.Type; + +// Output Schema +export const CreateBroadcastOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type CreateBroadcastOutput = typeof CreateBroadcastOutput.Type; + +// The operation +/** + * Create a broadcast + */ +export const createBroadcast = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateBroadcastInput, + outputSchema: CreateBroadcastOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createContact.ts b/packages/resend/src/operations/createContact.ts new file mode 100644 index 0000000000..6a46f90aa1 --- /dev/null +++ b/packages/resend/src/operations/createContact.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateContactInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email: Schema.String, + first_name: Schema.optional(Schema.String), + last_name: Schema.optional(Schema.String), + unsubscribed: Schema.optional(Schema.Boolean), + properties: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + segments: Schema.optional(Schema.Array(Schema.String)), + topics: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + subscription: Schema.optional(Schema.Literals(["opt_in", "opt_out"])), + }), + ), + ), + audience_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/contacts" })); +export type CreateContactInput = typeof CreateContactInput.Type; + +// Output Schema +export const CreateContactOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type CreateContactOutput = typeof CreateContactOutput.Type; + +// The operation +/** + * Create a new contact + */ +export const createContact = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateContactInput, + outputSchema: CreateContactOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createContactImport.ts b/packages/resend/src/operations/createContactImport.ts new file mode 100644 index 0000000000..6a9a99b1f4 --- /dev/null +++ b/packages/resend/src/operations/createContactImport.ts @@ -0,0 +1,39 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateContactImportInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + file: Schema.String, + column_map: Schema.optional(Schema.String), + on_conflict: Schema.optional(Schema.Literals(["upsert", "skip"])), + segments: Schema.optional(Schema.String), + topics: Schema.optional(Schema.String), + }).pipe( + T.Http({ + method: "POST", + path: "/contacts/imports", + contentType: "multipart", + }), + ); +export type CreateContactImportInput = typeof CreateContactImportInput.Type; + +// Output Schema +export const CreateContactImportOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + }); +export type CreateContactImportOutput = typeof CreateContactImportOutput.Type; + +// The operation +/** + * Create a contact import + */ +export const createContactImport = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateContactImportInput, + outputSchema: CreateContactImportOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createContactProperty.ts b/packages/resend/src/operations/createContactProperty.ts new file mode 100644 index 0000000000..9310d7191e --- /dev/null +++ b/packages/resend/src/operations/createContactProperty.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { Conflict, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateContactPropertyInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + key: Schema.String, + type: Schema.Literals(["string", "number"]), + fallback_value: Schema.optional(Schema.Unknown), + }).pipe(T.Http({ method: "POST", path: "/contact-properties" })); +export type CreateContactPropertyInput = typeof CreateContactPropertyInput.Type; + +// Output Schema +export const CreateContactPropertyOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + }); +export type CreateContactPropertyOutput = + typeof CreateContactPropertyOutput.Type; + +// The operation +/** + * Create a new contact property + */ +export const createContactProperty = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateContactPropertyInput, + outputSchema: CreateContactPropertyOutput, + errors: [Conflict, UnprocessableEntity] as const, + }), +); diff --git a/packages/resend/src/operations/createContactSegment.ts b/packages/resend/src/operations/createContactSegment.ts new file mode 100644 index 0000000000..144b99db8c --- /dev/null +++ b/packages/resend/src/operations/createContactSegment.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const CreateContactSegmentInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + contact_id: Schema.String.pipe(T.PathParam()), + segment_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "POST", + path: "/contacts/{contact_id}/segments/{segment_id}", + }), + ); +export type CreateContactSegmentInput = typeof CreateContactSegmentInput.Type; + +// Output Schema +export const CreateContactSegmentOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + contact_id: Schema.optional(Schema.String), + segment_id: Schema.optional(Schema.String), + }); +export type CreateContactSegmentOutput = typeof CreateContactSegmentOutput.Type; + +// The operation +/** + * Add a contact to a segment + * + * @param contact_id - The Contact ID or email address. + * @param segment_id - The Segment ID. + */ +export const createContactSegment = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: CreateContactSegmentInput, + outputSchema: CreateContactSegmentOutput, + errors: [NotFound] as const, + }), +); diff --git a/packages/resend/src/operations/createDomain.ts b/packages/resend/src/operations/createDomain.ts new file mode 100644 index 0000000000..0e84f60811 --- /dev/null +++ b/packages/resend/src/operations/createDomain.ts @@ -0,0 +1,91 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { Forbidden, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateDomainInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + region: Schema.optional( + Schema.Literals(["us-east-1", "eu-west-1", "sa-east-1", "ap-northeast-1"]), + ), + custom_return_path: Schema.optional(Schema.String), + open_tracking: Schema.optional(Schema.Boolean), + click_tracking: Schema.optional(Schema.Boolean), + tls: Schema.optional(Schema.Literals(["opportunistic", "enforced"])), + capabilities: Schema.optional( + Schema.Struct({ + sending: Schema.optional(Schema.Literals(["enabled", "disabled"])), + receiving: Schema.optional(Schema.Literals(["enabled", "disabled"])), + }), + ), + tracking_subdomain: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/domains" })); +export type CreateDomainInput = typeof CreateDomainInput.Type; + +// Output Schema +export const CreateDomainOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals([ + "pending", + "verified", + "failed", + "not_started", + "partially_verified", + "partially_failed", + ]), + ), + capabilities: Schema.optional( + Schema.Struct({ + sending: Schema.optional(Schema.Literals(["enabled", "disabled"])), + receiving: Schema.optional(Schema.Literals(["enabled", "disabled"])), + }), + ), + records: Schema.optional( + Schema.Array( + Schema.Struct({ + record: Schema.optional( + Schema.Literals([ + "SPF", + "DKIM", + "Receiving", + "Tracking", + "TrackingCAA", + ]), + ), + name: Schema.optional(Schema.String), + type: Schema.optional(Schema.Literals(["MX", "TXT", "CNAME", "CAA"])), + ttl: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals([ + "pending", + "verified", + "failed", + "temporary_failure", + "not_started", + ]), + ), + value: Schema.optional(Schema.String), + priority: Schema.optional(Schema.Number), + }), + ), + ), + region: Schema.optional(Schema.String), + open_tracking: Schema.optional(Schema.Boolean), + click_tracking: Schema.optional(Schema.Boolean), + tracking_subdomain: Schema.optional(Schema.String), +}); +export type CreateDomainOutput = typeof CreateDomainOutput.Type; + +// The operation +/** + * Create a new domain + */ +export const createDomain = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateDomainInput, + outputSchema: CreateDomainOutput, + errors: [Forbidden, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createEmail.ts b/packages/resend/src/operations/createEmail.ts new file mode 100644 index 0000000000..949d83870d --- /dev/null +++ b/packages/resend/src/operations/createEmail.ts @@ -0,0 +1,77 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { + BadRequest, + Forbidden, + MethodNotAllowed, + Conflict, + UnprocessableEntity, + UnavailableForLegalReasons, +} from "../errors.ts"; + +// Input Schema +export const CreateEmailInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + from: Schema.String, + to: Schema.Unknown, + subject: Schema.String, + bcc: Schema.optional(Schema.Unknown), + cc: Schema.optional(Schema.Unknown), + reply_to: Schema.optional(Schema.Unknown), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + template: Schema.optional( + Schema.Struct({ + id: Schema.String, + variables: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), + }), + ), + headers: Schema.optional(Schema.Unknown), + scheduled_at: Schema.optional(Schema.String), + attachments: Schema.optional( + Schema.Array( + Schema.Struct({ + content: Schema.optional(Schema.String), + filename: Schema.optional(Schema.String), + path: Schema.optional(Schema.String), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + }), + ), + ), + tags: Schema.optional( + Schema.Array( + Schema.Struct({ + name: Schema.optional(Schema.String), + value: Schema.optional(Schema.String), + }), + ), + ), + topic_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/emails" })); +export type CreateEmailInput = typeof CreateEmailInput.Type; + +// Output Schema +export const CreateEmailOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), +}); +export type CreateEmailOutput = typeof CreateEmailOutput.Type; + +// The operation +/** + * Send an email + * + * @param Idempotency-Key - A unique identifier for the request to ensure emails are only sent once. [Learn more](https://resend.com/docs/dashboard/emails/idempotency-keys) + */ +export const createEmail = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateEmailInput, + outputSchema: CreateEmailOutput, + errors: [ + BadRequest, + Forbidden, + MethodNotAllowed, + Conflict, + UnprocessableEntity, + UnavailableForLegalReasons, + ] as const, +})); diff --git a/packages/resend/src/operations/createEvent.ts b/packages/resend/src/operations/createEvent.ts new file mode 100644 index 0000000000..66a14ed9d9 --- /dev/null +++ b/packages/resend/src/operations/createEvent.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateEventInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + schema: Schema.optional(Schema.NullOr(Schema.Unknown)), +}).pipe(T.Http({ method: "POST", path: "/events" })); +export type CreateEventInput = typeof CreateEventInput.Type; + +// Output Schema +export const CreateEventOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type CreateEventOutput = typeof CreateEventOutput.Type; + +// The operation +/** + * Create an event + */ +export const createEvent = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateEventInput, + outputSchema: CreateEventOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createSegment.ts b/packages/resend/src/operations/createSegment.ts new file mode 100644 index 0000000000..21faa99336 --- /dev/null +++ b/packages/resend/src/operations/createSegment.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateSegmentInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + audience_id: Schema.optional(Schema.String), + filter: Schema.optional(Schema.Unknown), +}).pipe(T.Http({ method: "POST", path: "/segments" })); +export type CreateSegmentInput = typeof CreateSegmentInput.Type; + +// Output Schema +export const CreateSegmentOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type CreateSegmentOutput = typeof CreateSegmentOutput.Type; + +// The operation +/** + * Create a new segment + */ +export const createSegment = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateSegmentInput, + outputSchema: CreateSegmentOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createTemplate.ts b/packages/resend/src/operations/createTemplate.ts new file mode 100644 index 0000000000..c623a0b450 --- /dev/null +++ b/packages/resend/src/operations/createTemplate.ts @@ -0,0 +1,48 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + alias: Schema.optional(Schema.String), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + reply_to: Schema.optional(Schema.Array(Schema.String)), + html: Schema.String, + text: Schema.optional(Schema.String), + variables: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.String, + type: Schema.Literals([ + "string", + "number", + "boolean", + "object", + "list", + ]), + fallback_value: Schema.optional(Schema.Unknown), + }), + ), + ), +}).pipe(T.Http({ method: "POST", path: "/templates" })); +export type CreateTemplateInput = typeof CreateTemplateInput.Type; + +// Output Schema +export const CreateTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type CreateTemplateOutput = typeof CreateTemplateOutput.Type; + +// The operation +/** + * Create a template + */ +export const createTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateTemplateInput, + outputSchema: CreateTemplateOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createTopic.ts b/packages/resend/src/operations/createTopic.ts new file mode 100644 index 0000000000..eaa5c11910 --- /dev/null +++ b/packages/resend/src/operations/createTopic.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const CreateTopicInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + name: Schema.String, + default_subscription: Schema.Literals(["opt_in", "opt_out"]), + description: Schema.optional(Schema.String), + visibility: Schema.optional(Schema.Literals(["public", "private"])), +}).pipe(T.Http({ method: "POST", path: "/topics" })); +export type CreateTopicInput = typeof CreateTopicInput.Type; + +// Output Schema +export const CreateTopicOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type CreateTopicOutput = typeof CreateTopicOutput.Type; + +// The operation +/** + * Create a new topic + */ +export const createTopic = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateTopicInput, + outputSchema: CreateTopicOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/createWebhook.ts b/packages/resend/src/operations/createWebhook.ts new file mode 100644 index 0000000000..917cff5886 --- /dev/null +++ b/packages/resend/src/operations/createWebhook.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; +import { SensitiveOutputString } from "../sensitive.ts"; + +// Input Schema +export const CreateWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + endpoint: Schema.String, + events: Schema.Array(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/webhooks" })); +export type CreateWebhookInput = typeof CreateWebhookInput.Type; + +// Output Schema +export const CreateWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + signing_secret: Schema.optional(SensitiveOutputString), +}); +export type CreateWebhookOutput = typeof CreateWebhookOutput.Type; + +// The operation +/** + * Create a new webhook + */ +export const createWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: CreateWebhookInput, + outputSchema: CreateWebhookOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/deleteApiKey.ts b/packages/resend/src/operations/deleteApiKey.ts new file mode 100644 index 0000000000..2ef5c50f99 --- /dev/null +++ b/packages/resend/src/operations/deleteApiKey.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteApiKeyInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + api_key_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/api-keys/{api_key_id}" })); +export type DeleteApiKeyInput = typeof DeleteApiKeyInput.Type; + +// Output Schema +export const DeleteApiKeyOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteApiKeyOutput = typeof DeleteApiKeyOutput.Type; + +// The operation +/** + * Remove an existing API key + * + * @param api_key_id - The API key ID. + */ +export const deleteApiKey = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteApiKeyInput, + outputSchema: DeleteApiKeyOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteAutomation.ts b/packages/resend/src/operations/deleteAutomation.ts new file mode 100644 index 0000000000..3bece79f64 --- /dev/null +++ b/packages/resend/src/operations/deleteAutomation.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteAutomationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/automations/{automation_id}" })); +export type DeleteAutomationInput = typeof DeleteAutomationInput.Type; + +// Output Schema +export const DeleteAutomationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), + }, +); +export type DeleteAutomationOutput = typeof DeleteAutomationOutput.Type; + +// The operation +/** + * Delete an automation + * + * @param automation_id - The ID of the automation. + */ +export const deleteAutomation = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteAutomationInput, + outputSchema: DeleteAutomationOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteBroadcast.ts b/packages/resend/src/operations/deleteBroadcast.ts new file mode 100644 index 0000000000..91921b063a --- /dev/null +++ b/packages/resend/src/operations/deleteBroadcast.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteBroadcastInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/broadcasts/{id}" })); +export type DeleteBroadcastInput = typeof DeleteBroadcastInput.Type; + +// Output Schema +export const DeleteBroadcastOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteBroadcastOutput = typeof DeleteBroadcastOutput.Type; + +// The operation +/** + * Remove an existing broadcast that is in the draft status + * + * @param id - The Broadcast ID. + */ +export const deleteBroadcast = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteBroadcastInput, + outputSchema: DeleteBroadcastOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteContact.ts b/packages/resend/src/operations/deleteContact.ts new file mode 100644 index 0000000000..fbc3169426 --- /dev/null +++ b/packages/resend/src/operations/deleteContact.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteContactInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/contacts/{id}" })); +export type DeleteContactInput = typeof DeleteContactInput.Type; + +// Output Schema +export const DeleteContactOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteContactOutput = typeof DeleteContactOutput.Type; + +// The operation +/** + * Remove an existing contact by ID or email + * + * @param id - The Contact ID or email address. + */ +export const deleteContact = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteContactInput, + outputSchema: DeleteContactOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteContactProperty.ts b/packages/resend/src/operations/deleteContactProperty.ts new file mode 100644 index 0000000000..5d8159329b --- /dev/null +++ b/packages/resend/src/operations/deleteContactProperty.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteContactPropertyInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "DELETE", path: "/contact-properties/{id}" })); +export type DeleteContactPropertyInput = typeof DeleteContactPropertyInput.Type; + +// Output Schema +export const DeleteContactPropertyOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), + }); +export type DeleteContactPropertyOutput = + typeof DeleteContactPropertyOutput.Type; + +// The operation +/** + * Remove an existing contact property + * + * @param id - The Contact Property ID. + */ +export const deleteContactProperty = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteContactPropertyInput, + outputSchema: DeleteContactPropertyOutput, + errors: [NotFound] as const, + }), +); diff --git a/packages/resend/src/operations/deleteContactSegment.ts b/packages/resend/src/operations/deleteContactSegment.ts new file mode 100644 index 0000000000..fa27692f81 --- /dev/null +++ b/packages/resend/src/operations/deleteContactSegment.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteContactSegmentInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + contact_id: Schema.String.pipe(T.PathParam()), + segment_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "DELETE", + path: "/contacts/{contact_id}/segments/{segment_id}", + }), + ); +export type DeleteContactSegmentInput = typeof DeleteContactSegmentInput.Type; + +// Output Schema +export const DeleteContactSegmentOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + contact_id: Schema.optional(Schema.String), + segment_id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), + }); +export type DeleteContactSegmentOutput = typeof DeleteContactSegmentOutput.Type; + +// The operation +/** + * Remove a contact from a segment + * + * @param contact_id - The Contact ID or email address. + * @param segment_id - The Segment ID. + */ +export const deleteContactSegment = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: DeleteContactSegmentInput, + outputSchema: DeleteContactSegmentOutput, + errors: [NotFound] as const, + }), +); diff --git a/packages/resend/src/operations/deleteDomain.ts b/packages/resend/src/operations/deleteDomain.ts new file mode 100644 index 0000000000..fd8ab718dc --- /dev/null +++ b/packages/resend/src/operations/deleteDomain.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteDomainInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + domain_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/domains/{domain_id}" })); +export type DeleteDomainInput = typeof DeleteDomainInput.Type; + +// Output Schema +export const DeleteDomainOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteDomainOutput = typeof DeleteDomainOutput.Type; + +// The operation +/** + * Remove an existing domain + * + * @param domain_id - The ID of the domain. + */ +export const deleteDomain = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteDomainInput, + outputSchema: DeleteDomainOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteEvent.ts b/packages/resend/src/operations/deleteEvent.ts new file mode 100644 index 0000000000..d61ba30714 --- /dev/null +++ b/packages/resend/src/operations/deleteEvent.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const DeleteEventInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + identifier: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/events/{identifier}" })); +export type DeleteEventInput = typeof DeleteEventInput.Type; + +// Output Schema +export const DeleteEventOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteEventOutput = typeof DeleteEventOutput.Type; + +// The operation +/** + * Delete an event + * + * @param identifier - The event ID (UUID) or event name. + */ +export const deleteEvent = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteEventInput, + outputSchema: DeleteEventOutput, +})); diff --git a/packages/resend/src/operations/deleteSegment.ts b/packages/resend/src/operations/deleteSegment.ts new file mode 100644 index 0000000000..b2e13b2d18 --- /dev/null +++ b/packages/resend/src/operations/deleteSegment.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteSegmentInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/segments/{id}" })); +export type DeleteSegmentInput = typeof DeleteSegmentInput.Type; + +// Output Schema +export const DeleteSegmentOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteSegmentOutput = typeof DeleteSegmentOutput.Type; + +// The operation +/** + * Remove an existing segment + * + * @param id - The Segment ID. + */ +export const deleteSegment = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteSegmentInput, + outputSchema: DeleteSegmentOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteTemplate.ts b/packages/resend/src/operations/deleteTemplate.ts new file mode 100644 index 0000000000..ff3a5a2c82 --- /dev/null +++ b/packages/resend/src/operations/deleteTemplate.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/templates/{id}" })); +export type DeleteTemplateInput = typeof DeleteTemplateInput.Type; + +// Output Schema +export const DeleteTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteTemplateOutput = typeof DeleteTemplateOutput.Type; + +// The operation +/** + * Remove an existing template + * + * @param id - The Template ID or alias. + */ +export const deleteTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteTemplateInput, + outputSchema: DeleteTemplateOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteTopic.ts b/packages/resend/src/operations/deleteTopic.ts new file mode 100644 index 0000000000..83cecdb224 --- /dev/null +++ b/packages/resend/src/operations/deleteTopic.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteTopicInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/topics/{id}" })); +export type DeleteTopicInput = typeof DeleteTopicInput.Type; + +// Output Schema +export const DeleteTopicOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteTopicOutput = typeof DeleteTopicOutput.Type; + +// The operation +/** + * Remove an existing topic + * + * @param id - The Topic ID. + */ +export const deleteTopic = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteTopicInput, + outputSchema: DeleteTopicOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/deleteWebhook.ts b/packages/resend/src/operations/deleteWebhook.ts new file mode 100644 index 0000000000..dd523e2d68 --- /dev/null +++ b/packages/resend/src/operations/deleteWebhook.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DeleteWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "DELETE", path: "/webhooks/{webhook_id}" })); +export type DeleteWebhookInput = typeof DeleteWebhookInput.Type; + +// Output Schema +export const DeleteWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + deleted: Schema.optional(Schema.Boolean), +}); +export type DeleteWebhookOutput = typeof DeleteWebhookOutput.Type; + +// The operation +/** + * Remove an existing webhook + * + * @param webhook_id - The Webhook ID. + */ +export const deleteWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DeleteWebhookInput, + outputSchema: DeleteWebhookOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/duplicateTemplate.ts b/packages/resend/src/operations/duplicateTemplate.ts new file mode 100644 index 0000000000..35d6e61570 --- /dev/null +++ b/packages/resend/src/operations/duplicateTemplate.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const DuplicateTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "POST", path: "/templates/{id}/duplicate" })); +export type DuplicateTemplateInput = typeof DuplicateTemplateInput.Type; + +// Output Schema +export const DuplicateTemplateOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + }); +export type DuplicateTemplateOutput = typeof DuplicateTemplateOutput.Type; + +// The operation +/** + * Duplicate a template + * + * @param id - The Template ID or alias. + */ +export const duplicateTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: DuplicateTemplateInput, + outputSchema: DuplicateTemplateOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getAutomation.ts b/packages/resend/src/operations/getAutomation.ts new file mode 100644 index 0000000000..ad634347ec --- /dev/null +++ b/packages/resend/src/operations/getAutomation.ts @@ -0,0 +1,70 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetAutomationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/automations/{automation_id}" })); +export type GetAutomationInput = typeof GetAutomationInput.Type; + +// Output Schema +export const GetAutomationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.String), + steps: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.optional(Schema.String), + type: Schema.optional( + Schema.Literals([ + "trigger", + "send_email", + "delay", + "wait_for_event", + "condition", + "contact_update", + "contact_delete", + "add_to_segment", + ]), + ), + config: Schema.optional(Schema.Unknown), + }), + ), + ), + connections: Schema.optional( + Schema.Array( + Schema.Struct({ + from: Schema.String, + to: Schema.String, + type: Schema.optional( + Schema.Literals([ + "default", + "condition_met", + "condition_not_met", + "timeout", + "event_received", + ]), + ), + }), + ), + ), +}); +export type GetAutomationOutput = typeof GetAutomationOutput.Type; + +// The operation +/** + * Retrieve a single automation + * + * @param automation_id - The ID of the automation. + */ +export const getAutomation = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetAutomationInput, + outputSchema: GetAutomationOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getAutomationRun.ts b/packages/resend/src/operations/getAutomationRun.ts new file mode 100644 index 0000000000..712aa8e011 --- /dev/null +++ b/packages/resend/src/operations/getAutomationRun.ts @@ -0,0 +1,66 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetAutomationRunInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), + run_id: Schema.String.pipe(T.PathParam()), +}).pipe( + T.Http({ method: "GET", path: "/automations/{automation_id}/runs/{run_id}" }), +); +export type GetAutomationRunInput = typeof GetAutomationRunInput.Type; + +// Output Schema +export const GetAutomationRunOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals(["running", "completed", "failed", "cancelled"]), + ), + started_at: Schema.optional(Schema.NullOr(Schema.String)), + completed_at: Schema.optional(Schema.NullOr(Schema.String)), + created_at: Schema.optional(Schema.String), + steps: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.optional(Schema.String), + type: Schema.optional( + Schema.Literals([ + "trigger", + "send_email", + "delay", + "wait_for_event", + "condition", + "contact_update", + "contact_delete", + "add_to_segment", + ]), + ), + status: Schema.optional(Schema.String), + started_at: Schema.optional(Schema.NullOr(Schema.String)), + completed_at: Schema.optional(Schema.NullOr(Schema.String)), + output: Schema.optional(Schema.NullOr(Schema.Unknown)), + error: Schema.optional(Schema.NullOr(Schema.Unknown)), + created_at: Schema.optional(Schema.String), + }), + ), + ), + }, +); +export type GetAutomationRunOutput = typeof GetAutomationRunOutput.Type; + +// The operation +/** + * Retrieve a single automation run + * + * @param automation_id - The ID of the automation. + * @param run_id - The ID of the automation run. + */ +export const getAutomationRun = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetAutomationRunInput, + outputSchema: GetAutomationRunOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getBroadcast.ts b/packages/resend/src/operations/getBroadcast.ts new file mode 100644 index 0000000000..af745a87ac --- /dev/null +++ b/packages/resend/src/operations/getBroadcast.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetBroadcastInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/broadcasts/{id}" })); +export type GetBroadcastInput = typeof GetBroadcastInput.Type; + +// Output Schema +export const GetBroadcastOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + audience_id: Schema.optional(Schema.NullOr(Schema.String)), + segment_id: Schema.optional(Schema.NullOr(Schema.String)), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + reply_to: Schema.optional(Schema.Array(Schema.String)), + preview_text: Schema.optional(Schema.String), + status: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + scheduled_at: Schema.optional(Schema.String), + sent_at: Schema.optional(Schema.String), + text: Schema.optional(Schema.NullOr(Schema.String)), + html: Schema.optional(Schema.NullOr(Schema.String)), + topic_id: Schema.optional(Schema.NullOr(Schema.String)), +}); +export type GetBroadcastOutput = typeof GetBroadcastOutput.Type; + +// The operation +/** + * Retrieve a single broadcast + * + * @param id - The Broadcast ID. + */ +export const getBroadcast = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetBroadcastInput, + outputSchema: GetBroadcastOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getContact.ts b/packages/resend/src/operations/getContact.ts new file mode 100644 index 0000000000..2ded4d3423 --- /dev/null +++ b/packages/resend/src/operations/getContact.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetContactInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/contacts/{id}" })); +export type GetContactInput = typeof GetContactInput.Type; + +// Output Schema +export const GetContactOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + email: Schema.optional(Schema.String), + first_name: Schema.optional(Schema.String), + last_name: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + unsubscribed: Schema.optional(Schema.Boolean), + properties: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}); +export type GetContactOutput = typeof GetContactOutput.Type; + +// The operation +/** + * Retrieve a single contact by ID or email + * + * @param id - The Contact ID or email address. + */ +export const getContact = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetContactInput, + outputSchema: GetContactOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getContactImport.ts b/packages/resend/src/operations/getContactImport.ts new file mode 100644 index 0000000000..dc5a558555 --- /dev/null +++ b/packages/resend/src/operations/getContactImport.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetContactImportInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/contacts/imports/{id}" })); +export type GetContactImportInput = typeof GetContactImportInput.Type; + +// Output Schema +export const GetContactImportOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals(["queued", "in_progress", "completed", "failed"]), + ), + created_at: Schema.optional(Schema.String), + completed_at: Schema.optional(Schema.NullOr(Schema.String)), + counts: Schema.optional( + Schema.Struct({ + total: Schema.optional(Schema.Number), + created: Schema.optional(Schema.Number), + updated: Schema.optional(Schema.Number), + skipped: Schema.optional(Schema.Number), + failed: Schema.optional(Schema.Number), + }), + ), + }, +); +export type GetContactImportOutput = typeof GetContactImportOutput.Type; + +// The operation +/** + * Retrieve a single contact import + * + * @param id - The Contact Import ID. + */ +export const getContactImport = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetContactImportInput, + outputSchema: GetContactImportOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getContactProperty.ts b/packages/resend/src/operations/getContactProperty.ts new file mode 100644 index 0000000000..53eb488b38 --- /dev/null +++ b/packages/resend/src/operations/getContactProperty.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetContactPropertyInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + }).pipe(T.Http({ method: "GET", path: "/contact-properties/{id}" })); +export type GetContactPropertyInput = typeof GetContactPropertyInput.Type; + +// Output Schema +export const GetContactPropertyOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + key: Schema.optional(Schema.String), + type: Schema.optional(Schema.String), + fallback_value: Schema.optional(Schema.Unknown), + created_at: Schema.optional(Schema.String), + }); +export type GetContactPropertyOutput = typeof GetContactPropertyOutput.Type; + +// The operation +/** + * Retrieve a single contact property + * + * @param id - The Contact Property ID. + */ +export const getContactProperty = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetContactPropertyInput, + outputSchema: GetContactPropertyOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getDomain.ts b/packages/resend/src/operations/getDomain.ts new file mode 100644 index 0000000000..06ceccdb51 --- /dev/null +++ b/packages/resend/src/operations/getDomain.ts @@ -0,0 +1,80 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetDomainInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + domain_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/domains/{domain_id}" })); +export type GetDomainInput = typeof GetDomainInput.Type; + +// Output Schema +export const GetDomainOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals([ + "pending", + "verified", + "failed", + "not_started", + "partially_verified", + "partially_failed", + ]), + ), + created_at: Schema.optional(Schema.String), + region: Schema.optional(Schema.String), + open_tracking: Schema.optional(Schema.Boolean), + click_tracking: Schema.optional(Schema.Boolean), + tracking_subdomain: Schema.optional(Schema.String), + capabilities: Schema.optional( + Schema.Struct({ + sending: Schema.optional(Schema.Literals(["enabled", "disabled"])), + receiving: Schema.optional(Schema.Literals(["enabled", "disabled"])), + }), + ), + records: Schema.optional( + Schema.Array( + Schema.Struct({ + record: Schema.optional( + Schema.Literals([ + "SPF", + "DKIM", + "Receiving", + "Tracking", + "TrackingCAA", + ]), + ), + name: Schema.optional(Schema.String), + type: Schema.optional(Schema.Literals(["MX", "TXT", "CNAME", "CAA"])), + ttl: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals([ + "pending", + "verified", + "failed", + "temporary_failure", + "not_started", + ]), + ), + value: Schema.optional(Schema.String), + priority: Schema.optional(Schema.Number), + }), + ), + ), +}); +export type GetDomainOutput = typeof GetDomainOutput.Type; + +// The operation +/** + * Retrieve a single domain + * + * @param domain_id - The ID of the domain. + */ +export const getDomain = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetDomainInput, + outputSchema: GetDomainOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getEmail.ts b/packages/resend/src/operations/getEmail.ts new file mode 100644 index 0000000000..a0b6617cac --- /dev/null +++ b/packages/resend/src/operations/getEmail.ts @@ -0,0 +1,54 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { Forbidden, NotFound } from "../errors.ts"; + +// Input Schema +export const GetEmailInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/emails/{email_id}" })); +export type GetEmailInput = typeof GetEmailInput.Type; + +// Output Schema +export const GetEmailOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + to: Schema.optional(Schema.Array(Schema.String)), + from: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + bcc: Schema.optional(Schema.Array(Schema.String)), + cc: Schema.optional(Schema.Array(Schema.String)), + reply_to: Schema.optional(Schema.Array(Schema.String)), + last_event: Schema.optional( + Schema.Literals([ + "bounced", + "canceled", + "clicked", + "complained", + "delivered", + "delivery_delayed", + "failed", + "opened", + "queued", + "scheduled", + "sent", + "suppressed", + ]), + ), +}); +export type GetEmailOutput = typeof GetEmailOutput.Type; + +// The operation +/** + * Retrieve a single email + * + * @param email_id - The ID of the email. + */ +export const getEmail = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEmailInput, + outputSchema: GetEmailOutput, + errors: [Forbidden, NotFound] as const, +})); diff --git a/packages/resend/src/operations/getEmailAttachment.ts b/packages/resend/src/operations/getEmailAttachment.ts new file mode 100644 index 0000000000..ff2a7778e2 --- /dev/null +++ b/packages/resend/src/operations/getEmailAttachment.ts @@ -0,0 +1,47 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetEmailAttachmentInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), + attachment_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/emails/{email_id}/attachments/{attachment_id}", + }), + ); +export type GetEmailAttachmentInput = typeof GetEmailAttachmentInput.Type; + +// Output Schema +export const GetEmailAttachmentOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + download_url: Schema.optional(Schema.String), + expires_at: Schema.optional(Schema.String), + size: Schema.optional(Schema.Number), + }); +export type GetEmailAttachmentOutput = typeof GetEmailAttachmentOutput.Type; + +// The operation +/** + * Retrieve a single attachment for a sent email + * + * @param email_id - The ID of the email. + * @param attachment_id - The ID of the attachment. + */ +export const getEmailAttachment = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEmailAttachmentInput, + outputSchema: GetEmailAttachmentOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getEmailReceiving.ts b/packages/resend/src/operations/getEmailReceiving.ts new file mode 100644 index 0000000000..0d0009e0c8 --- /dev/null +++ b/packages/resend/src/operations/getEmailReceiving.ts @@ -0,0 +1,57 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetEmailReceivingInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + email_id: Schema.String.pipe(T.PathParam()), + }, +).pipe(T.Http({ method: "GET", path: "/emails/receiving/{email_id}" })); +export type GetEmailReceivingInput = typeof GetEmailReceivingInput.Type; + +// Output Schema +export const GetEmailReceivingOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + to: Schema.optional(Schema.Array(Schema.String)), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + message_id: Schema.optional(Schema.String), + bcc: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + cc: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + reply_to: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + html: Schema.optional(Schema.NullOr(Schema.String)), + text: Schema.optional(Schema.NullOr(Schema.String)), + headers: Schema.optional(Schema.NullOr(Schema.Unknown)), + created_at: Schema.optional(Schema.String), + attachments: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + size: Schema.optional(Schema.Number), + }), + ), + ), + }); +export type GetEmailReceivingOutput = typeof GetEmailReceivingOutput.Type; + +// The operation +/** + * Retrieve a single received email + * + * @param email_id - The ID of the received email. + */ +export const getEmailReceiving = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEmailReceivingInput, + outputSchema: GetEmailReceivingOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getEmailReceivingAttachment.ts b/packages/resend/src/operations/getEmailReceivingAttachment.ts new file mode 100644 index 0000000000..f67d31be4f --- /dev/null +++ b/packages/resend/src/operations/getEmailReceivingAttachment.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetEmailReceivingAttachmentInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), + attachment_id: Schema.String.pipe(T.PathParam()), + }).pipe( + T.Http({ + method: "GET", + path: "/emails/receiving/{email_id}/attachments/{attachment_id}", + }), + ); +export type GetEmailReceivingAttachmentInput = + typeof GetEmailReceivingAttachmentInput.Type; + +// Output Schema +export const GetEmailReceivingAttachmentOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + download_url: Schema.optional(Schema.String), + expires_at: Schema.optional(Schema.String), + size: Schema.optional(Schema.Number), + }); +export type GetEmailReceivingAttachmentOutput = + typeof GetEmailReceivingAttachmentOutput.Type; + +// The operation +/** + * Retrieve a single attachment for a received email + * + * @param email_id - The ID of the received email. + * @param attachment_id - The ID of the attachment. + */ +export const getEmailReceivingAttachment = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: GetEmailReceivingAttachmentInput, + outputSchema: GetEmailReceivingAttachmentOutput, + errors: [NotFound] as const, + }), +); diff --git a/packages/resend/src/operations/getEvent.ts b/packages/resend/src/operations/getEvent.ts new file mode 100644 index 0000000000..6cb383f22c --- /dev/null +++ b/packages/resend/src/operations/getEvent.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetEventInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + identifier: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/events/{identifier}" })); +export type GetEventInput = typeof GetEventInput.Type; + +// Output Schema +export const GetEventOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + schema: Schema.optional(Schema.NullOr(Schema.Unknown)), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.NullOr(Schema.String)), +}); +export type GetEventOutput = typeof GetEventOutput.Type; + +// The operation +/** + * Retrieve a single event + * + * @param identifier - The event ID (UUID) or event name. + */ +export const getEvent = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetEventInput, + outputSchema: GetEventOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getLog.ts b/packages/resend/src/operations/getLog.ts new file mode 100644 index 0000000000..adad0cf64c --- /dev/null +++ b/packages/resend/src/operations/getLog.ts @@ -0,0 +1,38 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetLogInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + log_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/logs/{log_id}" })); +export type GetLogInput = typeof GetLogInput.Type; + +// Output Schema +export const GetLogOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + endpoint: Schema.optional(Schema.String), + method: Schema.optional( + Schema.Literals(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]), + ), + response_status: Schema.optional(Schema.Number), + user_agent: Schema.optional(Schema.NullOr(Schema.String)), + request_body: Schema.optional(Schema.NullOr(Schema.Unknown)), + response_body: Schema.optional(Schema.NullOr(Schema.Unknown)), +}); +export type GetLogOutput = typeof GetLogOutput.Type; + +// The operation +/** + * Retrieve a single log + * + * @param log_id - The ID of the log. + */ +export const getLog = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetLogInput, + outputSchema: GetLogOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getSegment.ts b/packages/resend/src/operations/getSegment.ts new file mode 100644 index 0000000000..de5454b633 --- /dev/null +++ b/packages/resend/src/operations/getSegment.ts @@ -0,0 +1,33 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetSegmentInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/segments/{id}" })); +export type GetSegmentInput = typeof GetSegmentInput.Type; + +// Output Schema +export const GetSegmentOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + audience_id: Schema.optional(Schema.String), + filter: Schema.optional(Schema.Unknown), + created_at: Schema.optional(Schema.String), +}); +export type GetSegmentOutput = typeof GetSegmentOutput.Type; + +// The operation +/** + * Retrieve a single segment + * + * @param id - The Segment ID. + */ +export const getSegment = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetSegmentInput, + outputSchema: GetSegmentOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getTemplate.ts b/packages/resend/src/operations/getTemplate.ts new file mode 100644 index 0000000000..eefd820c92 --- /dev/null +++ b/packages/resend/src/operations/getTemplate.ts @@ -0,0 +1,60 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/templates/{id}" })); +export type GetTemplateInput = typeof GetTemplateInput.Type; + +// Output Schema +export const GetTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + current_version_id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + alias: Schema.optional(Schema.String), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + reply_to: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + variables: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + key: Schema.String, + type: Schema.Literals([ + "string", + "number", + "boolean", + "object", + "list", + ]), + fallback_value: Schema.optional(Schema.Unknown), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.String), + }), + ), + ), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.String), + status: Schema.optional(Schema.Literals(["draft", "published"])), + published_at: Schema.optional(Schema.NullOr(Schema.String)), + has_unpublished_versions: Schema.optional(Schema.Boolean), +}); +export type GetTemplateOutput = typeof GetTemplateOutput.Type; + +// The operation +/** + * Retrieve a single template + * + * @param id - The Template ID or alias. + */ +export const getTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetTemplateInput, + outputSchema: GetTemplateOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getTopic.ts b/packages/resend/src/operations/getTopic.ts new file mode 100644 index 0000000000..c6ab31ef5e --- /dev/null +++ b/packages/resend/src/operations/getTopic.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const GetTopicInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/topics/{id}" })); +export type GetTopicInput = typeof GetTopicInput.Type; + +// Output Schema +export const GetTopicOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + default_subscription: Schema.optional(Schema.Literals(["opt_in", "opt_out"])), + visibility: Schema.optional(Schema.Literals(["public", "private"])), + created_at: Schema.optional(Schema.String), +}); +export type GetTopicOutput = typeof GetTopicOutput.Type; + +// The operation +/** + * Retrieve a single topic + * + * @param id - The Topic ID. + */ +export const getTopic = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetTopicInput, + outputSchema: GetTopicOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/getWebhook.ts b/packages/resend/src/operations/getWebhook.ts new file mode 100644 index 0000000000..d95c0135bc --- /dev/null +++ b/packages/resend/src/operations/getWebhook.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; +import { SensitiveOutputString } from "../sensitive.ts"; + +// Input Schema +export const GetWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "GET", path: "/webhooks/{webhook_id}" })); +export type GetWebhookInput = typeof GetWebhookInput.Type; + +// Output Schema +export const GetWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + endpoint: Schema.optional(Schema.String), + events: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + status: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + signing_secret: Schema.optional(SensitiveOutputString), +}); +export type GetWebhookOutput = typeof GetWebhookOutput.Type; + +// The operation +/** + * Retrieve a single webhook + * + * @param webhook_id - The Webhook ID. + */ +export const getWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: GetWebhookInput, + outputSchema: GetWebhookOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/index.ts b/packages/resend/src/operations/index.ts new file mode 100644 index 0000000000..a637f5cf9a --- /dev/null +++ b/packages/resend/src/operations/index.ts @@ -0,0 +1,82 @@ +export * from "./listEmails.ts"; +export * from "./createEmail.ts"; +export * from "./getEmail.ts"; +export * from "./updateEmail.ts"; +export * from "./cancelEmail.ts"; +export * from "./batchEmail.ts"; +export * from "./listEmailAttachments.ts"; +export * from "./getEmailAttachment.ts"; +export * from "./listEmailReceiving.ts"; +export * from "./getEmailReceiving.ts"; +export * from "./listEmailReceivingAttachments.ts"; +export * from "./getEmailReceivingAttachment.ts"; +export * from "./listDomains.ts"; +export * from "./createDomain.ts"; +export * from "./getDomain.ts"; +export * from "./updateDomain.ts"; +export * from "./deleteDomain.ts"; +export * from "./verifyDomain.ts"; +export * from "./listApiKeys.ts"; +export * from "./createApiKey.ts"; +export * from "./deleteApiKey.ts"; +export * from "./listTemplates.ts"; +export * from "./createTemplate.ts"; +export * from "./getTemplate.ts"; +export * from "./updateTemplate.ts"; +export * from "./deleteTemplate.ts"; +export * from "./publishTemplate.ts"; +export * from "./duplicateTemplate.ts"; +export * from "./listContacts.ts"; +export * from "./createContact.ts"; +export * from "./listContactImports.ts"; +export * from "./createContactImport.ts"; +export * from "./getContactImport.ts"; +export * from "./getContact.ts"; +export * from "./updateContact.ts"; +export * from "./deleteContact.ts"; +export * from "./listBroadcasts.ts"; +export * from "./createBroadcast.ts"; +export * from "./getBroadcast.ts"; +export * from "./updateBroadcast.ts"; +export * from "./deleteBroadcast.ts"; +export * from "./sendBroadcast.ts"; +export * from "./listWebhooks.ts"; +export * from "./createWebhook.ts"; +export * from "./getWebhook.ts"; +export * from "./updateWebhook.ts"; +export * from "./deleteWebhook.ts"; +export * from "./listSegments.ts"; +export * from "./createSegment.ts"; +export * from "./getSegment.ts"; +export * from "./deleteSegment.ts"; +export * from "./listTopics.ts"; +export * from "./createTopic.ts"; +export * from "./getTopic.ts"; +export * from "./updateTopic.ts"; +export * from "./deleteTopic.ts"; +export * from "./listContactProperties.ts"; +export * from "./createContactProperty.ts"; +export * from "./getContactProperty.ts"; +export * from "./updateContactProperty.ts"; +export * from "./deleteContactProperty.ts"; +export * from "./listContactSegments.ts"; +export * from "./createContactSegment.ts"; +export * from "./deleteContactSegment.ts"; +export * from "./listContactTopics.ts"; +export * from "./updateContactTopic.ts"; +export * from "./listLogs.ts"; +export * from "./getLog.ts"; +export * from "./listAutomations.ts"; +export * from "./createAutomation.ts"; +export * from "./getAutomation.ts"; +export * from "./updateAutomation.ts"; +export * from "./deleteAutomation.ts"; +export * from "./stopAutomation.ts"; +export * from "./listAutomationRuns.ts"; +export * from "./getAutomationRun.ts"; +export * from "./listEvents.ts"; +export * from "./createEvent.ts"; +export * from "./sendEvent.ts"; +export * from "./getEvent.ts"; +export * from "./updateEvent.ts"; +export * from "./deleteEvent.ts"; diff --git a/packages/resend/src/operations/listApiKeys.ts b/packages/resend/src/operations/listApiKeys.ts new file mode 100644 index 0000000000..dd4599eca8 --- /dev/null +++ b/packages/resend/src/operations/listApiKeys.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListApiKeysInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/api-keys" })); +export type ListApiKeysInput = typeof ListApiKeysInput.Type; + +// Output Schema +export const ListApiKeysOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + last_used_at: Schema.optional(Schema.NullOr(Schema.String)), + }), + ), + ), +}); +export type ListApiKeysOutput = typeof ListApiKeysOutput.Type; + +// The operation +/** + * Retrieve a list of API keys + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listApiKeys = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListApiKeysInput, + outputSchema: ListApiKeysOutput, +})); diff --git a/packages/resend/src/operations/listAutomationRuns.ts b/packages/resend/src/operations/listAutomationRuns.ts new file mode 100644 index 0000000000..39e0856ad9 --- /dev/null +++ b/packages/resend/src/operations/listAutomationRuns.ts @@ -0,0 +1,52 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const ListAutomationRunsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), + status: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/automations/{automation_id}/runs" })); +export type ListAutomationRunsInput = typeof ListAutomationRunsInput.Type; + +// Output Schema +export const ListAutomationRunsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals(["running", "completed", "failed", "cancelled"]), + ), + started_at: Schema.optional(Schema.NullOr(Schema.String)), + completed_at: Schema.optional(Schema.NullOr(Schema.String)), + created_at: Schema.optional(Schema.String), + }), + ), + ), + }); +export type ListAutomationRunsOutput = typeof ListAutomationRunsOutput.Type; + +// The operation +/** + * Retrieve a list of automation runs + * + * @param automation_id - The ID of the automation. + * @param status - Filter runs by status. Comma-separated list of: running, completed, failed, cancelled. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listAutomationRuns = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListAutomationRunsInput, + outputSchema: ListAutomationRunsOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/listAutomations.ts b/packages/resend/src/operations/listAutomations.ts new file mode 100644 index 0000000000..e20a716d0b --- /dev/null +++ b/packages/resend/src/operations/listAutomations.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListAutomationsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/automations" })); +export type ListAutomationsInput = typeof ListAutomationsInput.Type; + +// Output Schema +export const ListAutomationsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.String), + }), + ), + ), +}); +export type ListAutomationsOutput = typeof ListAutomationsOutput.Type; + +// The operation +/** + * Retrieve a list of automations + * + * @param status - Filter automations by status. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listAutomations = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListAutomationsInput, + outputSchema: ListAutomationsOutput, +})); diff --git a/packages/resend/src/operations/listBroadcasts.ts b/packages/resend/src/operations/listBroadcasts.ts new file mode 100644 index 0000000000..41c10a8700 --- /dev/null +++ b/packages/resend/src/operations/listBroadcasts.ts @@ -0,0 +1,46 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListBroadcastsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/broadcasts" })); +export type ListBroadcastsInput = typeof ListBroadcastsInput.Type; + +// Output Schema +export const ListBroadcastsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + audience_id: Schema.optional(Schema.String), + segment_id: Schema.optional(Schema.String), + status: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + scheduled_at: Schema.optional(Schema.String), + sent_at: Schema.optional(Schema.String), + topic_id: Schema.optional(Schema.String), + }), + ), + ), +}); +export type ListBroadcastsOutput = typeof ListBroadcastsOutput.Type; + +// The operation +/** + * Retrieve a list of broadcasts + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listBroadcasts = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListBroadcastsInput, + outputSchema: ListBroadcastsOutput, +})); diff --git a/packages/resend/src/operations/listContactImports.ts b/packages/resend/src/operations/listContactImports.ts new file mode 100644 index 0000000000..8ee7834918 --- /dev/null +++ b/packages/resend/src/operations/listContactImports.ts @@ -0,0 +1,59 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListContactImportsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + status: Schema.optional( + Schema.Literals(["queued", "in_progress", "completed", "failed"]), + ), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/contacts/imports" })); +export type ListContactImportsInput = typeof ListContactImportsInput.Type; + +// Output Schema +export const ListContactImportsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals(["queued", "in_progress", "completed", "failed"]), + ), + created_at: Schema.optional(Schema.String), + completed_at: Schema.optional(Schema.NullOr(Schema.String)), + counts: Schema.optional( + Schema.Struct({ + total: Schema.optional(Schema.Number), + created: Schema.optional(Schema.Number), + updated: Schema.optional(Schema.Number), + skipped: Schema.optional(Schema.Number), + failed: Schema.optional(Schema.Number), + }), + ), + }), + ), + ), + }); +export type ListContactImportsOutput = typeof ListContactImportsOutput.Type; + +// The operation +/** + * Retrieve a list of contact imports + * + * @param status - Filter contact imports by status. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listContactImports = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListContactImportsInput, + outputSchema: ListContactImportsOutput, +})); diff --git a/packages/resend/src/operations/listContactProperties.ts b/packages/resend/src/operations/listContactProperties.ts new file mode 100644 index 0000000000..6c32add1c6 --- /dev/null +++ b/packages/resend/src/operations/listContactProperties.ts @@ -0,0 +1,47 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListContactPropertiesInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/contact-properties" })); +export type ListContactPropertiesInput = typeof ListContactPropertiesInput.Type; + +// Output Schema +export const ListContactPropertiesOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + key: Schema.optional(Schema.String), + type: Schema.optional(Schema.String), + fallback_value: Schema.optional(Schema.Unknown), + created_at: Schema.optional(Schema.String), + }), + ), + ), + }); +export type ListContactPropertiesOutput = + typeof ListContactPropertiesOutput.Type; + +// The operation +/** + * Retrieve a list of contact properties + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listContactProperties = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListContactPropertiesInput, + outputSchema: ListContactPropertiesOutput, + }), +); diff --git a/packages/resend/src/operations/listContactSegments.ts b/packages/resend/src/operations/listContactSegments.ts new file mode 100644 index 0000000000..972ae403a4 --- /dev/null +++ b/packages/resend/src/operations/listContactSegments.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListContactSegmentsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + contact_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/contacts/{contact_id}/segments" })); +export type ListContactSegmentsInput = typeof ListContactSegmentsInput.Type; + +// Output Schema +export const ListContactSegmentsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + }), + ), + ), + }); +export type ListContactSegmentsOutput = typeof ListContactSegmentsOutput.Type; + +// The operation +/** + * Retrieve a list of segments for a contact + * + * @param contact_id - The Contact ID or email address. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listContactSegments = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListContactSegmentsInput, + outputSchema: ListContactSegmentsOutput, +})); diff --git a/packages/resend/src/operations/listContactTopics.ts b/packages/resend/src/operations/listContactTopics.ts new file mode 100644 index 0000000000..2f5e3df35a --- /dev/null +++ b/packages/resend/src/operations/listContactTopics.ts @@ -0,0 +1,46 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListContactTopicsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + contact_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }, +).pipe(T.Http({ method: "GET", path: "/contacts/{contact_id}/topics" })); +export type ListContactTopicsInput = typeof ListContactTopicsInput.Type; + +// Output Schema +export const ListContactTopicsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + subscription: Schema.optional(Schema.Literals(["opt_in", "opt_out"])), + }), + ), + ), + }); +export type ListContactTopicsOutput = typeof ListContactTopicsOutput.Type; + +// The operation +/** + * Retrieve topics for a contact + * + * @param contact_id - The Contact ID or email address. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listContactTopics = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListContactTopicsInput, + outputSchema: ListContactTopicsOutput, +})); diff --git a/packages/resend/src/operations/listContacts.ts b/packages/resend/src/operations/listContacts.ts new file mode 100644 index 0000000000..5b3d6e3362 --- /dev/null +++ b/packages/resend/src/operations/listContacts.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListContactsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + segment_id: Schema.optional(Schema.String), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/contacts" })); +export type ListContactsInput = typeof ListContactsInput.Type; + +// Output Schema +export const ListContactsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + email: Schema.optional(Schema.String), + first_name: Schema.optional(Schema.String), + last_name: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + unsubscribed: Schema.optional(Schema.Boolean), + }), + ), + ), +}); +export type ListContactsOutput = typeof ListContactsOutput.Type; + +// The operation +/** + * Retrieve a list of contacts + * + * @param segment_id - Filter contacts by segment ID. + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listContacts = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListContactsInput, + outputSchema: ListContactsOutput, +})); diff --git a/packages/resend/src/operations/listDomains.ts b/packages/resend/src/operations/listDomains.ts new file mode 100644 index 0000000000..81742edb22 --- /dev/null +++ b/packages/resend/src/operations/listDomains.ts @@ -0,0 +1,59 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListDomainsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/domains" })); +export type ListDomainsInput = typeof ListDomainsInput.Type; + +// Output Schema +export const ListDomainsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + status: Schema.optional( + Schema.Literals([ + "pending", + "verified", + "failed", + "not_started", + "partially_verified", + "partially_failed", + ]), + ), + created_at: Schema.optional(Schema.String), + region: Schema.optional(Schema.String), + capabilities: Schema.optional( + Schema.Struct({ + sending: Schema.optional(Schema.Literals(["enabled", "disabled"])), + receiving: Schema.optional( + Schema.Literals(["enabled", "disabled"]), + ), + }), + ), + }), + ), + ), +}); +export type ListDomainsOutput = typeof ListDomainsOutput.Type; + +// The operation +/** + * Retrieve a list of domains + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listDomains = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListDomainsInput, + outputSchema: ListDomainsOutput, +})); diff --git a/packages/resend/src/operations/listEmailAttachments.ts b/packages/resend/src/operations/listEmailAttachments.ts new file mode 100644 index 0000000000..bdeed98168 --- /dev/null +++ b/packages/resend/src/operations/listEmailAttachments.ts @@ -0,0 +1,55 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const ListEmailAttachmentsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/emails/{email_id}/attachments" })); +export type ListEmailAttachmentsInput = typeof ListEmailAttachmentsInput.Type; + +// Output Schema +export const ListEmailAttachmentsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + download_url: Schema.optional(Schema.String), + expires_at: Schema.optional(Schema.String), + size: Schema.optional(Schema.Number), + }), + ), + ), + }); +export type ListEmailAttachmentsOutput = typeof ListEmailAttachmentsOutput.Type; + +// The operation +/** + * Retrieve a list of attachments for a sent email + * + * @param email_id - The ID of the email. + * @param limit - Maximum number of attachments to return. + * @param after - Pagination cursor to fetch results after this attachment ID. Cannot be used with 'before'. + * @param before - Pagination cursor to fetch results before this attachment ID. Cannot be used with 'after'. + */ +export const listEmailAttachments = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: ListEmailAttachmentsInput, + outputSchema: ListEmailAttachmentsOutput, + errors: [NotFound] as const, + }), +); diff --git a/packages/resend/src/operations/listEmailReceiving.ts b/packages/resend/src/operations/listEmailReceiving.ts new file mode 100644 index 0000000000..496d5fba49 --- /dev/null +++ b/packages/resend/src/operations/listEmailReceiving.ts @@ -0,0 +1,62 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListEmailReceivingInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe(T.Http({ method: "GET", path: "/emails/receiving" })); +export type ListEmailReceivingInput = typeof ListEmailReceivingInput.Type; + +// Output Schema +export const ListEmailReceivingOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + to: Schema.optional(Schema.Array(Schema.String)), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.NullOr(Schema.String)), + message_id: Schema.optional(Schema.String), + bcc: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + cc: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + reply_to: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + created_at: Schema.optional(Schema.String), + attachments: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + size: Schema.optional(Schema.Number), + }), + ), + ), + }), + ), + ), + }); +export type ListEmailReceivingOutput = typeof ListEmailReceivingOutput.Type; + +// The operation +/** + * Retrieve a list of received emails + * + * @param limit - Maximum number of received emails to return. + * @param after - Pagination cursor to fetch results after this email ID. Cannot be used with 'before'. + * @param before - Pagination cursor to fetch results before this email ID. Cannot be used with 'after'. + */ +export const listEmailReceiving = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListEmailReceivingInput, + outputSchema: ListEmailReceivingOutput, +})); diff --git a/packages/resend/src/operations/listEmailReceivingAttachments.ts b/packages/resend/src/operations/listEmailReceivingAttachments.ts new file mode 100644 index 0000000000..8e2aec1fe0 --- /dev/null +++ b/packages/resend/src/operations/listEmailReceivingAttachments.ts @@ -0,0 +1,58 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const ListEmailReceivingAttachmentsInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), + }).pipe( + T.Http({ method: "GET", path: "/emails/receiving/{email_id}/attachments" }), + ); +export type ListEmailReceivingAttachmentsInput = + typeof ListEmailReceivingAttachmentsInput.Type; + +// Output Schema +export const ListEmailReceivingAttachmentsOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + filename: Schema.optional(Schema.NullOr(Schema.String)), + content_type: Schema.optional(Schema.String), + content_id: Schema.optional(Schema.String), + content_disposition: Schema.optional( + Schema.NullOr(Schema.Literals(["inline", "attachment"])), + ), + download_url: Schema.optional(Schema.String), + expires_at: Schema.optional(Schema.String), + size: Schema.optional(Schema.Number), + }), + ), + ), + }); +export type ListEmailReceivingAttachmentsOutput = + typeof ListEmailReceivingAttachmentsOutput.Type; + +// The operation +/** + * Retrieve a list of attachments for a received email + * + * @param email_id - The ID of the received email. + * @param limit - Maximum number of attachments to return. + * @param after - Pagination cursor to fetch results after this attachment ID. Cannot be used with 'before'. + * @param before - Pagination cursor to fetch results before this attachment ID. Cannot be used with 'after'. + */ +export const listEmailReceivingAttachments = + /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListEmailReceivingAttachmentsInput, + outputSchema: ListEmailReceivingAttachmentsOutput, + errors: [NotFound] as const, + })); diff --git a/packages/resend/src/operations/listEmails.ts b/packages/resend/src/operations/listEmails.ts new file mode 100644 index 0000000000..04b58fcf11 --- /dev/null +++ b/packages/resend/src/operations/listEmails.ts @@ -0,0 +1,64 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListEmailsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/emails" })); +export type ListEmailsInput = typeof ListEmailsInput.Type; + +// Output Schema +export const ListEmailsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + to: Schema.optional(Schema.Array(Schema.String)), + from: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + bcc: Schema.optional(Schema.Array(Schema.String)), + cc: Schema.optional(Schema.Array(Schema.String)), + reply_to: Schema.optional(Schema.Array(Schema.String)), + last_event: Schema.optional( + Schema.Literals([ + "bounced", + "canceled", + "clicked", + "complained", + "delivered", + "delivery_delayed", + "failed", + "opened", + "queued", + "scheduled", + "sent", + "suppressed", + ]), + ), + }), + ), + ), +}); +export type ListEmailsOutput = typeof ListEmailsOutput.Type; + +// The operation +/** + * Retrieve a list of emails + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listEmails = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListEmailsInput, + outputSchema: ListEmailsOutput, +})); diff --git a/packages/resend/src/operations/listEvents.ts b/packages/resend/src/operations/listEvents.ts new file mode 100644 index 0000000000..010dc72eb5 --- /dev/null +++ b/packages/resend/src/operations/listEvents.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListEventsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/events" })); +export type ListEventsInput = typeof ListEventsInput.Type; + +// Output Schema +export const ListEventsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + schema: Schema.optional(Schema.NullOr(Schema.Unknown)), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.NullOr(Schema.String)), + }), + ), + ), +}); +export type ListEventsOutput = typeof ListEventsOutput.Type; + +// The operation +/** + * Retrieve a list of events + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listEvents = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListEventsInput, + outputSchema: ListEventsOutput, +})); diff --git a/packages/resend/src/operations/listLogs.ts b/packages/resend/src/operations/listLogs.ts new file mode 100644 index 0000000000..cbda9e1644 --- /dev/null +++ b/packages/resend/src/operations/listLogs.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListLogsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/logs" })); +export type ListLogsInput = typeof ListLogsInput.Type; + +// Output Schema +export const ListLogsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + endpoint: Schema.optional(Schema.String), + method: Schema.optional( + Schema.Literals(["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"]), + ), + response_status: Schema.optional(Schema.Number), + user_agent: Schema.optional(Schema.NullOr(Schema.String)), + }), + ), + ), +}); +export type ListLogsOutput = typeof ListLogsOutput.Type; + +// The operation +/** + * Retrieve a list of logs + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listLogs = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListLogsInput, + outputSchema: ListLogsOutput, +})); diff --git a/packages/resend/src/operations/listSegments.ts b/packages/resend/src/operations/listSegments.ts new file mode 100644 index 0000000000..b24c187b10 --- /dev/null +++ b/packages/resend/src/operations/listSegments.ts @@ -0,0 +1,41 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListSegmentsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/segments" })); +export type ListSegmentsInput = typeof ListSegmentsInput.Type; + +// Output Schema +export const ListSegmentsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + audience_id: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + }), + ), + ), +}); +export type ListSegmentsOutput = typeof ListSegmentsOutput.Type; + +// The operation +/** + * Retrieve a list of segments + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listSegments = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListSegmentsInput, + outputSchema: ListSegmentsOutput, +})); diff --git a/packages/resend/src/operations/listTemplates.ts b/packages/resend/src/operations/listTemplates.ts new file mode 100644 index 0000000000..ec7b624c68 --- /dev/null +++ b/packages/resend/src/operations/listTemplates.ts @@ -0,0 +1,44 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListTemplatesInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/templates" })); +export type ListTemplatesInput = typeof ListTemplatesInput.Type; + +// Output Schema +export const ListTemplatesOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + status: Schema.optional(Schema.Literals(["draft", "published"])), + published_at: Schema.optional(Schema.NullOr(Schema.String)), + created_at: Schema.optional(Schema.String), + updated_at: Schema.optional(Schema.String), + alias: Schema.optional(Schema.String), + }), + ), + ), + has_more: Schema.optional(Schema.Boolean), +}); +export type ListTemplatesOutput = typeof ListTemplatesOutput.Type; + +// The operation +/** + * Retrieve a list of templates + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listTemplates = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListTemplatesInput, + outputSchema: ListTemplatesOutput, +})); diff --git a/packages/resend/src/operations/listTopics.ts b/packages/resend/src/operations/listTopics.ts new file mode 100644 index 0000000000..066d48775b --- /dev/null +++ b/packages/resend/src/operations/listTopics.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListTopicsInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/topics" })); +export type ListTopicsInput = typeof ListTopicsInput.Type; + +// Output Schema +export const ListTopicsOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + default_subscription: Schema.optional( + Schema.Literals(["opt_in", "opt_out"]), + ), + visibility: Schema.optional(Schema.Literals(["public", "private"])), + created_at: Schema.optional(Schema.String), + }), + ), + ), +}); +export type ListTopicsOutput = typeof ListTopicsOutput.Type; + +// The operation +/** + * Retrieve a list of topics + * + * @param limit - Number of items to return. + * @param after - Return items after this cursor. + * @param before - Return items before this cursor. + */ +export const listTopics = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListTopicsInput, + outputSchema: ListTopicsOutput, +})); diff --git a/packages/resend/src/operations/listWebhooks.ts b/packages/resend/src/operations/listWebhooks.ts new file mode 100644 index 0000000000..81fc0126de --- /dev/null +++ b/packages/resend/src/operations/listWebhooks.ts @@ -0,0 +1,42 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const ListWebhooksInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + limit: Schema.optional(Schema.Number), + after: Schema.optional(Schema.String), + before: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "GET", path: "/webhooks" })); +export type ListWebhooksInput = typeof ListWebhooksInput.Type; + +// Output Schema +export const ListWebhooksOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + has_more: Schema.optional(Schema.Boolean), + data: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + endpoint: Schema.optional(Schema.String), + events: Schema.optional(Schema.NullOr(Schema.Array(Schema.String))), + status: Schema.optional(Schema.String), + created_at: Schema.optional(Schema.String), + }), + ), + ), +}); +export type ListWebhooksOutput = typeof ListWebhooksOutput.Type; + +// The operation +/** + * Retrieve a list of webhooks + * + * @param limit - Maximum number of webhooks to return. + * @param after - Pagination cursor to fetch results after this webhook ID. Cannot be used with 'before'. + * @param before - Pagination cursor to fetch results before this webhook ID. Cannot be used with 'after'. + */ +export const listWebhooks = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: ListWebhooksInput, + outputSchema: ListWebhooksOutput, +})); diff --git a/packages/resend/src/operations/publishTemplate.ts b/packages/resend/src/operations/publishTemplate.ts new file mode 100644 index 0000000000..69e43ef86e --- /dev/null +++ b/packages/resend/src/operations/publishTemplate.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const PublishTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "POST", path: "/templates/{id}/publish" })); +export type PublishTemplateInput = typeof PublishTemplateInput.Type; + +// Output Schema +export const PublishTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type PublishTemplateOutput = typeof PublishTemplateOutput.Type; + +// The operation +/** + * Publish a template + * + * @param id - The Template ID or alias. + */ +export const publishTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: PublishTemplateInput, + outputSchema: PublishTemplateOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/sendBroadcast.ts b/packages/resend/src/operations/sendBroadcast.ts new file mode 100644 index 0000000000..b8512be574 --- /dev/null +++ b/packages/resend/src/operations/sendBroadcast.ts @@ -0,0 +1,29 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const SendBroadcastInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + scheduled_at: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "POST", path: "/broadcasts/{id}/send" })); +export type SendBroadcastInput = typeof SendBroadcastInput.Type; + +// Output Schema +export const SendBroadcastOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), +}); +export type SendBroadcastOutput = typeof SendBroadcastOutput.Type; + +// The operation +/** + * Send or schedule a broadcast + * + * @param id - The Broadcast ID. + */ +export const sendBroadcast = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SendBroadcastInput, + outputSchema: SendBroadcastOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/sendEvent.ts b/packages/resend/src/operations/sendEvent.ts new file mode 100644 index 0000000000..fe2905e5e4 --- /dev/null +++ b/packages/resend/src/operations/sendEvent.ts @@ -0,0 +1,27 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const SendEventInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + event: Schema.String, + contact_id: Schema.optional(Schema.String), + email: Schema.optional(Schema.String), + payload: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}).pipe(T.Http({ method: "POST", path: "/events/send" })); +export type SendEventInput = typeof SendEventInput.Type; + +// Output Schema +export const SendEventOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Void; +export type SendEventOutput = typeof SendEventOutput.Type; + +// The operation +/** + * Send an event + */ +export const sendEvent = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: SendEventInput, + outputSchema: SendEventOutput, + errors: [UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/stopAutomation.ts b/packages/resend/src/operations/stopAutomation.ts new file mode 100644 index 0000000000..035efd246a --- /dev/null +++ b/packages/resend/src/operations/stopAutomation.ts @@ -0,0 +1,30 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const StopAutomationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "POST", path: "/automations/{automation_id}/stop" })); +export type StopAutomationInput = typeof StopAutomationInput.Type; + +// Output Schema +export const StopAutomationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + status: Schema.optional(Schema.String), +}); +export type StopAutomationOutput = typeof StopAutomationOutput.Type; + +// The operation +/** + * Stop an automation + * + * @param automation_id - The ID of the automation. + */ +export const stopAutomation = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: StopAutomationInput, + outputSchema: StopAutomationOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/operations/updateAutomation.ts b/packages/resend/src/operations/updateAutomation.ts new file mode 100644 index 0000000000..f3b30c78a0 --- /dev/null +++ b/packages/resend/src/operations/updateAutomation.ts @@ -0,0 +1,68 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateAutomationInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + automation_id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), + steps: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.String, + type: Schema.Literals([ + "trigger", + "send_email", + "delay", + "wait_for_event", + "condition", + "contact_update", + "contact_delete", + "add_to_segment", + ]), + config: Schema.Unknown, + }), + ), + ), + connections: Schema.optional( + Schema.Array( + Schema.Struct({ + from: Schema.String, + to: Schema.String, + type: Schema.optional( + Schema.Literals([ + "default", + "condition_met", + "condition_not_met", + "timeout", + "event_received", + ]), + ), + }), + ), + ), +}).pipe(T.Http({ method: "PATCH", path: "/automations/{automation_id}" })); +export type UpdateAutomationInput = typeof UpdateAutomationInput.Type; + +// Output Schema +export const UpdateAutomationOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct( + { + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), + }, +); +export type UpdateAutomationOutput = typeof UpdateAutomationOutput.Type; + +// The operation +/** + * Update an automation + * + * @param automation_id - The ID of the automation. + */ +export const updateAutomation = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateAutomationInput, + outputSchema: UpdateAutomationOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateBroadcast.ts b/packages/resend/src/operations/updateBroadcast.ts new file mode 100644 index 0000000000..d9503cd72f --- /dev/null +++ b/packages/resend/src/operations/updateBroadcast.ts @@ -0,0 +1,39 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateBroadcastInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + audience_id: Schema.optional(Schema.String), + segment_id: Schema.optional(Schema.String), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + reply_to: Schema.optional(Schema.Array(Schema.String)), + preview_text: Schema.optional(Schema.String), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + topic_id: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "PATCH", path: "/broadcasts/{id}" })); +export type UpdateBroadcastInput = typeof UpdateBroadcastInput.Type; + +// Output Schema +export const UpdateBroadcastOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type UpdateBroadcastOutput = typeof UpdateBroadcastOutput.Type; + +// The operation +/** + * Update an existing broadcast + * + * @param id - The Broadcast ID. + */ +export const updateBroadcast = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateBroadcastInput, + outputSchema: UpdateBroadcastOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateContact.ts b/packages/resend/src/operations/updateContact.ts new file mode 100644 index 0000000000..fd22e07450 --- /dev/null +++ b/packages/resend/src/operations/updateContact.ts @@ -0,0 +1,34 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateContactInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + email: Schema.optional(Schema.String), + first_name: Schema.optional(Schema.String), + last_name: Schema.optional(Schema.String), + unsubscribed: Schema.optional(Schema.Boolean), + properties: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)), +}).pipe(T.Http({ method: "PATCH", path: "/contacts/{id}" })); +export type UpdateContactInput = typeof UpdateContactInput.Type; + +// Output Schema +export const UpdateContactOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type UpdateContactOutput = typeof UpdateContactOutput.Type; + +// The operation +/** + * Update a single contact by ID or email + * + * @param id - The Contact ID or email address. + */ +export const updateContact = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateContactInput, + outputSchema: UpdateContactOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateContactProperty.ts b/packages/resend/src/operations/updateContactProperty.ts new file mode 100644 index 0000000000..68f167324a --- /dev/null +++ b/packages/resend/src/operations/updateContactProperty.ts @@ -0,0 +1,35 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateContactPropertyInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + fallback_value: Schema.optional(Schema.Unknown), + }).pipe(T.Http({ method: "PATCH", path: "/contact-properties/{id}" })); +export type UpdateContactPropertyInput = typeof UpdateContactPropertyInput.Type; + +// Output Schema +export const UpdateContactPropertyOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), + }); +export type UpdateContactPropertyOutput = + typeof UpdateContactPropertyOutput.Type; + +// The operation +/** + * Update an existing contact property + * + * @param id - The Contact Property ID. + */ +export const updateContactProperty = /*@__PURE__*/ /*#__PURE__*/ API.make( + () => ({ + inputSchema: UpdateContactPropertyInput, + outputSchema: UpdateContactPropertyOutput, + errors: [NotFound, UnprocessableEntity] as const, + }), +); diff --git a/packages/resend/src/operations/updateContactTopic.ts b/packages/resend/src/operations/updateContactTopic.ts new file mode 100644 index 0000000000..042b95226f --- /dev/null +++ b/packages/resend/src/operations/updateContactTopic.ts @@ -0,0 +1,45 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateContactTopicInput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + contact_id: Schema.String.pipe(T.PathParam()), + topics: Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + subscription: Schema.optional(Schema.Literals(["opt_in", "opt_out"])), + }), + ), + }).pipe(T.Http({ method: "PATCH", path: "/contacts/{contact_id}/topics" })); +export type UpdateContactTopicInput = typeof UpdateContactTopicInput.Type; + +// Output Schema +export const UpdateContactTopicOutput = + /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + contact_id: Schema.optional(Schema.String), + topics: Schema.optional( + Schema.Array( + Schema.Struct({ + id: Schema.optional(Schema.String), + subscription: Schema.optional(Schema.Literals(["opt_in", "opt_out"])), + }), + ), + ), + }); +export type UpdateContactTopicOutput = typeof UpdateContactTopicOutput.Type; + +// The operation +/** + * Update topics for a contact + * + * @param contact_id - The Contact ID or email address. + */ +export const updateContactTopic = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateContactTopicInput, + outputSchema: UpdateContactTopicOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateDomain.ts b/packages/resend/src/operations/updateDomain.ts new file mode 100644 index 0000000000..fa5513d528 --- /dev/null +++ b/packages/resend/src/operations/updateDomain.ts @@ -0,0 +1,39 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateDomainInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + domain_id: Schema.String.pipe(T.PathParam()), + open_tracking: Schema.optional(Schema.Boolean), + click_tracking: Schema.optional(Schema.Boolean), + tls: Schema.optional(Schema.String), + capabilities: Schema.optional( + Schema.Struct({ + sending: Schema.optional(Schema.Literals(["enabled", "disabled"])), + receiving: Schema.optional(Schema.Literals(["enabled", "disabled"])), + }), + ), + tracking_subdomain: Schema.optional(Schema.String), +}).pipe(T.Http({ method: "PATCH", path: "/domains/{domain_id}" })); +export type UpdateDomainInput = typeof UpdateDomainInput.Type; + +// Output Schema +export const UpdateDomainOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type UpdateDomainOutput = typeof UpdateDomainOutput.Type; + +// The operation +/** + * Update an existing domain + * + * @param domain_id - The ID of the domain. + */ +export const updateDomain = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateDomainInput, + outputSchema: UpdateDomainOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateEmail.ts b/packages/resend/src/operations/updateEmail.ts new file mode 100644 index 0000000000..57f0d63d67 --- /dev/null +++ b/packages/resend/src/operations/updateEmail.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { Forbidden, NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateEmailInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + email_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "PATCH", path: "/emails/{email_id}" })); +export type UpdateEmailInput = typeof UpdateEmailInput.Type; + +// Output Schema +export const UpdateEmailOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + scheduled_at: Schema.optional(Schema.String), +}); +export type UpdateEmailOutput = typeof UpdateEmailOutput.Type; + +// The operation +/** + * Update a single email + * + * @param email_id - The ID of the email. + */ +export const updateEmail = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateEmailInput, + outputSchema: UpdateEmailOutput, + errors: [Forbidden, NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateEvent.ts b/packages/resend/src/operations/updateEvent.ts new file mode 100644 index 0000000000..57279ec2d5 --- /dev/null +++ b/packages/resend/src/operations/updateEvent.ts @@ -0,0 +1,28 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; + +// Input Schema +export const UpdateEventInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + identifier: Schema.String.pipe(T.PathParam()), + schema: Schema.NullOr(Schema.Unknown), +}).pipe(T.Http({ method: "PATCH", path: "/events/{identifier}" })); +export type UpdateEventInput = typeof UpdateEventInput.Type; + +// Output Schema +export const UpdateEventOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type UpdateEventOutput = typeof UpdateEventOutput.Type; + +// The operation +/** + * Update an event + * + * @param identifier - The event ID (UUID) or event name. + */ +export const updateEvent = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateEventInput, + outputSchema: UpdateEventOutput, +})); diff --git a/packages/resend/src/operations/updateTemplate.ts b/packages/resend/src/operations/updateTemplate.ts new file mode 100644 index 0000000000..0fab136ef6 --- /dev/null +++ b/packages/resend/src/operations/updateTemplate.ts @@ -0,0 +1,51 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateTemplateInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + alias: Schema.optional(Schema.String), + from: Schema.optional(Schema.String), + subject: Schema.optional(Schema.String), + reply_to: Schema.optional(Schema.Array(Schema.String)), + html: Schema.optional(Schema.String), + text: Schema.optional(Schema.String), + variables: Schema.optional( + Schema.Array( + Schema.Struct({ + key: Schema.String, + type: Schema.Literals([ + "string", + "number", + "boolean", + "object", + "list", + ]), + fallback_value: Schema.optional(Schema.Unknown), + }), + ), + ), +}).pipe(T.Http({ method: "PATCH", path: "/templates/{id}" })); +export type UpdateTemplateInput = typeof UpdateTemplateInput.Type; + +// Output Schema +export const UpdateTemplateOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type UpdateTemplateOutput = typeof UpdateTemplateOutput.Type; + +// The operation +/** + * Update an existing template + * + * @param id - The Template ID or alias. + */ +export const updateTemplate = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateTemplateInput, + outputSchema: UpdateTemplateOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateTopic.ts b/packages/resend/src/operations/updateTopic.ts new file mode 100644 index 0000000000..09f9201bf1 --- /dev/null +++ b/packages/resend/src/operations/updateTopic.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateTopicInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.String.pipe(T.PathParam()), + name: Schema.optional(Schema.String), + description: Schema.optional(Schema.String), + visibility: Schema.optional(Schema.Literals(["public", "private"])), +}).pipe(T.Http({ method: "PATCH", path: "/topics/{id}" })); +export type UpdateTopicInput = typeof UpdateTopicInput.Type; + +// Output Schema +export const UpdateTopicOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + id: Schema.optional(Schema.String), + object: Schema.optional(Schema.String), +}); +export type UpdateTopicOutput = typeof UpdateTopicOutput.Type; + +// The operation +/** + * Update an existing topic + * + * @param id - The Topic ID. + */ +export const updateTopic = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateTopicInput, + outputSchema: UpdateTopicOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/updateWebhook.ts b/packages/resend/src/operations/updateWebhook.ts new file mode 100644 index 0000000000..7ca017a328 --- /dev/null +++ b/packages/resend/src/operations/updateWebhook.ts @@ -0,0 +1,32 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound, UnprocessableEntity } from "../errors.ts"; + +// Input Schema +export const UpdateWebhookInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + webhook_id: Schema.String.pipe(T.PathParam()), + endpoint: Schema.optional(Schema.String), + events: Schema.optional(Schema.Array(Schema.String)), + status: Schema.optional(Schema.Literals(["enabled", "disabled"])), +}).pipe(T.Http({ method: "PATCH", path: "/webhooks/{webhook_id}" })); +export type UpdateWebhookInput = typeof UpdateWebhookInput.Type; + +// Output Schema +export const UpdateWebhookOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type UpdateWebhookOutput = typeof UpdateWebhookOutput.Type; + +// The operation +/** + * Update an existing webhook + * + * @param webhook_id - The Webhook ID. + */ +export const updateWebhook = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: UpdateWebhookInput, + outputSchema: UpdateWebhookOutput, + errors: [NotFound, UnprocessableEntity] as const, +})); diff --git a/packages/resend/src/operations/verifyDomain.ts b/packages/resend/src/operations/verifyDomain.ts new file mode 100644 index 0000000000..3bea15ef24 --- /dev/null +++ b/packages/resend/src/operations/verifyDomain.ts @@ -0,0 +1,31 @@ +import * as Schema from "effect/Schema"; +import { API } from "../client.ts"; +import * as T from "../traits.ts"; +import { NotFound } from "../errors.ts"; + +// Input Schema +export const VerifyDomainInput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + domain_id: Schema.String.pipe(T.PathParam()), +}).pipe(T.Http({ method: "POST", path: "/domains/{domain_id}/verify" })); +export type VerifyDomainInput = typeof VerifyDomainInput.Type; + +// Output Schema +export const VerifyDomainOutput = /*@__PURE__*/ /*#__PURE__*/ Schema.Struct({ + object: Schema.optional(Schema.String), + id: Schema.optional(Schema.String), +}); +export type VerifyDomainOutput = typeof VerifyDomainOutput.Type; + +// The operation +/** + * Verify an existing domain + * + * Triggers verification of the domain's DNS records including DKIM, SPF, and the tracking CNAME (if a tracking subdomain is configured). + * + * @param domain_id - The ID of the domain. + */ +export const verifyDomain = /*@__PURE__*/ /*#__PURE__*/ API.make(() => ({ + inputSchema: VerifyDomainInput, + outputSchema: VerifyDomainOutput, + errors: [NotFound] as const, +})); diff --git a/packages/resend/src/retry.ts b/packages/resend/src/retry.ts new file mode 100644 index 0000000000..bebc00686a --- /dev/null +++ b/packages/resend/src/retry.ts @@ -0,0 +1,47 @@ +/** + * Resend retry configuration. + * + * Defines the per-SDK `Retry` Context.Service tag that + * `packages/resend/src/client.ts` wires into `makeAPI`. Callers can install a + * blanket retry policy at the layer level and have every Resend API call + * below it pick it up. + */ +import * as Context from "effect/Context"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import { + type Policy, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +export { + type Options, + type Factory, + type Policy, + makeDefault, + jittered, + capped, + throttlingOptions, + transientOptions, + throttlingFactory, + transientFactory, +} from "@distilled.cloud/core/retry"; + +/** Context tag for configuring retry behavior of Resend API calls. */ +export class Retry extends Context.Service()("ResendRetry") {} + +/** Provides a custom retry policy to every Resend API call below it. */ +export const policy = (optionsOrFactory: Policy) => + Effect.provide(Layer.succeed(Retry, optionsOrFactory)); + +/** Disables all automatic retries. */ +export const none = Effect.provide( + Layer.succeed(Retry, { while: () => false }), +); + +/** Apply the throttling retry policy (retries throttling errors indefinitely). */ +export const throttling = policy(throttlingFactory); + +/** Apply the transient retry policy (retries all transient errors indefinitely). */ +export const transient = policy(transientFactory); diff --git a/packages/resend/src/sensitive.ts b/packages/resend/src/sensitive.ts new file mode 100644 index 0000000000..2167a39b2a --- /dev/null +++ b/packages/resend/src/sensitive.ts @@ -0,0 +1,4 @@ +/** + * Re-export sensitive data schemas from sdk-core. + */ +export * from "@distilled.cloud/core/sensitive"; diff --git a/packages/resend/src/traits.ts b/packages/resend/src/traits.ts new file mode 100644 index 0000000000..cf13e396a9 --- /dev/null +++ b/packages/resend/src/traits.ts @@ -0,0 +1,4 @@ +/** + * Re-export the shared traits system from sdk-core. + */ +export * from "@distilled.cloud/core/traits"; diff --git a/packages/resend/test/batchEmail.test.ts b/packages/resend/test/batchEmail.test.ts new file mode 100644 index 0000000000..094502f53e --- /dev/null +++ b/packages/resend/test/batchEmail.test.ts @@ -0,0 +1,73 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { batchEmail } from "../src/operations/batchEmail"; +import { runEffect, testRunId } from "./setup"; + +// NOTE: BatchEmailInput is currently `Schema.Struct({})` — the spec upstream +// does not declare a request body for POST /emails/batch, so the SDK sends +// an empty JSON body regardless of what is passed. Every call below therefore +// exercises the same wire request; the assertions target the documented +// per-status error tags so the test suite covers each documented response. + +describe("batchEmail", () => { + it("accepts a batch send request", async () => { + // Tag the run in tracing context even though the body is empty. + const result = await runEffect( + batchEmail({} as never).pipe( + Effect.withSpan(`batchEmail-${testRunId}`), + ), + ); + + expect(result).toBeDefined(); + }); + + it("fails with BadRequest for a malformed request body", async () => { + // Resend documents 400 validation_error / invalid_idempotency_key for + // malformed POST /emails/batch bodies. + const error = await runEffect( + batchEmail({} as never).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); + + it("fails with Forbidden when the from domain is not verified", async () => { + // Resend documents 403 with validation_error when the from domain is not + // verified for batch sends. + const error = await runEffect( + batchEmail({} as never).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); + + it("fails with Conflict for an idempotent request collision", async () => { + // Resend documents 409 invalid_idempotent_request / concurrent_idempotent_requests + // when an Idempotency-Key is reused with a different payload. + const error = await runEffect( + batchEmail({} as never).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Conflict"); + }); + + it("fails with UnprocessableEntity when required fields are missing", async () => { + // Resend documents 422 missing_required_field / invalid_attachment / + // invalid_from_address for semantically invalid batch payloads. + const error = await runEffect( + batchEmail({} as never).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); + + it("fails with UnavailableForLegalReasons when content is flagged for security", async () => { + // Resend documents 451 security_error when a batch request is rejected + // by its security filters. + const error = await runEffect( + batchEmail({} as never).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnavailableForLegalReasons"); + }); +}); diff --git a/packages/resend/test/cancelEmail.test.ts b/packages/resend/test/cancelEmail.test.ts new file mode 100644 index 0000000000..703217d774 --- /dev/null +++ b/packages/resend/test/cancelEmail.test.ts @@ -0,0 +1,69 @@ +import { Effect, Schedule } from "effect"; +import { describe, expect, it } from "vitest"; +import { cancelEmail } from "../src/operations/cancelEmail"; +import { createEmail } from "../src/operations/createEmail"; +import { getEmail } from "../src/operations/getEmail"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +// Wait for a freshly-created email to be readable. Newly-created ids are +// briefly not indexed and return NotFound. +const waitForEmailReady = (email_id: string) => + getEmail({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + +describe("cancelEmail", () => { + it("cancels a scheduled email", async () => { + const result = await runEffect( + Effect.gen(function* () { + const scheduledAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-cancelEmail-${testRunId}`, + html: `

Scheduled email for cancel test ${testRunId}

`, + scheduled_at: scheduledAt, + }); + + expect(typeof created.id).toBe("string"); + const email_id = created.id as string; + + yield* waitForEmailReady(email_id); + + return yield* cancelEmail({ email_id }); + }), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent email id", async () => { + const error = await runEffect( + cancelEmail({ email_id: NON_EXISTENT_EMAIL_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with Forbidden for an unauthorized email id", async () => { + // Resend documents 403 invalid_api_key on POST /emails/{email_id}/cancel. + // We exercise the documented error path with a well-formed but unauthorized id. + const error = await runEffect( + cancelEmail({ + email_id: `forbidden-${testRunId}-00000000-0000-4000-8000-000000000000`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); +}); diff --git a/packages/resend/test/createApiKey.test.ts b/packages/resend/test/createApiKey.test.ts new file mode 100644 index 0000000000..0a1ae61088 --- /dev/null +++ b/packages/resend/test/createApiKey.test.ts @@ -0,0 +1,72 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createApiKey } from "../src/operations/createApiKey"; +import { deleteApiKey } from "../src/operations/deleteApiKey"; +import { runEffect, testRunId } from "./setup"; + +describe("createApiKey", () => { + it("creates an API key with default permissions", async () => { + const name = `distilled-resend-createApiKey-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createApiKey({ name }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteApiKey({ api_key_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(typeof result.token).toBe("string"); + }); + + it("creates an API key with sending_access permission", async () => { + const name = `distilled-resend-createApiKey-sending-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createApiKey({ + name, + permission: "sending_access", + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteApiKey({ api_key_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(typeof result.token).toBe("string"); + }); + + it("fails with UnprocessableEntity for an invalid domain_id", async () => { + const error = await runEffect( + createApiKey({ + name: `distilled-resend-createApiKey-bad-${testRunId}`, + permission: "sending_access", + domain_id: "00000000-0000-4000-8000-000000000000", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createAutomation.test.ts b/packages/resend/test/createAutomation.test.ts new file mode 100644 index 0000000000..d65533b534 --- /dev/null +++ b/packages/resend/test/createAutomation.test.ts @@ -0,0 +1,86 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { runEffect, testRunId } from "./setup"; + +describe("createAutomation", () => { + it("creates an automation with a trigger and contact_delete step", async () => { + const name = `distilled-resend-createAutomation-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const eventName = `distilled-event-unproc-${testRunId}`; + + const error = await runEffect( + createAutomation({ + name: "", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + ], + connections: [], + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); + + it("fails with UnprocessableEntity for no steps", async () => { + const name = `distilled-resend-createAutomation-nosteps-${testRunId}`; + + const error = await runEffect( + createAutomation({ + name, + steps: [], + connections: [], + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createBroadcast.test.ts b/packages/resend/test/createBroadcast.test.ts new file mode 100644 index 0000000000..b811019f6e --- /dev/null +++ b/packages/resend/test/createBroadcast.test.ts @@ -0,0 +1,88 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createBroadcast } from "../src/operations/createBroadcast"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; + +describe("createBroadcast", () => { + it("creates a draft broadcast targeting a created segment", async () => { + const segmentName = `distilled-resend-createBroadcast-seg-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: `distilled-resend-createBroadcast-${testRunId}`, + segment_id: segment.id, + from: SENDER, + subject: "Distilled test broadcast", + html: "

Hello from Distilled

", + send: false, + }); + broadcastId = broadcast.id; + return broadcast; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an invalid from address", async () => { + const segmentName = `distilled-resend-createBroadcast-bad-${testRunId}`; + let segmentId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + return yield* createBroadcast({ + segment_id: segment.id, + from: "not-a-valid-email", + subject: "Distilled test broadcast", + html: "

Hello

", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createContact.test.ts b/packages/resend/test/createContact.test.ts new file mode 100644 index 0000000000..ecee21a6f2 --- /dev/null +++ b/packages/resend/test/createContact.test.ts @@ -0,0 +1,81 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUDIENCE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("createContact", () => { + it("creates a contact with an email only", async () => { + const email = `distilled-resend-createContact-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("creates a contact with first/last name and properties", async () => { + const email = `distilled-resend-createContact-named-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ + email, + first_name: "Distilled", + last_name: "Tester", + unsubscribed: false, + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent audience_id", async () => { + const error = await runEffect( + createContact({ + email: `distilled-resend-createContact-noaud-${testRunId}@example.com`, + audience_id: NON_EXISTENT_AUDIENCE_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid email", async () => { + const error = await runEffect( + createContact({ email: "not-a-valid-email" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createContactImport.test.ts b/packages/resend/test/createContactImport.test.ts new file mode 100644 index 0000000000..dc3297efe9 --- /dev/null +++ b/packages/resend/test/createContactImport.test.ts @@ -0,0 +1,28 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactImport } from "../src/operations/createContactImport"; +import { runEffect, testRunId } from "./setup"; + +const MINIMAL_CSV = `email,first_name,last_name\nimport-${testRunId}-1@example.com,Alice,Tester\nimport-${testRunId}-2@example.com,Bob,Tester\n`; + +describe("createContactImport", () => { + it("creates a contact import from a CSV", async () => { + const result = await runEffect( + createContactImport({ + file: MINIMAL_CSV, + on_conflict: "skip", + }), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty file", async () => { + const error = await runEffect( + createContactImport({ file: "" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createContactProperty.test.ts b/packages/resend/test/createContactProperty.test.ts new file mode 100644 index 0000000000..4d6f31bd29 --- /dev/null +++ b/packages/resend/test/createContactProperty.test.ts @@ -0,0 +1,100 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactProperty } from "../src/operations/createContactProperty"; +import { deleteContactProperty } from "../src/operations/deleteContactProperty"; +import { runEffect, testRunId } from "./setup"; + +// Resend contact-property keys must be lowercase letters / numbers / underscores. +const propertyKey = (suffix: string): string => + `distilled_${suffix}_${testRunId}`.toLowerCase(); + +describe("createContactProperty", () => { + it("creates a string contact property", async () => { + const key = propertyKey("create"); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "string", + fallback_value: "default", + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("creates a number contact property", async () => { + const key = propertyKey("create_num"); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "number", + fallback_value: 0, + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with Conflict when creating a property with a duplicate key", async () => { + const key = propertyKey("conflict"); + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ key, type: "string" }); + createdId = created.id; + return yield* createContactProperty({ key, type: "string" }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("Conflict"); + }); + + it("fails with UnprocessableEntity for an empty key", async () => { + const error = await runEffect( + createContactProperty({ key: "", type: "string" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createContactSegment.test.ts b/packages/resend/test/createContactSegment.test.ts new file mode 100644 index 0000000000..fb1c5a6b03 --- /dev/null +++ b/packages/resend/test/createContactSegment.test.ts @@ -0,0 +1,99 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { createContactSegment } from "../src/operations/createContactSegment"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteContact } from "../src/operations/deleteContact"; +import { deleteContactSegment } from "../src/operations/deleteContactSegment"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("createContactSegment", () => { + it("adds a contact to a segment", async () => { + const email = `distilled-resend-createContactSegment-${testRunId}@example.com`; + const segmentName = `distilled-resend-createContactSegment-seg-${testRunId}`; + let contactId: string | undefined; + let segmentId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const contact = yield* createContact({ email }); + if (!contact.id) { + return yield* Effect.die("createContact did not return an id"); + } + contactId = contact.id; + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + return yield* createContactSegment({ + contact_id: contact.id, + segment_id: segment.id, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + contactId && segmentId + ? deleteContactSegment({ + contact_id: contactId, + segment_id: segmentId, + }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + contactId + ? deleteContact({ id: contactId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.contact_id).toBe(contactId); + expect(result.segment_id).toBe(segmentId); + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const segmentName = `distilled-resend-createContactSegment-noc-${testRunId}`; + let segmentId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + return yield* createContactSegment({ + contact_id: NON_EXISTENT_ID, + segment_id: segment.id, + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/createDomain.test.ts b/packages/resend/test/createDomain.test.ts new file mode 100644 index 0000000000..a2b6fb7030 --- /dev/null +++ b/packages/resend/test/createDomain.test.ts @@ -0,0 +1,84 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createDomain } from "../src/operations/createDomain"; +import { deleteDomain } from "../src/operations/deleteDomain"; +import { runEffect, testRunId } from "./setup"; + +const domainName = (suffix: string) => + `distilled-resend-${suffix}-${testRunId}.example.com`; + +describe("createDomain", () => { + it("creates a domain and returns its id", async () => { + const name = domainName("create"); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ name }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(result.name).toBe(name); + }); + + it("creates a domain with optional region and tracking flags", async () => { + const name = domainName("opts"); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ + name, + region: "us-east-1", + open_tracking: false, + click_tracking: false, + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.id).toBeDefined(); + expect(result.name).toBe(name); + }); + + it("fails with UnprocessableEntity for an invalid domain name", async () => { + // Resend documents 422 invalid_parameter for malformed domain names. + const error = await runEffect( + createDomain({ name: "not a valid domain!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); + + it("fails with Forbidden when the API key cannot create domains", async () => { + // Resend documents 403 invalid_api_key / validation_error on POST /domains + // when the API key lacks permission to create domains. We exercise the + // documented error path; the actual trigger depends on the account / key. + const error = await runEffect( + createDomain({ name: domainName("forbidden") }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); +}); diff --git a/packages/resend/test/createEmail.test.ts b/packages/resend/test/createEmail.test.ts new file mode 100644 index 0000000000..7303828be9 --- /dev/null +++ b/packages/resend/test/createEmail.test.ts @@ -0,0 +1,129 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEmail } from "../src/operations/createEmail"; +import { runEffect, testRunId } from "./setup"; + +// Resend's `resend.dev` sender is whitelisted for any account and +// `delivered@resend.dev` is a sandbox recipient that always accepts mail. +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +describe("createEmail", () => { + it("sends an email and returns an id", async () => { + const result = await runEffect( + createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-createEmail-${testRunId}`, + html: `

Test email from distilled SDK run ${testRunId}

`, + }), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with BadRequest for a malformed request body", async () => { + // `to` must be a string or array of strings — a number is malformed and + // Resend documents 400 validation_error for malformed bodies. + const error = await runEffect( + createEmail({ + from: SENDER, + to: 12345 as unknown, + subject: `distilled-resend-badrequest-${testRunId}`, + html: "

test

", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); + + it("fails with Forbidden when sending from an unverified domain", async () => { + // Resend returns 403 with validation_error when the from-address is on a + // domain that the account has not verified. + const error = await runEffect( + createEmail({ + from: `noreply@unverified-${testRunId}.distilled-test.invalid`, + to: [RECIPIENT], + subject: `distilled-resend-forbidden-${testRunId}`, + html: "

test

", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); + + it("fails with MethodNotAllowed when the method is rejected by the API", async () => { + // Resend documents 405 method_not_allowed on POST /emails. This is hard to + // trigger via the typed POST, but the error tuple includes it so we exercise + // the path and assert the tag. + const error = await runEffect( + createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-405-${testRunId}`, + html: "

test

", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("MethodNotAllowed"); + }); + + it("fails with Conflict when an idempotent request collides", async () => { + // Resend returns 409 invalid_idempotent_request when the same Idempotency-Key + // is reused with a different payload. We exercise the conflict path by + // sending two near-identical sends back-to-back and asserting the second + // raises Conflict. + const subject = `distilled-resend-conflict-${testRunId}`; + const error = await runEffect( + Effect.gen(function* () { + yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject, + html: "

first

", + }); + return yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject, + html: "

second

", + }); + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Conflict"); + }); + + it("fails with UnprocessableEntity for an invalid scheduled_at value", async () => { + // Resend returns 422 invalid_parameter when scheduled_at is not a valid + // ISO-8601 / natural-language timestamp. + const error = await runEffect( + createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-422-${testRunId}`, + html: "

test

", + scheduled_at: "not-a-real-iso-date", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); + + it("fails with UnavailableForLegalReasons when content is flagged for security", async () => { + // Resend returns 451 security_error when its security filters reject a + // request. The exact triggers are not public; this exercises the documented + // error path and asserts the typed tag. + const error = await runEffect( + createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-451-${testRunId}`, + html: "

test

", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnavailableForLegalReasons"); + }); +}); diff --git a/packages/resend/test/createEvent.test.ts b/packages/resend/test/createEvent.test.ts new file mode 100644 index 0000000000..79d0a38173 --- /dev/null +++ b/packages/resend/test/createEvent.test.ts @@ -0,0 +1,73 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEvent } from "../src/operations/createEvent"; +import { deleteEvent } from "../src/operations/deleteEvent"; +import { runEffect, testRunId } from "./setup"; + +describe("createEvent", () => { + it("creates an event with a name", async () => { + const name = `distilled_resend_createEvent_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ name }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("creates an event with a JSON schema", async () => { + const name = `distilled_resend_createEvent_schema_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ + name, + schema: { + type: "object", + properties: { + user_id: { type: "string" }, + plan: { type: "string" }, + }, + required: ["user_id"], + }, + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const error = await runEffect( + createEvent({ name: "" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createSegment.test.ts b/packages/resend/test/createSegment.test.ts new file mode 100644 index 0000000000..c5310951be --- /dev/null +++ b/packages/resend/test/createSegment.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +describe("createSegment", () => { + it("creates a segment with a name", async () => { + const name = `distilled-resend-createSegment-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createSegment({ name }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteSegment({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const error = await runEffect( + createSegment({ name: "" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createTemplate.test.ts b/packages/resend/test/createTemplate.test.ts new file mode 100644 index 0000000000..9cd67161bb --- /dev/null +++ b/packages/resend/test/createTemplate.test.ts @@ -0,0 +1,80 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { runEffect, testRunId } from "./setup"; + +describe("createTemplate", () => { + it("creates a template with minimum required fields", async () => { + const name = `distilled-resend-createTemplate-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + html: "

Hello {{name}}

", + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("creates a template with variables and metadata", async () => { + const name = `distilled-resend-createTemplate-vars-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + alias: `distilled-resend-alias-${testRunId}`, + from: "Distilled Test ", + subject: "Hello {{name}}", + html: "

Hello {{name}}, you have {{count}} messages.

", + text: "Hello {{name}}, you have {{count}} messages.", + variables: [ + { key: "name", type: "string", fallback_value: "friend" }, + { key: "count", type: "number", fallback_value: 0 }, + ], + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const error = await runEffect( + createTemplate({ + name: "", + html: "

Hello

", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createTopic.test.ts b/packages/resend/test/createTopic.test.ts new file mode 100644 index 0000000000..770f8e787a --- /dev/null +++ b/packages/resend/test/createTopic.test.ts @@ -0,0 +1,73 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTopic } from "../src/operations/createTopic"; +import { deleteTopic } from "../src/operations/deleteTopic"; +import { runEffect, testRunId } from "./setup"; + +describe("createTopic", () => { + it("creates a topic with the minimum required fields", async () => { + const name = `distilled-resend-createTopic-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name, + default_subscription: "opt_in", + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTopic({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("creates a topic with description and visibility", async () => { + const name = `distilled-resend-createTopic-full-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name, + default_subscription: "opt_out", + description: "Distilled SDK integration test topic", + visibility: "private", + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTopic({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const error = await runEffect( + createTopic({ name: "", default_subscription: "opt_in" }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/createWebhook.test.ts b/packages/resend/test/createWebhook.test.ts new file mode 100644 index 0000000000..ea02fdcc00 --- /dev/null +++ b/packages/resend/test/createWebhook.test.ts @@ -0,0 +1,73 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook"; +import { deleteWebhook } from "../src/operations/deleteWebhook"; +import { runEffect, testRunId } from "./setup"; + +describe("createWebhook", () => { + it("creates a webhook with a single event", async () => { + const endpoint = `https://example.com/distilled-resend-createWebhook-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint, + events: ["email.sent"], + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteWebhook({ webhook_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(result.signing_secret).toBeDefined(); + }); + + it("creates a webhook with multiple events", async () => { + const endpoint = `https://example.com/distilled-resend-createWebhook-multi-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint, + events: ["email.sent", "email.delivered", "email.bounced"], + }); + createdId = created.id; + return created; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteWebhook({ webhook_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with UnprocessableEntity for an invalid endpoint URL", async () => { + const error = await runEffect( + createWebhook({ + endpoint: "not-a-valid-url", + events: ["email.sent"], + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/deleteApiKey.test.ts b/packages/resend/test/deleteApiKey.test.ts new file mode 100644 index 0000000000..a3d7070a60 --- /dev/null +++ b/packages/resend/test/deleteApiKey.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createApiKey } from "../src/operations/createApiKey"; +import { deleteApiKey } from "../src/operations/deleteApiKey"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_API_KEY_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteApiKey", () => { + it("deletes an API key created in the test", async () => { + const name = `distilled-resend-deleteApiKey-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createApiKey({ name }); + if (!created.id) { + return yield* Effect.die("createApiKey did not return an id"); + } + return yield* deleteApiKey({ api_key_id: created.id }); + }), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent API key id", async () => { + const error = await runEffect( + deleteApiKey({ api_key_id: NON_EXISTENT_API_KEY_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteAutomation.test.ts b/packages/resend/test/deleteAutomation.test.ts new file mode 100644 index 0000000000..b126335e01 --- /dev/null +++ b/packages/resend/test/deleteAutomation.test.ts @@ -0,0 +1,57 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteAutomation", () => { + it("deletes an automation created in the test", async () => { + const name = `distilled-resend-deleteAutomation-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + return yield* deleteAutomation({ automation_id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + deleteAutomation({ automation_id: NON_EXISTENT_AUTOMATION_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteBroadcast.test.ts b/packages/resend/test/deleteBroadcast.test.ts new file mode 100644 index 0000000000..dab1dab056 --- /dev/null +++ b/packages/resend/test/deleteBroadcast.test.ts @@ -0,0 +1,60 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createBroadcast } from "../src/operations/createBroadcast"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const NON_EXISTENT_BROADCAST_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteBroadcast", () => { + it("deletes a draft broadcast created in the test", async () => { + const segmentName = `distilled-resend-deleteBroadcast-seg-${testRunId}`; + let segmentId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: `distilled-resend-deleteBroadcast-${testRunId}`, + segment_id: segment.id, + from: SENDER, + subject: "Distilled test broadcast", + html: "

Hello

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + return yield* deleteBroadcast({ id: broadcast.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent broadcast id", async () => { + const error = await runEffect( + deleteBroadcast({ id: NON_EXISTENT_BROADCAST_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteContact.test.ts b/packages/resend/test/deleteContact.test.ts new file mode 100644 index 0000000000..9a8ea124d5 --- /dev/null +++ b/packages/resend/test/deleteContact.test.ts @@ -0,0 +1,36 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteContact", () => { + it("deletes a contact created in the test", async () => { + const email = `distilled-resend-deleteContact-${testRunId}@example.com`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + return yield* deleteContact({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const error = await runEffect( + deleteContact({ id: NON_EXISTENT_CONTACT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteContactProperty.test.ts b/packages/resend/test/deleteContactProperty.test.ts new file mode 100644 index 0000000000..d3d50afa41 --- /dev/null +++ b/packages/resend/test/deleteContactProperty.test.ts @@ -0,0 +1,41 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactProperty } from "../src/operations/createContactProperty"; +import { deleteContactProperty } from "../src/operations/deleteContactProperty"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_PROPERTY_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteContactProperty", () => { + it("deletes a contact property created in the test", async () => { + const key = `distilled_delete_${testRunId}`.toLowerCase(); + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "string", + }); + if (!created.id) { + return yield* Effect.die( + "createContactProperty did not return an id", + ); + } + return yield* deleteContactProperty({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent contact property id", async () => { + const error = await runEffect( + deleteContactProperty({ id: NON_EXISTENT_PROPERTY_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteContactSegment.test.ts b/packages/resend/test/deleteContactSegment.test.ts new file mode 100644 index 0000000000..f2e6be7a2e --- /dev/null +++ b/packages/resend/test/deleteContactSegment.test.ts @@ -0,0 +1,94 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { createContactSegment } from "../src/operations/createContactSegment"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteContact } from "../src/operations/deleteContact"; +import { deleteContactSegment } from "../src/operations/deleteContactSegment"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteContactSegment", () => { + it("removes a contact from a segment", async () => { + const email = `distilled-resend-deleteContactSegment-${testRunId}@example.com`; + const segmentName = `distilled-resend-deleteContactSegment-seg-${testRunId}`; + let contactId: string | undefined; + let segmentId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const contact = yield* createContact({ email }); + if (!contact.id) { + return yield* Effect.die("createContact did not return an id"); + } + contactId = contact.id; + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + yield* createContactSegment({ + contact_id: contact.id, + segment_id: segment.id, + }); + return yield* deleteContactSegment({ + contact_id: contact.id, + segment_id: segment.id, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + contactId + ? deleteContact({ id: contactId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const segmentName = `distilled-resend-deleteContactSegment-noc-${testRunId}`; + let segmentId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + return yield* deleteContactSegment({ + contact_id: NON_EXISTENT_ID, + segment_id: segment.id, + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteDomain.test.ts b/packages/resend/test/deleteDomain.test.ts new file mode 100644 index 0000000000..b86370a527 --- /dev/null +++ b/packages/resend/test/deleteDomain.test.ts @@ -0,0 +1,45 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createDomain } from "../src/operations/createDomain"; +import { deleteDomain } from "../src/operations/deleteDomain"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_DOMAIN_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteDomain", () => { + it("deletes a domain", async () => { + const name = `distilled-resend-deleteDomain-${testRunId}.example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ name }); + createdId = created.id; + expect(typeof createdId).toBe("string"); + return yield* deleteDomain({ domain_id: createdId as string }); + }).pipe( + Effect.ensuring( + // Defensive cleanup if the in-body delete never ran (e.g. the create + // succeeded but an earlier assertion threw). The expected case is + // that the domain has already been removed and this is a no-op. + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.deleted).toBe(true); + }); + + it("fails with NotFound for a non-existent domain id", async () => { + const error = await runEffect( + deleteDomain({ domain_id: NON_EXISTENT_DOMAIN_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteEvent.test.ts b/packages/resend/test/deleteEvent.test.ts new file mode 100644 index 0000000000..c8072f1476 --- /dev/null +++ b/packages/resend/test/deleteEvent.test.ts @@ -0,0 +1,49 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEvent } from "../src/operations/createEvent"; +import { deleteEvent } from "../src/operations/deleteEvent"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_EVENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteEvent", () => { + it("deletes an event created in the test", async () => { + const name = `distilled_resend_deleteEvent_${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ name }); + if (!created.id) { + return yield* Effect.die("createEvent did not return an id"); + } + return yield* deleteEvent({ identifier: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("deletes an event by name", async () => { + const name = `distilled_resend_deleteEvent_byname_${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + yield* createEvent({ name }); + return yield* deleteEvent({ identifier: name }); + }), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent event id", async () => { + const error = await runEffect( + deleteEvent({ identifier: NON_EXISTENT_EVENT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteSegment.test.ts b/packages/resend/test/deleteSegment.test.ts new file mode 100644 index 0000000000..41975f6270 --- /dev/null +++ b/packages/resend/test/deleteSegment.test.ts @@ -0,0 +1,36 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_SEGMENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteSegment", () => { + it("deletes a segment created in the test", async () => { + const name = `distilled-resend-deleteSegment-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createSegment({ name }); + if (!created.id) { + return yield* Effect.die("createSegment did not return an id"); + } + return yield* deleteSegment({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent segment id", async () => { + const error = await runEffect( + deleteSegment({ id: NON_EXISTENT_SEGMENT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteTemplate.test.ts b/packages/resend/test/deleteTemplate.test.ts new file mode 100644 index 0000000000..010b732268 --- /dev/null +++ b/packages/resend/test/deleteTemplate.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TEMPLATE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteTemplate", () => { + it("deletes a template created in the test", async () => { + const name = `distilled-resend-deleteTemplate-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + html: "

Hello

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + return yield* deleteTemplate({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent template id", async () => { + const error = await runEffect( + deleteTemplate({ id: NON_EXISTENT_TEMPLATE_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteTopic.test.ts b/packages/resend/test/deleteTopic.test.ts new file mode 100644 index 0000000000..bfd260026b --- /dev/null +++ b/packages/resend/test/deleteTopic.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTopic } from "../src/operations/createTopic"; +import { deleteTopic } from "../src/operations/deleteTopic"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TOPIC_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteTopic", () => { + it("deletes a topic created in the test", async () => { + const name = `distilled-resend-deleteTopic-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name, + default_subscription: "opt_in", + }); + if (!created.id) { + return yield* Effect.die("createTopic did not return an id"); + } + return yield* deleteTopic({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent topic id", async () => { + const error = await runEffect( + deleteTopic({ id: NON_EXISTENT_TOPIC_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/deleteWebhook.test.ts b/packages/resend/test/deleteWebhook.test.ts new file mode 100644 index 0000000000..30d2b8d7e4 --- /dev/null +++ b/packages/resend/test/deleteWebhook.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook"; +import { deleteWebhook } from "../src/operations/deleteWebhook"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_WEBHOOK_ID = "00000000-0000-4000-8000-000000000000"; + +describe("deleteWebhook", () => { + it("deletes a webhook created in the test", async () => { + const endpoint = `https://example.com/distilled-resend-deleteWebhook-${testRunId}`; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint, + events: ["email.sent"], + }); + if (!created.id) { + return yield* Effect.die("createWebhook did not return an id"); + } + return yield* deleteWebhook({ webhook_id: created.id }); + }), + ); + + expect(result).toBeDefined(); + if (result.deleted !== undefined) { + expect(result.deleted).toBe(true); + } + }); + + it("fails with NotFound for a non-existent webhook id", async () => { + const error = await runEffect( + deleteWebhook({ webhook_id: NON_EXISTENT_WEBHOOK_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/duplicateTemplate.test.ts b/packages/resend/test/duplicateTemplate.test.ts new file mode 100644 index 0000000000..d12eb08651 --- /dev/null +++ b/packages/resend/test/duplicateTemplate.test.ts @@ -0,0 +1,59 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { duplicateTemplate } from "../src/operations/duplicateTemplate"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TEMPLATE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("duplicateTemplate", () => { + it("duplicates a template created in the test", async () => { + const name = `distilled-resend-duplicateTemplate-${testRunId}`; + let createdId: string | undefined; + let duplicateId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + html: "

Original {{name}}

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + createdId = created.id; + const duplicated = yield* duplicateTemplate({ id: created.id }); + duplicateId = duplicated.id; + return duplicated; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + duplicateId + ? deleteTemplate({ id: duplicateId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(result.id).not.toBe(createdId); + }); + + it("fails with NotFound for a non-existent template id", async () => { + const error = await runEffect( + duplicateTemplate({ id: NON_EXISTENT_TEMPLATE_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getAutomation.test.ts b/packages/resend/test/getAutomation.test.ts new file mode 100644 index 0000000000..7ee844954f --- /dev/null +++ b/packages/resend/test/getAutomation.test.ts @@ -0,0 +1,69 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { getAutomation } from "../src/operations/getAutomation"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getAutomation", () => { + it("retrieves a single automation by id", async () => { + const name = `distilled-resend-getAutomation-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* getAutomation({ automation_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(result.name).toBe(name); + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + getAutomation({ automation_id: NON_EXISTENT_AUTOMATION_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getAutomationRun.test.ts b/packages/resend/test/getAutomationRun.test.ts new file mode 100644 index 0000000000..9e6dd0f804 --- /dev/null +++ b/packages/resend/test/getAutomationRun.test.ts @@ -0,0 +1,138 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { getAutomationRun } from "../src/operations/getAutomationRun"; +import { listAutomationRuns } from "../src/operations/listAutomationRuns"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; +const NON_EXISTENT_RUN_ID = "00000000-0000-4000-8000-000000000001"; + +describe("getAutomationRun", () => { + it("retrieves an automation run if one exists, otherwise no-op", async () => { + const name = `distilled-resend-getAutomationRun-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + + const runs = yield* listAutomationRuns({ + automation_id: created.id, + limit: 1, + }); + const first = runs.data?.[0]; + if (!first?.id) { + return null; + } + return yield* getAutomationRun({ + automation_id: created.id, + run_id: first.id, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + if (result === null) { + // Freshly created automation has no runs; the list call succeeded. + return; + } + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent run id under a real automation", async () => { + const name = `distilled-resend-getAutomationRun-nf-${testRunId}`; + const eventName = `distilled-event-nf-${testRunId}`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* getAutomationRun({ + automation_id: created.id, + run_id: NON_EXISTENT_RUN_ID, + }).pipe(Effect.flip); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + getAutomationRun({ + automation_id: NON_EXISTENT_AUTOMATION_ID, + run_id: NON_EXISTENT_RUN_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getBroadcast.test.ts b/packages/resend/test/getBroadcast.test.ts new file mode 100644 index 0000000000..d7da39e021 --- /dev/null +++ b/packages/resend/test/getBroadcast.test.ts @@ -0,0 +1,71 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createBroadcast } from "../src/operations/createBroadcast"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { getBroadcast } from "../src/operations/getBroadcast"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const NON_EXISTENT_BROADCAST_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getBroadcast", () => { + it("retrieves a broadcast created in the test", async () => { + const segmentName = `distilled-resend-getBroadcast-seg-${testRunId}`; + const broadcastName = `distilled-resend-getBroadcast-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: broadcastName, + segment_id: segment.id, + from: SENDER, + subject: "Distilled test broadcast", + html: "

Hello from Distilled

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + broadcastId = broadcast.id; + return yield* getBroadcast({ id: broadcast.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(broadcastId); + expect(result.name).toBe(broadcastName); + expect(result.subject).toBe("Distilled test broadcast"); + }); + + it("fails with NotFound for a non-existent broadcast id", async () => { + const error = await runEffect( + getBroadcast({ id: NON_EXISTENT_BROADCAST_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getContact.test.ts b/packages/resend/test/getContact.test.ts new file mode 100644 index 0000000000..85c120dcef --- /dev/null +++ b/packages/resend/test/getContact.test.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { getContact } from "../src/operations/getContact"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getContact", () => { + it("retrieves a contact created in the test", async () => { + const email = `distilled-resend-getContact-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ + email, + first_name: "Distilled", + last_name: "Tester", + }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* getContact({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.email).toBe(email); + expect(result.first_name).toBe("Distilled"); + expect(result.last_name).toBe("Tester"); + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const error = await runEffect( + getContact({ id: NON_EXISTENT_CONTACT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getContactImport.test.ts b/packages/resend/test/getContactImport.test.ts new file mode 100644 index 0000000000..8b079608ef --- /dev/null +++ b/packages/resend/test/getContactImport.test.ts @@ -0,0 +1,42 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactImport } from "../src/operations/createContactImport"; +import { getContactImport } from "../src/operations/getContactImport"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_IMPORT_ID = "00000000-0000-4000-8000-000000000000"; +const VALID_STATUSES = ["queued", "in_progress", "completed", "failed"]; +const MINIMAL_CSV = `email,first_name,last_name\nget-import-${testRunId}-1@example.com,Alice,Tester\n`; + +describe("getContactImport", () => { + it("retrieves a contact import created in the test", async () => { + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactImport({ + file: MINIMAL_CSV, + on_conflict: "skip", + }); + if (!created.id) { + return yield* Effect.die( + "createContactImport did not return an id", + ); + } + return yield* getContactImport({ id: created.id }); + }), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + if (result.status !== undefined) { + expect(VALID_STATUSES).toContain(result.status); + } + }); + + it("fails with NotFound for a non-existent import id", async () => { + const error = await runEffect( + getContactImport({ id: NON_EXISTENT_IMPORT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getContactProperty.test.ts b/packages/resend/test/getContactProperty.test.ts new file mode 100644 index 0000000000..5f1187a18f --- /dev/null +++ b/packages/resend/test/getContactProperty.test.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactProperty } from "../src/operations/createContactProperty"; +import { deleteContactProperty } from "../src/operations/deleteContactProperty"; +import { getContactProperty } from "../src/operations/getContactProperty"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_PROPERTY_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getContactProperty", () => { + it("retrieves a contact property created in the test", async () => { + const key = `distilled_get_${testRunId}`.toLowerCase(); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "string", + fallback_value: "default", + }); + if (!created.id) { + return yield* Effect.die( + "createContactProperty did not return an id", + ); + } + createdId = created.id; + return yield* getContactProperty({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.key).toBe(key); + }); + + it("fails with NotFound for a non-existent contact property id", async () => { + const error = await runEffect( + getContactProperty({ id: NON_EXISTENT_PROPERTY_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getDomain.test.ts b/packages/resend/test/getDomain.test.ts new file mode 100644 index 0000000000..c3131a4d39 --- /dev/null +++ b/packages/resend/test/getDomain.test.ts @@ -0,0 +1,44 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createDomain } from "../src/operations/createDomain"; +import { deleteDomain } from "../src/operations/deleteDomain"; +import { getDomain } from "../src/operations/getDomain"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_DOMAIN_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getDomain", () => { + it("retrieves a domain by id", async () => { + const name = `distilled-resend-getDomain-${testRunId}.example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ name }); + createdId = created.id; + expect(typeof createdId).toBe("string"); + return yield* getDomain({ domain_id: createdId as string }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.name).toBe(name); + }); + + it("fails with NotFound for a non-existent domain id", async () => { + const error = await runEffect( + getDomain({ domain_id: NON_EXISTENT_DOMAIN_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getEmail.test.ts b/packages/resend/test/getEmail.test.ts new file mode 100644 index 0000000000..ea5569c97f --- /dev/null +++ b/packages/resend/test/getEmail.test.ts @@ -0,0 +1,66 @@ +import { Effect, Schedule } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEmail } from "../src/operations/createEmail"; +import { getEmail } from "../src/operations/getEmail"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +// A well-formed UUID that should not exist in the account. Used to trigger +// NotFound for non-existent email ids. +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getEmail", () => { + it("retrieves an email by id", async () => { + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-getEmail-${testRunId}`, + html: `

Test email from distilled SDK run ${testRunId}

`, + }); + + expect(typeof created.id).toBe("string"); + const email_id = created.id as string; + + // Newly-created emails may not be immediately readable — retry on + // NotFound until the email is indexed. + return yield* getEmail({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + }), + ); + + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent email id", async () => { + const error = await runEffect( + getEmail({ email_id: NON_EXISTENT_EMAIL_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with Forbidden for an unauthorized email id", async () => { + // Resend documents 403 invalid_api_key on GET /emails/{email_id}. The + // documented trigger surfaces when the API key is not permitted to read + // the email — we exercise this path with a well-formed but unauthorized id. + const error = await runEffect( + getEmail({ + email_id: `forbidden-${testRunId}-00000000-0000-4000-8000-000000000000`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); +}); diff --git a/packages/resend/test/getEmailAttachment.test.ts b/packages/resend/test/getEmailAttachment.test.ts new file mode 100644 index 0000000000..3fbdbcca9f --- /dev/null +++ b/packages/resend/test/getEmailAttachment.test.ts @@ -0,0 +1,106 @@ +import { Effect, Schedule } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEmail } from "../src/operations/createEmail"; +import { getEmail } from "../src/operations/getEmail"; +import { getEmailAttachment } from "../src/operations/getEmailAttachment"; +import { listEmailAttachments } from "../src/operations/listEmailAttachments"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; +const NON_EXISTENT_ATTACHMENT_ID = "00000000-0000-4000-8000-000000000001"; + +const waitForEmailReady = (email_id: string) => + getEmail({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + +const waitForAttachmentsReady = (email_id: string) => + listEmailAttachments({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + +describe("getEmailAttachment", () => { + it("retrieves a single attachment for an email", async () => { + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-getEmailAttachment-${testRunId}`, + html: `

Test email with attachment ${testRunId}

`, + attachments: [ + { + filename: `attachment-${testRunId}.txt`, + content: Buffer.from( + `Hello from distilled SDK test ${testRunId}`, + ).toString("base64"), + content_type: "text/plain", + }, + ], + }); + + const email_id = created.id as string; + yield* waitForEmailReady(email_id); + + const attachments = yield* waitForAttachmentsReady(email_id); + expect(attachments.data).toBeDefined(); + expect(attachments.data!.length).toBeGreaterThan(0); + + const attachment_id = attachments.data![0]!.id as string; + expect(typeof attachment_id).toBe("string"); + + return yield* getEmailAttachment({ email_id, attachment_id }); + }), + ); + + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + }); + + it("fails with NotFound for a non-existent email id", async () => { + const error = await runEffect( + getEmailAttachment({ + email_id: NON_EXISTENT_EMAIL_ID, + attachment_id: NON_EXISTENT_ATTACHMENT_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with NotFound for a non-existent attachment id on a real email", async () => { + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-getEmailAttachment-404att-${testRunId}`, + html: `

Test email ${testRunId}

`, + }); + + const email_id = created.id as string; + yield* waitForEmailReady(email_id); + + return yield* getEmailAttachment({ + email_id, + attachment_id: NON_EXISTENT_ATTACHMENT_ID, + }).pipe(Effect.flip); + }), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getEmailReceiving.test.ts b/packages/resend/test/getEmailReceiving.test.ts new file mode 100644 index 0000000000..cef0239104 --- /dev/null +++ b/packages/resend/test/getEmailReceiving.test.ts @@ -0,0 +1,36 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { getEmailReceiving } from "../src/operations/getEmailReceiving"; +import { listEmailReceiving } from "../src/operations/listEmailReceiving"; +import { runEffect } from "./setup"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getEmailReceiving", () => { + it("retrieves a received email by id", async () => { + // Received emails come from inbound mail to a configured Resend domain; + // they cannot be created via this SDK. Pick the first existing received + // email in the account and fetch it by id. + const result = await runEffect( + Effect.gen(function* () { + const list = yield* listEmailReceiving({ limit: 1 }); + expect(list.data).toBeDefined(); + expect(list.data!.length).toBeGreaterThan(0); + const email_id = list.data![0]!.id as string; + expect(typeof email_id).toBe("string"); + return yield* getEmailReceiving({ email_id }); + }), + ); + + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + }); + + it("fails with NotFound for a non-existent received email id", async () => { + const error = await runEffect( + getEmailReceiving({ email_id: NON_EXISTENT_EMAIL_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getEmailReceivingAttachment.test.ts b/packages/resend/test/getEmailReceivingAttachment.test.ts new file mode 100644 index 0000000000..029c6481b7 --- /dev/null +++ b/packages/resend/test/getEmailReceivingAttachment.test.ts @@ -0,0 +1,70 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { getEmailReceivingAttachment } from "../src/operations/getEmailReceivingAttachment"; +import { listEmailReceiving } from "../src/operations/listEmailReceiving"; +import { listEmailReceivingAttachments } from "../src/operations/listEmailReceivingAttachments"; +import { runEffect } from "./setup"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; +const NON_EXISTENT_ATTACHMENT_ID = "00000000-0000-4000-8000-000000000001"; + +describe("getEmailReceivingAttachment", () => { + it("retrieves a single attachment for a received email", async () => { + // Received emails come from inbound mail to a configured Resend domain; + // they cannot be created via this SDK. Walk the most recent received + // emails until one with an attachment is found. + const result = await runEffect( + Effect.gen(function* () { + const list = yield* listEmailReceiving({ limit: 25 }); + expect(list.data).toBeDefined(); + expect(list.data!.length).toBeGreaterThan(0); + + for (const email of list.data!) { + const email_id = email.id as string; + const atts = yield* listEmailReceivingAttachments({ email_id }); + if (atts.data && atts.data.length > 0) { + const attachment_id = atts.data[0]!.id as string; + return yield* getEmailReceivingAttachment({ + email_id, + attachment_id, + }); + } + } + return yield* Effect.die( + "no received email with attachments found in the test account", + ); + }), + ); + + expect(result).toBeDefined(); + expect(result.id).toBeDefined(); + }); + + it("fails with NotFound for a non-existent received email id", async () => { + const error = await runEffect( + getEmailReceivingAttachment({ + email_id: NON_EXISTENT_EMAIL_ID, + attachment_id: NON_EXISTENT_ATTACHMENT_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with NotFound for a non-existent attachment id on a real received email", async () => { + const error = await runEffect( + Effect.gen(function* () { + const list = yield* listEmailReceiving({ limit: 1 }); + expect(list.data).toBeDefined(); + expect(list.data!.length).toBeGreaterThan(0); + const email_id = list.data![0]!.id as string; + return yield* getEmailReceivingAttachment({ + email_id, + attachment_id: NON_EXISTENT_ATTACHMENT_ID, + }).pipe(Effect.flip); + }), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getEvent.test.ts b/packages/resend/test/getEvent.test.ts new file mode 100644 index 0000000000..97292ce837 --- /dev/null +++ b/packages/resend/test/getEvent.test.ts @@ -0,0 +1,70 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEvent } from "../src/operations/createEvent"; +import { deleteEvent } from "../src/operations/deleteEvent"; +import { getEvent } from "../src/operations/getEvent"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_EVENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getEvent", () => { + it("retrieves an event by id", async () => { + const name = `distilled_resend_getEvent_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ name }); + if (!created.id) { + return yield* Effect.die("createEvent did not return an id"); + } + createdId = created.id; + return yield* getEvent({ identifier: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + expect(result.name).toBe(name); + }); + + it("retrieves an event by name", async () => { + const name = `distilled_resend_getEvent_byname_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ name }); + createdId = created.id; + return yield* getEvent({ identifier: name }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.name).toBe(name); + }); + + it("fails with NotFound for a non-existent event id", async () => { + const error = await runEffect( + getEvent({ identifier: NON_EXISTENT_EVENT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getLog.test.ts b/packages/resend/test/getLog.test.ts new file mode 100644 index 0000000000..065b8943a2 --- /dev/null +++ b/packages/resend/test/getLog.test.ts @@ -0,0 +1,39 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { getLog } from "../src/operations/getLog"; +import { listLogs } from "../src/operations/listLogs"; +import { runEffect } from "./setup"; + +const NON_EXISTENT_LOG_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getLog", () => { + it("retrieves a single log by id", async () => { + const result = await runEffect( + Effect.gen(function* () { + const list = yield* listLogs({ limit: 1 }); + const first = list.data?.[0]; + if (!first?.id) { + return null; + } + return yield* getLog({ log_id: first.id }); + }), + ); + + if (result === null) { + // No logs available on this account yet; nothing to assert beyond + // the listLogs call completing without error. + return; + } + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent log id", async () => { + const error = await runEffect( + getLog({ log_id: NON_EXISTENT_LOG_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getSegment.test.ts b/packages/resend/test/getSegment.test.ts new file mode 100644 index 0000000000..2ea325f321 --- /dev/null +++ b/packages/resend/test/getSegment.test.ts @@ -0,0 +1,46 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { getSegment } from "../src/operations/getSegment"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_SEGMENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getSegment", () => { + it("retrieves a segment created in the test", async () => { + const name = `distilled-resend-getSegment-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createSegment({ name }); + if (!created.id) { + return yield* Effect.die("createSegment did not return an id"); + } + createdId = created.id; + return yield* getSegment({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteSegment({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.name).toBe(name); + }); + + it("fails with NotFound for a non-existent segment id", async () => { + const error = await runEffect( + getSegment({ id: NON_EXISTENT_SEGMENT_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getTemplate.test.ts b/packages/resend/test/getTemplate.test.ts new file mode 100644 index 0000000000..b24055079c --- /dev/null +++ b/packages/resend/test/getTemplate.test.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { getTemplate } from "../src/operations/getTemplate"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TEMPLATE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getTemplate", () => { + it("retrieves a template created in the test", async () => { + const name = `distilled-resend-getTemplate-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + html: "

Hello {{name}}

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + createdId = created.id; + return yield* getTemplate({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.name).toBe(name); + if (result.status !== undefined) { + expect(["draft", "published"]).toContain(result.status); + } + }); + + it("fails with NotFound for a non-existent template id", async () => { + const error = await runEffect( + getTemplate({ id: NON_EXISTENT_TEMPLATE_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getTopic.test.ts b/packages/resend/test/getTopic.test.ts new file mode 100644 index 0000000000..ee2d63eaf2 --- /dev/null +++ b/packages/resend/test/getTopic.test.ts @@ -0,0 +1,50 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTopic } from "../src/operations/createTopic"; +import { deleteTopic } from "../src/operations/deleteTopic"; +import { getTopic } from "../src/operations/getTopic"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TOPIC_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getTopic", () => { + it("retrieves a topic created in the test", async () => { + const name = `distilled-resend-getTopic-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name, + default_subscription: "opt_in", + }); + if (!created.id) { + return yield* Effect.die("createTopic did not return an id"); + } + createdId = created.id; + return yield* getTopic({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTopic({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.name).toBe(name); + expect(result.default_subscription).toBe("opt_in"); + }); + + it("fails with NotFound for a non-existent topic id", async () => { + const error = await runEffect( + getTopic({ id: NON_EXISTENT_TOPIC_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/getWebhook.test.ts b/packages/resend/test/getWebhook.test.ts new file mode 100644 index 0000000000..b61e8c337e --- /dev/null +++ b/packages/resend/test/getWebhook.test.ts @@ -0,0 +1,52 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook"; +import { deleteWebhook } from "../src/operations/deleteWebhook"; +import { getWebhook } from "../src/operations/getWebhook"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_WEBHOOK_ID = "00000000-0000-4000-8000-000000000000"; + +describe("getWebhook", () => { + it("retrieves a webhook created in the test", async () => { + const endpoint = `https://example.com/distilled-resend-getWebhook-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint, + events: ["email.sent"], + }); + if (!created.id) { + return yield* Effect.die("createWebhook did not return an id"); + } + createdId = created.id; + return yield* getWebhook({ webhook_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteWebhook({ webhook_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + expect(result.endpoint).toBe(endpoint); + if (result.events != null) { + expect(result.events).toContain("email.sent"); + } + }); + + it("fails with NotFound for a non-existent webhook id", async () => { + const error = await runEffect( + getWebhook({ webhook_id: NON_EXISTENT_WEBHOOK_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listApiKeys.test.ts b/packages/resend/test/listApiKeys.test.ts new file mode 100644 index 0000000000..6e193a7d45 --- /dev/null +++ b/packages/resend/test/listApiKeys.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listApiKeys } from "../src/operations/listApiKeys"; +import { runEffect } from "./setup"; + +describe("listApiKeys", () => { + it("returns a paginated list of API keys", async () => { + const result = await runEffect(listApiKeys({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect(listApiKeys({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with BadRequest for an invalid pagination cursor", async () => { + const error = await runEffect( + listApiKeys({ after: "not-a-real-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); +}); diff --git a/packages/resend/test/listAutomationRuns.test.ts b/packages/resend/test/listAutomationRuns.test.ts new file mode 100644 index 0000000000..1ddf41b548 --- /dev/null +++ b/packages/resend/test/listAutomationRuns.test.ts @@ -0,0 +1,124 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { listAutomationRuns } from "../src/operations/listAutomationRuns"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; + +describe("listAutomationRuns", () => { + it("lists runs for an automation created in the test", async () => { + const name = `distilled-resend-listAutomationRuns-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* listAutomationRuns({ automation_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists runs with a limit parameter", async () => { + const name = `distilled-resend-listAutomationRuns-limit-${testRunId}`; + const eventName = `distilled-event-limit-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* listAutomationRuns({ + automation_id: created.id, + limit: 5, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + listAutomationRuns({ + automation_id: NON_EXISTENT_AUTOMATION_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listAutomations.test.ts b/packages/resend/test/listAutomations.test.ts new file mode 100644 index 0000000000..c6a8fb89ab --- /dev/null +++ b/packages/resend/test/listAutomations.test.ts @@ -0,0 +1,47 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listAutomations } from "../src/operations/listAutomations"; +import { runEffect } from "./setup"; + +describe("listAutomations", () => { + it("lists automations with default parameters", async () => { + const result = await runEffect(listAutomations({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists automations with a limit parameter", async () => { + const result = await runEffect(listAutomations({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("lists automations filtered by status", async () => { + const result = await runEffect(listAutomations({ status: "enabled" })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const automation of result.data) { + if (automation.status !== undefined) { + expect(automation.status).toBe("enabled"); + } + } + } + }); + + it("fails with InvalidRequestError for an invalid cursor", async () => { + const error = await runEffect( + listAutomations({ after: "not-a-valid-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listBroadcasts.test.ts b/packages/resend/test/listBroadcasts.test.ts new file mode 100644 index 0000000000..62c9765e57 --- /dev/null +++ b/packages/resend/test/listBroadcasts.test.ts @@ -0,0 +1,38 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listBroadcasts } from "../src/operations/listBroadcasts"; +import { runEffect } from "./setup"; + +describe("listBroadcasts", () => { + it("lists broadcasts in the test account", async () => { + const result = await runEffect(listBroadcasts({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const broadcast of result.data) { + if (broadcast.id !== undefined) { + expect(typeof broadcast.id).toBe("string"); + } + } + } + }); + + it("lists broadcasts with a limit parameter", async () => { + const result = await runEffect(listBroadcasts({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listBroadcasts({ after: "not-a-valid-cursor-!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listContactImports.test.ts b/packages/resend/test/listContactImports.test.ts new file mode 100644 index 0000000000..84b442a74f --- /dev/null +++ b/packages/resend/test/listContactImports.test.ts @@ -0,0 +1,50 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listContactImports } from "../src/operations/listContactImports"; +import { runEffect } from "./setup"; + +const VALID_STATUSES = ["queued", "in_progress", "completed", "failed"]; + +describe("listContactImports", () => { + it("lists contact imports in the test account", async () => { + const result = await runEffect(listContactImports({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const imp of result.data) { + if (imp.id !== undefined) { + expect(typeof imp.id).toBe("string"); + } + if (imp.status !== undefined) { + expect(VALID_STATUSES).toContain(imp.status); + } + } + } + }); + + it("lists contact imports filtered by status with a limit", async () => { + const result = await runEffect( + listContactImports({ status: "completed", limit: 5 }), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + for (const imp of result.data) { + if (imp.status !== undefined) { + expect(imp.status).toBe("completed"); + } + } + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listContactImports({ after: "not-a-valid-cursor-!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listContactProperties.test.ts b/packages/resend/test/listContactProperties.test.ts new file mode 100644 index 0000000000..58f54d14f6 --- /dev/null +++ b/packages/resend/test/listContactProperties.test.ts @@ -0,0 +1,43 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listContactProperties } from "../src/operations/listContactProperties"; +import { runEffect } from "./setup"; + +describe("listContactProperties", () => { + it("lists contact properties in the test account", async () => { + const result = await runEffect(listContactProperties({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const property of result.data) { + if (property.id !== undefined) { + expect(typeof property.id).toBe("string"); + } + if (property.key !== undefined) { + expect(typeof property.key).toBe("string"); + } + } + } + }); + + it("lists contact properties with a limit parameter", async () => { + const result = await runEffect(listContactProperties({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listContactProperties({ after: "not-a-valid-cursor-!!!" }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listContactSegments.test.ts b/packages/resend/test/listContactSegments.test.ts new file mode 100644 index 0000000000..89fc9e0327 --- /dev/null +++ b/packages/resend/test/listContactSegments.test.ts @@ -0,0 +1,82 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { listContactSegments } from "../src/operations/listContactSegments"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("listContactSegments", () => { + it("lists segments for a contact created in the test", async () => { + const email = `distilled-resend-listContactSegments-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* listContactSegments({ contact_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists contact segments with a limit parameter", async () => { + const email = `distilled-resend-listContactSegments-limit-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* listContactSegments({ + contact_id: created.id, + limit: 5, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const error = await runEffect( + listContactSegments({ contact_id: NON_EXISTENT_CONTACT_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listContactTopics.test.ts b/packages/resend/test/listContactTopics.test.ts new file mode 100644 index 0000000000..7a8495b7ec --- /dev/null +++ b/packages/resend/test/listContactTopics.test.ts @@ -0,0 +1,82 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { listContactTopics } from "../src/operations/listContactTopics"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("listContactTopics", () => { + it("lists topics for a contact created in the test", async () => { + const email = `distilled-resend-listContactTopics-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* listContactTopics({ contact_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists contact topics with a limit parameter", async () => { + const email = `distilled-resend-listContactTopics-limit-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* listContactTopics({ + contact_id: created.id, + limit: 5, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const error = await runEffect( + listContactTopics({ contact_id: NON_EXISTENT_CONTACT_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listContacts.test.ts b/packages/resend/test/listContacts.test.ts new file mode 100644 index 0000000000..c304ae3bcb --- /dev/null +++ b/packages/resend/test/listContacts.test.ts @@ -0,0 +1,43 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listContacts } from "../src/operations/listContacts"; +import { runEffect } from "./setup"; + +describe("listContacts", () => { + it("lists contacts in the test account", async () => { + const result = await runEffect(listContacts({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const contact of result.data) { + if (contact.id !== undefined) { + expect(typeof contact.id).toBe("string"); + } + if (contact.email !== undefined) { + expect(typeof contact.email).toBe("string"); + } + } + } + }); + + it("lists contacts with a limit parameter", async () => { + const result = await runEffect(listContacts({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a non-existent segment_id", async () => { + const error = await runEffect( + listContacts({ + segment_id: "00000000-0000-4000-8000-000000000000", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listDomains.test.ts b/packages/resend/test/listDomains.test.ts new file mode 100644 index 0000000000..9388db5d0b --- /dev/null +++ b/packages/resend/test/listDomains.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listDomains } from "../src/operations/listDomains"; +import { runEffect } from "./setup"; + +describe("listDomains", () => { + it("returns a paginated list of domains", async () => { + const result = await runEffect(listDomains({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect(listDomains({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with BadRequest for an invalid pagination cursor", async () => { + const error = await runEffect( + listDomains({ after: "not-a-real-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); +}); diff --git a/packages/resend/test/listEmailAttachments.test.ts b/packages/resend/test/listEmailAttachments.test.ts new file mode 100644 index 0000000000..7bc889af99 --- /dev/null +++ b/packages/resend/test/listEmailAttachments.test.ts @@ -0,0 +1,118 @@ +import { Effect, Schedule } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEmail } from "../src/operations/createEmail"; +import { getEmail } from "../src/operations/getEmail"; +import { listEmailAttachments } from "../src/operations/listEmailAttachments"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +// Wait for a freshly-created email to be readable. Newly-created ids are +// briefly not indexed and return NotFound. +const waitForEmailReady = (email_id: string) => + getEmail({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + +describe("listEmailAttachments", () => { + it("lists attachments for an email", async () => { + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-listEmailAttachments-${testRunId}`, + html: `

Test email with attachment ${testRunId}

`, + attachments: [ + { + filename: `attachment-${testRunId}.txt`, + content: Buffer.from( + `Hello from distilled SDK test ${testRunId}`, + ).toString("base64"), + content_type: "text/plain", + }, + ], + }); + + const email_id = created.id as string; + yield* waitForEmailReady(email_id); + + // Attachment indexing may also be eventually-consistent — retry while + // the email reports NotFound on the attachments sub-resource. + return yield* listEmailAttachments({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + }), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-listEmailAttachments-limit-${testRunId}`, + html: `

Test email for limit ${testRunId}

`, + attachments: [ + { + filename: `a-${testRunId}.txt`, + content: Buffer.from("a").toString("base64"), + content_type: "text/plain", + }, + { + filename: `b-${testRunId}.txt`, + content: Buffer.from("b").toString("base64"), + content_type: "text/plain", + }, + ], + }); + + const email_id = created.id as string; + yield* waitForEmailReady(email_id); + + return yield* listEmailAttachments({ email_id, limit: 1 }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + }), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(result.data.length).toBeLessThanOrEqual(1); + } + }); + + it("fails with NotFound for a non-existent email id", async () => { + const error = await runEffect( + listEmailAttachments({ email_id: NON_EXISTENT_EMAIL_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listEmailReceiving.test.ts b/packages/resend/test/listEmailReceiving.test.ts new file mode 100644 index 0000000000..c0abde970a --- /dev/null +++ b/packages/resend/test/listEmailReceiving.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listEmailReceiving } from "../src/operations/listEmailReceiving"; +import { runEffect } from "./setup"; + +describe("listEmailReceiving", () => { + it("returns a paginated list of received emails", async () => { + const result = await runEffect(listEmailReceiving({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect(listEmailReceiving({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with BadRequest for an invalid pagination cursor", async () => { + const error = await runEffect( + listEmailReceiving({ after: "not-a-real-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); +}); diff --git a/packages/resend/test/listEmailReceivingAttachments.test.ts b/packages/resend/test/listEmailReceivingAttachments.test.ts new file mode 100644 index 0000000000..200c020228 --- /dev/null +++ b/packages/resend/test/listEmailReceivingAttachments.test.ts @@ -0,0 +1,56 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listEmailReceiving } from "../src/operations/listEmailReceiving"; +import { listEmailReceivingAttachments } from "../src/operations/listEmailReceivingAttachments"; +import { runEffect } from "./setup"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +describe("listEmailReceivingAttachments", () => { + it("lists attachments for a received email", async () => { + // Received emails come from inbound mail to a configured Resend domain; + // they cannot be created via this SDK. Pick the first existing received + // email in the account and list its attachments. + const result = await runEffect( + Effect.gen(function* () { + const list = yield* listEmailReceiving({ limit: 1 }); + expect(list.data).toBeDefined(); + expect(list.data!.length).toBeGreaterThan(0); + const email_id = list.data![0]!.id as string; + return yield* listEmailReceivingAttachments({ email_id }); + }), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect( + Effect.gen(function* () { + const list = yield* listEmailReceiving({ limit: 1 }); + expect(list.data).toBeDefined(); + expect(list.data!.length).toBeGreaterThan(0); + const email_id = list.data![0]!.id as string; + return yield* listEmailReceivingAttachments({ email_id, limit: 1 }); + }), + ); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(result.data.length).toBeLessThanOrEqual(1); + } + }); + + it("fails with NotFound for a non-existent received email id", async () => { + const error = await runEffect( + listEmailReceivingAttachments({ + email_id: NON_EXISTENT_EMAIL_ID, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/listEmails.test.ts b/packages/resend/test/listEmails.test.ts new file mode 100644 index 0000000000..3d748059b6 --- /dev/null +++ b/packages/resend/test/listEmails.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listEmails } from "../src/operations/listEmails"; +import { runEffect } from "./setup"; + +describe("listEmails", () => { + it("returns a paginated list of emails", async () => { + const result = await runEffect(listEmails({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("respects the limit parameter", async () => { + const result = await runEffect(listEmails({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with BadRequest for an invalid pagination cursor", async () => { + const error = await runEffect( + listEmails({ after: "not-a-real-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("BadRequest"); + }); +}); diff --git a/packages/resend/test/listEvents.test.ts b/packages/resend/test/listEvents.test.ts new file mode 100644 index 0000000000..5a52d1abc9 --- /dev/null +++ b/packages/resend/test/listEvents.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listEvents } from "../src/operations/listEvents"; +import { runEffect } from "./setup"; + +describe("listEvents", () => { + it("lists events with default parameters", async () => { + const result = await runEffect(listEvents({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists events with a limit parameter", async () => { + const result = await runEffect(listEvents({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for an invalid cursor", async () => { + const error = await runEffect( + listEvents({ after: "not-a-valid-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listLogs.test.ts b/packages/resend/test/listLogs.test.ts new file mode 100644 index 0000000000..f2d103e67b --- /dev/null +++ b/packages/resend/test/listLogs.test.ts @@ -0,0 +1,33 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listLogs } from "../src/operations/listLogs"; +import { runEffect } from "./setup"; + +describe("listLogs", () => { + it("lists logs with default parameters", async () => { + const result = await runEffect(listLogs({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + } + }); + + it("lists logs with a limit parameter", async () => { + const result = await runEffect(listLogs({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for an invalid cursor", async () => { + const error = await runEffect( + listLogs({ after: "not-a-valid-cursor" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listSegments.test.ts b/packages/resend/test/listSegments.test.ts new file mode 100644 index 0000000000..5332afd63a --- /dev/null +++ b/packages/resend/test/listSegments.test.ts @@ -0,0 +1,38 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listSegments } from "../src/operations/listSegments"; +import { runEffect } from "./setup"; + +describe("listSegments", () => { + it("lists segments in the test account", async () => { + const result = await runEffect(listSegments({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const segment of result.data) { + if (segment.id !== undefined) { + expect(typeof segment.id).toBe("string"); + } + } + } + }); + + it("lists segments with a limit parameter", async () => { + const result = await runEffect(listSegments({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listSegments({ after: "not-a-valid-cursor-!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listTemplates.test.ts b/packages/resend/test/listTemplates.test.ts new file mode 100644 index 0000000000..ad53d07568 --- /dev/null +++ b/packages/resend/test/listTemplates.test.ts @@ -0,0 +1,41 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listTemplates } from "../src/operations/listTemplates"; +import { runEffect } from "./setup"; + +describe("listTemplates", () => { + it("lists templates in the test account", async () => { + const result = await runEffect(listTemplates({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const template of result.data) { + if (template.id !== undefined) { + expect(typeof template.id).toBe("string"); + } + if (template.status !== undefined) { + expect(["draft", "published"]).toContain(template.status); + } + } + } + }); + + it("lists templates with a limit parameter", async () => { + const result = await runEffect(listTemplates({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listTemplates({ after: "not-a-valid-cursor-!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listTopics.test.ts b/packages/resend/test/listTopics.test.ts new file mode 100644 index 0000000000..2f6b46a082 --- /dev/null +++ b/packages/resend/test/listTopics.test.ts @@ -0,0 +1,47 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listTopics } from "../src/operations/listTopics"; +import { runEffect } from "./setup"; + +const VALID_SUBSCRIPTIONS = ["opt_in", "opt_out"]; +const VALID_VISIBILITIES = ["public", "private"]; + +describe("listTopics", () => { + it("lists topics in the test account", async () => { + const result = await runEffect(listTopics({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const topic of result.data) { + if (topic.id !== undefined) { + expect(typeof topic.id).toBe("string"); + } + if (topic.default_subscription !== undefined) { + expect(VALID_SUBSCRIPTIONS).toContain(topic.default_subscription); + } + if (topic.visibility !== undefined) { + expect(VALID_VISIBILITIES).toContain(topic.visibility); + } + } + } + }); + + it("lists topics with a limit parameter", async () => { + const result = await runEffect(listTopics({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError for a malformed cursor", async () => { + const error = await runEffect( + listTopics({ after: "not-a-valid-cursor-!!!" }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/listWebhooks.test.ts b/packages/resend/test/listWebhooks.test.ts new file mode 100644 index 0000000000..4d9f0dffab --- /dev/null +++ b/packages/resend/test/listWebhooks.test.ts @@ -0,0 +1,44 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { listWebhooks } from "../src/operations/listWebhooks"; +import { runEffect } from "./setup"; + +describe("listWebhooks", () => { + it("lists webhooks in the test account", async () => { + const result = await runEffect(listWebhooks({})); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + for (const webhook of result.data) { + if (webhook.id !== undefined) { + expect(typeof webhook.id).toBe("string"); + } + if (webhook.endpoint !== undefined) { + expect(typeof webhook.endpoint).toBe("string"); + } + } + } + }); + + it("lists webhooks with a limit parameter", async () => { + const result = await runEffect(listWebhooks({ limit: 5 })); + + expect(result).toBeDefined(); + if (result.data !== undefined) { + expect(Array.isArray(result.data)).toBe(true); + expect(result.data.length).toBeLessThanOrEqual(5); + } + }); + + it("fails with InvalidRequestError when after and before are combined", async () => { + const error = await runEffect( + listWebhooks({ + after: "00000000-0000-4000-8000-000000000000", + before: "00000000-0000-4000-8000-000000000000", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("InvalidRequestError"); + }); +}); diff --git a/packages/resend/test/publishTemplate.test.ts b/packages/resend/test/publishTemplate.test.ts new file mode 100644 index 0000000000..036418f059 --- /dev/null +++ b/packages/resend/test/publishTemplate.test.ts @@ -0,0 +1,50 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { publishTemplate } from "../src/operations/publishTemplate"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TEMPLATE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("publishTemplate", () => { + it("publishes a template created in the test", async () => { + const name = `distilled-resend-publishTemplate-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + from: "Distilled Test ", + subject: "Hello", + html: "

Hello {{name}}

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + createdId = created.id; + return yield* publishTemplate({ id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + }); + + it("fails with NotFound for a non-existent template id", async () => { + const error = await runEffect( + publishTemplate({ id: NON_EXISTENT_TEMPLATE_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/sendBroadcast.test.ts b/packages/resend/test/sendBroadcast.test.ts new file mode 100644 index 0000000000..3c71d07a24 --- /dev/null +++ b/packages/resend/test/sendBroadcast.test.ts @@ -0,0 +1,130 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createBroadcast } from "../src/operations/createBroadcast"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { sendBroadcast } from "../src/operations/sendBroadcast"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const NON_EXISTENT_BROADCAST_ID = "00000000-0000-4000-8000-000000000000"; + +// A scheduled_at far in the future so the broadcast does not actually send +// during the test run (Resend allows scheduling weeks/months ahead). +const farFuture = (): string => { + const d = new Date(); + d.setFullYear(d.getFullYear() + 1); + return d.toISOString(); +}; + +describe("sendBroadcast", () => { + it("schedules a broadcast for a future delivery time", async () => { + const segmentName = `distilled-resend-sendBroadcast-seg-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: `distilled-resend-sendBroadcast-${testRunId}`, + segment_id: segment.id, + from: SENDER, + subject: "Distilled test broadcast", + html: "

Hello

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + broadcastId = broadcast.id; + return yield* sendBroadcast({ + id: broadcast.id, + scheduled_at: farFuture(), + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(broadcastId); + }); + + it("fails with NotFound for a non-existent broadcast id", async () => { + const error = await runEffect( + sendBroadcast({ id: NON_EXISTENT_BROADCAST_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for a malformed scheduled_at", async () => { + const segmentName = `distilled-resend-sendBroadcast-bad-seg-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: `distilled-resend-sendBroadcast-bad-${testRunId}`, + segment_id: segment.id, + from: SENDER, + subject: "Distilled test broadcast", + html: "

Hello

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + broadcastId = broadcast.id; + return yield* sendBroadcast({ + id: broadcast.id, + scheduled_at: "not-a-valid-date", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/sendEvent.test.ts b/packages/resend/test/sendEvent.test.ts new file mode 100644 index 0000000000..aaaf1f8573 --- /dev/null +++ b/packages/resend/test/sendEvent.test.ts @@ -0,0 +1,62 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { createEvent } from "../src/operations/createEvent"; +import { deleteContact } from "../src/operations/deleteContact"; +import { deleteEvent } from "../src/operations/deleteEvent"; +import { sendEvent } from "../src/operations/sendEvent"; +import { runEffect, testRunId } from "./setup"; + +describe("sendEvent", () => { + it("sends an event for a contact", async () => { + const eventName = `distilled_resend_sendEvent_${testRunId}`; + const email = `distilled-resend-sendEvent-${testRunId}@example.com`; + let createdEventId: string | undefined; + let createdContactId: string | undefined; + + await runEffect( + Effect.gen(function* () { + const event = yield* createEvent({ name: eventName }); + createdEventId = event.id; + + const contact = yield* createContact({ email }); + if (!contact.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdContactId = contact.id; + + yield* sendEvent({ + event: eventName, + contact_id: contact.id, + payload: { test_run_id: testRunId }, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdContactId + ? deleteContact({ id: createdContactId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + createdEventId + ? deleteEvent({ identifier: createdEventId }).pipe(Effect.ignore) + : deleteEvent({ identifier: eventName }).pipe(Effect.ignore), + ), + ), + ), + ); + }); + + it("fails with UnprocessableEntity for an empty event name", async () => { + const error = await runEffect( + sendEvent({ + event: "", + email: `distilled-resend-sendEvent-empty-${testRunId}@example.com`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/setup.ts b/packages/resend/test/setup.ts new file mode 100644 index 0000000000..2eb37fb884 --- /dev/null +++ b/packages/resend/test/setup.ts @@ -0,0 +1,28 @@ +import { config } from "dotenv"; +import { Effect, Layer } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import { CredentialsFromEnv } from "../src/credentials"; + +// Load environment variables from .env file +config(); + +// Main layer providing credentials and HTTP client for all tests +export const TestLayer = Layer.merge(CredentialsFromEnv, FetchHttpClient.layer); + +/** + * Short random hex string generated once per test run. + * Append this to resource names so parallel test runs don't collide. + */ +export const testRunId: string = crypto + .randomUUID() + .replace(/-/g, "") + .slice(0, 8); + +/** + * Run an Effect with the TestLayer provided. + */ +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const runEffect = (effect: Effect.Effect): Promise => + Effect.runPromise( + effect.pipe(Effect.provide(TestLayer)) as Effect.Effect, + ); diff --git a/packages/resend/test/stopAutomation.test.ts b/packages/resend/test/stopAutomation.test.ts new file mode 100644 index 0000000000..55e2d1759a --- /dev/null +++ b/packages/resend/test/stopAutomation.test.ts @@ -0,0 +1,68 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { stopAutomation } from "../src/operations/stopAutomation"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; + +describe("stopAutomation", () => { + it("stops an enabled automation", async () => { + const name = `distilled-resend-stopAutomation-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "enabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* stopAutomation({ automation_id: created.id }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + stopAutomation({ automation_id: NON_EXISTENT_AUTOMATION_ID }).pipe( + Effect.flip, + ), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/updateAutomation.test.ts b/packages/resend/test/updateAutomation.test.ts new file mode 100644 index 0000000000..9d4a69c36b --- /dev/null +++ b/packages/resend/test/updateAutomation.test.ts @@ -0,0 +1,123 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createAutomation } from "../src/operations/createAutomation"; +import { deleteAutomation } from "../src/operations/deleteAutomation"; +import { updateAutomation } from "../src/operations/updateAutomation"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_AUTOMATION_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateAutomation", () => { + it("updates an automation's name", async () => { + const name = `distilled-resend-updateAutomation-${testRunId}`; + const newName = `distilled-resend-updateAutomation-updated-${testRunId}`; + const eventName = `distilled-event-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* updateAutomation({ + automation_id: created.id, + name: newName, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("fails with NotFound for a non-existent automation id", async () => { + const error = await runEffect( + updateAutomation({ + automation_id: NON_EXISTENT_AUTOMATION_ID, + name: `distilled-resend-updateAutomation-nf-${testRunId}`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for invalid steps", async () => { + const name = `distilled-resend-updateAutomation-unproc-${testRunId}`; + const eventName = `distilled-event-unproc-${testRunId}`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createAutomation({ + name, + status: "disabled", + steps: [ + { + key: "trigger-1", + type: "trigger", + config: { event_name: eventName }, + }, + { + key: "delete-1", + type: "contact_delete", + config: {}, + }, + ], + connections: [ + { from: "trigger-1", to: "delete-1", type: "default" }, + ], + }); + if (!created.id) { + return yield* Effect.die("createAutomation did not return an id"); + } + createdId = created.id; + return yield* updateAutomation({ + automation_id: created.id, + steps: [], + }).pipe(Effect.flip); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteAutomation({ automation_id: createdId }).pipe( + Effect.ignore, + ) + : Effect.void, + ), + ), + ), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateBroadcast.test.ts b/packages/resend/test/updateBroadcast.test.ts new file mode 100644 index 0000000000..ba73d71b3d --- /dev/null +++ b/packages/resend/test/updateBroadcast.test.ts @@ -0,0 +1,134 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createBroadcast } from "../src/operations/createBroadcast"; +import { createSegment } from "../src/operations/createSegment"; +import { deleteBroadcast } from "../src/operations/deleteBroadcast"; +import { deleteSegment } from "../src/operations/deleteSegment"; +import { getBroadcast } from "../src/operations/getBroadcast"; +import { updateBroadcast } from "../src/operations/updateBroadcast"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const NON_EXISTENT_BROADCAST_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateBroadcast", () => { + it("updates an existing broadcast's name and subject", async () => { + const segmentName = `distilled-resend-updateBroadcast-seg-${testRunId}`; + const originalName = `distilled-resend-updateBroadcast-${testRunId}`; + const updatedName = `distilled-resend-updateBroadcast-renamed-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: originalName, + segment_id: segment.id, + from: SENDER, + subject: "Original subject", + html: "

Original

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + broadcastId = broadcast.id; + const updated = yield* updateBroadcast({ + id: broadcast.id, + name: updatedName, + subject: "Updated subject", + html: "

Updated

", + }); + const refetched = yield* getBroadcast({ id: broadcast.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(broadcastId); + expect(result.refetched.name).toBe(updatedName); + expect(result.refetched.subject).toBe("Updated subject"); + }); + + it("fails with NotFound for a non-existent broadcast id", async () => { + const error = await runEffect( + updateBroadcast({ + id: NON_EXISTENT_BROADCAST_ID, + subject: "Anything", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid from address", async () => { + const segmentName = `distilled-resend-updateBroadcast-bad-seg-${testRunId}`; + let segmentId: string | undefined; + let broadcastId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const segment = yield* createSegment({ name: segmentName }); + if (!segment.id) { + return yield* Effect.die("createSegment did not return an id"); + } + segmentId = segment.id; + const broadcast = yield* createBroadcast({ + name: `distilled-resend-updateBroadcast-bad-${testRunId}`, + segment_id: segment.id, + from: SENDER, + subject: "Original subject", + html: "

Original

", + send: false, + }); + if (!broadcast.id) { + return yield* Effect.die("createBroadcast did not return an id"); + } + broadcastId = broadcast.id; + return yield* updateBroadcast({ + id: broadcast.id, + from: "not-a-valid-email", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + broadcastId + ? deleteBroadcast({ id: broadcastId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + segmentId + ? deleteSegment({ id: segmentId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateContact.test.ts b/packages/resend/test/updateContact.test.ts new file mode 100644 index 0000000000..e2f5197f06 --- /dev/null +++ b/packages/resend/test/updateContact.test.ts @@ -0,0 +1,94 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { deleteContact } from "../src/operations/deleteContact"; +import { getContact } from "../src/operations/getContact"; +import { updateContact } from "../src/operations/updateContact"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateContact", () => { + it("updates a contact's name and unsubscribed flag", async () => { + const email = `distilled-resend-updateContact-${testRunId}@example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ + email, + first_name: "Original", + last_name: "Name", + }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + const updated = yield* updateContact({ + id: created.id, + first_name: "Updated", + last_name: "Person", + unsubscribed: true, + }); + const refetched = yield* getContact({ id: created.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(createdId); + expect(result.refetched.first_name).toBe("Updated"); + expect(result.refetched.last_name).toBe("Person"); + expect(result.refetched.unsubscribed).toBe(true); + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const error = await runEffect( + updateContact({ + id: NON_EXISTENT_CONTACT_ID, + first_name: "Ghost", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid email", async () => { + const email = `distilled-resend-updateContact-bad-${testRunId}@example.com`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createContact({ email }); + if (!created.id) { + return yield* Effect.die("createContact did not return an id"); + } + createdId = created.id; + return yield* updateContact({ + id: created.id, + email: "not-a-valid-email", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContact({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateContactProperty.test.ts b/packages/resend/test/updateContactProperty.test.ts new file mode 100644 index 0000000000..4df80c4307 --- /dev/null +++ b/packages/resend/test/updateContactProperty.test.ts @@ -0,0 +1,98 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContactProperty } from "../src/operations/createContactProperty"; +import { deleteContactProperty } from "../src/operations/deleteContactProperty"; +import { getContactProperty } from "../src/operations/getContactProperty"; +import { updateContactProperty } from "../src/operations/updateContactProperty"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_PROPERTY_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateContactProperty", () => { + it("updates a contact property's fallback_value", async () => { + const key = `distilled_update_${testRunId}`.toLowerCase(); + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "string", + fallback_value: "original", + }); + if (!created.id) { + return yield* Effect.die( + "createContactProperty did not return an id", + ); + } + createdId = created.id; + const updated = yield* updateContactProperty({ + id: created.id, + fallback_value: "updated", + }); + const refetched = yield* getContactProperty({ id: created.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(createdId); + expect(result.refetched.fallback_value).toBe("updated"); + }); + + it("fails with NotFound for a non-existent contact property id", async () => { + const error = await runEffect( + updateContactProperty({ + id: NON_EXISTENT_PROPERTY_ID, + fallback_value: "anything", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity when fallback_value type mismatches the property type", async () => { + const key = `distilled_update_bad_${testRunId}`.toLowerCase(); + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createContactProperty({ + key, + type: "number", + fallback_value: 0, + }); + if (!created.id) { + return yield* Effect.die( + "createContactProperty did not return an id", + ); + } + createdId = created.id; + return yield* updateContactProperty({ + id: created.id, + fallback_value: "not-a-number", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteContactProperty({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateContactTopic.test.ts b/packages/resend/test/updateContactTopic.test.ts new file mode 100644 index 0000000000..7b178dca9a --- /dev/null +++ b/packages/resend/test/updateContactTopic.test.ts @@ -0,0 +1,123 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createContact } from "../src/operations/createContact"; +import { createTopic } from "../src/operations/createTopic"; +import { deleteContact } from "../src/operations/deleteContact"; +import { deleteTopic } from "../src/operations/deleteTopic"; +import { updateContactTopic } from "../src/operations/updateContactTopic"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_CONTACT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateContactTopic", () => { + it("updates a contact's topic subscription", async () => { + const email = `distilled-resend-updateContactTopic-${testRunId}@example.com`; + const topicName = `distilled-resend-updateContactTopic-${testRunId}`; + let contactId: string | undefined; + let topicId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const contact = yield* createContact({ email }); + if (!contact.id) { + return yield* Effect.die("createContact did not return an id"); + } + contactId = contact.id; + + const topic = yield* createTopic({ + name: topicName, + default_subscription: "opt_in", + }); + if (!topic.id) { + return yield* Effect.die("createTopic did not return an id"); + } + topicId = topic.id; + + return yield* updateContactTopic({ + contact_id: contact.id, + topics: [{ id: topic.id, subscription: "opt_out" }], + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + topicId + ? deleteTopic({ id: topicId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + Effect.ensuring( + Effect.suspend(() => + contactId + ? deleteContact({ id: contactId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent contact id", async () => { + const topicName = `distilled-resend-updateContactTopic-nf-${testRunId}`; + let topicId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const topic = yield* createTopic({ + name: topicName, + default_subscription: "opt_in", + }); + if (!topic.id) { + return yield* Effect.die("createTopic did not return an id"); + } + topicId = topic.id; + + return yield* updateContactTopic({ + contact_id: NON_EXISTENT_CONTACT_ID, + topics: [{ id: topic.id, subscription: "opt_in" }], + }).pipe(Effect.flip); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + topicId + ? deleteTopic({ id: topicId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid topic id", async () => { + const email = `distilled-resend-updateContactTopic-unproc-${testRunId}@example.com`; + let contactId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const contact = yield* createContact({ email }); + if (!contact.id) { + return yield* Effect.die("createContact did not return an id"); + } + contactId = contact.id; + + return yield* updateContactTopic({ + contact_id: contact.id, + topics: [{ id: "not-a-valid-uuid", subscription: "opt_in" }], + }).pipe(Effect.flip); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + contactId + ? deleteContact({ id: contactId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateDomain.test.ts b/packages/resend/test/updateDomain.test.ts new file mode 100644 index 0000000000..2dd3400a1c --- /dev/null +++ b/packages/resend/test/updateDomain.test.ts @@ -0,0 +1,84 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createDomain } from "../src/operations/createDomain"; +import { deleteDomain } from "../src/operations/deleteDomain"; +import { updateDomain } from "../src/operations/updateDomain"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_DOMAIN_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateDomain", () => { + it("updates tracking flags on a domain", async () => { + const name = `distilled-resend-updateDomain-${testRunId}.example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ + name, + open_tracking: false, + click_tracking: false, + }); + createdId = created.id; + expect(typeof createdId).toBe("string"); + + return yield* updateDomain({ + domain_id: createdId as string, + open_tracking: true, + click_tracking: true, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + }); + + it("fails with NotFound for a non-existent domain id", async () => { + const error = await runEffect( + updateDomain({ + domain_id: NON_EXISTENT_DOMAIN_ID, + open_tracking: true, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid tls value", async () => { + const name = `distilled-resend-updateDomain-422-${testRunId}.example.com`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ name }); + createdId = created.id; + // Resend documents 422 invalid_parameter for semantically invalid + // fields. `tls` must be "opportunistic" or "enforced" — any other + // value is rejected. + return yield* updateDomain({ + domain_id: createdId as string, + tls: "not-a-real-tls-mode", + }).pipe(Effect.flip); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateEmail.test.ts b/packages/resend/test/updateEmail.test.ts new file mode 100644 index 0000000000..7fd4361cb7 --- /dev/null +++ b/packages/resend/test/updateEmail.test.ts @@ -0,0 +1,92 @@ +import { Effect, Schedule } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEmail } from "../src/operations/createEmail"; +import { getEmail } from "../src/operations/getEmail"; +import { updateEmail } from "../src/operations/updateEmail"; +import { runEffect, testRunId } from "./setup"; + +const SENDER = "Distilled Test "; +const RECIPIENT = "delivered@resend.dev"; + +const NON_EXISTENT_EMAIL_ID = "00000000-0000-4000-8000-000000000000"; + +// Wait for a freshly-created email to be readable. Newly-created ids are +// briefly not indexed and return NotFound. +const waitForEmailReady = (email_id: string) => + getEmail({ email_id }).pipe( + Effect.retry({ + while: (e) => e._tag === "NotFound", + schedule: Schedule.spaced("1 second").pipe( + Schedule.both(Schedule.recurs(10)), + ), + }), + ); + +describe("updateEmail", () => { + it("updates a scheduled email", async () => { + const result = await runEffect( + Effect.gen(function* () { + const scheduledAt = new Date(Date.now() + 60 * 60 * 1000).toISOString(); + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-updateEmail-${testRunId}`, + html: `

Scheduled email for update test ${testRunId}

`, + scheduled_at: scheduledAt, + }); + + expect(typeof created.id).toBe("string"); + const email_id = created.id as string; + + yield* waitForEmailReady(email_id); + + return yield* updateEmail({ email_id }); + }), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent email id", async () => { + const error = await runEffect( + updateEmail({ email_id: NON_EXISTENT_EMAIL_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with Forbidden for an unauthorized email id", async () => { + // Resend documents 403 invalid_api_key on PATCH /emails/{email_id}. We + // exercise the documented error path with a well-formed but unauthorized id. + const error = await runEffect( + updateEmail({ + email_id: `forbidden-${testRunId}-00000000-0000-4000-8000-000000000000`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("Forbidden"); + }); + + it("fails with UnprocessableEntity when updating a non-scheduled email", async () => { + // PATCH /emails/{email_id} is for rescheduling scheduled emails. Attempting + // to update an already-sent (non-scheduled) email surfaces 422 + // invalid_parameter per Resend's documented response set. + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createEmail({ + from: SENDER, + to: [RECIPIENT], + subject: `distilled-resend-updateEmail-422-${testRunId}`, + html: `

Non-scheduled email for 422 test ${testRunId}

`, + }); + + const email_id = created.id as string; + yield* waitForEmailReady(email_id); + + return yield* updateEmail({ email_id }).pipe(Effect.flip); + }), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateEvent.test.ts b/packages/resend/test/updateEvent.test.ts new file mode 100644 index 0000000000..a59c73daf8 --- /dev/null +++ b/packages/resend/test/updateEvent.test.ts @@ -0,0 +1,93 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createEvent } from "../src/operations/createEvent"; +import { deleteEvent } from "../src/operations/deleteEvent"; +import { updateEvent } from "../src/operations/updateEvent"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_EVENT_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateEvent", () => { + it("updates an event's schema", async () => { + const name = `distilled_resend_updateEvent_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ name }); + if (!created.id) { + return yield* Effect.die("createEvent did not return an id"); + } + createdId = created.id; + return yield* updateEvent({ + identifier: created.id, + schema: { + type: "object", + properties: { + user_id: { type: "string" }, + plan: { type: "string" }, + }, + required: ["user_id"], + }, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(typeof result.id).toBe("string"); + }); + + it("clears an event's schema by setting it to null", async () => { + const name = `distilled_resend_updateEvent_clear_${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createEvent({ + name, + schema: { + type: "object", + properties: { user_id: { type: "string" } }, + }, + }); + if (!created.id) { + return yield* Effect.die("createEvent did not return an id"); + } + createdId = created.id; + return yield* updateEvent({ + identifier: created.id, + schema: null, + }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteEvent({ identifier: createdId }).pipe(Effect.ignore) + : deleteEvent({ identifier: name }).pipe(Effect.ignore), + ), + ), + ), + ); + + expect(result).toBeDefined(); + }); + + it("fails with NotFound for a non-existent event id", async () => { + const error = await runEffect( + updateEvent({ + identifier: NON_EXISTENT_EVENT_ID, + schema: { type: "object" }, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/test/updateTemplate.test.ts b/packages/resend/test/updateTemplate.test.ts new file mode 100644 index 0000000000..6a4f3d4c90 --- /dev/null +++ b/packages/resend/test/updateTemplate.test.ts @@ -0,0 +1,96 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTemplate } from "../src/operations/createTemplate"; +import { deleteTemplate } from "../src/operations/deleteTemplate"; +import { getTemplate } from "../src/operations/getTemplate"; +import { updateTemplate } from "../src/operations/updateTemplate"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TEMPLATE_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateTemplate", () => { + it("updates an existing template's name and html", async () => { + const originalName = `distilled-resend-updateTemplate-${testRunId}`; + const updatedName = `distilled-resend-updateTemplate-renamed-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name: originalName, + html: "

Original

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + createdId = created.id; + const updated = yield* updateTemplate({ + id: created.id, + name: updatedName, + html: "

Updated {{name}}

", + subject: "Updated subject", + }); + const refetched = yield* getTemplate({ id: created.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(createdId); + expect(result.refetched.name).toBe(updatedName); + expect(result.refetched.subject).toBe("Updated subject"); + }); + + it("fails with NotFound for a non-existent template id", async () => { + const error = await runEffect( + updateTemplate({ + id: NON_EXISTENT_TEMPLATE_ID, + name: `distilled-resend-updateTemplate-missing-${testRunId}`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid variable definition", async () => { + const name = `distilled-resend-updateTemplate-bad-${testRunId}`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createTemplate({ + name, + html: "

Hello

", + }); + if (!created.id) { + return yield* Effect.die("createTemplate did not return an id"); + } + createdId = created.id; + return yield* updateTemplate({ + id: created.id, + variables: [{ key: "", type: "string" }], + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTemplate({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateTopic.test.ts b/packages/resend/test/updateTopic.test.ts new file mode 100644 index 0000000000..b7efbc44c7 --- /dev/null +++ b/packages/resend/test/updateTopic.test.ts @@ -0,0 +1,97 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createTopic } from "../src/operations/createTopic"; +import { deleteTopic } from "../src/operations/deleteTopic"; +import { getTopic } from "../src/operations/getTopic"; +import { updateTopic } from "../src/operations/updateTopic"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_TOPIC_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateTopic", () => { + it("updates a topic's name, description, and visibility", async () => { + const originalName = `distilled-resend-updateTopic-${testRunId}`; + const updatedName = `distilled-resend-updateTopic-renamed-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name: originalName, + default_subscription: "opt_in", + }); + if (!created.id) { + return yield* Effect.die("createTopic did not return an id"); + } + createdId = created.id; + const updated = yield* updateTopic({ + id: created.id, + name: updatedName, + description: "Updated description", + visibility: "private", + }); + const refetched = yield* getTopic({ id: created.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTopic({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(createdId); + expect(result.refetched.name).toBe(updatedName); + expect(result.refetched.description).toBe("Updated description"); + expect(result.refetched.visibility).toBe("private"); + }); + + it("fails with NotFound for a non-existent topic id", async () => { + const error = await runEffect( + updateTopic({ + id: NON_EXISTENT_TOPIC_ID, + name: `distilled-resend-updateTopic-missing-${testRunId}`, + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an empty name", async () => { + const name = `distilled-resend-updateTopic-bad-${testRunId}`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createTopic({ + name, + default_subscription: "opt_in", + }); + if (!created.id) { + return yield* Effect.die("createTopic did not return an id"); + } + createdId = created.id; + return yield* updateTopic({ + id: created.id, + name: "", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteTopic({ id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/updateWebhook.test.ts b/packages/resend/test/updateWebhook.test.ts new file mode 100644 index 0000000000..8c17d0288c --- /dev/null +++ b/packages/resend/test/updateWebhook.test.ts @@ -0,0 +1,95 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createWebhook } from "../src/operations/createWebhook"; +import { deleteWebhook } from "../src/operations/deleteWebhook"; +import { getWebhook } from "../src/operations/getWebhook"; +import { updateWebhook } from "../src/operations/updateWebhook"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_WEBHOOK_ID = "00000000-0000-4000-8000-000000000000"; + +describe("updateWebhook", () => { + it("updates a webhook's endpoint and status", async () => { + const originalEndpoint = `https://example.com/distilled-resend-updateWebhook-${testRunId}`; + const updatedEndpoint = `https://example.com/distilled-resend-updateWebhook-renamed-${testRunId}`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint: originalEndpoint, + events: ["email.sent"], + }); + if (!created.id) { + return yield* Effect.die("createWebhook did not return an id"); + } + createdId = created.id; + const updated = yield* updateWebhook({ + webhook_id: created.id, + endpoint: updatedEndpoint, + status: "disabled", + }); + const refetched = yield* getWebhook({ webhook_id: created.id }); + return { updated, refetched }; + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteWebhook({ webhook_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result.updated).toBeDefined(); + expect(result.updated.id).toBe(createdId); + expect(result.refetched.endpoint).toBe(updatedEndpoint); + expect(result.refetched.status).toBe("disabled"); + }); + + it("fails with NotFound for a non-existent webhook id", async () => { + const error = await runEffect( + updateWebhook({ + webhook_id: NON_EXISTENT_WEBHOOK_ID, + status: "disabled", + }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); + + it("fails with UnprocessableEntity for an invalid endpoint URL", async () => { + const endpoint = `https://example.com/distilled-resend-updateWebhook-bad-${testRunId}`; + let createdId: string | undefined; + + const error = await runEffect( + Effect.gen(function* () { + const created = yield* createWebhook({ + endpoint, + events: ["email.sent"], + }); + if (!created.id) { + return yield* Effect.die("createWebhook did not return an id"); + } + createdId = created.id; + return yield* updateWebhook({ + webhook_id: created.id, + endpoint: "not-a-valid-url", + }); + }) + .pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteWebhook({ webhook_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ) + .pipe(Effect.flip), + ); + + expect(error._tag).toBe("UnprocessableEntity"); + }); +}); diff --git a/packages/resend/test/verifyDomain.test.ts b/packages/resend/test/verifyDomain.test.ts new file mode 100644 index 0000000000..08f4bd743d --- /dev/null +++ b/packages/resend/test/verifyDomain.test.ts @@ -0,0 +1,43 @@ +import { Effect } from "effect"; +import { describe, expect, it } from "vitest"; +import { createDomain } from "../src/operations/createDomain"; +import { deleteDomain } from "../src/operations/deleteDomain"; +import { verifyDomain } from "../src/operations/verifyDomain"; +import { runEffect, testRunId } from "./setup"; + +const NON_EXISTENT_DOMAIN_ID = "00000000-0000-4000-8000-000000000000"; + +describe("verifyDomain", () => { + it("triggers verification on an existing domain", async () => { + const name = `distilled-resend-verifyDomain-${testRunId}.example.com`; + let createdId: string | undefined; + + const result = await runEffect( + Effect.gen(function* () { + const created = yield* createDomain({ name }); + createdId = created.id; + expect(typeof createdId).toBe("string"); + return yield* verifyDomain({ domain_id: createdId as string }); + }).pipe( + Effect.ensuring( + Effect.suspend(() => + createdId + ? deleteDomain({ domain_id: createdId }).pipe(Effect.ignore) + : Effect.void, + ), + ), + ), + ); + + expect(result).toBeDefined(); + expect(result.id).toBe(createdId); + }); + + it("fails with NotFound for a non-existent domain id", async () => { + const error = await runEffect( + verifyDomain({ domain_id: NON_EXISTENT_DOMAIN_ID }).pipe(Effect.flip), + ); + + expect(error._tag).toBe("NotFound"); + }); +}); diff --git a/packages/resend/tsconfig.json b/packages/resend/tsconfig.json new file mode 100644 index 0000000000..3760701b6a --- /dev/null +++ b/packages/resend/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*.ts" + ], + "compilerOptions": { + "outDir": "./lib", + "rootDir": "./src", + "paths": { + "~/*": [ + "./src/*" + ] + } + }, + "references": [ + { + "path": "../core" + } + ] +} \ No newline at end of file diff --git a/packages/resend/tsconfig.test.json b/packages/resend/tsconfig.test.json new file mode 100644 index 0000000000..b2af39b257 --- /dev/null +++ b/packages/resend/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig.base.json", + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ], + "compilerOptions": { + "rootDir": ".", + "noEmit": true, + "paths": { + "~/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/packages/resend/vitest.config.ts b/packages/resend/vitest.config.ts new file mode 100644 index 0000000000..fdad534022 --- /dev/null +++ b/packages/resend/vitest.config.ts @@ -0,0 +1,17 @@ +import { config } from "dotenv"; +import { resolve } from "path"; + +config({ path: resolve(__dirname, "../../.env") }); +config({ path: resolve(__dirname, ".env") }); + +export default { + test: { + include: ["test/**/*.test.ts"], + testTimeout: 120000, + }, + resolve: { + alias: { + "~": new URL("./src", import.meta.url).pathname, + }, + }, +};