diff --git a/packages/core/src/routes/organization-scopes.test.ts b/packages/core/src/routes/organization-scopes.test.ts new file mode 100644 index 000000000..a8216ac4b --- /dev/null +++ b/packages/core/src/routes/organization-scopes.test.ts @@ -0,0 +1,23 @@ +import { UniqueIntegrityConstraintViolationError } from 'slonik'; + +import RequestError from '#src/errors/RequestError/index.js'; +import { MockTenant } from '#src/test-utils/tenant.js'; + +import { OrganizationScopeActions } from './organization-scopes.js'; + +describe('OrganizationScopeActions', () => { + it('should throw RequestError if UniqueIntegrityConstraintViolationError is thrown inside', async () => { + const tenantContext = new MockTenant(undefined, { + organizationScopes: { + insert: async () => { + throw new UniqueIntegrityConstraintViolationError(new Error('test'), 'unique'); + }, + }, + }); + const actions = new OrganizationScopeActions(tenantContext.queries.organizationScopes); + + await expect(actions.post({ name: 'test' })).rejects.toThrowError( + new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' }) + ); + }); +}); diff --git a/packages/core/src/routes/organization-scopes.ts b/packages/core/src/routes/organization-scopes.ts index a65045223..092fc8581 100644 --- a/packages/core/src/routes/organization-scopes.ts +++ b/packages/core/src/routes/organization-scopes.ts @@ -1,9 +1,36 @@ -import { OrganizationScopes } from '@logto/schemas'; +import { + type CreateOrganizationScope, + type OrganizationScope, + type OrganizationScopeKeys, + OrganizationScopes, +} from '@logto/schemas'; +import { UniqueIntegrityConstraintViolationError } from 'slonik'; +import RequestError from '#src/errors/RequestError/index.js'; import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js'; import { type AuthedRouter, type RouterInitArgs } from './types.js'; +export class OrganizationScopeActions extends SchemaActions< + OrganizationScopeKeys, + CreateOrganizationScope, + OrganizationScope +> { + override async post( + data: Omit + ): Promise> { + try { + return await super.post(data); + } catch (error: unknown) { + if (error instanceof UniqueIntegrityConstraintViolationError) { + throw new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' }); + } + + throw error; + } + } +} + export default function organizationScopeRoutes( ...[ originalRouter, @@ -12,7 +39,10 @@ export default function organizationScopeRoutes( }, ]: RouterInitArgs ) { - const router = new SchemaRouter(OrganizationScopes, new SchemaActions(organizationScopes)); + const router = new SchemaRouter( + OrganizationScopes, + new OrganizationScopeActions(organizationScopes) + ); originalRouter.use(router.routes()); } diff --git a/packages/integration-tests/src/tests/api/organization-scope.test.ts b/packages/integration-tests/src/tests/api/organization-scope.test.ts index d6b8d3535..ac46ab0d2 100644 --- a/packages/integration-tests/src/tests/api/organization-scope.test.ts +++ b/packages/integration-tests/src/tests/api/organization-scope.test.ts @@ -1,4 +1,7 @@ +import assert from 'node:assert'; + import { generateStandardId } from '@logto/shared'; +import { isKeyInObject } from '@silverhand/essentials'; import { HTTPError } from 'got'; import { @@ -12,6 +15,19 @@ import { const randomId = () => generateStandardId(4); describe('organization scopes', () => { + it('should fail if the name of the new organization scope already exists', async () => { + const name = 'test' + randomId(); + await createOrganizationScope(name); + const response = await createOrganizationScope(name).catch((error: unknown) => error); + + assert(response instanceof HTTPError); + + const { statusCode, body: raw } = response.response; + const body: unknown = JSON.parse(String(raw)); + expect(statusCode).toBe(400); + expect(isKeyInObject(body, 'code') && body.code).toBe('entity.duplicate_value_of_unique_field'); + }); + it('should get organization scopes successfully', async () => { const [name1, name2] = ['test' + randomId(), 'test' + randomId()]; await createOrganizationScope(name1, 'A test organization scope.'); diff --git a/packages/phrases/src/locales/de/errors/entity.ts b/packages/phrases/src/locales/de/errors/entity.ts index b645df292..6c976a91c 100644 --- a/packages/phrases/src/locales/de/errors/entity.ts +++ b/packages/phrases/src/locales/de/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} existiert nicht.', not_exists_with_id: '{{name}} mit ID `{{id}}` existiert nicht.', not_found: 'Die Ressource wurde nicht gefunden.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/en/errors/entity.ts b/packages/phrases/src/locales/en/errors/entity.ts index 8c0c22136..25bd3b6cf 100644 --- a/packages/phrases/src/locales/en/errors/entity.ts +++ b/packages/phrases/src/locales/en/errors/entity.ts @@ -5,6 +5,7 @@ const entity = { not_exists: 'The {{name}} does not exist.', not_exists_with_id: 'The {{name}} with ID `{{id}}` does not exist.', not_found: 'The resource does not exist.', + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/es/errors/entity.ts b/packages/phrases/src/locales/es/errors/entity.ts index 0e291941d..80432fccc 100644 --- a/packages/phrases/src/locales/es/errors/entity.ts +++ b/packages/phrases/src/locales/es/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: 'El {{name}} no existe.', not_exists_with_id: 'El {{name}} con ID `{{id}}` no existe.', not_found: 'El recurso no existe.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/fr/errors/entity.ts b/packages/phrases/src/locales/fr/errors/entity.ts index 6a83ca638..58f4bb2cf 100644 --- a/packages/phrases/src/locales/fr/errors/entity.ts +++ b/packages/phrases/src/locales/fr/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: "Le {{name}} n'existe pas.", not_exists_with_id: "Le {{name}} avec l'ID `{{id}}` n'existe pas.", not_found: "La ressource n'existe pas.", + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/it/errors/entity.ts b/packages/phrases/src/locales/it/errors/entity.ts index b26f18c75..c5a988479 100644 --- a/packages/phrases/src/locales/it/errors/entity.ts +++ b/packages/phrases/src/locales/it/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} non esiste.', not_exists_with_id: '{{name}} con ID `{{id}}` non esiste.', not_found: 'La risorsa non esiste.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/ja/errors/entity.ts b/packages/phrases/src/locales/ja/errors/entity.ts index 7ba5b4ab8..3a0094d11 100644 --- a/packages/phrases/src/locales/ja/errors/entity.ts +++ b/packages/phrases/src/locales/ja/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}}は存在しません。', not_exists_with_id: 'IDが`{{id}}`の{{name}}は存在しません。', not_found: 'リソースが存在しません。', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/ko/errors/entity.ts b/packages/phrases/src/locales/ko/errors/entity.ts index a81639331..5f3abba7d 100644 --- a/packages/phrases/src/locales/ko/errors/entity.ts +++ b/packages/phrases/src/locales/ko/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}}는 존재하지 않아요.', not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.', not_found: '리소스가 존재하지 않아요.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/pl-pl/errors/entity.ts b/packages/phrases/src/locales/pl-pl/errors/entity.ts index 723a3e7e2..7cfbab2f9 100644 --- a/packages/phrases/src/locales/pl-pl/errors/entity.ts +++ b/packages/phrases/src/locales/pl-pl/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} nie istnieje.', not_exists_with_id: '{{name}} o identyfikatorze `{{id}}` nie istnieje.', not_found: 'Zasób nie istnieje.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/pt-br/errors/entity.ts b/packages/phrases/src/locales/pt-br/errors/entity.ts index d8a1c636f..1a7ad53cc 100644 --- a/packages/phrases/src/locales/pt-br/errors/entity.ts +++ b/packages/phrases/src/locales/pt-br/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: 'O {{name}} não existe.', not_exists_with_id: 'O {{name}} com ID `{{id}}` não existe.', not_found: 'O recurso não existe.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/pt-pt/errors/entity.ts b/packages/phrases/src/locales/pt-pt/errors/entity.ts index be19d9f16..9652b4abe 100644 --- a/packages/phrases/src/locales/pt-pt/errors/entity.ts +++ b/packages/phrases/src/locales/pt-pt/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} não existe.', not_exists_with_id: '{{name}} com o ID `{{id}}` não existe.', not_found: 'O recurso não existe.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/ru/errors/entity.ts b/packages/phrases/src/locales/ru/errors/entity.ts index 10ebe43cf..5b59de227 100644 --- a/packages/phrases/src/locales/ru/errors/entity.ts +++ b/packages/phrases/src/locales/ru/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} не существует.', not_exists_with_id: '{{name}} с ID `{{id}}` не существует.', not_found: 'Ресурс не существует.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/tr-tr/errors/entity.ts b/packages/phrases/src/locales/tr-tr/errors/entity.ts index e08462fd9..dc2d6e968 100644 --- a/packages/phrases/src/locales/tr-tr/errors/entity.ts +++ b/packages/phrases/src/locales/tr-tr/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} mevcut değil.', not_exists_with_id: ' `{{id}}` id kimliğine sahip {{name}} mevcut değil.', not_found: 'Kaynak mevcut değil.', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/zh-cn/errors/entity.ts b/packages/phrases/src/locales/zh-cn/errors/entity.ts index e2813140a..d4ed8a178 100644 --- a/packages/phrases/src/locales/zh-cn/errors/entity.ts +++ b/packages/phrases/src/locales/zh-cn/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '该 {{name}} 不存在。', not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在。', not_found: '该资源不存在。', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/zh-hk/errors/entity.ts b/packages/phrases/src/locales/zh-hk/errors/entity.ts index 92366f524..be338f09f 100644 --- a/packages/phrases/src/locales/zh-hk/errors/entity.ts +++ b/packages/phrases/src/locales/zh-hk/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '該 {{name}} 不存在。', not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。', not_found: '該資源不存在。', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity); diff --git a/packages/phrases/src/locales/zh-tw/errors/entity.ts b/packages/phrases/src/locales/zh-tw/errors/entity.ts index 38c6549b4..0bed3b82b 100644 --- a/packages/phrases/src/locales/zh-tw/errors/entity.ts +++ b/packages/phrases/src/locales/zh-tw/errors/entity.ts @@ -5,6 +5,8 @@ const entity = { not_exists: '{{name}} 不存在。', not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。', not_found: '資源不存在。', + /** UNTRANSLATED */ + duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.', }; export default Object.freeze(entity);