-
-
Notifications
You must be signed in to change notification settings - Fork 216
add campaign api #274
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
add campaign api #274
Conversation
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughAdds public API endpoints for campaigns: create (/v1/campaigns), get by ID, schedule, pause, and resume, with OpenAPI specs and Zod schemas. Introduces campaign validation, scheduling date parsing, and response schemas. Updates campaign service to support API-created campaigns, including domain/API key validation, unsubscribe placeholder checks, HTML/content rendering utilities, and per-contact rendering. Exposes createCampaignFromApi and getCampaignForTeam. Extends campaign update router to accept html and return html; duplicate now copies html. UI disables editing for API-created campaigns based on new Campaign.isApi field added via Prisma schema and migration. Public API index wires new routes. Possibly related PRs
Suggested labels
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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 |
Deploying usesend with
|
Latest commit: |
64e4f6a
|
Status: | ✅ Deploy successful! |
Preview URL: | https://6290c179.usesend.pages.dev |
Branch Preview URL: | https://km-2025-10-12-campaign-api.usesend.pages.dev |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 6
🧹 Nitpick comments (13)
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts (1)
53-56
: Avoid redundant DB read
getCampaignForTeam
result is not used. Drop it to save a query.- await getCampaignForTeam({ - campaignId, - teamId: team.id, - }); + // No-op fetch removedapps/docs/api-reference/openapi.json (3)
1914-2014
: Document “content OR html” and natural‑language scheduledAtAlign schema with server validation.
"schema": { "type": "object", "properties": { @@ "scheduledAt": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" }, @@ }, "required": [ "name", "from", "subject", "contactBookId" ], + "oneOf": [ + { "required": ["content"] }, + { "required": ["html"] } + ] }
2310-2341
: Clarify scheduledAt format for scheduling endpointServer accepts natural language; reflect in docs.
"scheduledAt": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "ISO 8601 or natural language (e.g., 'tomorrow 9am', 'next monday 10:30')" },
2157-2437
: Consider adding standard error responses (400/403/404) for new campaign endpointsOpenAPI currently lists only 200. Add error shapes to help clients.
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts (2)
65-68
: Drop unused fetch to save a query
getCampaignForTeam
is called but unused.- await getCampaignForTeam({ - campaignId, - teamId: team.id, - }); + // No-op fetch removed
29-36
: Optional: allow omitting body for “send now”You can set
required: false
to permit no body;{}
still validates today.- body: { - required: true, + body: { + required: false, content: {apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx (2)
182-184
: Prefer readOnly over disabled to allow text selection/copyDisabled inputs are unfocusable and can hurt accessibility. Use readOnly (with aria-readonly) and keep visual style.
- disabled={isApiCampaign} - readOnly={isApiCampaign} + readOnly={isApiCampaign} + aria-readonly={isApiCampaign}Apply similarly to Subject, From, Reply To, and Preview fields.
Also applies to: 260-262, 299-301, 335-337, 374-376
431-453
: Nice gating; consider linking to API docsAdd a link to the API docs to guide users on how to update API-created campaigns.
apps/web/src/server/public-api/schemas/campaign-schema.ts (1)
44-51
: Cap scheduledAt length to avoid heavy parsingLimit input length in schemas (and optionally validate in parser).
Based on learnings
- scheduledAt: z - .string() + scheduledAt: z + .string() + .max(256) .optional() @@ export const campaignScheduleSchema = z.object({ - scheduledAt: z - .string() + scheduledAt: z + .string() + .max(256) .optional()Optionally add a defensive check in
parseScheduledAt
:export const parseScheduledAt = (scheduledAt?: string): Date | undefined => { if (!scheduledAt) return undefined; + if (scheduledAt.length > 256) { + throw new UnsendApiError({ + code: "BAD_REQUEST", + message: "scheduledAt is too long", + }); + }Also applies to: 57-65
apps/web/src/server/service/campaign-service.ts (4)
32-36
: Precompile regexes and avoid re-creating them on replaceCurrent pattern rebuilds RegExp objects during replacement. Precompile with "gi" and reuse to reduce overhead. Static analysis warning is benign here (constants), but this also silences it.
-const CAMPAIGN_UNSUB_PLACEHOLDER_REGEXES = - CAMPAIGN_UNSUB_PLACEHOLDER_TOKENS.map((placeholder) => { - const inner = placeholder.replace(/[{}]/g, "").trim(); - return new RegExp(`\\{\\{\\s*${inner}\\s*\\}}`, "i"); - }); +const CAMPAIGN_UNSUB_PLACEHOLDER_REGEXES = + CAMPAIGN_UNSUB_PLACEHOLDER_TOKENS.map((placeholder) => { + const inner = placeholder.replace(/[{}]/g, "").trim(); + return new RegExp(`\\{\\{\\s*${inner}\\s*\\}}`, "gi"); + }); -function replaceUnsubscribePlaceholders(html: string, url: string) { - return CAMPAIGN_UNSUB_PLACEHOLDER_REGEXES.reduce((acc, regex) => { - return acc.replace(new RegExp(regex.source, "gi"), url); - }, html); -} +function replaceUnsubscribePlaceholders(html: string, url: string) { + const safeUrl = escapeHtml(url); + return CAMPAIGN_UNSUB_PLACEHOLDER_REGEXES.reduce( + (acc, regex) => acc.replace(regex, safeUrl), + html + ); +}Note: escapeHtml is introduced below.
Also applies to: 49-53
246-256
: Message clarity for API create-time validationThe error says “before sending” but this is enforced at creation. Consider: “Campaign must include an unsubscribe link.”
- message: "Campaign must include an unsubscribe link before sending", + message: "Campaign must include an unsubscribe link",
258-276
: Validate batchSize boundsEnforce positive integer and reasonable upper bound to avoid overload.
- ...(typeof batchSize === "number" ? { batchSize } : {}), + ...(typeof batchSize === "number" ? { batchSize } : {}),Add pre-validation above create:
if (typeof batchSize === "number") { if (!Number.isInteger(batchSize) || batchSize <= 0 || batchSize > 10000) { throw new UnsendApiError({ code: "BAD_REQUEST", message: "batchSize must be a positive integer <= 10000", }); } }
119-151
: Avoid per-contact JSON.parse/renderer instantiation (hot path)This runs for every contact. Consider parsing content once per campaign batch and reusing the JSON object (or a renderer instance) for render calls.
E.g., parse in CampaignBatchService before the loop and pass parsedContent into processContactEmail, or cache by campaign.id within the batch execution.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (13)
apps/docs/api-reference/openapi.json
(1 hunks)apps/web/prisma/migrations/20251013114734_add_api_to_campaign/migration.sql
(1 hunks)apps/web/prisma/schema.prisma
(1 hunks)apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
(13 hunks)apps/web/src/server/api/routers/campaign.ts
(3 hunks)apps/web/src/server/public-api/api/campaigns/create-campaign.ts
(1 hunks)apps/web/src/server/public-api/api/campaigns/get-campaign.ts
(1 hunks)apps/web/src/server/public-api/api/campaigns/pause-campaign.ts
(1 hunks)apps/web/src/server/public-api/api/campaigns/resume-campaign.ts
(1 hunks)apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
(1 hunks)apps/web/src/server/public-api/index.ts
(2 hunks)apps/web/src/server/public-api/schemas/campaign-schema.ts
(1 hunks)apps/web/src/server/service/campaign-service.ts
(4 hunks)
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{js,jsx,ts,tsx}
📄 CodeRabbit inference engine (.cursor/rules/general.mdc)
Include all required imports, and ensure proper naming of key components.
Files:
apps/web/src/server/public-api/api/campaigns/create-campaign.ts
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
apps/web/src/server/public-api/schemas/campaign-schema.ts
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts
apps/web/src/server/public-api/api/campaigns/get-campaign.ts
apps/web/src/server/api/routers/campaign.ts
apps/web/src/server/service/campaign-service.ts
apps/web/src/server/public-api/index.ts
apps/web/src/server/public-api/api/campaigns/pause-campaign.ts
**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
**/*.{ts,tsx}
: TypeScript-first: use .ts/.tsx for source code (avoid JavaScript source files)
Use 2-space indentation and semicolons (Prettier 3 enforces these)
Adhere to @usesend/eslint-config; fix all ESLint warnings (CI fails on warnings)
Do not use dynamic imports; always place imports at the top of the module
Files:
apps/web/src/server/public-api/api/campaigns/create-campaign.ts
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
apps/web/src/server/public-api/schemas/campaign-schema.ts
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts
apps/web/src/server/public-api/api/campaigns/get-campaign.ts
apps/web/src/server/api/routers/campaign.ts
apps/web/src/server/service/campaign-service.ts
apps/web/src/server/public-api/index.ts
apps/web/src/server/public-api/api/campaigns/pause-campaign.ts
**/*.{ts,tsx,md}
📄 CodeRabbit inference engine (AGENTS.md)
Format code with Prettier 3 (run pnpm format)
Files:
apps/web/src/server/public-api/api/campaigns/create-campaign.ts
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
apps/web/src/server/public-api/schemas/campaign-schema.ts
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts
apps/web/src/server/public-api/api/campaigns/get-campaign.ts
apps/web/src/server/api/routers/campaign.ts
apps/web/src/server/service/campaign-service.ts
apps/web/src/server/public-api/index.ts
apps/web/src/server/public-api/api/campaigns/pause-campaign.ts
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (AGENTS.md)
apps/web/**/*.{ts,tsx}
: In apps/web, use the/ alias for src imports (e.g., import { x } from "/utils/x")
Prefer using tRPC in apps/web unless explicitly asked otherwise
Files:
apps/web/src/server/public-api/api/campaigns/create-campaign.ts
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts
apps/web/src/server/public-api/schemas/campaign-schema.ts
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts
apps/web/src/server/public-api/api/campaigns/get-campaign.ts
apps/web/src/server/api/routers/campaign.ts
apps/web/src/server/service/campaign-service.ts
apps/web/src/server/public-api/index.ts
apps/web/src/server/public-api/api/campaigns/pause-campaign.ts
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
Name React component files in PascalCase (e.g., AppSideBar.tsx)
Files:
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx
🧬 Code graph analysis (10)
apps/web/src/server/public-api/api/campaigns/create-campaign.ts (3)
apps/web/src/server/public-api/schemas/campaign-schema.ts (4)
campaignCreateSchema
(31-55)campaignResponseSchema
(70-97)CampaignCreateInput
(67-67)parseScheduledAt
(10-29)apps/web/src/server/public-api/hono.ts (1)
PublicAPIApp
(136-136)apps/web/src/server/service/campaign-service.ts (3)
createCampaignFromApi
(163-280)scheduleCampaign
(384-475)getCampaignForTeam
(282-329)
apps/web/src/app/(dashboard)/campaigns/[campaignId]/edit/page.tsx (1)
packages/email-editor/src/editor.tsx (1)
Editor
(74-116)
apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts (3)
apps/web/src/server/public-api/schemas/campaign-schema.ts (3)
campaignScheduleSchema
(57-65)CampaignScheduleInput
(68-68)parseScheduledAt
(10-29)apps/web/src/server/service/campaign-service.ts (2)
scheduleCampaign
(384-475)getCampaignForTeam
(282-329)apps/web/src/server/public-api/hono.ts (1)
PublicAPIApp
(136-136)
apps/web/src/server/public-api/schemas/campaign-schema.ts (1)
apps/web/src/server/public-api/api-error.ts (1)
UnsendApiError
(62-75)
apps/web/src/server/public-api/api/campaigns/resume-campaign.ts (2)
apps/web/src/server/service/campaign-service.ts (2)
resumeCampaign
(503-534)getCampaignForTeam
(282-329)apps/web/src/server/public-api/hono.ts (1)
PublicAPIApp
(136-136)
apps/web/src/server/public-api/api/campaigns/get-campaign.ts (3)
apps/web/src/server/public-api/schemas/campaign-schema.ts (1)
campaignResponseSchema
(70-97)apps/web/src/server/public-api/hono.ts (1)
PublicAPIApp
(136-136)apps/web/src/server/service/campaign-service.ts (1)
getCampaignForTeam
(282-329)
apps/web/src/server/api/routers/campaign.ts (1)
packages/email-editor/src/renderer.tsx (1)
EmailRenderer
(172-774)
apps/web/src/server/service/campaign-service.ts (2)
packages/email-editor/src/renderer.tsx (1)
EmailRenderer
(172-774)apps/web/src/server/service/domain-service.ts (2)
validateApiKeyDomainAccess
(130-152)validateDomainFromEmail
(85-128)
apps/web/src/server/public-api/index.ts (1)
apps/web/src/server/service/campaign-service.ts (3)
scheduleCampaign
(384-475)pauseCampaign
(477-501)resumeCampaign
(503-534)
apps/web/src/server/public-api/api/campaigns/pause-campaign.ts (2)
apps/web/src/server/service/campaign-service.ts (2)
pauseCampaign
(477-501)getCampaignForTeam
(282-329)apps/web/src/server/public-api/hono.ts (1)
PublicAPIApp
(136-136)
🪛 ast-grep (0.39.6)
apps/web/src/server/service/campaign-service.ts
[warning] 34-34: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(\\{\\{\\s*${inner}\\s*\\}}
, "i")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
[warning] 50-50: Regular expression constructed from variable input detected. This can lead to Regular Expression Denial of Service (ReDoS) attacks if the variable contains malicious patterns. Use libraries like 'recheck' to validate regex safety or use static patterns.
Context: new RegExp(regex.source, "gi")
Note: [CWE-1333] Inefficient Regular Expression Complexity [REFERENCES]
- https://owasp.org/www-community/attacks/Regular_expression_Denial_of_Service_-_ReDoS
- https://cwe.mitre.org/data/definitions/1333.html
(regexp-from-variable)
🔇 Additional comments (6)
apps/web/src/server/public-api/api/campaigns/create-campaign.ts (1)
39-79
: LGTM: create-and-optional-schedule flow is soundValidation, scheduling precedence (sendNow over scheduledAt), and response shaping look good.
apps/web/src/server/api/routers/campaign.ts (1)
243-252
: LGTM: duplicate now copies html tooCopying
html
ensures visual fidelity of duplicated campaigns.apps/web/src/server/service/campaign-service.ts (4)
340-356
: LGTM: robust unsubscribe placeholder validation before sendingUsing prepared HTML + content ensures both editor JSON and raw HTML paths are covered.
405-414
: LGTM: error mapping for invalid content in schedulingGood propagation of parse/render errors into BAD_REQUEST.
743-747
: LGTM: per-contact rendering integrates unsubscribe + variablesCorrectly leverages the new renderer utility for contact-specific HTML.
211-214
: Prisma composite unique verification
Ensure the Prisma schema declares a composite unique key or compound primary key onContactBook(id, teamId)
. If it exists,findUnique({ where: { id: contactBookId, teamId } })
is valid; otherwise usefindFirst
or add the composite constraint.
.mutation(async ({ ctx: { db, team, campaign: campaignOld }, input }) => { | ||
const { campaignId, ...data } = input; | ||
const { campaignId, html: htmlInput, ...data } = input; | ||
if (data.contactBookId) { | ||
const contactBook = await db.contactBook.findUnique({ | ||
where: { id: data.contactBookId }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use ctx.campaign id instead of input; avoid id forgery and missing input
Rely on campaignOld.id
from context (already authorized) instead of input.campaignId
.
As per coding guidelines
- const { campaignId, html: htmlInput, ...data } = input;
+ const { html: htmlInput, ...data } = input;
@@
- const campaign = await db.campaign.update({
- where: { id: campaignId },
- data: campaignUpdateData,
- });
+ const campaign = await db.campaign.update({
+ where: { id: campaignOld.id },
+ data: campaignUpdateData,
+ });
Also applies to: 167-170
🤖 Prompt for AI Agents
In apps/web/src/server/api/routers/campaign.ts around lines 127-131 (and
similarly at lines 167-170), the mutation uses input.campaignId which allows id
forgery or missing input; replace uses of input.campaignId with the authorized
campaign id from the context (campaignOld.id or ctx.campaign.id) everywhere in
this handler so the DB queries and updates always use the server-provided,
authorized campaign id; remove or ignore campaignId from input and adjust
variable extraction accordingly.
if (data.content) { | ||
const jsonContent = data.content ? JSON.parse(data.content) : null; | ||
|
||
const renderer = new EmailRenderer(jsonContent); | ||
html = await renderer.render(); | ||
htmlToSave = await renderer.render(); | ||
} else if (typeof htmlInput === "string") { | ||
htmlToSave = htmlInput; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Guard JSON parsing and drop unnecessary await
Prevent crashes on invalid JSON; render()
is synchronous.
- if (data.content) {
- const jsonContent = data.content ? JSON.parse(data.content) : null;
-
- const renderer = new EmailRenderer(jsonContent);
- htmlToSave = await renderer.render();
- } else if (typeof htmlInput === "string") {
+ if (data.content) {
+ let jsonContent: unknown = null;
+ try {
+ jsonContent = JSON.parse(data.content);
+ } catch {
+ throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid content JSON" });
+ }
+ const renderer = new EmailRenderer(jsonContent as any);
+ htmlToSave = renderer.render();
+ } else if (typeof htmlInput === "string") {
htmlToSave = htmlInput;
}
📝 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.
if (data.content) { | |
const jsonContent = data.content ? JSON.parse(data.content) : null; | |
const renderer = new EmailRenderer(jsonContent); | |
html = await renderer.render(); | |
htmlToSave = await renderer.render(); | |
} else if (typeof htmlInput === "string") { | |
htmlToSave = htmlInput; | |
} | |
if (data.content) { | |
let jsonContent: unknown = null; | |
try { | |
jsonContent = JSON.parse(data.content); | |
} catch { | |
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid content JSON" }); | |
} | |
const renderer = new EmailRenderer(jsonContent as any); | |
htmlToSave = renderer.render(); | |
} else if (typeof htmlInput === "string") { | |
htmlToSave = htmlInput; | |
} |
🤖 Prompt for AI Agents
In apps/web/src/server/api/routers/campaign.ts around lines 149 to 156, the code
currently unconditionally JSON.parse(data.content) which can throw on invalid
JSON and calls await on renderer.render() though render() is synchronous; update
it to first check data.content is a non-empty string, then try to JSON.parse
inside a try/catch and handle parse errors (e.g., set jsonContent = null or
return a validation error), instantiate EmailRenderer with the parsed or null
value, and call renderer.render() without await (assign directly) so no
unnecessary await is used.
import { createRoute, z } from "@hono/zod-openapi"; | ||
import { PublicAPIApp } from "~/server/public-api/hono"; | ||
import { | ||
getCampaignForTeam, | ||
pauseCampaign as pauseCampaignService, | ||
} from "~/server/service/campaign-service"; | ||
import { campaignResponseSchema } from "~/server/public-api/schemas/campaign-schema"; | ||
|
||
const route = createRoute({ | ||
method: "post", | ||
path: "/v1/campaigns/{campaignId}/pause", | ||
request: { | ||
params: z.object({ | ||
campaignId: z | ||
.string() | ||
.min(1) | ||
.openapi({ | ||
param: { | ||
name: "campaignId", | ||
in: "path", | ||
}, | ||
example: "cmp_123", | ||
}), | ||
}), | ||
}, | ||
responses: { | ||
200: { | ||
description: "Pause a campaign", | ||
content: { | ||
"application/json": { | ||
schema: z.object({ | ||
success: z.boolean(), | ||
}), | ||
}, | ||
}, | ||
}, | ||
}, | ||
}); | ||
|
||
function pauseCampaign(app: PublicAPIApp) { | ||
app.openapi(route, async (c) => { | ||
const team = c.var.team; | ||
const campaignId = c.req.param("campaignId"); | ||
|
||
await pauseCampaignService({ | ||
campaignId, | ||
teamId: team.id, | ||
}); | ||
|
||
await getCampaignForTeam({ | ||
campaignId, | ||
teamId: team.id, | ||
}); | ||
|
||
return c.json({ success: true }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove the unused import and redundant query
campaignResponseSchema
is imported but never used, which ESLint treats as an error in this repo. Additionally, the extra getCampaignForTeam
call only repeats the database lookup and its result is discarded. Please drop both to keep the handler lint-clean and avoid the needless query.
-import {
- getCampaignForTeam,
- pauseCampaign as pauseCampaignService,
-} from "~/server/service/campaign-service";
-import { campaignResponseSchema } from "~/server/public-api/schemas/campaign-schema";
+import { pauseCampaign as pauseCampaignService } from "~/server/service/campaign-service";
@@
- await pauseCampaignService({
- campaignId,
- teamId: team.id,
- });
-
- await getCampaignForTeam({
- campaignId,
- teamId: team.id,
- });
+ await pauseCampaignService({
+ campaignId,
+ teamId: team.id,
+ });
🤖 Prompt for AI Agents
In apps/web/src/server/public-api/api/campaigns/pause-campaign.ts around lines
1-55, remove the unused import campaignResponseSchema from the top of the file
and delete the redundant await getCampaignForTeam(...) call inside the route
handler (the call’s result is never used); keep the pauseCampaignService call
and the final return c.json({ success: true }) so the handler behavior remains
the same and ESLint no longer flags the unused import or unnecessary DB query.
campaignResponseSchema, | ||
parseScheduledAt, | ||
} from "~/server/public-api/schemas/campaign-schema"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove unused imports to satisfy ESLint and reduce bundle
campaignResponseSchema
and parseScheduledAt
are not used.
As per coding guidelines
-import {
- campaignResponseSchema,
- parseScheduledAt,
-} from "~/server/public-api/schemas/campaign-schema";
+// (no imports needed from campaign-schema here)
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In apps/web/src/server/public-api/api/campaigns/resume-campaign.ts around lines
8 to 10, the imports campaignResponseSchema and parseScheduledAt are unused;
remove them from the import list (or from the file entirely if only referenced
in that import) to satisfy ESLint and reduce bundle size, then run the
linter/build to confirm no remaining references or type errors.
campaignScheduleSchema, | ||
CampaignScheduleInput, | ||
campaignResponseSchema, | ||
parseScheduledAt, | ||
} from "~/server/public-api/schemas/campaign-schema"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major
Remove unused import
campaignResponseSchema
isn’t used.
As per coding guidelines
-import {
- campaignScheduleSchema,
- CampaignScheduleInput,
- campaignResponseSchema,
- parseScheduledAt,
-} from "~/server/public-api/schemas/campaign-schema";
+import {
+ campaignScheduleSchema,
+ CampaignScheduleInput,
+ parseScheduledAt,
+} from "~/server/public-api/schemas/campaign-schema";
📝 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.
campaignScheduleSchema, | |
CampaignScheduleInput, | |
campaignResponseSchema, | |
parseScheduledAt, | |
} from "~/server/public-api/schemas/campaign-schema"; | |
import { | |
campaignScheduleSchema, | |
CampaignScheduleInput, | |
parseScheduledAt, | |
} from "~/server/public-api/schemas/campaign-schema"; |
🤖 Prompt for AI Agents
In apps/web/src/server/public-api/api/campaigns/schedule-campaign.ts around
lines 4 to 8, the import list includes campaignResponseSchema which is not used;
remove campaignResponseSchema from the import statement (leaving
campaignScheduleSchema, CampaignScheduleInput, parseScheduledAt) so the file no
longer imports an unused symbol and adheres to coding guidelines.
function replaceContactVariables(html: string, contact: Contact) { | ||
return html.replace( | ||
CONTACT_VARIABLE_REGEX, | ||
(_, key: string, fallback?: string) => { | ||
const valueMap: Record<string, string | null | undefined> = { | ||
email: contact.email, | ||
firstname: contact.firstName, | ||
lastname: contact.lastName, | ||
}; | ||
|
||
const normalizedKey = key.toLowerCase(); | ||
const contactValue = valueMap[normalizedKey]; | ||
|
||
if (contactValue && contactValue.length > 0) { | ||
return contactValue; | ||
} | ||
|
||
return fallback ?? ""; | ||
} | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Escape contact variables to prevent HTML injection in raw-HTML path
Raw replacements can inject arbitrary HTML if contact data contains angle brackets/quotes. Escape both contact values and fallbacks.
-function replaceContactVariables(html: string, contact: Contact) {
- return html.replace(
- CONTACT_VARIABLE_REGEX,
- (_, key: string, fallback?: string) => {
- const valueMap: Record<string, string | null | undefined> = {
- email: contact.email,
- firstname: contact.firstName,
- lastname: contact.lastName,
- };
-
- const normalizedKey = key.toLowerCase();
- const contactValue = valueMap[normalizedKey];
-
- if (contactValue && contactValue.length > 0) {
- return contactValue;
- }
-
- return fallback ?? "";
- }
- );
-}
+function replaceContactVariables(html: string, contact: Contact) {
+ return html.replace(
+ CONTACT_VARIABLE_REGEX,
+ (_: unknown, key: string, fallback?: string) => {
+ const valueMap: Record<string, string | null | undefined> = {
+ email: contact.email,
+ firstname: contact.firstName,
+ lastname: contact.lastName,
+ };
+ const normalizedKey = key.toLowerCase();
+ const contactValue = valueMap[normalizedKey];
+ const valueToUse =
+ contactValue && contactValue.length > 0 ? contactValue : fallback ?? "";
+ return escapeHtml(valueToUse);
+ }
+ );
+}
Add this helper (outside the above range):
function escapeHtml(s: string) {
return s
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
🤖 Prompt for AI Agents
In apps/web/src/server/service/campaign-service.ts around lines 55 to 75,
contact variable replacements are inserted as raw HTML which allows HTML
injection if contact values or fallbacks contain special characters; add the
provided escapeHtml helper (placed outside the shown range) and call it on both
the resolved contactValue and the fallback before returning them from the
replace callback so that all returned strings are HTML-escaped (use
escapeHtml(contactValue) and escapeHtml(fallback) or empty string).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
4 issues found across 13 files
Prompt for AI agents (all 4 issues)
Understand the root cause of the following 4 issues and fix them.
<file name="apps/docs/api-reference/openapi.json">
<violation number="1" location="apps/docs/api-reference/openapi.json:1918">
Document that either content or html must be supplied so the OpenAPI spec matches backend validation.</violation>
<violation number="2" location="apps/docs/api-reference/openapi.json:2000">
Adjust the schedule endpoint schema to describe the broader accepted scheduledAt values instead of the restrictive date-time format.</violation>
</file>
<file name="apps/web/src/server/service/campaign-service.ts">
<violation number="1" location="apps/web/src/server/service/campaign-service.ts:72">
HTML-escape contact values and fallbacks before inserting into raw HTML to prevent injection when data contains special characters</violation>
</file>
<file name="apps/web/src/server/public-api/api/campaigns/pause-campaign.ts">
<violation number="1" location="apps/web/src/server/public-api/api/campaigns/pause-campaign.ts:50">
Remove this redundant database call; the result is unused and only adds overhead</violation>
</file>
React with 👍 or 👎 to teach cubic. Mention @cubic-dev-ai
to give feedback, ask questions, or re-run the review.
}, | ||
"scheduledAt": { | ||
"type": "string", | ||
"format": "date-time" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adjust the schedule endpoint schema to describe the broader accepted scheduledAt values instead of the restrictive date-time format.
Prompt for AI agents
Address the following comment on apps/docs/api-reference/openapi.json at line 2000:
<comment>Adjust the schedule endpoint schema to describe the broader accepted scheduledAt values instead of the restrictive date-time format.</comment>
<file context>
@@ -1910,6 +1910,531 @@
+ },
+ "scheduledAt": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "batchSize": {
</file context>
"format": "date-time" | |
"description": "Timestamp in ISO 8601 format or natural language (e.g., 'tomorrow 9am')" |
"post": { | ||
"requestBody": { | ||
"required": true, | ||
"content": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Document that either content or html must be supplied so the OpenAPI spec matches backend validation.
Prompt for AI agents
Address the following comment on apps/docs/api-reference/openapi.json at line 1918:
<comment>Document that either content or html must be supplied so the OpenAPI spec matches backend validation.</comment>
<file context>
@@ -1910,6 +1910,531 @@
+ "post": {
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
</file context>
return contactValue; | ||
} | ||
|
||
return fallback ?? ""; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTML-escape contact values and fallbacks before inserting into raw HTML to prevent injection when data contains special characters
Prompt for AI agents
Address the following comment on apps/web/src/server/service/campaign-service.ts at line 72:
<comment>HTML-escape contact values and fallbacks before inserting into raw HTML to prevent injection when data contains special characters</comment>
<file context>
@@ -19,52 +19,339 @@ import { logger } from "../logger/log";
+ return contactValue;
+ }
+
+ return fallback ?? "";
+ }
+ );
</file context>
teamId: team.id, | ||
}); | ||
|
||
await getCampaignForTeam({ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove this redundant database call; the result is unused and only adds overhead
Prompt for AI agents
Address the following comment on apps/web/src/server/public-api/api/campaigns/pause-campaign.ts at line 50:
<comment>Remove this redundant database call; the result is unused and only adds overhead</comment>
<file context>
@@ -0,0 +1,59 @@
+ teamId: team.id,
+ });
+
+ await getCampaignForTeam({
+ campaignId,
+ teamId: team.id,
</file context>
Summary by cubic
Adds a public Campaign API with create/get/schedule/pause/resume endpoints and makes API-created campaigns read-only in the dashboard. Improves HTML rendering, scheduling, and unsubscribe safety.
New Features
Migration
Summary by CodeRabbit
New Features
Documentation