diff --git a/package-lock.json b/package-lock.json index 350179e..486362f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@athenna/database", - "version": "5.50.0", + "version": "5.51.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@athenna/database", - "version": "5.50.0", + "version": "5.51.0", "license": "MIT", "dependencies": { "@faker-js/faker": "^8.4.1" diff --git a/package.json b/package.json index 9aa932c..45db16a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@athenna/database", - "version": "5.50.0", + "version": "5.51.0", "description": "The Athenna database handler for SQL/NoSQL.", "license": "MIT", "author": "João Lenon ", diff --git a/src/database/drivers/BaseKnexDriver.ts b/src/database/drivers/BaseKnexDriver.ts index ecac017..37e0f97 100644 --- a/src/database/drivers/BaseKnexDriver.ts +++ b/src/database/drivers/BaseKnexDriver.ts @@ -520,9 +520,26 @@ export class BaseKnexDriver extends Driver { /** * Verify if a value should be serialized before persisting. + * + * Knex `Raw` instances must NEVER be stringified: they hold an internal + * reference to the database client (and its connection pool, which contains + * Node.js `Timeout` objects with circular references), so calling + * `JSON.stringify` on them throws `TypeError: Converting circular structure + * to JSON`. Knex marks every `Raw` instance with `isRawInstance = true` and + * relies on it internally during query compilation, so we trust the same + * marker here. The same precaution applies to Knex `QueryBuilder` instances + * (subqueries) which expose `isQueryBuilder = true`. */ private shouldStringifyJsonValue(value: any) { - return !!value && (Is.Array(value) || Is.Object(value)) + if (!value) { + return false + } + + if (value.isRawInstance || value.isQueryBuilder) { + return false + } + + return Is.Array(value) || Is.Object(value) } /** diff --git a/tests/unit/drivers/MySqlDriverTest.ts b/tests/unit/drivers/MySqlDriverTest.ts index 088f079..07f5667 100644 --- a/tests/unit/drivers/MySqlDriverTest.ts +++ b/tests/unit/drivers/MySqlDriverTest.ts @@ -898,6 +898,33 @@ export default class MySqlDriverTest { ]) } + @Test() + public async shouldBeAbleToUpdateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await this.driver.table('users').create({ id: '1', name: 'Robert Kiyosaki' }) + + await assert.doesNotReject(() => + this.driver + .table('users') + .where('id', '1') + .update({ name: this.driver.raw("'Warren Buffet'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Warren Buffet' }) + } + + @Test() + public async shouldBeAbleToCreateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await assert.doesNotReject(() => + this.driver.table('users').create({ id: '1', name: this.driver.raw("'Robert Kiyosaki'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Robert Kiyosaki' }) + } + @Test() public async shouldBeAbleToDeleteDataUsingDeleteMethod({ assert }: Context) { const data = { id: '1', name: 'Robert Kiyosaki' } diff --git a/tests/unit/drivers/PostgresDriverTest.ts b/tests/unit/drivers/PostgresDriverTest.ts index 3721f6d..1ba4f4e 100644 --- a/tests/unit/drivers/PostgresDriverTest.ts +++ b/tests/unit/drivers/PostgresDriverTest.ts @@ -897,6 +897,33 @@ export default class PostgresDriverTest { ]) } + @Test() + public async shouldBeAbleToUpdateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await this.driver.table('users').create({ id: '1', name: 'Robert Kiyosaki' }) + + await assert.doesNotReject(() => + this.driver + .table('users') + .where('id', '1') + .update({ name: this.driver.raw("'Warren Buffet'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Warren Buffet' }) + } + + @Test() + public async shouldBeAbleToCreateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await assert.doesNotReject(() => + this.driver.table('users').create({ id: '1', name: this.driver.raw("'Robert Kiyosaki'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Robert Kiyosaki' }) + } + @Test() public async shouldBeAbleToDeleteDataUsingDeleteMethod({ assert }: Context) { const data = { id: '1', name: 'Robert Kiyosaki' } diff --git a/tests/unit/drivers/SqliteDriverTest.ts b/tests/unit/drivers/SqliteDriverTest.ts index 25232fb..ddfe1cd 100644 --- a/tests/unit/drivers/SqliteDriverTest.ts +++ b/tests/unit/drivers/SqliteDriverTest.ts @@ -899,6 +899,49 @@ export default class SqliteDriverTest { ]) } + @Test() + public async shouldBeAbleToUpdateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await this.driver.table('users').create({ id: '1', name: 'Robert Kiyosaki' }) + + await assert.doesNotReject(() => + this.driver + .table('users') + .where('id', '1') + .update({ name: this.driver.raw("'Warren Buffet'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Warren Buffet' }) + } + + @Test() + public async shouldBeAbleToCreateDataUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await assert.doesNotReject(() => + this.driver.table('users').create({ id: '1', name: this.driver.raw("'Robert Kiyosaki'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Robert Kiyosaki' }) + } + + @Test() + public async shouldBeAbleToCreateOrUpdateUsingARawValueWithoutThrowingCircularStructureError({ assert }: Context) { + await this.driver.table('users').create({ id: '1', name: 'Robert Kiyosaki' }) + + await assert.doesNotReject(() => + this.driver + .table('users') + .where('id', '1') + .createOrUpdate({ id: '1', name: this.driver.raw("'Warren Buffet'") } as any) + ) + + const result = await this.driver.table('users').where('id', '1').find() + + assert.containSubset(result, { id: '1', name: 'Warren Buffet' }) + } + @Test() public async shouldBeAbleToDeleteDataUsingDeleteMethod({ assert }: Context) { const data = { id: '1', name: 'Robert Kiyosaki' }