0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-16 20:26:19 -05:00

feat(core): add PUT /custom-phrases/:languageKey route (#1907)

This commit is contained in:
IceHe 2022-09-15 14:49:23 +08:00 committed by GitHub
parent 31ecfd0b96
commit 0ae13f091b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 103 additions and 8 deletions

View file

@ -1,4 +1,4 @@
import { SchemaLike, GeneratedSchema } from '@logto/schemas';
import { GeneratedSchema, SchemaLike } from '@logto/schemas';
import { has } from '@silverhand/essentials';
import { IdentifierSqlToken, sql } from 'slonik';
@ -72,7 +72,6 @@ export const buildInsertInto: BuildInsertInto = <
insertingKeys.map((key) => convertToPrimitiveOrSql(key, data[key] ?? null)),
sql`, `
)})
${conditionalSql(returning, () => sql`returning *`)}
${conditionalSql(
onConflict,
({ fields, setExcludedFields }) => sql`
@ -80,6 +79,7 @@ export const buildInsertInto: BuildInsertInto = <
set ${setExcluded(...setExcludedFields)}
`
)}
${conditionalSql(returning, () => sql`returning *`)}
`);
assertThat(!returning || entry, new InsertionError(schema, data));

View file

@ -1,6 +1,7 @@
import { CustomPhrase, CustomPhrases } from '@logto/schemas';
import { CreateCustomPhrase, CustomPhrase, CustomPhrases } from '@logto/schemas';
import { sql } from 'slonik';
import { buildInsertInto } from '@/database/insert-into';
import { convertToIdentifiers } from '@/database/utils';
import envSet from '@/env-set';
import { DeletionError } from '@/errors/SlonikError';
@ -14,6 +15,14 @@ export const findCustomPhraseByLanguageKey = async (languageKey: string): Promis
where ${fields.languageKey} = ${languageKey}
`);
export const upsertCustomPhrase = buildInsertInto<CreateCustomPhrase, CustomPhrase>(CustomPhrases, {
returning: true,
onConflict: {
fields: [fields.languageKey],
setExcludedFields: [fields.translation],
},
});
export const deleteCustomPhraseByLanguageKey = async (languageKey: string) => {
const { rowCount } = await envSet.pool.query(sql`
delete from ${table}

View file

@ -37,9 +37,12 @@ const findCustomPhraseByLanguageKey = jest.fn(async (languageKey: string) => {
return mockCustomPhrase;
});
const upsertCustomPhrase = jest.fn(async (customPhrase: CustomPhrase) => customPhrase);
jest.mock('@/queries/custom-phrase', () => ({
deleteCustomPhraseByLanguageKey: async (key: string) => deleteCustomPhraseByLanguageKey(key),
findCustomPhraseByLanguageKey: async (key: string) => findCustomPhraseByLanguageKey(key),
upsertCustomPhrase: async (customPhrase: CustomPhrase) => upsertCustomPhrase(customPhrase),
}));
describe('customPhraseRoutes', () => {
@ -67,6 +70,23 @@ describe('customPhraseRoutes', () => {
});
});
describe('PUT /custom-phrases/:languageKey', () => {
it('should call upsertCustomPhrase with specified language key', async () => {
await customPhraseRequest
.put(`/custom-phrases/${mockLanguageKey}`)
.send(mockCustomPhrases[mockLanguageKey]?.translation);
expect(upsertCustomPhrase).toBeCalledWith(mockCustomPhrases[mockLanguageKey]);
});
it('should return the custom phrase after upserting', async () => {
const response = await customPhraseRequest
.put(`/custom-phrases/${mockLanguageKey}`)
.send(mockCustomPhrases[mockLanguageKey]?.translation);
expect(response.status).toEqual(200);
expect(response.body).toEqual(mockCustomPhrases[mockLanguageKey]);
});
});
describe('DELETE /custom-phrases/:languageKey', () => {
it('should call deleteCustomPhraseByLanguageKey', async () => {
await customPhraseRequest.delete(`/custom-phrases/${mockLanguageKey}`);

View file

@ -1,9 +1,10 @@
import { CustomPhrases } from '@logto/schemas';
import { CustomPhrases, translationGuard } from '@logto/schemas';
import koaGuard from '@/middleware/koa-guard';
import {
deleteCustomPhraseByLanguageKey,
findCustomPhraseByLanguageKey,
upsertCustomPhrase,
} from '@/queries/custom-phrase';
import { AuthedRouter } from './types';
@ -27,6 +28,25 @@ export default function customPhraseRoutes<T extends AuthedRouter>(router: T) {
}
);
router.put(
'/custom-phrases/:languageKey',
koaGuard({
params: CustomPhrases.createGuard.pick({ languageKey: true }),
body: translationGuard,
response: CustomPhrases.guard,
}),
async (ctx, next) => {
const {
params: { languageKey },
body,
} = ctx.guard;
ctx.body = await upsertCustomPhrase({ languageKey, translation: body });
return next();
}
);
router.delete(
'/custom-phrases/:languageKey',
koaGuard({

View file

@ -9,7 +9,7 @@ import { ZodObject, ZodOptional } from 'zod';
import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
import { fallbackDefaultPageSize, isPaginationMiddleware } from '@/middleware/koa-pagination';
import assertThat from '@/utils/assert-that';
import { zodTypeToSwagger } from '@/utils/zod';
import { translationSchemas, zodTypeToSwagger } from '@/utils/zod';
import { AnonymousRouter } from './types';
@ -163,6 +163,7 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
version: '0.1.0',
},
paths: Object.fromEntries(pathMap),
components: { schemas: translationSchemas },
};
ctx.body = document;

View file

@ -1,4 +1,4 @@
import { ApplicationType, arbitraryObjectGuard } from '@logto/schemas';
import { ApplicationType, arbitraryObjectGuard, translationGuard } from '@logto/schemas';
import { string, boolean, number, object, nativeEnum, unknown, literal, union } from 'zod';
import RequestError from '@/errors/RequestError';
@ -13,6 +13,12 @@ describe('zodTypeToSwagger', () => {
});
});
it('translation object guard', () => {
expect(zodTypeToSwagger(translationGuard)).toEqual({
$ref: '#/components/schemas/TranslationObject',
});
});
describe('string type', () => {
const notStartingWithDigitRegex = /^\D/;

View file

@ -1,4 +1,4 @@
import { arbitraryObjectGuard } from '@logto/schemas';
import { arbitraryObjectGuard, translationGuard } from '@logto/schemas';
import { conditional, ValuesOf } from '@silverhand/essentials';
import { OpenAPIV3 } from 'openapi-types';
import {
@ -19,6 +19,37 @@ import {
import RequestError from '@/errors/RequestError';
export const translationSchemas: Record<string, OpenAPIV3.SchemaObject> = {
TranslationObject: {
type: 'object',
properties: {
'[translationKey]': {
$ref: '#/components/schemas/Translation',
},
},
example: {
input: {
username: 'Username',
password: 'Password',
},
action: {
sign_in: 'Sign In',
continue: 'Continue',
},
},
},
Translation: {
oneOf: [
{
type: 'string',
},
{
$ref: '#/components/schemas/Translation',
},
],
},
};
export type ZodStringCheck = ValuesOf<ZodStringDef['checks']>;
const zodStringCheckToSwaggerFormat = (zodStringCheck: ZodStringCheck) => {
@ -93,7 +124,9 @@ const zodLiteralToSwagger = (zodLiteral: ZodLiteral<unknown>): OpenAPIV3.SchemaO
}
};
export const zodTypeToSwagger = (config: unknown): OpenAPIV3.SchemaObject => {
export const zodTypeToSwagger = (
config: unknown
): OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject => {
if (config === arbitraryObjectGuard) {
return {
type: 'object',
@ -101,6 +134,12 @@ export const zodTypeToSwagger = (config: unknown): OpenAPIV3.SchemaObject => {
};
}
if (config === translationGuard) {
return {
$ref: '#/components/schemas/TranslationObject',
};
}
if (config instanceof ZodOptional) {
return zodTypeToSwagger(config._def.innerType);
}