feat: Add resend and stub as an email provider#852
Conversation
🦋 Changeset detectedLatest commit: 1b88c2c The changes in this PR will be included in the next version bump. This PR includes changesets to release 23 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (1)
📝 WalkthroughWalkthroughTwo new email providers (Resend and Stub/Custom) are added to the plugin-email package, each with configuration schemas, web editors, code generators, and email adapter implementations. GraphQL blog post types and mutations are introduced to the example application. Plugin ordering in the project definition is adjusted. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User/Admin
participant WebUI as Web UI<br/>(Definition Editor)
participant Form as Form Handler<br/>(useResettableForm)
participant Schema as Zod Schema<br/>(Plugin Definition)
participant Config as projectDefinition<br/>(pluginConfig)
participant Backend as Backend Compiler
User->>WebUI: Open Resend/Stub<br/>Provider Config
activate WebUI
WebUI->>Schema: Build schema from<br/>createXxxPluginDefinitionSchema
WebUI->>Form: Initialize form with<br/>default values from config
activate Form
User->>Form: Enter provider settings
User->>Form: Submit form
Form->>Schema: Validate against schema
Schema-->>Form: Validation result
Form->>Config: setPluginConfig with<br/>updated definition
activate Config
Config-->>Form: Success response
deactivate Config
Form->>WebUI: onSave callback
deactivate Form
WebUI->>User: Show success message
deactivate WebUI
Note over Backend: During Build Phase
Backend->>Config: Retrieve plugin config<br/>by pluginKey
Config-->>Backend: Plugin definition
Backend->>Backend: Invoke generator<br/>(resendGenerator/stubGenerator)<br/>with config.stubOptions.providerName
Backend->>Backend: Render email adapter service<br/>template with variables
Backend-->>Backend: Compiled app with<br/>email provider integration
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 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. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
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: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@examples/blog-with-auth/apps/backend/schema.graphql`:
- Around line 27-33: Add the missing id field to the GraphQL BlogPost type so
clients can read object identifiers: update the type definition for BlogPost to
include an id: Uuid! field (matching Prisma model) so queries like blogPost(id:
Uuid!) and mutations updateBlogPost/deleteBlogPost and create responses return
the id for Apollo Client cache normalization and subsequent operations.
In `@plugins/plugin-email/src/resend/core/generators/resend/resend.generator.ts`:
- Around line 53-64: The email adapter registration currently calls
emailConfig.emailAdapter.set(...) which overwrites any previously-registered
adapter and makes the selected provider order-dependent; change this to detect
an existing registration and fail-fast: before calling .set(...) (in
resend.generator.ts for resendEmailAdapter and likewise in stub.generator.ts),
check the current value (e.g., emailConfig.emailAdapter.get() or equivalent) and
if a value exists throw a clear error/raise a conflict that names the existing
provider and the attempted provider (paths.resendService/resendEmailAdapter),
instead of silently replacing it; ensure the check runs inside the
createGeneratorTask run() so the generator errors when a second provider
registers.
In `@plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts`:
- Around line 13-15: The descriptorSchema currently permits arbitrary
providerName strings and those are used to build adapterName and are
interpolated into generated single-quoted string literals in stub.generator.ts,
which can produce invalid TypeScript for values like O'Hare or strings with
newlines; constrain providerName with a safe regex (e.g. allow only
alphanumerics, dashes/underscores) in descriptorSchema and the
editor/plugin-definition schema (plugin-definition.ts) and ensure adapterName is
derived only from the validated/sanitized providerName; additionally, when
emitting providerName into templates in stub.generator.ts (where template
variables are inserted into single-quoted literals), wrap the value with
JSON.stringify() (or otherwise escape it) so the generated code is always valid
TypeScript.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 226693c4-4580-4182-925f-c6ab786e4d50
⛔ Files ignored due to path filters (20)
examples/blog-with-auth/pnpm-lock.yamlis excluded by!**/pnpm-lock.yamlpackages/fastify-generators/src/generators/prisma/data-utils/generated/ts-import-providers.tsis excluded by!**/generated/**,!**/generated/**packages/fastify-generators/src/generators/prisma/data-utils/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**packages/react-generators/src/generators/core/react-tailwind/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/index.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/ts-import-providers.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/index.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/ts-import-providers.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/resend/core/generators/resend/generated/index.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/resend/core/generators/resend/generated/template-paths.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/resend/core/generators/resend/generated/template-renderers.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/resend/core/generators/resend/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/resend/static/icon.svgis excluded by!**/*.svgplugins/plugin-email/src/stub/core/generators/stub/generated/index.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/stub/core/generators/stub/generated/template-paths.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/stub/core/generators/stub/generated/template-renderers.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/stub/core/generators/stub/generated/typed-templates.tsis excluded by!**/generated/**,!**/generated/**plugins/plugin-email/src/stub/static/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (37)
.changeset/add-resend-email-provider.mdexamples/blog-with-auth/apps/admin/src/gql/graphql.tsexamples/blog-with-auth/apps/backend/schema.graphqlexamples/blog-with-auth/baseplate/project-definition.jsonpackages/project-builder-common/index.jspackages/react-generators/src/generators/core/react-tailwind/extractor.jsonplugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/extractor.jsonplugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/index.tsplugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/extractor.jsonplugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/index.tsplugins/plugin-email/src/index.tsplugins/plugin-email/src/resend/core/common.tsplugins/plugin-email/src/resend/core/components/resend-definition-editor.tsxplugins/plugin-email/src/resend/core/generators/index.tsplugins/plugin-email/src/resend/core/generators/resend/extractor.jsonplugins/plugin-email/src/resend/core/generators/resend/index.tsplugins/plugin-email/src/resend/core/generators/resend/resend.generator.tsplugins/plugin-email/src/resend/core/generators/resend/templates/module/emails/services/resend.service.tsplugins/plugin-email/src/resend/core/index.tsplugins/plugin-email/src/resend/core/node.tsplugins/plugin-email/src/resend/core/schema/plugin-definition.tsplugins/plugin-email/src/resend/core/web.tsplugins/plugin-email/src/resend/index.tsplugins/plugin-email/src/resend/plugin.jsonplugins/plugin-email/src/stub/core/common.tsplugins/plugin-email/src/stub/core/components/stub-definition-editor.tsxplugins/plugin-email/src/stub/core/generators/index.tsplugins/plugin-email/src/stub/core/generators/stub/extractor.jsonplugins/plugin-email/src/stub/core/generators/stub/index.tsplugins/plugin-email/src/stub/core/generators/stub/stub.generator.tsplugins/plugin-email/src/stub/core/generators/stub/templates/module/emails/services/stub.service.tsplugins/plugin-email/src/stub/core/index.tsplugins/plugin-email/src/stub/core/node.tsplugins/plugin-email/src/stub/core/schema/plugin-definition.tsplugins/plugin-email/src/stub/core/web.tsplugins/plugin-email/src/stub/index.tsplugins/plugin-email/src/stub/plugin.json
💤 Files with no reviewable changes (5)
- packages/project-builder-common/index.js
- plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/extractor.json
- plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/extractor.json
- plugins/plugin-auth/src/better-auth/generators/fastify/seed-initial-user/index.ts
- plugins/plugin-auth/src/local-auth/core/generators/seed-initial-user/index.ts
| type BlogPost { | ||
| blogId: Uuid! | ||
| content: String! | ||
| metadata: JSON | ||
| publisherId: Uuid! | ||
| title: String! | ||
| } |
There was a problem hiding this comment.
Critical: Missing id field in BlogPost type.
The BlogPost type is missing the id field that exists in the Prisma model. This will cause issues because:
- Apollo Client requires
idfor cache normalization - The
blogPost(id: Uuid!)query accepts anidbut clients cannot retrieve it from the response - Clients cannot obtain the
idneeded forupdateBlogPostanddeleteBlogPostmutations after creating a post
Other entity types like Blog and User correctly expose their id field.
🐛 Proposed fix to add id field
type BlogPost {
+ id: ID!
blogId: Uuid!
content: String!
metadata: JSON
publisherId: Uuid!
title: String!
}📝 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.
| type BlogPost { | |
| blogId: Uuid! | |
| content: String! | |
| metadata: JSON | |
| publisherId: Uuid! | |
| title: String! | |
| } | |
| type BlogPost { | |
| id: ID! | |
| blogId: Uuid! | |
| content: String! | |
| metadata: JSON | |
| publisherId: Uuid! | |
| title: String! | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@examples/blog-with-auth/apps/backend/schema.graphql` around lines 27 - 33,
Add the missing id field to the GraphQL BlogPost type so clients can read object
identifiers: update the type definition for BlogPost to include an id: Uuid!
field (matching Prisma model) so queries like blogPost(id: Uuid!) and mutations
updateBlogPost/deleteBlogPost and create responses return the id for Apollo
Client cache normalization and subsequent operations.
| emailConfig: createGeneratorTask({ | ||
| dependencies: { | ||
| paths: GENERATED_TEMPLATES.paths.provider, | ||
| emailConfig: emailConfigProvider, | ||
| }, | ||
| run({ paths, emailConfig }) { | ||
| emailConfig.emailAdapter.set( | ||
| tsCodeFragment( | ||
| 'resendEmailAdapter', | ||
| tsImportBuilder(['resendEmailAdapter']).from(paths.resendService), | ||
| ), | ||
| ); |
There was a problem hiding this comment.
Fail fast when a second email provider registers.
emailAdapter is a scalar config field, so .set() here replaces any adapter that was already registered by another provider. With the same registration pattern also added in plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts, the effective provider becomes generator-order dependent instead of explicit. Please enforce mutual exclusivity or raise a conflict when a second provider tries to register.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugins/plugin-email/src/resend/core/generators/resend/resend.generator.ts`
around lines 53 - 64, The email adapter registration currently calls
emailConfig.emailAdapter.set(...) which overwrites any previously-registered
adapter and makes the selected provider order-dependent; change this to detect
an existing registration and fail-fast: before calling .set(...) (in
resend.generator.ts for resendEmailAdapter and likewise in stub.generator.ts),
check the current value (e.g., emailConfig.emailAdapter.get() or equivalent) and
if a value exists throw a clear error/raise a conflict that names the existing
provider and the attempted provider (paths.resendService/resendEmailAdapter),
instead of silently replacing it; ensure the check runs inside the
createGeneratorTask run() so the generator errors when a second provider
registers.
| const descriptorSchema = z.object({ | ||
| providerName: z.string().min(1), | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '13,25p;54,57p' plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts
python - <<'PY'
samples = ["O'Hare", "line\nbreak"]
for value in samples:
print("providerName =", repr(value))
print("current TPL_PROVIDER_NAME =", f"'{value}'")
print("current TPL_LOG_MESSAGE =", f"'{value} email adapter: email logged (not sent)'")
print()
PYRepository: halfdomelabs/baseplate
Length of output: 939
🏁 Script executed:
cat -n plugins/plugin-email/src/stub/core/schema/plugin-definition.ts | head -40Repository: halfdomelabs/baseplate
Length of output: 992
🏁 Script executed:
rg 'TPL_PROVIDER_NAME|TPL_LOG_MESSAGE' plugins/plugin-email/src --type tsRepository: halfdomelabs/baseplate
Length of output: 822
🏁 Script executed:
fd '\.ts$' plugins/plugin-email/src --exec grep -l 'buildTasks.*providerName' {} \;Repository: halfdomelabs/baseplate
Length of output: 134
🏁 Script executed:
cat -n plugins/plugin-email/src/stub/core/generators/stub/templates/module/emails/services/stub.service.tsRepository: halfdomelabs/baseplate
Length of output: 1759
Escape and constrain providerName before generating code.
providerName is validated only with .min(1) in the descriptor schema (lines 13–15) and the editor schema, but it is interpolated directly into single-quoted string literals on lines 56–57. Inputs like O'Hare or newlines will emit invalid TypeScript source code. The same free-form value also drives adapterName on line 25. Add a regex constraint to both schemas and use JSON.stringify() for template variable values to ensure safe code generation.
♻️ Proposed fix
const descriptorSchema = z.object({
- providerName: z.string().min(1),
+ providerName: z
+ .string()
+ .regex(
+ /^[A-Za-z][A-Za-z0-9 _-]*$/,
+ 'Use letters, numbers, spaces, "_" or "-", and start with a letter.',
+ ),
});
/**
* Generator for stub/core/stub
*/
export const stubGenerator = createGenerator({
@@
descriptorSchema,
buildTasks: ({ providerName }) => {
const adapterName = `${camelCase(providerName)}EmailAdapter`;
+ const providerNameLiteral = JSON.stringify(providerName);
+ const logMessageLiteral = JSON.stringify(
+ `${providerName} email adapter: email logged (not sent)`,
+ );
return {
@@
renderers.stubService.render({
variables: {
TPL_ADAPTER_NAME: adapterName,
- TPL_PROVIDER_NAME: `'${providerName}'`,
- TPL_LOG_MESSAGE: `'${providerName} email adapter: email logged (not sent)'`,
+ TPL_PROVIDER_NAME: providerNameLiteral,
+ TPL_LOG_MESSAGE: logMessageLiteral,
},
}),
);Also update plugins/plugin-email/src/stub/core/schema/plugin-definition.ts with the same regex constraint on the providerName field to prevent invalid input at the editor level.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@plugins/plugin-email/src/stub/core/generators/stub/stub.generator.ts` around
lines 13 - 15, The descriptorSchema currently permits arbitrary providerName
strings and those are used to build adapterName and are interpolated into
generated single-quoted string literals in stub.generator.ts, which can produce
invalid TypeScript for values like O'Hare or strings with newlines; constrain
providerName with a safe regex (e.g. allow only alphanumerics,
dashes/underscores) in descriptorSchema and the editor/plugin-definition schema
(plugin-definition.ts) and ensure adapterName is derived only from the
validated/sanitized providerName; additionally, when emitting providerName into
templates in stub.generator.ts (where template variables are inserted into
single-quoted literals), wrap the value with JSON.stringify() (or otherwise
escape it) so the generated code is always valid TypeScript.
Summary by CodeRabbit
New Features
Chores