Skip to content

Commit

Permalink
feat(core): add get email-templates api (#7016)
Browse files Browse the repository at this point in the history
add get email-templates api
  • Loading branch information
simeng-li authored Feb 11, 2025
1 parent dd8cd13 commit bf7f399
Show file tree
Hide file tree
Showing 5 changed files with 163 additions and 4 deletions.
40 changes: 38 additions & 2 deletions packages/core/src/queries/email-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Pick<EmailTemplate, 'languageTag' | 'templateType'>>
): Promise<readonly EmailTemplate[]> {
const { fields, table } = convertToIdentifiers(EmailTemplates);

return manyRows(
this.pool.query<EmailTemplate>(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}
`)
);
}
}
35 changes: 34 additions & 1 deletion packages/core/src/routes/email-template/index.openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -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`"
Expand Down Expand Up @@ -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}": {
Expand All @@ -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."
}
}
}
}
}
Expand Down
37 changes: 36 additions & 1 deletion packages/core/src/routes/email-template/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,14 +31,49 @@ export default function emailTemplateRoutes<T extends ManagementApiRouter>(
}),
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();
}
);
Expand Down
10 changes: 10 additions & 0 deletions packages/integration-tests/src/api/email-templates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,14 @@ export class EmailTemplatesApi {
async delete(id: string): Promise<void> {
await authedAdminApi.delete(`${path}/${id}`);
}

async findById(id: string): Promise<EmailTemplate> {
return authedAdminApi.get(`${path}/${id}`).json<EmailTemplate>();
}

async findAll(
where?: Partial<Pick<EmailTemplate, 'languageTag' | 'templateType'>>
): Promise<EmailTemplate[]> {
return authedAdminApi.get(path, { searchParams: where }).json<EmailTemplate[]>();
}
}
45 changes: 45 additions & 0 deletions packages/integration-tests/src/tests/api/email-templates.test.ts
Original file line number Diff line number Diff line change
@@ -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', () => {
Expand Down Expand Up @@ -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,
});
});
});

0 comments on commit bf7f399

Please sign in to comment.