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:
parent
31ecfd0b96
commit
0ae13f091b
7 changed files with 103 additions and 8 deletions
|
@ -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));
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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}`);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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/;
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue