feat: add contactBooks to sdk, add delete campaign public endpoint#352
feat: add contactBooks to sdk, add delete campaign public endpoint#352magicspon wants to merge 7 commits intousesend:mainfrom
Conversation
|
@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. |
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds 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
Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ 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. Comment |
There was a problem hiding this comment.
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 usesendas a parameter property already assignsthis.usesend. The explicitthis.usesend = usesendon Line 53 is unnecessary.✏️ Suggested fix
export class ContactBooks { - constructor(private readonly usesend: UseSend) { - this.usesend = usesend; - } + constructor(private readonly usesend: UseSend) {}
apps/web/src/server/public-api/api/campaigns/delete-campaign.ts
Outdated
Show resolved
Hide resolved
| function deleteCampaign(app: PublicAPIApp) { | ||
| app.openapi(route, async (c) => { | ||
| const campaignId = c.req.param("campaignId"); | ||
|
|
||
| const campaign = await deleteCamapaignService(campaignId); | ||
|
|
||
| return c.json(campaign); | ||
| }); | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check how other campaign endpoints handle authorization/team scoping
rg -n "teamId" apps/web/src/server/public-api/api/campaigns/ -C3Repository: 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.tsRepository: 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 fRepository: 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 10Repository: usesend/useSend
Length of output: 865
🏁 Script executed:
# Check the middleware that extracts team context
cat apps/web/src/server/public-api/hono.tsRepository: 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 OverviewGreptile SummaryThis PR adds a public API endpoint for deleting campaigns (
Overall, these changes extend the existing public API + SDK pattern (route definition via Confidence Score: 4/5
|
| @@ -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"; | |||
There was a problem hiding this comment.
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.
| import { deleteCampaign as deleteCamapaignService } from "~/server/service/campaign-service"; | |
| import { deleteCampaign as deleteCampaignService } from "~/server/service/campaign-service"; |
| type GetAllCampaignsQuery = { | ||
| page?: string; | ||
| status?: string; | ||
| search?: string; | ||
| }; |
There was a problem hiding this comment.
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.
| type GetAllCampaignsQuery = { | |
| page?: string; | |
| status?: string; | |
| search?: string; | |
| }; | |
| type GetAllCampaignsQuery = { | |
| page?: string; | |
| status?: paths["/v1/campaigns"]["get"]["parameters"]["query"]["status"]; | |
| search?: string; | |
| }; |
| type CreateCampaignResponseSuccess = | ||
| paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"]; | ||
| paths["/v1/campaigns"]["post"]["responses"]["200"]["content"]["application/json"]; | ||
|
|
There was a problem hiding this comment.
Inconsistent indentation
CreateCampaignResponseSuccess was reindented with a tab, which breaks the file’s existing formatting consistency and can create noisy diffs later.
| 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!
There was a problem hiding this comment.
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: Addenumconstraint to campaignstatusfield in response schemas within the OpenAPI spec.The query parameter on line 1274 correctly uses the enum union type (
"DRAFT" | "SCHEDULED" | ...), but the responsestatusfield (lines ~1300, 1358, 1425, 1479) is typed as plainstring. This weakens type safety — SDK consumers lose autocomplete and compile-time checks on campaign status values. The root cause is inapps/docs/api-reference/openapi.json: thestatusparameter has an enum constraint while the response schemas lack it. Sincepackages/sdk/types/schema.d.tsis auto-generated by openapi-typescript from this spec, the OpenAPI definition must be updated to include the enum constraint on the responsestatusfields.
apps/docs/api-reference/openapi.json
Outdated
| "subject": { "type": "string" }, | ||
| "createdAt": { "type": "string", "format": "date-time" }, | ||
| "updatedAt": { "type": "string", "format": "date-time" }, | ||
| "status": { "type": "string" }, |
There was a problem hiding this comment.
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.
| "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.
There was a problem hiding this comment.
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
getContactBookutility throwsBAD_REQUESTandNOT_FOUNDerrors, but the OpenAPI route definition only declares a 200 response. Other endpoints like/v1/contactBooks/{contactBookId}(inschema.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.tsroute.apps/web/src/server/public-api/index.ts (1)
31-32: Minor naming inconsistency between bulk handlers.
bulkAddContactsHandleincludes aHandlesuffix whilebulkDeleteContactsdoes not. Every other handler in this file uses a consistent naming style without a suffix (e.g.,deleteContact,addContact). Consider renamingbulkAddContactsHandle→bulkAddContactsinbulk-add-contacts.tsfor consistency.
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
Refactors
Written for commit a5d5726. Summary will update on new commits.
Summary by CodeRabbit
New Features
SDK
Documentation