diff --git a/packages/core/src/database/find-many.ts b/packages/core/src/database/find-many.ts index 55f2e82dd..c2a1625d6 100644 --- a/packages/core/src/database/find-many.ts +++ b/packages/core/src/database/find-many.ts @@ -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 = { - where?: Partial; - limit?: number; - offset?: number; -}; - export const buildFindMany = ( pool: DatabasePoolType, schema: GeneratedSchema diff --git a/packages/core/src/database/insert-into.test.ts b/packages/core/src/database/insert-into.test.ts index d54d5a213..ec16b7d99 100644 --- a/packages/core/src/database/insert-into.test.ts +++ b/packages/core/src/database/insert-into.test.ts @@ -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) ); }); }); diff --git a/packages/core/src/database/insert-into.ts b/packages/core/src/database/insert-into.ts index c512c90c9..873acb566 100644 --- a/packages/core/src/database/insert-into.ts +++ b/packages/core/src/database/insert-into.ts @@ -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: GeneratedSchema, 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; }; diff --git a/packages/core/src/database/types.ts b/packages/core/src/database/types.ts index f47a8f0ca..bb3383a8b 100644 --- a/packages/core/src/database/types.ts +++ b/packages/core/src/database/types.ts @@ -1,6 +1,18 @@ +import { SchemaLike } from '@logto/schemas'; import { IdentifierSqlTokenType } from 'slonik'; export type Table = { table: string; fields: Record }; export type FieldIdentifiers = { [key in Key]: IdentifierSqlTokenType; }; + +export type FindManyData = { + where?: Partial; + limit?: number; + offset?: number; +}; + +export type UpdateWhereData = { + set: Partial; + where: Partial; +}; diff --git a/packages/core/src/database/update-where.test.ts b/packages/core/src/database/update-where.test.ts index 810a3acdd..d16e04e72 100644 --- a/packages/core/src/database/update-where.test.ts +++ b/packages/core/src/database/update-where.test.ts @@ -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)); }); }); diff --git a/packages/core/src/database/update-where.ts b/packages/core/src/database/update-where.ts index 13f6a5866..3fa90da52 100644 --- a/packages/core/src/database/update-where.ts +++ b/packages/core/src/database/update-where.ts @@ -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 = { - set: Partial; - where: Partial; -}; - interface BuildUpdateWhere { ( 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; }; diff --git a/packages/core/src/errors/SlonikError/index.ts b/packages/core/src/errors/SlonikError/index.ts index 20585f937..fea843546 100644 --- a/packages/core/src/errors/SlonikError/index.ts +++ b/packages/core/src/errors/SlonikError/index.ts @@ -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 extends SlonikError { + schema: GeneratedSchema; + detail: UpdateWhereData; + + public constructor(schema: GeneratedSchema, detail: UpdateWhereData) { + super('Resource not found.'); + + this.schema = schema; + this.detail = detail; + } +} + +export class InsertionError extends SlonikError { + schema: GeneratedSchema; + detail?: OmitAutoSetFields; + + public constructor(schema: GeneratedSchema, detail?: OmitAutoSetFields) { + super('Create Error.'); + + this.schema = schema; + this.detail = detail; + } +} diff --git a/packages/core/src/middleware/koa-slonik-error-handler.test.ts b/packages/core/src/middleware/koa-slonik-error-handler.test.ts index ba9e34c05..8892324c7 100644 --- a/packages/core/src/middleware/koa-slonik-error-handler.test.ts +++ b/packages/core/src/middleware/koa-slonik-error-handler.test.ts @@ -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(() => { diff --git a/packages/core/src/middleware/koa-slonik-error-handler.ts b/packages/core/src/middleware/koa-slonik-error-handler.ts index 36a7386ef..7b069e202 100644 --- a/packages/core/src/middleware/koa-slonik-error-handler.ts +++ b/packages/core/src/middleware/koa-slonik-error-handler.ts @@ -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(): Middleware { return async (ctx, next) => { @@ -36,6 +37,17 @@ export default function koaSlonikErrorHandler(): Middleware).schema.tableSingular, + }); + case UpdateError: + throw new RequestError({ + code: 'entity.not_exists', + name: (error as InsertionError).schema.tableSingular, + }); + // TODO: LOG-1665 Refactor Deletion Handle case DeletionError: case NotFoundError: throw new RequestError({ diff --git a/packages/core/src/queries/user.ts b/packages/core/src/queries/user.ts index 449d50e2d..0923827d5 100644 --- a/packages/core/src/queries/user.ts +++ b/packages/core/src/queries/user.ts @@ -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 } }); } };