0
Fork 0
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:
Xiao Yijun 2022-03-02 15:44:57 +08:00 committed by GitHub
parent 16396cf231
commit dcd84086f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 105 additions and 57 deletions

View file

@ -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>

View file

@ -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)
);
});
});

View file

@ -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;
};

View file

@ -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>;
};

View file

@ -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));
});
});

View file

@ -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;
};

View file

@ -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;
}
}

View file

@ -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(() => {

View file

@ -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({

View file

@ -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 } });
}
};