From 09302b94eb701c8ef1a54c97b079a744e65c63af Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Tue, 17 Feb 2026 16:04:18 +0100 Subject: [PATCH 1/3] refactor(errors): move HTTP error classes from datasource-toolkit to agent-toolkit Base HTTP error classes (BusinessError, ValidationError, BadRequestError, UnprocessableError, ForbiddenError, NotFoundError) are conceptually agent-level errors. Move them to agent-toolkit and re-export from datasource-toolkit for backward compatibility. Co-Authored-By: Claude Opus 4.6 --- packages/agent-toolkit/src/errors.ts | 57 +++++++++++++++++++ packages/agent-toolkit/src/index.ts | 1 + packages/datasource-toolkit/package.json | 1 + packages/datasource-toolkit/src/errors.ts | 69 ++++------------------- 4 files changed, 71 insertions(+), 57 deletions(-) create mode 100644 packages/agent-toolkit/src/errors.ts diff --git a/packages/agent-toolkit/src/errors.ts b/packages/agent-toolkit/src/errors.ts new file mode 100644 index 0000000000..49aceca12d --- /dev/null +++ b/packages/agent-toolkit/src/errors.ts @@ -0,0 +1,57 @@ +// eslint-disable-next-line max-classes-per-file +export class BusinessError extends Error { + // INTERNAL USAGES + public readonly isBusinessError = true; + public baseBusinessErrorName: string; + + public readonly data: Record | undefined; + + constructor(message?: string, data?: Record, name?: string) { + super(message); + this.name = name ?? this.constructor.name; + this.data = data; + } + + /** + * We cannot rely on `instanceof` because there can be some mismatch between + * packages versions as dependencies of different packages. + * So this function is a workaround to check if an error is of a specific type. + */ + static isOfType(error: Error, ErrorConstructor: new (...args: never[]) => Error): boolean { + return ( + error.name === ErrorConstructor.name || + (error as BusinessError).baseBusinessErrorName === ErrorConstructor.name + ); + } +} + +export class ValidationError extends BusinessError { + constructor(message?: string, data?: Record, name?: string) { + super(message, data, name); + this.baseBusinessErrorName = 'ValidationError'; + } +} +export class BadRequestError extends BusinessError { + constructor(message?: string, data?: Record, name?: string) { + super(message, data, name); + this.baseBusinessErrorName = 'BadRequestError'; + } +} +export class UnprocessableError extends BusinessError { + constructor(message?: string, data?: Record, name?: string) { + super(message, data, name); + this.baseBusinessErrorName = 'UnprocessableError'; + } +} +export class ForbiddenError extends BusinessError { + constructor(message?: string, data?: Record, name?: string) { + super(message, data, name); + this.baseBusinessErrorName = 'ForbiddenError'; + } +} +export class NotFoundError extends BusinessError { + constructor(message?: string, data?: Record, name?: string) { + super(message, data, name); + this.baseBusinessErrorName = 'NotFoundError'; + } +} diff --git a/packages/agent-toolkit/src/index.ts b/packages/agent-toolkit/src/index.ts index 1e0956c3d1..b4d405c10d 100644 --- a/packages/agent-toolkit/src/index.ts +++ b/packages/agent-toolkit/src/index.ts @@ -1 +1,2 @@ +export * from './errors'; export type { AiProviderDefinition, AiProviderMeta, AiRouter } from './interfaces/ai'; diff --git a/packages/datasource-toolkit/package.json b/packages/datasource-toolkit/package.json index 0abe0c9d03..5044f24b24 100644 --- a/packages/datasource-toolkit/package.json +++ b/packages/datasource-toolkit/package.json @@ -28,6 +28,7 @@ "@types/uuid": "^10.0.0" }, "dependencies": { + "@forestadmin/agent-toolkit": "1.0.0", "luxon": "^3.2.1", "object-hash": "^3.0.0", "uuid": "11.0.2" diff --git a/packages/datasource-toolkit/src/errors.ts b/packages/datasource-toolkit/src/errors.ts index 1668e41ebd..304a84a056 100644 --- a/packages/datasource-toolkit/src/errors.ts +++ b/packages/datasource-toolkit/src/errors.ts @@ -1,60 +1,15 @@ -// eslint-disable-next-line max-classes-per-file -export class BusinessError extends Error { - // INTERNAL USAGES - public readonly isBusinessError = true; - public baseBusinessErrorName: string; - - public readonly data: Record | undefined; - - constructor(message?: string, data?: Record, name?: string) { - super(message); - this.name = name ?? this.constructor.name; - this.data = data; - } - - /** - * We cannot rely on `instanceof` because there can be some mismatch between - * packages versions as dependencies of different packages. - * So this function is a workaround to check if an error is of a specific type. - */ - static isOfType(error: Error, ErrorConstructor: new (...args: never[]) => Error): boolean { - return ( - error.name === ErrorConstructor.name || - (error as BusinessError).baseBusinessErrorName === ErrorConstructor.name - ); - } -} - -export class ValidationError extends BusinessError { - constructor(message?: string, data?: Record, name?: string) { - super(message, data, name); - this.baseBusinessErrorName = 'ValidationError'; - } -} -export class BadRequestError extends BusinessError { - constructor(message?: string, data?: Record, name?: string) { - super(message, data, name); - this.baseBusinessErrorName = 'BadRequestError'; - } -} -export class UnprocessableError extends BusinessError { - constructor(message?: string, data?: Record, name?: string) { - super(message, data, name); - this.baseBusinessErrorName = 'UnprocessableError'; - } -} -export class ForbiddenError extends BusinessError { - constructor(message?: string, data?: Record, name?: string) { - super(message, data, name); - this.baseBusinessErrorName = 'ForbiddenError'; - } -} -export class NotFoundError extends BusinessError { - constructor(message?: string, data?: Record, name?: string) { - super(message, data, name); - this.baseBusinessErrorName = 'NotFoundError'; - } -} +/* eslint-disable max-classes-per-file */ +import { BusinessError, ValidationError } from '@forestadmin/agent-toolkit'; + +// Re-export base errors from agent-toolkit for backward compatibility +export { + BusinessError, + ValidationError, + BadRequestError, + UnprocessableError, + ForbiddenError, + NotFoundError, +} from '@forestadmin/agent-toolkit'; export class IntrospectionFormatError extends BusinessError { constructor(sourcePackageName: '@forestadmin/datasource-sql' | '@forestadmin/datasource-mongo') { From 21f5e948646cb517ee18351142384aaf5e84b052 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Wed, 18 Feb 2026 09:43:48 +0100 Subject: [PATCH 2/3] test(agent-toolkit): add unit tests for error classes Tests for BusinessError (name, message, data, isOfType) and baseBusinessErrorName on all error subclasses. Co-Authored-By: Claude Opus 4.6 --- packages/agent-toolkit/test/errors.test.ts | 76 ++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 packages/agent-toolkit/test/errors.test.ts diff --git a/packages/agent-toolkit/test/errors.test.ts b/packages/agent-toolkit/test/errors.test.ts new file mode 100644 index 0000000000..1188e224bc --- /dev/null +++ b/packages/agent-toolkit/test/errors.test.ts @@ -0,0 +1,76 @@ +/* eslint-disable max-classes-per-file */ +import { + BadRequestError, + BusinessError, + ForbiddenError, + NotFoundError, + UnprocessableError, + ValidationError, +} from '../src/errors'; + +describe('errors', () => { + describe('BusinessError', () => { + describe('name', () => { + it('should have a name', () => { + const error = new BusinessError('test'); + expect(error.name).toEqual('BusinessError'); + }); + + it('should name the error after its constructor name', () => { + class CustomError extends BusinessError {} + const error = new CustomError('test'); + expect(error.name).toEqual('CustomError'); + }); + + it('should use the name when it is given', () => { + const error = new BusinessError('test', {}, 'MyName'); + expect(error.name).toEqual('MyName'); + }); + }); + + describe('message', () => { + it('should have a message', () => { + const error = new BusinessError('test'); + expect(error.message).toEqual('test'); + }); + }); + + describe('data', () => { + it('should have data', () => { + const error = new BusinessError('test', { foo: 'bar' }); + expect(error.data).toEqual({ foo: 'bar' }); + }); + + it('should set the data to undefined by default', () => { + const error = new BusinessError('test'); + expect(error.data).toBeUndefined(); + }); + }); + + describe('isOfType', () => { + it('should return true when the error is of the given type', () => { + const error = new BusinessError('test'); + expect(BusinessError.isOfType(error, BusinessError)).toBeTruthy(); + }); + + it('should return false when the error is not of the given type', () => { + const error = new Error('test'); + expect(BusinessError.isOfType(error, BusinessError)).toBeFalsy(); + }); + }); + }); + + describe('baseBusinessErrorName', () => { + it.each([ + { ErrorClass: ValidationError, errorName: 'ValidationError' }, + { ErrorClass: BadRequestError, errorName: 'BadRequestError' }, + { ErrorClass: UnprocessableError, errorName: 'UnprocessableError' }, + { ErrorClass: ForbiddenError, errorName: 'ForbiddenError' }, + { ErrorClass: NotFoundError, errorName: 'NotFoundError' }, + ])('$errorName should have the correct baseBusinessErrorName', ({ ErrorClass, errorName }) => { + const error = new ErrorClass('test'); + expect(error.baseBusinessErrorName).toEqual(errorName); + expect(error.isBusinessError).toBe(true); + }); + }); +}); From 25f5b5dbd82c83a0882759f3057829cd55b22612 Mon Sep 17 00:00:00 2001 From: alban bertolini Date: Wed, 18 Feb 2026 09:46:51 +0100 Subject: [PATCH 3/3] test(datasource-toolkit): remove duplicated BusinessError tests BusinessError tests now live in agent-toolkit where the class is defined. Only IntrospectionFormatError tests remain here. Co-Authored-By: Claude Opus 4.6 --- .../datasource-toolkit/test/errors.test.ts | 54 +------------------ 1 file changed, 1 insertion(+), 53 deletions(-) diff --git a/packages/datasource-toolkit/test/errors.test.ts b/packages/datasource-toolkit/test/errors.test.ts index 3311523044..9315d7777f 100644 --- a/packages/datasource-toolkit/test/errors.test.ts +++ b/packages/datasource-toolkit/test/errors.test.ts @@ -1,58 +1,6 @@ -/* eslint-disable max-classes-per-file */ -import { BusinessError, IntrospectionFormatError } from '../src/errors'; +import { IntrospectionFormatError } from '../src/errors'; describe('errors', () => { - describe('BusinessError', () => { - describe('name', () => { - it('should have a name', () => { - const error = new BusinessError('test'); - expect(error.name).toEqual('BusinessError'); - }); - - it('should name the error after its constructor name', () => { - class CustomError extends BusinessError {} - const error = new CustomError('test'); - expect(error.name).toEqual('CustomError'); - }); - - it('should use the name when it is given', () => { - const error = new BusinessError('test', {}, 'MyName'); - expect(error.name).toEqual('MyName'); - }); - }); - - describe('message', () => { - it('should have a message', () => { - const error = new BusinessError('test'); - expect(error.message).toEqual('test'); - }); - }); - - describe('data', () => { - it('should have data', () => { - const error = new BusinessError('test', { foo: 'bar' }); - expect(error.data).toEqual({ foo: 'bar' }); - }); - - it('should set the data to undefined by default', () => { - const error = new BusinessError('test'); - expect(error.data).toBeUndefined(); - }); - }); - - describe('isOfType', () => { - it('should return true when the error is of the given type', () => { - const error = new BusinessError('test'); - expect(BusinessError.isOfType(error, BusinessError)).toBeTruthy(); - }); - - it('should return false when the error is not of the given type', () => { - const error = new Error('test'); - expect(BusinessError.isOfType(error, BusinessError)).toBeFalsy(); - }); - }); - }); - describe('IntrospectionFormatError', () => { describe('message', () => { it.each([['@forestadmin/datasource-mongo'], ['@forestadmin/datasource-sql']])(