mirror of
https://github.com/logto-io/logto.git
synced 2025-03-03 22:15:32 -05:00
refactor(core): slonik error handling (#307)
This commit is contained in:
parent
16396cf231
commit
dcd84086f6
10 changed files with 105 additions and 57 deletions
|
@ -4,14 +4,9 @@ import { DatabasePoolType, sql } from 'slonik';
|
|||
|
||||
import { isKeyOf } from '@/utils/schema';
|
||||
|
||||
import { FindManyData } from './types';
|
||||
import { conditionalSql, convertToIdentifiers, convertToPrimitiveOrSql } from './utils';
|
||||
|
||||
export type FindManyData<Schema extends SchemaLike> = {
|
||||
where?: Partial<Schema>;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export const buildFindMany = <Schema extends SchemaLike, ReturnType extends SchemaLike>(
|
||||
pool: DatabasePoolType,
|
||||
schema: GeneratedSchema<Schema>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { CreateUser, Users } from '@logto/schemas';
|
||||
import decamelize from 'decamelize';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { InsertionError } from '@/errors/SlonikError';
|
||||
import { createTestPool } from '@/utils/test-utils';
|
||||
|
||||
import { buildInsertInto } from './insert-into';
|
||||
|
@ -59,19 +59,15 @@ describe('buildInsertInto()', () => {
|
|||
).resolves.toStrictEqual(user);
|
||||
});
|
||||
|
||||
it('throws `entity.create_failed` error with `undefined` when `returning` is true', async () => {
|
||||
it('throws `InsertionError` error when `returning` is true', async () => {
|
||||
const user: CreateUser = { id: 'foo', username: '123', primaryEmail: 'foo@bar.com' };
|
||||
const expectInsertIntoSql = buildExpectedInsertIntoSql(Object.keys(user));
|
||||
const pool = createTestPool([...expectInsertIntoSql, 'returning *'].join('\n'));
|
||||
const insertInto = buildInsertInto(pool, Users, { returning: true });
|
||||
const dataToInsert = { id: 'foo', username: '123', primaryEmail: 'foo@bar.com' };
|
||||
|
||||
await expect(
|
||||
insertInto({ id: 'foo', username: '123', primaryEmail: 'foo@bar.com' })
|
||||
).rejects.toMatchError(
|
||||
new RequestError({
|
||||
code: 'entity.create_failed',
|
||||
name: Users.tableSingular,
|
||||
})
|
||||
await expect(insertInto(dataToInsert)).rejects.toMatchError(
|
||||
new InsertionError(Users, dataToInsert)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,6 +2,7 @@ import { SchemaLike, GeneratedSchema } from '@logto/schemas';
|
|||
import { has } from '@silverhand/essentials';
|
||||
import { DatabasePoolType, IdentifierSqlTokenType, sql } from 'slonik';
|
||||
|
||||
import { InsertionError } from '@/errors/SlonikError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
|
||||
import {
|
||||
|
@ -51,9 +52,10 @@ export const buildInsertInto: BuildInsertInto = <
|
|||
ReturnType extends SchemaLike
|
||||
>(
|
||||
pool: DatabasePoolType,
|
||||
{ fieldKeys, ...rest }: GeneratedSchema<Schema>,
|
||||
schema: GeneratedSchema<Schema>,
|
||||
config?: InsertIntoConfig | InsertIntoConfigReturning
|
||||
) => {
|
||||
const { fieldKeys, ...rest } = schema;
|
||||
const { table, fields } = convertToIdentifiers(rest);
|
||||
const keys = excludeAutoSetFields(fieldKeys);
|
||||
const returning = Boolean(config?.returning);
|
||||
|
@ -82,7 +84,7 @@ export const buildInsertInto: BuildInsertInto = <
|
|||
)}
|
||||
`);
|
||||
|
||||
assertThat(!returning || entry, 'entity.create_failed', { name: rest.tableSingular });
|
||||
assertThat(!returning || entry, new InsertionError(schema, data));
|
||||
|
||||
return entry;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,18 @@
|
|||
import { SchemaLike } from '@logto/schemas';
|
||||
import { IdentifierSqlTokenType } from 'slonik';
|
||||
|
||||
export type Table = { table: string; fields: Record<string, string> };
|
||||
export type FieldIdentifiers<Key extends string | number | symbol> = {
|
||||
[key in Key]: IdentifierSqlTokenType;
|
||||
};
|
||||
|
||||
export type FindManyData<Schema extends SchemaLike> = {
|
||||
where?: Partial<Schema>;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
};
|
||||
|
||||
export type UpdateWhereData<Schema extends SchemaLike> = {
|
||||
set: Partial<Schema>;
|
||||
where: Partial<Schema>;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { CreateUser, Users, Applications } from '@logto/schemas';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { UpdateError } from '@/errors/SlonikError';
|
||||
import { createTestPool } from '@/utils/test-utils';
|
||||
|
||||
import { buildUpdateWhere } from './update-where';
|
||||
|
@ -70,34 +70,20 @@ describe('buildUpdateWhere()', () => {
|
|||
it('throws `entity.not_exists_with_id` error with `undefined` when `returning` is true', async () => {
|
||||
const pool = createTestPool('update "users"\nset "username"=$1\nwhere "id"=$2\nreturning *');
|
||||
const updateWhere = buildUpdateWhere(pool, Users, true);
|
||||
const updateWhereData = { set: { username: '123' }, where: { id: 'foo' } };
|
||||
|
||||
await expect(
|
||||
updateWhere({ set: { username: '123' }, where: { id: 'foo' } })
|
||||
).rejects.toMatchError(
|
||||
new RequestError({
|
||||
code: 'entity.not_exists_with_id',
|
||||
name: Users.tableSingular,
|
||||
id: 'foo',
|
||||
status: 404,
|
||||
})
|
||||
await expect(updateWhere(updateWhereData)).rejects.toMatchError(
|
||||
new UpdateError(Users, updateWhereData)
|
||||
);
|
||||
});
|
||||
|
||||
it('throws `entity.not_exists` error with `undefined` when `returning` is true and no id in where clause', async () => {
|
||||
it('throws `UpdateError` error when `returning` is true and no id in where clause', async () => {
|
||||
const pool = createTestPool(
|
||||
'update "users"\nset "username"=$1\nwhere "username"=$2\nreturning *'
|
||||
);
|
||||
const updateWhere = buildUpdateWhere(pool, Users, true);
|
||||
const updateData = { set: { username: '123' }, where: { username: 'foo' } };
|
||||
|
||||
await expect(
|
||||
updateWhere({ set: { username: '123' }, where: { username: 'foo' } })
|
||||
).rejects.toMatchError(
|
||||
new RequestError({
|
||||
code: 'entity.not_exists',
|
||||
name: Users.tableSingular,
|
||||
id: undefined,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
await expect(updateWhere(updateData)).rejects.toMatchError(new UpdateError(Users, updateData));
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,17 +2,13 @@ import { SchemaLike, GeneratedSchema } from '@logto/schemas';
|
|||
import { notFalsy, Truthy } from '@silverhand/essentials';
|
||||
import { DatabasePoolType, sql } from 'slonik';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { UpdateError } from '@/errors/SlonikError';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { isKeyOf } from '@/utils/schema';
|
||||
|
||||
import { UpdateWhereData } from './types';
|
||||
import { conditionalSql, convertToIdentifiers, convertToPrimitiveOrSql } from './utils';
|
||||
|
||||
export type UpdateWhereData<Schema extends SchemaLike> = {
|
||||
set: Partial<Schema>;
|
||||
where: Partial<Schema>;
|
||||
};
|
||||
|
||||
interface BuildUpdateWhere {
|
||||
<Schema extends SchemaLike, ReturnType extends SchemaLike>(
|
||||
pool: DatabasePoolType,
|
||||
|
@ -69,15 +65,7 @@ export const buildUpdateWhere: BuildUpdateWhere = <
|
|||
${conditionalSql(returning, () => sql`returning *`)}
|
||||
`);
|
||||
|
||||
assertThat(
|
||||
!returning || data,
|
||||
new RequestError({
|
||||
code: where.id ? 'entity.not_exists_with_id' : 'entity.not_exists',
|
||||
name: schema.tableSingular,
|
||||
id: where.id,
|
||||
status: 404,
|
||||
})
|
||||
);
|
||||
assertThat(!returning || data, new UpdateError(schema, { set, where }));
|
||||
|
||||
return data;
|
||||
};
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
import { SchemaLike, GeneratedSchema } from '@logto/schemas';
|
||||
import { SlonikError } from 'slonik';
|
||||
|
||||
import { UpdateWhereData } from '@/database/types';
|
||||
import { OmitAutoSetFields } from '@/database/utils';
|
||||
|
||||
export class DeletionError extends SlonikError {
|
||||
table?: string;
|
||||
id?: string;
|
||||
|
@ -11,3 +15,27 @@ export class DeletionError extends SlonikError {
|
|||
this.id = id;
|
||||
}
|
||||
}
|
||||
|
||||
export class UpdateError<Schema extends SchemaLike> extends SlonikError {
|
||||
schema: GeneratedSchema<Schema>;
|
||||
detail: UpdateWhereData<Schema>;
|
||||
|
||||
public constructor(schema: GeneratedSchema<Schema>, detail: UpdateWhereData<Schema>) {
|
||||
super('Resource not found.');
|
||||
|
||||
this.schema = schema;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
||||
export class InsertionError<Schema extends SchemaLike> extends SlonikError {
|
||||
schema: GeneratedSchema<Schema>;
|
||||
detail?: OmitAutoSetFields<Schema>;
|
||||
|
||||
public constructor(schema: GeneratedSchema<Schema>, detail?: OmitAutoSetFields<Schema>) {
|
||||
super('Create Error.');
|
||||
|
||||
this.schema = schema;
|
||||
this.detail = detail;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { Users } from '@logto/schemas';
|
||||
import { NotFoundError, SlonikError } from 'slonik';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { DeletionError } from '@/errors/SlonikError';
|
||||
import { DeletionError, InsertionError, UpdateError } from '@/errors/SlonikError';
|
||||
import { createContextWithRouteParameters } from '@/utils/test-utils';
|
||||
|
||||
import koaSlonikErrorHandler from './koa-slonik-error-handler';
|
||||
|
@ -34,6 +35,34 @@ describe('koaSlonikErrorHandler middleware', () => {
|
|||
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(error);
|
||||
});
|
||||
|
||||
it('Insertion Error', async () => {
|
||||
const error = new InsertionError(Users, { id: '123' });
|
||||
next.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(
|
||||
new RequestError({
|
||||
code: 'entity.create_failed',
|
||||
name: Users.tableSingular,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('Update Error', async () => {
|
||||
const error = new UpdateError(Users, { set: { name: 'punk' }, where: { id: '123' } });
|
||||
next.mockImplementationOnce(() => {
|
||||
throw error;
|
||||
});
|
||||
|
||||
await expect(koaSlonikErrorHandler()(ctx, next)).rejects.toMatchError(
|
||||
new RequestError({
|
||||
code: 'entity.not_exists',
|
||||
name: Users.tableSingular,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('Deletion Error', async () => {
|
||||
const error = new DeletionError();
|
||||
next.mockImplementationOnce(() => {
|
||||
|
|
|
@ -20,11 +20,12 @@
|
|||
* (reference)[https://github.com/gajus/slonik#error-handling]
|
||||
*/
|
||||
|
||||
import { SchemaLike } from '@logto/schemas';
|
||||
import { Middleware } from 'koa';
|
||||
import { SlonikError, NotFoundError } from 'slonik';
|
||||
|
||||
import RequestError from '@/errors/RequestError';
|
||||
import { DeletionError } from '@/errors/SlonikError';
|
||||
import { DeletionError, InsertionError, UpdateError } from '@/errors/SlonikError';
|
||||
|
||||
export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<StateT, ContextT> {
|
||||
return async (ctx, next) => {
|
||||
|
@ -36,6 +37,17 @@ export default function koaSlonikErrorHandler<StateT, ContextT>(): Middleware<St
|
|||
}
|
||||
|
||||
switch (error.constructor) {
|
||||
case InsertionError:
|
||||
throw new RequestError({
|
||||
code: 'entity.create_failed',
|
||||
name: (error as InsertionError<SchemaLike>).schema.tableSingular,
|
||||
});
|
||||
case UpdateError:
|
||||
throw new RequestError({
|
||||
code: 'entity.not_exists',
|
||||
name: (error as InsertionError<SchemaLike>).schema.tableSingular,
|
||||
});
|
||||
// TODO: LOG-1665 Refactor Deletion Handle
|
||||
case DeletionError:
|
||||
case NotFoundError:
|
||||
throw new RequestError({
|
||||
|
|
|
@ -5,7 +5,7 @@ import { buildInsertInto } from '@/database/insert-into';
|
|||
import pool from '@/database/pool';
|
||||
import { buildUpdateWhere } from '@/database/update-where';
|
||||
import { conditionalSql, convertToIdentifiers, OmitAutoSetFields } from '@/database/utils';
|
||||
import { DeletionError } from '@/errors/SlonikError';
|
||||
import { DeletionError, UpdateError } from '@/errors/SlonikError';
|
||||
|
||||
const { table, fields } = convertToIdentifiers(Users);
|
||||
|
||||
|
@ -134,6 +134,6 @@ export const clearUserCustomDataById = async (id: string) => {
|
|||
`);
|
||||
|
||||
if (rowCount < 1) {
|
||||
throw new DeletionError(Users.table, id);
|
||||
throw new UpdateError(Users, { set: { customData: {} }, where: { id } });
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Add table
Reference in a new issue