diff --git a/registry/package.json b/registry/package.json index 7e77e2ec0..1584bd4c0 100644 --- a/registry/package.json +++ b/registry/package.json @@ -102,4 +102,4 @@ "node": "^20.8.0" }, "engineStrict": true -} +} \ No newline at end of file diff --git a/registry/server/middleware/validatelocales.ts b/registry/server/middleware/validatelocales.ts new file mode 100644 index 000000000..fc8f8bbf4 --- /dev/null +++ b/registry/server/middleware/validatelocales.ts @@ -0,0 +1,46 @@ +import { Request, Response } from 'express'; +import Joi from 'joi'; + +import { SettingKeys } from '../settings/interfaces'; +import settingsService from '../settings/services/SettingsService'; +import { getJoiErr, joiErrorToResponse } from '../util/helpers'; + +export const validateLocalesMiddleware = + ( + localesFromRequest: (req: Request) => string[], + unsupportedLocalesToFieldName: (unsupportedLocales: string[]) => string, + ) => + async (req: Request, res: Response, next: () => void) => { + const locales = localesFromRequest(req); + + if (locales.length === 0) { + next(); + return; + } + + const unsupportedLocales = await getUnsupportedLocales(locales); + if (unsupportedLocales.length > 0) { + res.status(422).send( + joiErrorToResponse( + unsupportedLocalesToJoiError(unsupportedLocales, unsupportedLocalesToFieldName(unsupportedLocales)), + ), + ); + return; + } + + next(); + }; + +async function getUnsupportedLocales(locales: string[]): Promise { + const supportedLocales = await settingsService.get(SettingKeys.I18nSupportedLocales); + return locales.filter((l) => !supportedLocales.includes(l)); +} + +function unsupportedLocalesToJoiError(unsupportedLocales: string[], field: string): Joi.ValidationError { + return getJoiErr( + `localizedVersions.${unsupportedLocales[0]}`, + `Next locales are not supported ${unsupportedLocales.join(',')}. Either change request or change ${ + SettingKeys.I18nSupportedLocales + } setting.`, + ); +} diff --git a/registry/server/templates/errors.ts b/registry/server/templates/errors.ts index 8cce0cb1f..f9e951ab1 100644 --- a/registry/server/templates/errors.ts +++ b/registry/server/templates/errors.ts @@ -3,4 +3,8 @@ import { extendError } from '../util/extendError'; export default { FetchIncludeError: extendError('FetchIncludeError', { defaultData: {} }), InvalidTemplateError: extendError('InvalidTemplateError', { defaultData: {} }), + TemplateNotFoundError: extendError('TemplateNotFoundError', { + defaultMessage: 'Template not found', + defaultData: {}, + }), }; diff --git a/registry/server/templates/interfaces/index.ts b/registry/server/templates/interfaces/index.ts index 665aad1db..ea64fc167 100644 --- a/registry/server/templates/interfaces/index.ts +++ b/registry/server/templates/interfaces/index.ts @@ -3,8 +3,22 @@ export default interface Template { content: string; } -export interface LocalizedTemplate { +export interface LocalizedTemplateRow { templateName: string; content: string; locale: string; } + +export interface LocalizedVersion { + content: string; +} + +export interface TemplateWithLocalizedVersions extends Template { + localizedVersions: Record; +} + +export type UpdateTemplatePayload = Omit & + Partial>; + +export type CreateTemplatePayload = Omit & + Partial>; diff --git a/registry/server/templates/routes/createTemplate.ts b/registry/server/templates/routes/createTemplate.ts index d106f4fbc..2d4f3d525 100644 --- a/registry/server/templates/routes/createTemplate.ts +++ b/registry/server/templates/routes/createTemplate.ts @@ -1,11 +1,9 @@ import { Request, Response } from 'express'; -import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; -import Template, { LocalizedTemplate } from '../interfaces'; -import { Tables } from '../../db/structure'; import { templatesRepository } from '../services/templatesRepository'; -import { templateSchema, validateLocalesAreSupported } from './validation'; +import { templateSchema } from './validation'; +import { validateLocalesMiddleware } from '../../middleware/validatelocales'; const validateRequestBeforeCreateTemplate = validateRequestFactory([ { @@ -14,47 +12,18 @@ const validateRequestBeforeCreateTemplate = validateRequestFactory([ }, ]); +const validateLocale = validateLocalesMiddleware( + (req) => Object.keys(req.body.localizedVersions ?? {}), + (unsupportedLocales) => `body.localizedVersions.${unsupportedLocales[0]}`, +); + const createTemplate = async (req: Request, res: Response): Promise => { try { - const request = req.body; - const template: Template = { - name: request.name, - content: request.content, - }; - - const locales = Object.keys(request.localizedVersions || {}); - let localesAreValid = await validateLocalesAreSupported(locales, res); - if (!localesAreValid) { - return; - } - - await db.versioning(req.user, { type: 'templates', id: template.name }, async (trx) => { - await db('templates').insert(template).transacting(trx); - }); - - if (locales.length > 0) { - await insertLocalizedVersions(locales, template, request); - } - - const savedTemplate = await templatesRepository.readTemplateWithAllVersions(template.name); - res.status(200).send(savedTemplate); + const result = await templatesRepository.createTemplate(req.body, req.user); + res.status(200).send(result); } catch (e) { res.status(500).send(JSON.stringify(e)); } }; -function insertLocalizedVersions(locales: string[], template: Template, request: Record) { - return Promise.all( - locales.map((locale) => { - const localizedTemplate: LocalizedTemplate = { - templateName: template.name, - content: request.localizedVersions[locale].content, - locale: locale, - }; - - return db(Tables.TemplatesLocalized).insert(localizedTemplate); - }), - ); -} - -export default [validateRequestBeforeCreateTemplate, createTemplate]; +export default [validateRequestBeforeCreateTemplate, validateLocale, createTemplate]; diff --git a/registry/server/templates/routes/deleteTemplateLocalizedVersion.ts b/registry/server/templates/routes/deleteTemplateLocalizedVersion.ts new file mode 100644 index 000000000..59d3cdb4b --- /dev/null +++ b/registry/server/templates/routes/deleteTemplateLocalizedVersion.ts @@ -0,0 +1,44 @@ +import { Request, Response } from 'express'; +import Joi from 'joi'; + +import validateRequestFactory from '../../common/services/validateRequest'; +import { exhaustiveCheck } from '../../util/exhaustiveCheck'; +import { templatesRepository } from '../services/templatesRepository'; +import { localeNameSchema, templateNameSchema } from './validation'; + +const validateRequestBeforeDeleteTemplateLocalizedVersion = validateRequestFactory([ + { + schema: Joi.object({ + name: templateNameSchema.required(), + locale: localeNameSchema.required(), + }), + selector: 'params', + }, +]); + +type Params = { + name: string; + locale: string; +}; + +const deleteTemplateLocalizedVersion = async (req: Request, res: Response): Promise => { + const { name: templateName, locale } = req.params; + + const result = await templatesRepository.deleteLocalizedVersion(templateName, locale); + switch (result.type) { + case 'notFound': { + res.status(404).send('Not found'); + return; + } + case 'ok': { + res.status(204).send(); + return; + } + default: { + /* istanbul ignore next */ + exhaustiveCheck(result); + } + } +}; + +export default [validateRequestBeforeDeleteTemplateLocalizedVersion, deleteTemplateLocalizedVersion]; diff --git a/registry/server/templates/routes/getRenderedTemplate.ts b/registry/server/templates/routes/getRenderedTemplate.ts index 0f98221ba..0d83a44b0 100644 --- a/registry/server/templates/routes/getRenderedTemplate.ts +++ b/registry/server/templates/routes/getRenderedTemplate.ts @@ -3,7 +3,7 @@ import Joi from 'joi'; import noticeError from '../../errorHandler/noticeError'; import db from '../../db'; -import Template, { LocalizedTemplate } from '../interfaces'; +import Template, { LocalizedTemplateRow } from '../interfaces'; import validateRequestFactory from '../../common/services/validateRequest'; import renderTemplate from '../services/renderTemplate'; import errors from '../errors'; @@ -89,7 +89,7 @@ async function getRenderedTemplate(req: Request(Tables.TemplatesLocalized) + .from(Tables.TemplatesLocalized) .where('templateName', templateName) .andWhere('locale', locale as string); diff --git a/registry/server/templates/routes/index.ts b/registry/server/templates/routes/index.ts index d395c8e31..7fafcd26e 100644 --- a/registry/server/templates/routes/index.ts +++ b/registry/server/templates/routes/index.ts @@ -6,6 +6,8 @@ import getTemplates from './getTemplates'; import updateTemplate from './updateTemplate'; import createTemplate from './createTemplate'; import deleteTemplate from './deleteTemplate'; +import upsertTemplateLocalizedVersion from './upsertTemplateLocalizedVersion'; +import deleteTemplateLocalizedVersion from './deleteTemplateLocalizedVersion'; export default (authMw: RequestHandler[]) => { const templatesRouter = express.Router(); @@ -16,6 +18,8 @@ export default (authMw: RequestHandler[]) => { templatesRouter.get('/:name', ...getTemplate); templatesRouter.put('/:name', authMw, ...updateTemplate); templatesRouter.delete('/:name', authMw, ...deleteTemplate); + templatesRouter.put('/:name/localized/:locale', authMw, ...upsertTemplateLocalizedVersion); + templatesRouter.delete('/:name/localized/:locale', authMw, ...deleteTemplateLocalizedVersion); return templatesRouter; }; diff --git a/registry/server/templates/routes/updateTemplate.ts b/registry/server/templates/routes/updateTemplate.ts index 5c7ab54ba..07ca8466e 100644 --- a/registry/server/templates/routes/updateTemplate.ts +++ b/registry/server/templates/routes/updateTemplate.ts @@ -1,10 +1,11 @@ import { Request, Response } from 'express'; import Joi from 'joi'; -import db from '../../db'; import validateRequestFactory from '../../common/services/validateRequest'; +import { validateLocalesMiddleware } from '../../middleware/validatelocales'; +import { exhaustiveCheck } from '../../util/exhaustiveCheck'; import { templatesRepository } from '../services/templatesRepository'; -import { partialTemplateSchema, templateNameSchema, validateLocalesAreSupported } from './validation'; +import { partialTemplateSchema, templateNameSchema } from './validation'; type UpdateTemplateRequestParams = { name: string; @@ -23,33 +24,28 @@ const validateRequestBeforeUpdateTemplate = validateRequestFactory([ }, ]); -const updateTemplate = async (req: Request, res: Response): Promise => { - const template = { - content: req.body.content, - }; - const templateName = req.params.name; - - const templatesToUpdate = await db('templates').where({ - name: templateName, - }); - if (!templatesToUpdate.length) { - res.status(404).send('Not found'); - return; - } +const validateLocale = validateLocalesMiddleware( + (req) => Object.keys(req.body.localizedVersions ?? {}), + (unsupportedLocales) => `body.localizedVersions.${unsupportedLocales[0]}`, +); - const localizedVersions = req.body.localizedVersions || {}; - const localesAreValid = await validateLocalesAreSupported(Object.keys(localizedVersions), res); - if (!localesAreValid) { - return; +const updateTemplate = async (req: Request, res: Response): Promise => { + const result = await templatesRepository.updateTemplate(req.params.name, req.body, req.user); + + switch (result.type) { + case 'notFound': { + res.status(404).send('Not found'); + return; + } + case 'ok': { + res.status(200).send(result.template); + return; + } + default: { + /* istanbul ignore next */ + exhaustiveCheck(result); + } } - - await db.versioning(req.user, { type: 'templates', id: templateName }, async (trx) => { - await db('templates').where({ name: templateName }).update(template).transacting(trx); - await templatesRepository.upsertLocalizedVersions(templateName, localizedVersions, trx); - }); - - const updatedTemplate = await templatesRepository.readTemplateWithAllVersions(templateName); - res.status(200).send(updatedTemplate); }; -export default [validateRequestBeforeUpdateTemplate, updateTemplate]; +export default [validateRequestBeforeUpdateTemplate, validateLocale, updateTemplate]; diff --git a/registry/server/templates/routes/upsertTemplateLocalizedVersion.ts b/registry/server/templates/routes/upsertTemplateLocalizedVersion.ts new file mode 100644 index 000000000..286765a78 --- /dev/null +++ b/registry/server/templates/routes/upsertTemplateLocalizedVersion.ts @@ -0,0 +1,59 @@ +import { Request, Response } from 'express'; +import Joi from 'joi'; + +import validateRequestFactory from '../../common/services/validateRequest'; +import { validateLocalesMiddleware } from '../../middleware/validatelocales'; +import { exhaustiveCheck } from '../../util/exhaustiveCheck'; +import { LocalizedVersion } from '../interfaces'; +import { templatesRepository } from '../services/templatesRepository'; +import { localeNameSchema, localizedVersionSchema, templateNameSchema } from './validation'; + +const validateRequestBeforeUpsertLocalizedVersion = validateRequestFactory([ + { + schema: Joi.object({ + name: templateNameSchema.required(), + locale: localeNameSchema.required(), + }), + selector: 'params', + }, + { + schema: localizedVersionSchema.required(), + selector: 'body', + }, +]); + +const validateLocale = validateLocalesMiddleware( + (req) => [req.params.locale], + () => 'params.locale', +); + +type Params = { + name: string; + locale: string; +}; + +const upsertTemplateLocalizedVersion = async ( + req: Request, + res: Response, +): Promise => { + const { name: templateName, locale } = req.params; + const localizedVersion = req.body; + + const result = await templatesRepository.upsertLocalizedVersion(templateName, locale, localizedVersion); + switch (result.type) { + case 'notFound': { + res.status(404).send('Not found'); + return; + } + case 'ok': { + res.status(200).send(result.localizedVersion); + return; + } + default: { + /* istanbul ignore next */ + exhaustiveCheck(result); + } + } +}; + +export default [validateRequestBeforeUpsertLocalizedVersion, validateLocale, upsertTemplateLocalizedVersion]; diff --git a/registry/server/templates/routes/validation.ts b/registry/server/templates/routes/validation.ts index d0f1e03f8..cd7d9648e 100644 --- a/registry/server/templates/routes/validation.ts +++ b/registry/server/templates/routes/validation.ts @@ -1,9 +1,8 @@ import Joi from 'joi'; -import renderTemplate from '../services/renderTemplate'; -import { getJoiErr, joiErrorToResponse } from '../../util/helpers'; -import { Response } from 'express'; -import settingsService from '../../settings/services/SettingsService'; import { SettingKeys } from '../../settings/interfaces'; +import settingsService from '../../settings/services/SettingsService'; +import { getJoiErr } from '../../util/helpers'; +import renderTemplate from '../services/renderTemplate'; export const templateNameSchema = Joi.string().min(1).max(50); @@ -24,15 +23,16 @@ const commonTemplate = { versionId: Joi.string().strip(), }; -const localizedVersions = Joi.object().pattern( - Joi.string() - .regex(/[a-z]{2}-[A-Z]{2,4}/) - .min(5) - .max(7), - Joi.object({ - content: commonTemplate.content.required(), - }), -); +export const localeNameSchema = Joi.string() + .regex(/[a-z]{2}-[A-Z]{2,4}/) + .min(5) + .max(7); + +export const localizedVersionSchema = Joi.object({ + content: commonTemplate.content.required(), +}); + +const localizedVersions = Joi.object().pattern(localeNameSchema, localizedVersionSchema); export const partialTemplateSchema = Joi.object({ ...commonTemplate, @@ -46,21 +46,3 @@ export const templateSchema = Joi.object({ content: commonTemplate.content.required(), localizedVersions, }); - -export async function validateLocalesAreSupported(locales: string[], res: Response) { - const supportedLocales = await settingsService.get(SettingKeys.I18nSupportedLocales); - let unsupportedLocales = locales.filter((l) => !supportedLocales.includes(l)); - if (unsupportedLocales.length > 0) { - let joiError = getJoiErr( - `localizedVersions.${unsupportedLocales[0]}`, - `Next locales are not supported ${unsupportedLocales.join(',')}. Either change request or change ${ - SettingKeys.I18nSupportedLocales - } setting.`, - ); - res.status(422); - res.send(joiErrorToResponse(joiError)); - return false; - } - - return true; -} diff --git a/registry/server/templates/services/templatesRepository.ts b/registry/server/templates/services/templatesRepository.ts index 3045893cc..40bffa89f 100644 --- a/registry/server/templates/services/templatesRepository.ts +++ b/registry/server/templates/services/templatesRepository.ts @@ -1,15 +1,22 @@ -import { ok } from 'assert'; import { Knex } from 'knex'; import { PaginatedResult } from '../../../typings/PaginatedResult'; +import { User } from '../../../typings/User'; import db, { VersionedKnex } from '../../db'; import { Tables } from '../../db/structure'; import { appendDigest } from '../../util/hmac'; -import { EntityTypes } from '../../versioning/interfaces'; -import Template, { LocalizedTemplate } from '../interfaces'; +import { normalizeArray } from '../../util/normalizeArray'; +import { EntityTypes, VersionedRecord } from '../../versioning/interfaces'; +import errors from '../errors'; +import Template, { + CreateTemplatePayload, + LocalizedTemplateRow, + LocalizedVersion, + TemplateWithLocalizedVersions, + UpdateTemplatePayload, +} from '../interfaces'; import Transaction = Knex.Transaction; -import { normalizeArray } from '../../util/normalizeArray'; export interface TemplatesGetListFilters { domainId?: number | 'null'; @@ -17,6 +24,42 @@ export interface TemplatesGetListFilters { name?: string[] | string; } +interface UpdateTemplateResultOk { + type: 'ok'; + template: VersionedRecord; +} + +interface UpdateTemplateResultNotFound { + type: 'notFound'; +} + +type UpdateTemplateResult = UpdateTemplateResultOk | UpdateTemplateResultNotFound; + +interface UpsertTemplateLocalizedVersionResultOk { + type: 'ok'; + localizedVersion: LocalizedVersion; +} + +interface UpsertTemplateLocalizedVersionResultNotFound { + type: 'notFound'; +} + +type UpsertTemplateLocalizedVersionResult = + | UpsertTemplateLocalizedVersionResultOk + | UpsertTemplateLocalizedVersionResultNotFound; + +interface DeleteTemplateLocalizedVersionResultOk { + type: 'ok'; +} + +interface DeleteTemplateLocalizedVersionResultNotFound { + type: 'notFound'; +} + +type DeleteTemplateLocalizedVersionResult = + | DeleteTemplateLocalizedVersionResultOk + | DeleteTemplateLocalizedVersionResultNotFound; + export class TemplatesRepository { constructor(private db: VersionedKnex) {} @@ -47,11 +90,15 @@ export class TemplatesRepository { }; } - async readTemplateWithAllVersions(templateName: string) { + async readTemplateWithAllVersions( + templateName: string, + ): Promise | undefined> { const { db } = this; const [template] = await db - .selectVersionedRowsFrom(Tables.Templates, 'name', EntityTypes.templates, [`${Tables.Templates}.*`]) + .selectVersionedRowsFrom