diff --git a/src/index.ts b/src/index.ts index fd328b3..2cfa318 100644 --- a/src/index.ts +++ b/src/index.ts @@ -12,6 +12,8 @@ import getFieldAttributes from './utils/field_attributes.ts'; import extend from './utils/extend.ts'; +import makeMethodsEnumerable from './utils/make_methods_enumerable.ts'; + import clone from 'tricks/object/clone.js'; import format_request from './format_request.ts'; @@ -439,8 +441,13 @@ export type GetRequestOptions = Omit< */ export type PatchRequestOptions = Omit< RequestOptions, - 'fields' | 'groupby' | 'query' ->; + 'fields' | 'groupby' | 'query' | 'body' +> & { + /** + * Body containing new data + */ + body?: Record; +}; /** * Dare.post Request Options @@ -1033,9 +1040,9 @@ export default class Dare { * @returns Affected Rows statement */ async patch( - table: string | PatchRequestOptions, - filter?: Record, - body?: Record, + table: PatchRequestOptions['table'] | PatchRequestOptions, + filter?: PatchRequestOptions['filter'], + body?: PatchRequestOptions['body'], options: Omit = {} ): Promise { const opts: QueryOptions = @@ -1440,6 +1447,12 @@ export default class Dare { } } +/* + * ES class methods are non-enumerable by default, + * restore the enumerable behaviour of the former prototype assignments + */ +makeMethodsEnumerable(Dare); + /** * Engine, database engine */ diff --git a/src/mysql57.ts b/src/mysql57.ts index 5c3611f..f35ec2b 100644 --- a/src/mysql57.ts +++ b/src/mysql57.ts @@ -1,4 +1,5 @@ import Dare, {type QueryOptions} from './index.ts'; +import makeMethodsEnumerable from './utils/make_methods_enumerable.ts'; import semverCompare from 'semver-compare'; /** @@ -82,6 +83,9 @@ class MySQL57Dare extends Dare { } } +// Restore the enumerable behaviour of the former prototype assignments +makeMethodsEnumerable(MySQL57Dare); + /** * Default engine for MySQL 5.7 */ diff --git a/src/postgres16.ts b/src/postgres16.ts index 9726a0e..bb9cbde 100644 --- a/src/postgres16.ts +++ b/src/postgres16.ts @@ -1,5 +1,6 @@ import SQL, {Sql, join} from 'sql-template-tag'; import Dare, {type QueryOptions} from './index.ts'; +import makeMethodsEnumerable from './utils/make_methods_enumerable.ts'; /** * PostgresDare @@ -104,6 +105,9 @@ class PostgresDare extends Dare { } } +// Restore the enumerable behaviour of the former prototype assignments +makeMethodsEnumerable(PostgresDare); + /** * Default engine for Postgres */ diff --git a/src/sqlite.ts b/src/sqlite.ts index a1d0ef1..0685901 100644 --- a/src/sqlite.ts +++ b/src/sqlite.ts @@ -1,5 +1,6 @@ import SQL, {Sql, raw} from 'sql-template-tag'; import Dare, {type QueryOptions} from './index.ts'; +import makeMethodsEnumerable from './utils/make_methods_enumerable.ts'; /** * SQLiteDare @@ -126,6 +127,9 @@ class SQLiteDare extends Dare { } } +// Restore the enumerable behaviour of the former prototype assignments +makeMethodsEnumerable(SQLiteDare); + /** * Default engine for SQLite */ diff --git a/src/utils/make_methods_enumerable.ts b/src/utils/make_methods_enumerable.ts new file mode 100644 index 0000000..8681349 --- /dev/null +++ b/src/utils/make_methods_enumerable.ts @@ -0,0 +1,28 @@ +/** + * Make the prototype methods of a class enumerable + * ES class methods are non-enumerable by default, whereas the previous + * `Class.prototype.method = function` assignments were enumerable. + * This restores the original enumerable behaviour. + * @param Class - Class whose prototype methods should be made enumerable + */ +export default function makeMethodsEnumerable(Class: { + prototype: object; +}): void { + for (const name of Object.getOwnPropertyNames(Class.prototype)) { + if (name === 'constructor') { + continue; + } + + const descriptor = Object.getOwnPropertyDescriptor( + Class.prototype, + name + ); + + if (descriptor?.enumerable === false) { + Object.defineProperty(Class.prototype, name, { + ...descriptor, + enumerable: true, + }); + } + } +} diff --git a/test/specs/enumerability.spec.ts b/test/specs/enumerability.spec.ts new file mode 100644 index 0000000..3f47d58 --- /dev/null +++ b/test/specs/enumerability.spec.ts @@ -0,0 +1,72 @@ +import assert from 'node:assert'; +import {describe, it} from 'node:test'; +import Dare from '../../src/index.ts'; +import MySQL57Dare from '../../src/mysql57.ts'; +import PostgresDare from '../../src/postgres16.ts'; +import SQLiteDare from '../../src/sqlite.ts'; + +/* + * ES class methods are non-enumerable by default + * Dare restores the enumerable behaviour of the former prototype assignments + * via makeMethodsEnumerable, so instance methods remain visible to `for...in` + */ +describe('enumerability', () => { + const engines = { + Dare, + MySQL57Dare, + PostgresDare, + SQLiteDare, + }; + + for (const [name, Klass] of Object.entries(engines)) { + describe(name, () => { + it('should define enumerable prototype methods', () => { + const methods = Object.getOwnPropertyNames( + Klass.prototype + ).filter(prop => prop !== 'constructor'); + + assert.ok(methods.length, 'has prototype methods'); + + for (const method of methods) { + const descriptor = Object.getOwnPropertyDescriptor( + Klass.prototype, + method + ); + assert.strictEqual( + descriptor?.enumerable, + true, + `${name}.prototype.${method} should be enumerable` + ); + } + }); + + it('should expose methods on instances via for...in', () => { + const instance = new Klass(); + + const enumerated = []; + for (const prop in instance) { + if (typeof instance[prop] === 'function') { + enumerated.push(prop); + } + } + + assert.ok( + enumerated.includes('get'), + 'get is enumerable on the instance' + ); + assert.ok( + enumerated.includes('post'), + 'post is enumerable on the instance' + ); + }); + + it('should keep the constructor non-enumerable', () => { + const descriptor = Object.getOwnPropertyDescriptor( + Klass.prototype, + 'constructor' + ); + assert.strictEqual(descriptor?.enumerable, false); + }); + }); + } +});