-
-
Notifications
You must be signed in to change notification settings - Fork 487
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(core): add PUT and DELETE email-templates api
add PUT and DELETE email-templates api
- Loading branch information
Showing
10 changed files
with
312 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { | ||
type EmailTemplate, | ||
type EmailTemplateKeys, | ||
EmailTemplates, | ||
type CreateEmailTemplate, | ||
} from '@logto/schemas'; | ||
import { type CommonQueryMethods } from '@silverhand/slonik'; | ||
|
||
import SchemaQueries from '#src/utils/SchemaQueries.js'; | ||
|
||
import { type WellKnownCache } from '../caches/well-known.js'; | ||
import { buildInsertIntoWithPool } from '../database/insert-into.js'; | ||
import { convertToIdentifiers, type OmitAutoSetFields } from '../utils/sql.js'; | ||
|
||
export default class EmailTemplatesQueries extends SchemaQueries< | ||
EmailTemplateKeys, | ||
CreateEmailTemplate, | ||
EmailTemplate | ||
> { | ||
constructor( | ||
pool: CommonQueryMethods, | ||
// TODO: Implement redis cache for email templates | ||
private readonly wellKnownCache: WellKnownCache | ||
) { | ||
super(pool, EmailTemplates); | ||
} | ||
|
||
/** | ||
* Upsert multiple email templates | ||
* | ||
* If the email template already exists with the same language tag, tenant ID, and template type, | ||
* template details will be updated. | ||
*/ | ||
async upsertMany( | ||
emailTemplates: ReadonlyArray<OmitAutoSetFields<CreateEmailTemplate>> | ||
): Promise<readonly EmailTemplate[]> { | ||
const { fields } = convertToIdentifiers(EmailTemplates); | ||
|
||
return this.pool.transaction(async (transaction) => { | ||
const insertIntoTransaction = buildInsertIntoWithPool(transaction)(EmailTemplates, { | ||
returning: true, | ||
onConflict: { | ||
fields: [fields.tenantId, fields.languageTag, fields.templateType], | ||
setExcludedFields: [fields.details], | ||
}, | ||
}); | ||
|
||
return Promise.all( | ||
emailTemplates.map(async (emailTemplate) => insertIntoTransaction(emailTemplate)) | ||
); | ||
}); | ||
} | ||
} |
80 changes: 80 additions & 0 deletions
80
packages/core/src/routes/email-template/index.openapi.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,80 @@ | ||
{ | ||
"tags": [ | ||
{ | ||
"name": "Email templates", | ||
"description": "Manage custom i18n email templates for various types of emails, such as sign-in verification codes and password resets." | ||
} | ||
], | ||
"paths": { | ||
"/api/email-templates": { | ||
"put": { | ||
"tags": ["Dev feature"], | ||
"summary": "Replace email templates", | ||
"description": "Create or replace a list of email templates. If an email template with the same language tag and template type already exists, its details will be updated.", | ||
"requestBody": { | ||
"content": { | ||
"application/json": { | ||
"schema": { | ||
"properties": { | ||
"templates": { | ||
"type": "array", | ||
"items": { | ||
"properties": { | ||
"languageTag": { | ||
"description": "The language tag of the email template, e.g., `en` or `zh-CN`." | ||
}, | ||
"templateType": { | ||
"description": "The type of the email template, e.g. `SignIn` or `ForgotPassword`" | ||
}, | ||
"details": { | ||
"description": "The details of the email template.", | ||
"properties": { | ||
"subject": { | ||
"description": "The template of the email subject." | ||
}, | ||
"content": { | ||
"description": "Thj template of the email body." | ||
}, | ||
"contentType": { | ||
"description": "The content type of the email body. (Only required by some specific email providers.)" | ||
}, | ||
"replyTo": { | ||
"description": "The reply name template of the email. If not provided, the target email address will be used. (The render logic may differ based on the email provider.)" | ||
}, | ||
"sendFrom": { | ||
"description": "The send from name template of the email. If not provided, the default Logto email address will be used. (The render logic may differ based on the email provider.)" | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"responses": { | ||
"200": { | ||
"description": "The list of newly created or replaced email templates." | ||
} | ||
} | ||
} | ||
}, | ||
"/api/email-templates/{id}": { | ||
"delete": { | ||
"tags": ["Dev feature"], | ||
"summary": "Delete an email template", | ||
"description": "Delete an email template by its ID.", | ||
"response": { | ||
"204": { | ||
"description": "The email template was deleted successfully." | ||
}, | ||
"404": { | ||
"description": "The email template was not found." | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
import { EmailTemplates } from '@logto/schemas'; | ||
import { generateStandardId } from '@logto/shared'; | ||
import { z } from 'zod'; | ||
|
||
import koaGuard from '#src/middleware/koa-guard.js'; | ||
|
||
import { type ManagementApiRouter, type RouterInitArgs } from '../types.js'; | ||
|
||
const pathPrefix = '/email-templates'; | ||
|
||
export default function emailTemplateRoutes<T extends ManagementApiRouter>( | ||
...[router, { queries }]: RouterInitArgs<T> | ||
) { | ||
const { emailTemplates: emailTemplatesQueries } = queries; | ||
|
||
router.put( | ||
pathPrefix, | ||
koaGuard({ | ||
body: z.object({ | ||
templates: EmailTemplates.createGuard | ||
.omit({ | ||
id: true, | ||
tenantId: true, | ||
createdAt: true, | ||
}) | ||
.array() | ||
.min(1), | ||
}), | ||
response: EmailTemplates.guard.array(), | ||
status: [200, 422], | ||
}), | ||
async (ctx, next) => { | ||
const { body } = ctx.guard; | ||
|
||
ctx.body = await emailTemplatesQueries.upsertMany( | ||
body.templates.map((template) => ({ | ||
id: generateStandardId(), | ||
...template, | ||
})) | ||
); | ||
|
||
return next(); | ||
} | ||
); | ||
|
||
router.delete( | ||
`${pathPrefix}/:id`, | ||
koaGuard({ | ||
params: z.object({ | ||
id: z.string(), | ||
}), | ||
status: [204, 404], | ||
}), | ||
async (ctx, next) => { | ||
const { | ||
params: { id }, | ||
} = ctx.guard; | ||
|
||
await emailTemplatesQueries.deleteById(id); | ||
ctx.status = 204; | ||
return next(); | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
31 changes: 31 additions & 0 deletions
31
packages/integration-tests/src/__mocks__/email-templates.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
import { type CreateEmailTemplate, TemplateType } from '@logto/schemas'; | ||
|
||
export const mockEmailTemplates: Array<Omit<CreateEmailTemplate, 'id'>> = [ | ||
{ | ||
languageTag: 'en', | ||
templateType: TemplateType.SignIn, | ||
details: { | ||
subject: 'Sign In', | ||
content: 'Sign in to your account', | ||
contentType: 'text/html', | ||
}, | ||
}, | ||
{ | ||
languageTag: 'en', | ||
templateType: TemplateType.Register, | ||
details: { | ||
subject: 'Register', | ||
content: 'Register for an account', | ||
contentType: 'text/html', | ||
}, | ||
}, | ||
{ | ||
languageTag: 'de', | ||
templateType: TemplateType.SignIn, | ||
details: { | ||
subject: 'Sign In', | ||
content: 'Sign in to your account', | ||
contentType: 'text/plain', | ||
}, | ||
}, | ||
]; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
import { type CreateEmailTemplate, type EmailTemplate } from '@logto/schemas'; | ||
|
||
import { authedAdminApi } from './index.js'; | ||
|
||
const path = 'email-templates'; | ||
|
||
export class EmailTemplatesApi { | ||
async create(templates: Array<Omit<CreateEmailTemplate, 'id'>>): Promise<EmailTemplate[]> { | ||
return authedAdminApi.put(path, { json: { templates } }).json<EmailTemplate[]>(); | ||
} | ||
|
||
async delete(id: string): Promise<void> { | ||
await authedAdminApi.delete(`${path}/${id}`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { type CreateEmailTemplate, type EmailTemplate } from '@logto/schemas'; | ||
|
||
import { EmailTemplatesApi } from '#src/api/email-templates.js'; | ||
|
||
export class EmailTemplatesApiTest extends EmailTemplatesApi { | ||
#emailTemplates: EmailTemplate[] = []; | ||
|
||
override async create( | ||
templates: Array<Omit<CreateEmailTemplate, 'id'>> | ||
): Promise<EmailTemplate[]> { | ||
const created = await super.create(templates); | ||
this.#emailTemplates.concat(created); | ||
return created; | ||
} | ||
|
||
async cleanUp(): Promise<void> { | ||
await Promise.all(this.#emailTemplates.map(async (template) => this.delete(template.id))); | ||
this.#emailTemplates = []; | ||
} | ||
} |
38 changes: 38 additions & 0 deletions
38
packages/integration-tests/src/tests/api/email-templates.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
import { mockEmailTemplates } from '#src/__mocks__/email-templates.js'; | ||
import { EmailTemplatesApiTest } from '#src/helpers/email-templates.js'; | ||
import { devFeatureTest } from '#src/utils.js'; | ||
|
||
devFeatureTest.describe('email templates', () => { | ||
const emailTemplatesApi = new EmailTemplatesApiTest(); | ||
|
||
afterEach(async () => { | ||
await emailTemplatesApi.cleanUp(); | ||
}); | ||
|
||
it('should create email templates successfully', async () => { | ||
const created = await emailTemplatesApi.create(mockEmailTemplates); | ||
expect(created).toHaveLength(mockEmailTemplates.length); | ||
}); | ||
|
||
it('should update existing email template details for specified language and type', async () => { | ||
const updatedTemplates: typeof mockEmailTemplates = mockEmailTemplates.map( | ||
({ details, ...rest }) => ({ | ||
...rest, | ||
details: { | ||
subject: `${details.subject} updated`, | ||
content: `${details.content} updated`, | ||
}, | ||
}) | ||
); | ||
|
||
await emailTemplatesApi.create(mockEmailTemplates); | ||
const created = await emailTemplatesApi.create(updatedTemplates); | ||
|
||
expect(created).toHaveLength(3); | ||
|
||
for (const [index, template] of created.entries()) { | ||
expect(template.details.subject).toBe(updatedTemplates[index]!.details.subject); | ||
expect(template.details.content).toBe(updatedTemplates[index]!.details.content); | ||
} | ||
}); | ||
}); |