From bf7f399844cb4921817b7bd2ab5dc70a2bdaebe9 Mon Sep 17 00:00:00 2001 From: simeng-li Date: Tue, 11 Feb 2025 12:44:13 +0800 Subject: [PATCH] feat(core): add get email-templates api (#7016) add get email-templates api --- packages/core/src/queries/email-templates.ts | 40 ++++++++++++++++- .../routes/email-template/index.openapi.json | 35 ++++++++++++++- .../core/src/routes/email-template/index.ts | 37 ++++++++++++++- .../src/api/email-templates.ts | 10 +++++ .../src/tests/api/email-templates.test.ts | 45 +++++++++++++++++++ 5 files changed, 163 insertions(+), 4 deletions(-) diff --git a/packages/core/src/queries/email-templates.ts b/packages/core/src/queries/email-templates.ts index 36579dbbb..aa9fa0338 100644 --- a/packages/core/src/queries/email-templates.ts +++ b/packages/core/src/queries/email-templates.ts @@ -4,13 +4,19 @@ import { EmailTemplates, type CreateEmailTemplate, } from '@logto/schemas'; -import { type CommonQueryMethods } from '@silverhand/slonik'; +import { sql, 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'; +import { expandFields } from '../database/utils.js'; +import { + conditionalSql, + convertToIdentifiers, + manyRows, + type OmitAutoSetFields, +} from '../utils/sql.js'; export default class EmailTemplatesQueries extends SchemaQueries< EmailTemplateKeys, @@ -50,4 +56,34 @@ export default class EmailTemplatesQueries extends SchemaQueries< ); }); } + + /** + * Find all email templates + * + * @param where - Optional where clause to filter email templates by language tag and template type + * @param where.languageTag - The language tag of the email template + * @param where.templateType - The type of the email template + */ + async findAllWhere( + where?: Partial> + ): Promise { + const { fields, table } = convertToIdentifiers(EmailTemplates); + + return manyRows( + this.pool.query(sql` + select ${expandFields(EmailTemplates)} + from ${table} + ${conditionalSql(where && Object.keys(where).length > 0 && where, (where) => { + return sql`where ${sql.join( + Object.entries(where).map( + // eslint-disable-next-line no-restricted-syntax -- Object.entries can not infer the key type properly. + ([key, value]) => sql`${fields[key as keyof EmailTemplate]} = ${value}` + ), + sql` and ` + )}`; + })} + order by ${fields.languageTag}, ${fields.templateType} + `) + ); + } } diff --git a/packages/core/src/routes/email-template/index.openapi.json b/packages/core/src/routes/email-template/index.openapi.json index 809d55b01..e744dccb0 100644 --- a/packages/core/src/routes/email-template/index.openapi.json +++ b/packages/core/src/routes/email-template/index.openapi.json @@ -23,7 +23,7 @@ "items": { "properties": { "languageTag": { - "description": "The language tag of the email template, e.g., `en` or `zh-CN`." + "description": "The language tag of the email template, e.g., `en` or `fr`." }, "templateType": { "description": "The type of the email template, e.g. `SignIn` or `ForgotPassword`" @@ -61,6 +61,27 @@ "description": "The list of newly created or replaced email templates." } } + }, + "get": { + "summary": "Get email templates", + "description": "Get the list of email templates.", + "parameters": [ + { + "name": "languageTag", + "in": "query", + "description": "The language tag of the email template, e.g., `en` or `fr`." + }, + { + "name": "templateType", + "in": "query", + "description": "The type of the email template, e.g. `SignIn` or `ForgotPassword`" + } + ], + "responses": { + "200": { + "description": "The list of matched email templates. Returns empty list, if no email template is found." + } + } } }, "/api/email-templates/{id}": { @@ -75,6 +96,18 @@ "description": "The email template was not found." } } + }, + "get": { + "summary": "Get email template by ID", + "description": "Get the email template by its ID.", + "responses": { + "200": { + "description": "The email template." + }, + "404": { + "description": "The email template was not found." + } + } } } } diff --git a/packages/core/src/routes/email-template/index.ts b/packages/core/src/routes/email-template/index.ts index 84abfdf8f..0cac848f7 100644 --- a/packages/core/src/routes/email-template/index.ts +++ b/packages/core/src/routes/email-template/index.ts @@ -31,14 +31,49 @@ export default function emailTemplateRoutes( }), async (ctx, next) => { const { body } = ctx.guard; - ctx.body = await emailTemplatesQueries.upsertMany( body.templates.map((template) => ({ id: generateStandardId(), ...template, })) ); + return next(); + } + ); + router.get( + pathPrefix, + koaGuard({ + query: EmailTemplates.guard + .pick({ + languageTag: true, + templateType: true, + }) + .partial(), + response: EmailTemplates.guard.array(), + status: [200], + }), + async (ctx, next) => { + const { query } = ctx.guard; + ctx.body = await emailTemplatesQueries.findAllWhere(query); + return next(); + } + ); + + router.get( + `${pathPrefix}/:id`, + koaGuard({ + params: z.object({ + id: z.string(), + }), + response: EmailTemplates.guard, + status: [200, 404], + }), + async (ctx, next) => { + const { + params: { id }, + } = ctx.guard; + ctx.body = await emailTemplatesQueries.findById(id); return next(); } ); diff --git a/packages/integration-tests/src/api/email-templates.ts b/packages/integration-tests/src/api/email-templates.ts index a9b24771d..f3864d448 100644 --- a/packages/integration-tests/src/api/email-templates.ts +++ b/packages/integration-tests/src/api/email-templates.ts @@ -12,4 +12,14 @@ export class EmailTemplatesApi { async delete(id: string): Promise { await authedAdminApi.delete(`${path}/${id}`); } + + async findById(id: string): Promise { + return authedAdminApi.get(`${path}/${id}`).json(); + } + + async findAll( + where?: Partial> + ): Promise { + return authedAdminApi.get(path, { searchParams: where }).json(); + } } diff --git a/packages/integration-tests/src/tests/api/email-templates.test.ts b/packages/integration-tests/src/tests/api/email-templates.test.ts index 057de5752..dd4de00d8 100644 --- a/packages/integration-tests/src/tests/api/email-templates.test.ts +++ b/packages/integration-tests/src/tests/api/email-templates.test.ts @@ -1,5 +1,8 @@ +import { TemplateType } from '@logto/connector-kit'; + import { mockEmailTemplates } from '#src/__mocks__/email-templates.js'; import { EmailTemplatesApiTest } from '#src/helpers/email-templates.js'; +import { expectRejects } from '#src/helpers/index.js'; import { devFeatureTest } from '#src/utils.js'; devFeatureTest.describe('email templates', () => { @@ -35,4 +38,46 @@ devFeatureTest.describe('email templates', () => { expect(template.details.content).toBe(updatedTemplates[index]!.details.content); } }); + + it('should get email templates with query search successfully', async () => { + await emailTemplatesApi.create(mockEmailTemplates); + + const templates = await emailTemplatesApi.findAll(); + expect(templates).toHaveLength(3); + + for (const mockTemplate of mockEmailTemplates) { + const template = templates.find( + ({ languageTag, templateType }) => + languageTag === mockTemplate.languageTag && templateType === mockTemplate.templateType + ); + + expect(template).toBeDefined(); + expect(template!.details).toEqual(mockTemplate.details); + } + + // Search by language tag + const enTemplates = await emailTemplatesApi.findAll({ languageTag: 'en' }); + expect(enTemplates).toHaveLength( + mockEmailTemplates.filter(({ languageTag }) => languageTag === 'en').length + ); + + // Search by template type + const signInTemplates = await emailTemplatesApi.findAll({ templateType: TemplateType.SignIn }); + expect(signInTemplates).toHaveLength( + mockEmailTemplates.filter(({ templateType }) => templateType === TemplateType.SignIn).length + ); + }); + + it('should get email template by ID successfully', async () => { + const [template] = await emailTemplatesApi.create(mockEmailTemplates); + const found = await emailTemplatesApi.findById(template!.id); + expect(found).toEqual(template); + }); + + it('should throw 404 error when email template not found by ID', async () => { + await expectRejects(emailTemplatesApi.findById('invalid-id'), { + code: 'entity.not_exists_with_id', + status: 404, + }); + }); });