mirror of
https://github.com/logto-io/logto.git
synced 2025-03-24 22:41:28 -05:00
refactor(core): organization apis error handling
This commit is contained in:
parent
406e54e84f
commit
c3219f6fcd
24 changed files with 177 additions and 83 deletions
|
@ -10,6 +10,7 @@ import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
|||
|
||||
import organizationRoleRoutes from './roles.js';
|
||||
import organizationScopeRoutes from './scopes.js';
|
||||
import { errorHandler } from './utils.js';
|
||||
|
||||
export default function organizationRoutes<T extends AuthedRouter>(...args: RouterInitArgs<T>) {
|
||||
const [
|
||||
|
@ -18,7 +19,9 @@ export default function organizationRoutes<T extends AuthedRouter>(...args: Rout
|
|||
queries: { organizations, users },
|
||||
},
|
||||
] = args;
|
||||
const router = new SchemaRouter(Organizations, new SchemaActions(organizations));
|
||||
const router = new SchemaRouter(Organizations, new SchemaActions(organizations), {
|
||||
errorHandler,
|
||||
});
|
||||
|
||||
router.addRelationRoutes(organizations.relations.users);
|
||||
|
||||
|
|
|
@ -1,37 +1,12 @@
|
|||
import {
|
||||
type CreateOrganizationRole,
|
||||
type OrganizationRole,
|
||||
type OrganizationRoleKeys,
|
||||
OrganizationRoles,
|
||||
} from '@logto/schemas';
|
||||
import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
||||
import { type CreateOrganizationRole, OrganizationRoles } from '@logto/schemas';
|
||||
import { z } from 'zod';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
import koaGuard from '#src/middleware/koa-guard.js';
|
||||
import SchemaRouter, { SchemaActions } from '#src/utils/SchemaRouter.js';
|
||||
|
||||
import { type AuthedRouter, type RouterInitArgs } from '../types.js';
|
||||
|
||||
class OrganizationRoleActions extends SchemaActions<
|
||||
OrganizationRoleKeys,
|
||||
CreateOrganizationRole,
|
||||
OrganizationRole
|
||||
> {
|
||||
override async post(
|
||||
data: Omit<CreateOrganizationRole, 'id'>
|
||||
): Promise<Readonly<OrganizationRole>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { errorHandler } from './utils.js';
|
||||
|
||||
export default function organizationRoleRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
|
@ -46,8 +21,11 @@ export default function organizationRoleRoutes<T extends AuthedRouter>(
|
|||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const actions = new OrganizationRoleActions(roles);
|
||||
const router = new SchemaRouter(OrganizationRoles, actions, { disabled: { post: true } });
|
||||
const actions = new SchemaActions(roles);
|
||||
const router = new SchemaRouter(OrganizationRoles, actions, {
|
||||
disabled: { post: true },
|
||||
errorHandler,
|
||||
});
|
||||
|
||||
/** Allows to carry an initial set of scopes for creating a new organization role. */
|
||||
type CreateOrganizationRolePayload = Omit<CreateOrganizationRole, 'id'> & {
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
import { OrganizationScopeActions } from './scopes.js';
|
||||
|
||||
describe('OrganizationScopeActions', () => {
|
||||
it('should throw RequestError if UniqueIntegrityConstraintViolationError is thrown inside', async () => {
|
||||
// @ts-expect-error for testing
|
||||
const actions = new OrganizationScopeActions({
|
||||
insert: async () => {
|
||||
throw new UniqueIntegrityConstraintViolationError(new Error('test'), 'unique');
|
||||
},
|
||||
});
|
||||
|
||||
await expect(actions.post({ name: 'test' })).rejects.toThrowError(
|
||||
new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' })
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,35 +1,10 @@
|
|||
import {
|
||||
type CreateOrganizationScope,
|
||||
type OrganizationScope,
|
||||
type OrganizationScopeKeys,
|
||||
OrganizationScopes,
|
||||
} from '@logto/schemas';
|
||||
import { UniqueIntegrityConstraintViolationError } from 'slonik';
|
||||
import { OrganizationScopes } from '@logto/schemas';
|
||||
|
||||
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<CreateOrganizationScope, 'id'>
|
||||
): Promise<Readonly<OrganizationScope>> {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
import { errorHandler } from './utils.js';
|
||||
|
||||
export default function organizationScopeRoutes<T extends AuthedRouter>(
|
||||
...[
|
||||
|
@ -41,7 +16,7 @@ export default function organizationScopeRoutes<T extends AuthedRouter>(
|
|||
},
|
||||
]: RouterInitArgs<T>
|
||||
) {
|
||||
const router = new SchemaRouter(OrganizationScopes, new OrganizationScopeActions(scopes));
|
||||
const router = new SchemaRouter(OrganizationScopes, new SchemaActions(scopes), { errorHandler });
|
||||
|
||||
originalRouter.use(router.routes());
|
||||
}
|
||||
|
|
18
packages/core/src/routes/organization/utils.ts
Normal file
18
packages/core/src/routes/organization/utils.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import {
|
||||
ForeignKeyIntegrityConstraintViolationError,
|
||||
UniqueIntegrityConstraintViolationError,
|
||||
} from 'slonik';
|
||||
|
||||
import RequestError from '#src/errors/RequestError/index.js';
|
||||
|
||||
export const errorHandler = (error: unknown) => {
|
||||
if (error instanceof UniqueIntegrityConstraintViolationError) {
|
||||
throw new RequestError({ code: 'entity.duplicate_value_of_unique_field', field: 'name' });
|
||||
}
|
||||
|
||||
if (error instanceof ForeignKeyIntegrityConstraintViolationError) {
|
||||
throw new RequestError({ code: 'entity.relation_foreign_key_not_found', status: 422 });
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
|
@ -4,6 +4,8 @@ import { sql, type CommonQueryMethods } from 'slonik';
|
|||
import snakecaseKeys from 'snakecase-keys';
|
||||
import { type z } from 'zod';
|
||||
|
||||
import { DeletionError } from '#src/errors/SlonikError/index.js';
|
||||
|
||||
type AtLeast2<T extends unknown[]> = `${T['length']}` extends '0' | '1' ? never : T;
|
||||
|
||||
type TableInfo<Table, TableSingular, Schema> = {
|
||||
|
@ -135,7 +137,7 @@ export default class RelationQueries<
|
|||
*/
|
||||
async delete(data: CamelCaseIdObject<Schemas[number]['tableSingular']>) {
|
||||
const snakeCaseData = snakecaseKeys(data);
|
||||
return this.pool.query(sql`
|
||||
const { rowCount } = await this.pool.query(sql`
|
||||
delete from ${this.table}
|
||||
where ${sql.join(
|
||||
Object.entries(snakeCaseData).map(
|
||||
|
@ -144,6 +146,10 @@ export default class RelationQueries<
|
|||
sql` and `
|
||||
)};
|
||||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError(this.relationTable);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -90,7 +90,7 @@ export class SchemaActions<
|
|||
* @param id The ID of the entity to be updated.
|
||||
* @param data The data of the entity to be updated.
|
||||
* @returns The updated entity.
|
||||
* @throws An `RequestError` with 404 status code if the entity is not found.
|
||||
* @throws An `UpdateError` if the entity is not found.
|
||||
*/
|
||||
public async patchById(id: string, data: Partial<Schema>): Promise<Readonly<Schema>> {
|
||||
return this.queries.updateById(id, data);
|
||||
|
@ -100,7 +100,7 @@ export class SchemaActions<
|
|||
* The function for `DELETE /:id` route to delete an entity by ID.
|
||||
*
|
||||
* @param id The ID of the entity to be deleted.
|
||||
* @throws An `RequestError` with 404 status code if the entity is not found.
|
||||
* @throws An `DeletionError` if the entity is not found.
|
||||
*/
|
||||
public async deleteById(id: string): Promise<void> {
|
||||
return this.queries.deleteById(id);
|
||||
|
@ -121,6 +121,7 @@ type SchemaRouterConfig = {
|
|||
/** Disable `DELETE /:id` route. */
|
||||
deleteById: boolean;
|
||||
};
|
||||
errorHandler?: (error: unknown) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -167,6 +168,17 @@ export default class SchemaRouter<
|
|||
config
|
||||
);
|
||||
|
||||
if (this.config.errorHandler) {
|
||||
this.use(async (_, next) => {
|
||||
try {
|
||||
await next();
|
||||
} catch (error: unknown) {
|
||||
this.config.errorHandler?.(error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const { disabled } = this.config;
|
||||
|
||||
if (!disabled.get) {
|
||||
|
@ -324,7 +336,7 @@ export default class SchemaRouter<
|
|||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1) }),
|
||||
body: z.object({ [columns.relationSchemaIds]: z.string().min(1).array().nonempty() }),
|
||||
status: [201, 404, 422],
|
||||
status: [201, 422],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
@ -344,7 +356,8 @@ export default class SchemaRouter<
|
|||
`/:id/${pathname}/:relationId`,
|
||||
koaGuard({
|
||||
params: z.object({ id: z.string().min(1), relationId: z.string().min(1) }),
|
||||
status: [204, 422],
|
||||
// Should be 422 if the relation doesn't exist, update until we change the error handling
|
||||
status: [204, 404],
|
||||
}),
|
||||
async (ctx, next) => {
|
||||
const {
|
||||
|
|
|
@ -57,6 +57,23 @@ describe('organization role APIs', () => {
|
|||
await roleApi.delete(role.id);
|
||||
});
|
||||
|
||||
it('should fail to create a role with some scopes if the scopes do not exist', async () => {
|
||||
const name = 'test' + randomId();
|
||||
const organizationScopeIds = ['0'];
|
||||
const response = await roleApi
|
||||
.create({ name, organizationScopeIds })
|
||||
.catch((error: unknown) => error);
|
||||
|
||||
assert(response instanceof HTTPError);
|
||||
|
||||
const { statusCode, body: raw } = response.response;
|
||||
const body: unknown = JSON.parse(String(raw));
|
||||
expect(statusCode).toBe(422);
|
||||
expect(isKeyInObject(body, 'code') && body.code).toBe(
|
||||
'entity.relation_foreign_key_not_found'
|
||||
);
|
||||
});
|
||||
|
||||
it('should get organization roles successfully', async () => {
|
||||
const [name1, name2] = ['test' + randomId(), 'test' + randomId()];
|
||||
const createdRoles = await Promise.all([
|
||||
|
@ -165,6 +182,31 @@ describe('organization role APIs', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should fail when try to add non-existent scopes to a role', async () => {
|
||||
const [role, scope1, scope2] = await Promise.all([
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
scopeApi.create({ name: 'test' + randomId() }),
|
||||
]);
|
||||
const response = await roleApi
|
||||
.addScopes(role.id, [scope1.id, scope2.id, '0'])
|
||||
.catch((error: unknown) => error);
|
||||
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.statusCode).toBe(422);
|
||||
expect(JSON.parse(String(response.response.body))).toMatchObject(
|
||||
expect.objectContaining({
|
||||
code: 'entity.relation_foreign_key_not_found',
|
||||
})
|
||||
);
|
||||
|
||||
await Promise.all([
|
||||
roleApi.delete(role.id),
|
||||
scopeApi.delete(scope1.id),
|
||||
scopeApi.delete(scope2.id),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should be able to remove scopes from a role', async () => {
|
||||
const [role, scope1, scope2] = await Promise.all([
|
||||
roleApi.create({ name: 'test' + randomId() }),
|
||||
|
@ -192,5 +234,16 @@ describe('organization role APIs', () => {
|
|||
scopeApi.delete(scope2.id),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should fail when try to remove non-existent scopes from a role', async () => {
|
||||
const [role] = await Promise.all([roleApi.create({ name: 'test' + randomId() })]);
|
||||
|
||||
const response = await roleApi.deleteScope(role.id, '0').catch((error: unknown) => error);
|
||||
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.statusCode).toBe(404);
|
||||
|
||||
await Promise.all([roleApi.delete(role.id)]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -115,6 +115,24 @@ describe('organization APIs', () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it('should fail when try to add empty user list', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const response = await organizationApi
|
||||
.addUsers(organization.id, [])
|
||||
.catch((error: unknown) => error);
|
||||
expect(response instanceof HTTPError && response.response.statusCode).toBe(400);
|
||||
await organizationApi.delete(organization.id);
|
||||
});
|
||||
|
||||
it('should fail when try to add user to an organization that does not exist', async () => {
|
||||
const response = await organizationApi.addUsers('0', ['0']).catch((error: unknown) => error);
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.statusCode).toBe(422);
|
||||
expect(JSON.parse(String(response.response.body))).toMatchObject(
|
||||
expect.objectContaining({ code: 'entity.relation_foreign_key_not_found' })
|
||||
);
|
||||
});
|
||||
|
||||
it('should be able to delete organization user', async () => {
|
||||
const organization = await organizationApi.create({ name: 'test' });
|
||||
const user = await createUser({ username: 'test' + randomId() });
|
||||
|
@ -125,6 +143,12 @@ describe('organization APIs', () => {
|
|||
expect(users).not.toContainEqual(user);
|
||||
await Promise.all([organizationApi.delete(organization.id), deleteUser(user.id)]);
|
||||
});
|
||||
|
||||
it('should fail when try to delete user from an organization that does not exist', async () => {
|
||||
const response = await organizationApi.deleteUser('0', '0').catch((error: unknown) => error);
|
||||
assert(response instanceof HTTPError);
|
||||
expect(response.response.statusCode).toBe(404);
|
||||
});
|
||||
});
|
||||
|
||||
describe('organization - user - organization role relation routes', () => {
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{name}} mit ID `{{id}}` existiert nicht.',
|
||||
not_found: 'Die Ressource wurde nicht gefunden.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -5,6 +5,8 @@ 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.',
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'El {{name}} con ID `{{id}}` no existe.',
|
||||
not_found: 'El recurso no existe.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: "Le {{name}} avec l'ID `{{id}}` n'existe pas.",
|
||||
not_found: "La ressource n'existe pas.",
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{name}} con ID `{{id}}` non esiste.',
|
||||
not_found: 'La risorsa non esiste.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'IDが`{{id}}`の{{name}}は存在しません。',
|
||||
not_found: 'リソースが存在しません。',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{id}} ID를 가진 {{name}}는 존재하지 않아요.',
|
||||
not_found: '리소스가 존재하지 않아요.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{name}} o identyfikatorze `{{id}}` nie istnieje.',
|
||||
not_found: 'Zasób nie istnieje.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'O {{name}} com ID `{{id}}` não existe.',
|
||||
not_found: 'O recurso não existe.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{name}} com o ID `{{id}}` não existe.',
|
||||
not_found: 'O recurso não existe.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: '{{name}} с ID `{{id}}` не существует.',
|
||||
not_found: 'Ресурс не существует.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: ' `{{id}}` id kimliğine sahip {{name}} mevcut değil.',
|
||||
not_found: 'Kaynak mevcut değil.',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'ID 为 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '该资源不存在。',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '該資源不存在。',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
|
@ -6,6 +6,9 @@ const entity = {
|
|||
not_exists_with_id: 'ID 為 `{{id}}` 的 {{name}} 不存在。',
|
||||
not_found: '資源不存在。',
|
||||
/** UNTRANSLATED */
|
||||
relation_foreign_key_not_found:
|
||||
'Cannot find one or more foreign keys. Please check the input and ensure that all referenced entities exist.',
|
||||
/** UNTRANSLATED */
|
||||
duplicate_value_of_unique_field: 'The value of the unique field `{{field}}` is duplicated.',
|
||||
};
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue