Skip to content

feat: add contactBooks to sdk, add delete campaign public endpoint#352

Open
magicspon wants to merge 7 commits intousesend:mainfrom
magicspon:feat-missing-sdk-features
Open

feat: add contactBooks to sdk, add delete campaign public endpoint#352
magicspon wants to merge 7 commits intousesend:mainfrom
magicspon:feat-missing-sdk-features

Conversation

@magicspon
Copy link
Contributor

@magicspon magicspon commented Feb 10, 2026

Updates the SDK to include some extra campaign endpints
Adds a new contactBooks sdk.


Summary by cubic

Adds ContactBooks to the SDK, bulk contact add/delete APIs, and campaign listing/deletion. Publishes a public DELETE /v1/campaigns/{campaignId} and updates OpenAPI/docs with tighter schemas and examples.

  • New Features

    • Public API: delete campaign (DELETE /v1/campaigns/{campaignId}); bulk contacts (POST/DELETE /v1/contactBooks/{contactBookId}/contacts/bulk; 1–1000).
    • SDK: ContactBooks client; Contacts.bulkCreate/bulkDelete; Campaigns.getAll({ page, status, search }) and delete(campaignId).
    • Docs/OpenAPI: new reference pages; status enums and pagination filters; optional Idempotency-Key for email sends.
  • Refactors

    • Renamed public API handlers for campaign pause/resume/delete for consistency.
    • Polished OpenAPI per review and regenerated SDK types.

Written for commit a5d5726. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • API: Delete campaign endpoint; GET campaigns with pagination, status, and search; bulk add/delete contacts and full Contact Books surface.
    • Server: Public routes for campaign deletion and contact bulk operations; contacts router exposes bulk-delete mutation; bulk-delete service emits deletion events.
  • SDK

    • Added ContactBooks client and contactBooks on main client; Contacts: bulkCreate/bulkDelete; Campaigns: list and delete.
  • Documentation

    • Added API reference pages for campaign delete and contact bulk operations; nav updated.

@vercel
Copy link

vercel bot commented Feb 10, 2026

@magicspon is attempting to deploy a commit to the kmkoushik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds OpenAPI docs and MDX pages for contact-book bulk operations and campaign deletion/listing. Implements server handlers and public-api routes for DELETE /v1/campaigns/{campaignId}, POST/DELETE /v1/contactBooks/{contactBookId}/contacts/bulk, and related router/service changes. Extends the SDK with Campaigns.getAll, Campaigns.delete, a new ContactBooks client, exposes ContactBooks on UseSend, updates types/schema (including Idempotency-Key header support), and updates docs navigation.

Possibly related PRs

Suggested labels

codex

Suggested reviewers

  • KMKoushik
🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding contactBooks to the SDK and adding the delete campaign public endpoint, both of which are significant parts of this changeset.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 10 files

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🤖 Fix all issues with AI agents
In `@apps/docs/api-reference/openapi.json`:
- Around line 2021-2109: The DELETE /v1/campaigns/{campaignId} operation only
documents a 200 response; add the standard error responses (403 and 404) to this
operation to match the other delete endpoints (e.g., /v1/domains/{id} and
/v1/contactBooks/{contactBookId}). Add "403" and "404" entries under "responses"
for the DELETE operation with descriptive "description" fields and the same
content/schema objects used by those other endpoints (or reference the shared
error schema in components/schemas) so SDKs and docs include the authorization
and not-found error cases.

In `@apps/web/src/server/public-api/api/campaigns/delete-campaign.ts`:
- Line 3: Rename the misspelled import alias `deleteCamapaignService` to
`deleteCampaignService` in the import statement and update all usages (e.g., the
call on line that references `deleteCamapaignService`, likely the invocation
around the handler at or near line 39) to use the corrected name
`deleteCampaignService` so the imported `deleteCampaign` service is referenced
consistently.
- Around line 35-43: The deleteCampaign route currently calls
deleteCamapaignService(campaignId) without team context and lacks not-found
handling; update the route handler (deleteCampaign) to pass the authenticated
team's id as teamId (e.g., deleteCamapaignService({ campaignId, teamId: team.id
})) and then modify deleteCamapaignService to scope its delete to both id and
teamId (use where: { id: campaignId, teamId }) instead of deleting by raw id;
also add existence/RecordNotFound handling in the service or route (try-catch
the Prisma RecordNotFound or validate via getCampaignForTeam-style lookup before
delete) to return a structured 404 when the campaign does not exist or does not
belong to the team.

In `@packages/sdk/src/campaign.ts`:
- Around line 16-20: The GetAllCampaignsQuery type currently declares status as
a plain string which widens the schema enum; update the type so status uses the
generated schema enum instead of string—either replace the manual type with the
generated union from schema.d.ts (the "DRAFT" | "SCHEDULED" | ... type) or
derive the whole query type from the generated paths type
(paths["/v1/campaigns"]["get"]["parameters"]["query"]) so GetAllCampaignsQuery
stays in sync with the schema.
🧹 Nitpick comments (1)
packages/sdk/src/contactBook.ts (1)

51-54: Redundant assignment in constructor.

private readonly usesend as a parameter property already assigns this.usesend. The explicit this.usesend = usesend on Line 53 is unnecessary.

✏️ Suggested fix
 export class ContactBooks {
-  constructor(private readonly usesend: UseSend) {
-    this.usesend = usesend;
-  }
+  constructor(private readonly usesend: UseSend) {}

Comment on lines 35 to 43
function deleteCampaign(app: PublicAPIApp) {
app.openapi(route, async (c) => {
const campaignId = c.req.param("campaignId");

const campaign = await deleteCamapaignService(campaignId);

return c.json(campaign);
});
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Check how other campaign endpoints handle authorization/team scoping
rg -n "teamId" apps/web/src/server/public-api/api/campaigns/ -C3

Repository: usesend/useSend

Length of output: 5444


🏁 Script executed:

# Check the delete-campaign service implementation
cat apps/web/src/server/public-api/api/campaigns/delete-campaign.ts

Repository: usesend/useSend

Length of output: 1141


🏁 Script executed:

# Find campaign-service.ts and check the deleteCampaignService implementation
fd "campaign-service" apps/web/src/server/ --type f

Repository: usesend/useSend

Length of output: 106


🏁 Script executed:

# Read the deleteCampaignService implementation
cat -n apps/web/src/server/service/campaign-service.ts | sed -n '665,695p'

Repository: usesend/useSend

Length of output: 919


🏁 Script executed:

# Check if there's a getCampaignForTeam function that enforces team scoping
rg -n "getCampaignForTeam" apps/web/src/server/service/ -A 10

Repository: usesend/useSend

Length of output: 865


🏁 Script executed:

# Check the middleware that extracts team context
cat apps/web/src/server/public-api/hono.ts

Repository: usesend/useSend

Length of output: 4325


Pass team context to service and implement ownership verification.

The endpoint extracts the authenticated team from middleware but fails to pass it to the delete service. Unlike all other campaign endpoints (schedule, resume, pause, get, create) which call services with teamId: team.id, this endpoint calls deleteCamapaignService(campaignId) with only the campaign ID.

The underlying service (campaign-service.ts:672-686) deletes by raw ID with no team scoping. Compare this to getCampaignForTeam (line 282-290), which properly enforces ownership with where: { id: campaignId, teamId }.

Any authenticated API key holder can delete campaigns from other teams. Pass teamId: team.id to the service and have the service include both id and teamId in the delete condition.

Additionally, the endpoint has no error handling for non-existent campaigns — Prisma's delete will throw a RecordNotFound error resulting in a raw 500 response instead of a structured 404. Add try-catch or use a helper that validates the campaign exists before deletion.

🤖 Prompt for AI Agents
In `@apps/web/src/server/public-api/api/campaigns/delete-campaign.ts` around lines
35 - 43, The deleteCampaign route currently calls
deleteCamapaignService(campaignId) without team context and lacks not-found
handling; update the route handler (deleteCampaign) to pass the authenticated
team's id as teamId (e.g., deleteCamapaignService({ campaignId, teamId: team.id
})) and then modify deleteCamapaignService to scope its delete to both id and
teamId (use where: { id: campaignId, teamId }) instead of deleting by raw id;
also add existence/RecordNotFound handling in the service or route (try-catch
the Prisma RecordNotFound or validate via getCampaignForTeam-style lookup before
delete) to return a structured 404 when the campaign does not exist or does not
belong to the team.

@greptile-apps
Copy link

greptile-apps bot commented Feb 10, 2026

Greptile Overview

Greptile Summary

This PR adds a public API endpoint for deleting campaigns (DELETE /v1/campaigns/{campaignId}) and wires it into the Hono public API router and docs/OpenAPI spec. It also expands the JS/TS SDK by:

  • Adding Campaigns.getAll() with query params (page/status/search)
  • Adding Campaigns.delete()
  • Introducing a new ContactBooks SDK resource and exporting it from the package
  • Updating generated OpenAPI TypeScript types to include the new endpoints and parameters (including idempotency header fields on some POSTs)

Overall, these changes extend the existing public API + SDK pattern (route definition via @hono/zod-openapi, OpenAPI JSON driving packages/sdk/types/schema.d.ts, and thin SDK wrappers calling UseSend HTTP helpers).

Confidence Score: 4/5

  • This PR is generally safe to merge; only minor consistency/typing issues were found.
  • Endpoint wiring and SDK wrappers follow existing patterns, and the OpenAPI/type updates are coherent. Remaining findings are mainly naming/formatting consistency and a too-loose query type that reduces type-safety.
  • packages/sdk/src/campaign.ts, apps/web/src/server/public-api/api/campaigns/delete-campaign.ts

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 3 comments

Edit Code Review Agent Settings | Greptile

@@ -0,0 +1,45 @@
import { createRoute, z } from "@hono/zod-openapi";
import { PublicAPIApp } from "~/server/public-api/hono";
import { deleteCampaign as deleteCamapaignService } from "~/server/service/campaign-service";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misspelled service alias

The import alias deleteCamapaignService is misspelled ("Camapaign"). This is easy to propagate to other callers and makes grepping/refactoring error-prone. Consider renaming it to deleteCampaignService for consistency with the actual exported symbol.

Suggested change
import { deleteCampaign as deleteCamapaignService } from "~/server/service/campaign-service";
import { deleteCampaign as deleteCampaignService } from "~/server/service/campaign-service";

Comment on lines 16 to 20
type GetAllCampaignsQuery = {
page?: string;
status?: string;
search?: string;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Query type too loose

GetAllCampaignsQuery.status is typed as string, but the generated OpenAPI types constrain it to the known enum ("DRAFT" | "SCHEDULED" | ...). As-is, the SDK will accept invalid statuses at compile time, defeating the point of the schema types.

Suggested change
type GetAllCampaignsQuery = {
page?: string;
status?: string;
search?: string;
};
type GetAllCampaignsQuery = {
page?: string;
status?: paths["/v1/campaigns"]["get"]["parameters"]["query"]["status"];
search?: string;
};

Comment on lines 13 to 15
type CreateCampaignResponseSuccess =
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation

CreateCampaignResponseSuccess was reindented with a tab, which breaks the file’s existing formatting consistency and can create noisy diffs later.

Suggested change
type CreateCampaignResponseSuccess =
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];
type CreateCampaignResponseSuccess =
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"];

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/docs/api-reference/openapi.json`:
- Line 1764: The response schemas for the campaign endpoints currently declare
the "status" property as a plain string; update each response "status" property
(GET list item, POST create response, GET detail response, DELETE response) to
use the same enum as the query parameter—i.e., enforce the values DRAFT,
SCHEDULED, IN_PROGRESS, PAUSED, COMPLETED, CANCELLED—by referencing the existing
enum definition (or replacing the "type":"string" with an equivalent
"type":"string","enum":[...]) so the contract matches the query parameter enum
exactly.

In `@packages/sdk/src/campaign.ts`:
- Around line 13-14: The type alias CreateCampaignResponseSuccess uses a tab
indentation on its continuation line; change the tab to two spaces to match the
project's 2-space indent convention (update the line defining
paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"]
so it is indented with two spaces rather than a tab).
🧹 Nitpick comments (2)
packages/sdk/types/schema.d.ts (2)

1453-1504: Campaign DELETE response shape is inconsistent with other DELETE endpoints.

Domain DELETE (line 384) and ContactBook DELETE (line 926) both return { id, success, message }, while Campaign DELETE returns the full campaign object. Consider standardizing the DELETE response shape across all resource types in the OpenAPI spec for a more predictable SDK experience.


1298-1302: Add enum constraint to campaign status field in response schemas within the OpenAPI spec.

The query parameter on line 1274 correctly uses the enum union type ("DRAFT" | "SCHEDULED" | ...), but the response status field (lines ~1300, 1358, 1425, 1479) is typed as plain string. This weakens type safety — SDK consumers lose autocomplete and compile-time checks on campaign status values. The root cause is in apps/docs/api-reference/openapi.json: the status parameter has an enum constraint while the response schemas lack it. Since packages/sdk/types/schema.d.ts is auto-generated by openapi-typescript from this spec, the OpenAPI definition must be updated to include the enum constraint on the response status fields.

"subject": { "type": "string" },
"createdAt": { "type": "string", "format": "date-time" },
"updatedAt": { "type": "string", "format": "date-time" },
"status": { "type": "string" },
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Campaign status in the GET list response should use the same enum as the query parameter.

The query parameter (Line 1722) correctly defines status as an enum of DRAFT | SCHEDULED | IN_PROGRESS | PAUSED | COMPLETED | CANCELLED, but the response item's status field (Line 1764) is typed as a bare "type": "string". This loses the contract for SDK consumers and documentation readers.

Proposed fix
-"status": { "type": "string" },
+"status": {
+  "type": "string",
+  "enum": [
+    "DRAFT",
+    "SCHEDULED",
+    "IN_PROGRESS",
+    "PAUSED",
+    "COMPLETED",
+    "CANCELLED"
+  ]
+},

The same issue applies to the status field in the POST create response (Line 1870), GET detail response (Line 1961), and DELETE response (Line 2038).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"status": { "type": "string" },
"status": {
"type": "string",
"enum": [
"DRAFT",
"SCHEDULED",
"IN_PROGRESS",
"PAUSED",
"COMPLETED",
"CANCELLED"
]
},
🤖 Prompt for AI Agents
In `@apps/docs/api-reference/openapi.json` at line 1764, The response schemas for
the campaign endpoints currently declare the "status" property as a plain
string; update each response "status" property (GET list item, POST create
response, GET detail response, DELETE response) to use the same enum as the
query parameter—i.e., enforce the values DRAFT, SCHEDULED, IN_PROGRESS, PAUSED,
COMPLETED, CANCELLED—by referencing the existing enum definition (or replacing
the "type":"string" with an equivalent "type":"string","enum":[...]) so the
contract matches the query parameter enum exactly.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In `@apps/docs/api-reference/openapi.json`:
- Around line 1520-1627: Add consistent error responses and a minimum items
constraint for the bulk contact endpoints: update the OpenAPI operation objects
under "/v1/contactBooks/{contactBookId}/contacts/bulk" for both the "delete" and
"post" operations to include "403" and "404" responses (matching the style used
by sibling endpoints) and modify the requestBody schema for the "post" operation
(the array items schema currently with "maxItems": 1000) to include "minItems":
1 to prevent empty-array submissions; locate these changes in the "delete" and
"post" operation objects in the existing diff to ensure names like the path
string and the "requestBody" / "responses" keys are updated accordingly.

In `@apps/web/src/server/api/routers/contacts.ts`:
- Around line 193-204: The bulkDeleteContacts TRPC mutation currently validates
input.contactIds with only .min(1), allowing unbounded arrays; update the input
schema on contactBookProcedure for bulkDeleteContacts to enforce an upper bound
(e.g., .max(1000)) so it matches the public API cap and prevents huge payloads,
and ensure contactService.bulkDeleteContactsInContactBook continues to receive
input.contactIds and contactBook.id/team.id unchanged; modify the z.object({
contactIds: z.array(z.string()).min(1) }) to
z.array(z.string()).min(1).max(1000) (or another agreed cap) to apply the limit.
🧹 Nitpick comments (2)
apps/web/src/server/public-api/api/contacts/bulk-delete-contacts.ts (1)

36-48: Consider documenting 403/404 error responses in the route schema.

The getContactBook utility throws BAD_REQUEST and NOT_FOUND errors, but the OpenAPI route definition only declares a 200 response. Other endpoints like /v1/contactBooks/{contactBookId} (in schema.d.ts) document 403 and 404 responses. Adding these would improve the generated API docs and SDK error handling.

This also applies to the sibling bulk-add-contacts.ts route.

apps/web/src/server/public-api/index.ts (1)

31-32: Minor naming inconsistency between bulk handlers.

bulkAddContactsHandle includes a Handle suffix while bulkDeleteContacts does not. Every other handler in this file uses a consistent naming style without a suffix (e.g., deleteContact, addContact). Consider renaming bulkAddContactsHandlebulkAddContacts in bulk-add-contacts.ts for consistency.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant