From 3ef1cd77238aa51c1a9e939e7a944f2915d24a1e Mon Sep 17 00:00:00 2001 From: fratzinger <22286818+fratzinger@users.noreply.github.com> Date: Sun, 2 Jun 2024 11:53:15 +0200 Subject: [PATCH 1/3] refactor(commons): use named exports - export every function directly - improve 'some' & 'every' for early exit - use 'Object.values' for 'values' instead of 'Object.keys.map' (2x perf) - isObject: first check for null - named imports from @feathersjs/commons in every package - deprecate 'keys', use `Object.keys` - deprecate 'values', use `Object.values` - deprecate 'extend', use `Object.assign` - use type guard for `isPromise`, `isObject` & `isObjectOrArray` - add jsdoc comments --- packages/adapter-commons/src/index.ts | 4 +- packages/adapter-commons/src/query.ts | 4 +- packages/authentication-oauth/src/strategy.ts | 8 +- packages/commons/src/index.ts | 244 ++++++++++++------ packages/commons/test/module.test.ts | 28 ++ packages/commons/test/utils.test.ts | 23 +- packages/knex/src/adapter.ts | 12 +- packages/memory/src/index.ts | 12 +- packages/mongodb/src/adapter.ts | 4 +- packages/rest-client/src/base.ts | 4 +- packages/schema/src/json-schema.ts | 4 +- 11 files changed, 230 insertions(+), 117 deletions(-) diff --git a/packages/adapter-commons/src/index.ts b/packages/adapter-commons/src/index.ts index 8007e44e00..2f02162667 100644 --- a/packages/adapter-commons/src/index.ts +++ b/packages/adapter-commons/src/index.ts @@ -1,4 +1,4 @@ -import { _ } from '@feathersjs/commons' +import { pick } from '@feathersjs/commons' import { Params } from '@feathersjs/feathers' export * from './declarations' @@ -17,7 +17,7 @@ export function select(params: Params, ...otherFields: string[]) { } const resultFields = queryFields.concat(otherFields) - const convert = (result: any) => _.pick(result, ...resultFields) + const convert = (result: any) => pick(result, ...resultFields) return (result: any) => { if (Array.isArray(result)) { diff --git a/packages/adapter-commons/src/query.ts b/packages/adapter-commons/src/query.ts index 1dc31ffdec..ee2952280f 100644 --- a/packages/adapter-commons/src/query.ts +++ b/packages/adapter-commons/src/query.ts @@ -1,11 +1,11 @@ -import { _ } from '@feathersjs/commons' +import { isObject } from '@feathersjs/commons' import { BadRequest } from '@feathersjs/errors' import { Query } from '@feathersjs/feathers' import { FilterQueryOptions, FilterSettings, PaginationParams } from './declarations' const parse = (value: any) => (typeof value !== 'undefined' ? parseInt(value, 10) : value) -const isPlainObject = (value: any) => _.isObject(value) && value.constructor === {}.constructor +const isPlainObject = (value: any) => isObject(value) && value.constructor === {}.constructor const validateQueryProperty = (query: any, operators: string[] = []): Query => { if (!isPlainObject(query)) { diff --git a/packages/authentication-oauth/src/strategy.ts b/packages/authentication-oauth/src/strategy.ts index 4bfe2589fc..abefa3213d 100644 --- a/packages/authentication-oauth/src/strategy.ts +++ b/packages/authentication-oauth/src/strategy.ts @@ -6,7 +6,7 @@ import { } from '@feathersjs/authentication' import { Params } from '@feathersjs/feathers' import { NotAuthenticated } from '@feathersjs/errors' -import { createDebug, _ } from '@feathersjs/commons' +import { createDebug, omit } from '@feathersjs/commons' import qs from 'qs' const debug = createDebug('@feathersjs/authentication-oauth/strategy') @@ -126,7 +126,7 @@ export class OAuthStrategy extends AuthenticationBaseStrategy { debug('createEntity with data', data) - return this.entityService.create(data, _.omit(params, 'query')) + return this.entityService.create(data, omit(params, 'query')) } async updateEntity(entity: any, profile: OAuthProfile, params: Params) { @@ -135,7 +135,7 @@ export class OAuthStrategy extends AuthenticationBaseStrategy { debug(`updateEntity with id ${id} and data`, data) - return this.entityService.patch(id, data, _.omit(params, 'query')) + return this.entityService.patch(id, data, omit(params, 'query')) } async getEntity(result: any, params: Params) { @@ -151,7 +151,7 @@ export class OAuthStrategy extends AuthenticationBaseStrategy { } return entityService.get(result[entityId], { - ..._.omit(params, 'query'), + ...omit(params, 'query'), [entity]: result }) } diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 9455252384..735fa6adad 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -5,98 +5,182 @@ export function stripSlashes(name: string) { export type KeyValueCallback = (value: any, key: string) => T -// A set of lodash-y utility functions that use ES6 -export const _ = { - each(obj: any, callback: KeyValueCallback) { - if (obj && typeof obj.forEach === 'function') { - obj.forEach(callback) - } else if (_.isObject(obj)) { - Object.keys(obj).forEach((key) => callback(obj[key], key)) +/** + * If the object is an array, it will iterate over every element. + * Otherwise, it will iterate over every key in the object. + */ +export function each(obj: Record | any[], callback: KeyValueCallback) { + if (Array.isArray(obj)) { + obj.forEach(callback as any) + } else if (isObject(obj)) { + Object.keys(obj).forEach((key) => callback(obj[key], key)) + } +} + +/** + * Check if some values in the object pass the test implemented by the provided function + * + * returns true if some values pass the test, otherwise false + * + * returns false if the object is empty + */ +export function some(value: Record, callback: KeyValueCallback) { + for (const key in value) { + if (callback(value[key], key)) { + return true } - }, - - some(value: any, callback: KeyValueCallback) { - return Object.keys(value) - .map((key) => [value[key], key]) - .some(([val, key]) => callback(val, key)) - }, - - every(value: any, callback: KeyValueCallback) { - return Object.keys(value) - .map((key) => [value[key], key]) - .every(([val, key]) => callback(val, key)) - }, - - keys(obj: any) { - return Object.keys(obj) - }, - - values(obj: any) { - return _.keys(obj).map((key) => obj[key]) - }, - - isMatch(obj: any, item: any) { - return _.keys(item).every((key) => obj[key] === item[key]) - }, - - isEmpty(obj: any) { - return _.keys(obj).length === 0 - }, - - isObject(item: any) { - return typeof item === 'object' && !Array.isArray(item) && item !== null - }, - - isObjectOrArray(value: any) { - return typeof value === 'object' && value !== null - }, - - extend(first: any, ...rest: any[]) { - return Object.assign(first, ...rest) - }, - - omit(obj: any, ...keys: string[]) { - const result = _.extend({}, obj) - keys.forEach((key) => delete result[key]) - return result - }, + } - pick(source: any, ...keys: string[]) { - return keys.reduce((result: { [key: string]: any }, key) => { - if (source[key] !== undefined) { - result[key] = source[key] - } + return false +} - return result - }, {}) - }, - - // Recursively merge the source object into the target object - merge(target: any, source: any) { - if (_.isObject(target) && _.isObject(source)) { - Object.keys(source).forEach((key) => { - if (_.isObject(source[key])) { - if (!target[key]) { - Object.assign(target, { [key]: {} }) - } - - _.merge(target[key], source[key]) - } else { - Object.assign(target, { [key]: source[key] }) - } - }) +/** + * Check if all values in the object pass the test implemented by the provided function + * + * returns true if all values pass the test, otherwise false + * + * returns true if the object is empty + */ +export function every(value: any, callback: KeyValueCallback) { + for (const key in value) { + if (!callback(value[key], key)) { + return false } + } + + return true +} + +/** + * @deprecated use `Object.keys` instead + */ +export function keys(obj: any) { + return Object.keys(obj) +} + +/** + * @deprecated use `Object.values` instead + */ +export function values(obj: any) { + return Object.values(obj) +} + +/** + * Check if values in the source object are equal to the target object + * + * Does a shallow comparison + */ +export function isMatch(target: any, source: any) { + return Object.keys(source).every((key) => target[key] === source[key]) +} + +/** + * Check if the object is empty + */ +export function isEmpty(obj: any) { + return Object.keys(obj).length === 0 +} + +/** + * Check if the item is an object and not an array + */ +export function isObject(item: any): item is Record { + return item !== null && typeof item === 'object' && !Array.isArray(item) +} + +/** + * Check if the value is an object or an array + */ +export function isObjectOrArray(value: any): value is Record | any[] { + return value !== null && typeof value === 'object' +} + +/** + * @deprecated use `Object.assign` instead. + */ +export function extend(first: any, ...rest: any[]) { + return Object.assign(first, ...rest) +} + +/** + * Return a shallow copy of the object with the keys removed + */ +export function omit(obj: any, ...keys: string[]) { + const result = { ...obj } + keys.forEach((key) => delete result[key]) + return result +} + +/** + * Return a shallow copy of the object with only the keys provided + */ +export function pick(source: any, ...keys: string[]) { + return keys.reduce((result: { [key: string]: any }, key) => { + if (source[key] !== undefined) { + result[key] = source[key] + } + + return result + }, {}) +} + +/** + * Recursively merge the source object into the target object + */ +export function merge(target: any, source: any) { + // If either the source or target are not objects, there is nothing to do + if (!isObject(target) || !isObject(source)) { return target } + + Object.keys(source).forEach((key) => { + if (isObject(source[key])) { + if (!target[key]) { + Object.assign(target, { [key]: {} }) + } + + merge(target[key], source[key]) + } else { + Object.assign(target, { [key]: source[key] }) + } + }) + + return target } -// Duck-checks if an object looks like a promise -export function isPromise(result: any) { - return _.isObject(result) && typeof result.then === 'function' +/** + * Duck-checks if an object looks like a promise + */ +export function isPromise(result: any): result is Promise { + return isObject(result) && typeof result.then === 'function' } export function createSymbol(name: string) { return typeof Symbol !== 'undefined' ? Symbol.for(name) : name } +/** + * A set of lodash-y utility functions that use ES6 + * + * @deprecated Don't use `import { _ } from '@feathersjs/commons'`. You're importing a bunch of functions. You probably only need a few. + * Import them directly instead. For example: `import { merge } from '@feathersjs/commons'`. + * + * If you really want to import all functions or do not care about cherry picking, you can use `import * as _ from '@feathersjs/commons'`. + */ +export const _ = { + each, + some, + every, + keys, + values, + isMatch, + isEmpty, + isObject, + isObjectOrArray, + extend, + omit, + pick, + merge +} + export * from './debug' diff --git a/packages/commons/test/module.test.ts b/packages/commons/test/module.test.ts index 06198d039e..5915cfd8d4 100644 --- a/packages/commons/test/module.test.ts +++ b/packages/commons/test/module.test.ts @@ -1,5 +1,6 @@ import { strict as assert } from 'assert' import { _ } from '../src' +import * as commons from '../src' describe('module', () => { it('is commonjs compatible', () => { @@ -9,6 +10,18 @@ describe('module', () => { assert.equal(typeof commons, 'object') assert.equal(typeof commons.stripSlashes, 'function') assert.equal(typeof commons._, 'object') + assert.equal(typeof commons.each, 'function') + assert.equal(typeof commons.some, 'function') + assert.equal(typeof commons.every, 'function') + assert.equal(typeof commons.keys, 'function') + assert.equal(typeof commons.values, 'function') + assert.equal(typeof commons.isMatch, 'function') + assert.equal(typeof commons.isEmpty, 'function') + assert.equal(typeof commons.isObject, 'function') + assert.equal(typeof commons.extend, 'function') + assert.equal(typeof commons.omit, 'function') + assert.equal(typeof commons.pick, 'function') + assert.equal(typeof commons.merge, 'function') }) it('exposes lodash methods under _', () => { @@ -25,4 +38,19 @@ describe('module', () => { assert.equal(typeof _.pick, 'function') assert.equal(typeof _.merge, 'function') }) + + it('exposes separate methods under _', () => { + assert.equal(typeof commons.each, 'function') + assert.equal(typeof commons.some, 'function') + assert.equal(typeof commons.every, 'function') + assert.equal(typeof commons.keys, 'function') + assert.equal(typeof commons.values, 'function') + assert.equal(typeof commons.isMatch, 'function') + assert.equal(typeof commons.isEmpty, 'function') + assert.equal(typeof commons.isObject, 'function') + assert.equal(typeof commons.extend, 'function') + assert.equal(typeof commons.omit, 'function') + assert.equal(typeof commons.pick, 'function') + assert.equal(typeof commons.merge, 'function') + }) }) diff --git a/packages/commons/test/utils.test.ts b/packages/commons/test/utils.test.ts index 53d51978ab..aaaaec94f7 100644 --- a/packages/commons/test/utils.test.ts +++ b/packages/commons/test/utils.test.ts @@ -1,31 +1,31 @@ /* tslint:disable:no-unused-expression */ import { strict as assert } from 'assert' -import { _, stripSlashes, isPromise, createSymbol } from '../src' +import * as _ from '../src' describe('@feathersjs/commons utils', () => { it('stripSlashes', () => { - assert.equal(stripSlashes('some/thing'), 'some/thing') - assert.equal(stripSlashes('/some/thing'), 'some/thing') - assert.equal(stripSlashes('some/thing/'), 'some/thing') - assert.equal(stripSlashes('/some/thing/'), 'some/thing') - assert.equal(stripSlashes('//some/thing/'), 'some/thing') - assert.equal(stripSlashes('//some//thing////'), 'some//thing') + assert.equal(_.stripSlashes('some/thing'), 'some/thing') + assert.equal(_.stripSlashes('/some/thing'), 'some/thing') + assert.equal(_.stripSlashes('some/thing/'), 'some/thing') + assert.equal(_.stripSlashes('/some/thing/'), 'some/thing') + assert.equal(_.stripSlashes('//some/thing/'), 'some/thing') + assert.equal(_.stripSlashes('//some//thing////'), 'some//thing') }) it('isPromise', () => { - assert.equal(isPromise(Promise.resolve()), true) + assert.equal(_.isPromise(Promise.resolve()), true) assert.ok( - isPromise({ + _.isPromise({ then() { return true } }) ) - assert.equal(isPromise(null), false) + assert.equal(_.isPromise(null), false) }) it('createSymbol', () => { - assert.equal(typeof createSymbol('a test'), 'symbol') + assert.equal(typeof _.createSymbol('a test'), 'symbol') }) describe('_', () => { @@ -52,6 +52,7 @@ describe('@feathersjs/commons utils', () => { assert.equal(value, 'hi') }) + // @ts-expect-error (should fail) _.each('moo', () => assert.fail('Should never get here')) }) diff --git a/packages/knex/src/adapter.ts b/packages/knex/src/adapter.ts index 8e6c6cb0b0..00e167731b 100644 --- a/packages/knex/src/adapter.ts +++ b/packages/knex/src/adapter.ts @@ -1,5 +1,5 @@ import { Id, NullableId, Paginated, Query } from '@feathersjs/feathers' -import { _ } from '@feathersjs/commons' +import { isObject, pick, omit } from '@feathersjs/commons' import { AdapterBase, PaginationOptions, AdapterQuery, getLimit } from '@feathersjs/adapter-commons' import { BadRequest, MethodNotAllowed, NotFound } from '@feathersjs/errors' import { Knex } from 'knex' @@ -86,7 +86,7 @@ export class KnexAdapter< return Object.keys(query || {}).reduce((currentQuery, key) => { const value = query[key] - if (_.isObject(value) && !(value instanceof Date)) { + if (isObject(value) && !(value instanceof Date)) { return knexify(currentQuery, value, key) } @@ -135,7 +135,7 @@ export class KnexAdapter< // build up the knex query out of the query params, include $and and $or filters this.knexify(builder, { ...query, - ..._.pick(filters, '$and', '$or') + ...pick(filters, '$and', '$or') }) // Handle $sort @@ -252,7 +252,7 @@ export class KnexAdapter< return this._get(id, { ...params, - query: _.pick(params?.query || {}, '$select') + query: pick(params?.query || {}, '$select') }) } @@ -273,7 +273,7 @@ export class KnexAdapter< } const { name, id: idField } = this.getOptions(params) - const data = _.omit(raw, this.id) + const data = omit(raw, this.id) const results = await this._findOrGet(id, { ...params, query: { @@ -311,7 +311,7 @@ export class KnexAdapter< throw new BadRequest("You can not replace multiple instances. Did you mean 'patch'?") } - const data = _.omit(_data, this.id) + const data = omit(_data, this.id) const oldData = await this._get(id, params) const newObject = Object.keys(oldData).reduce((result: any, key) => { if (key !== this.id) { diff --git a/packages/memory/src/index.ts b/packages/memory/src/index.ts index 543592aec1..5f394ca148 100644 --- a/packages/memory/src/index.ts +++ b/packages/memory/src/index.ts @@ -1,5 +1,5 @@ import { BadRequest, MethodNotAllowed, NotFound } from '@feathersjs/errors' -import { _ } from '@feathersjs/commons' +import { omit } from '@feathersjs/commons' import { sorter, select, @@ -75,11 +75,11 @@ export class MemoryAdapter< const { paginate } = this.getOptions(params) const { query, filters } = this.getQuery(params) - let values = _.values(this.store) + let values = Object.values(this.store) const hasSkip = filters.$skip !== undefined const hasSort = filters.$sort !== undefined const hasLimit = filters.$limit !== undefined - const hasQuery = _.keys(query).length > 0 + const hasQuery = Object.keys(query).length > 0 if (hasSort) { values.sort(this.options.sorter(filters.$sort)) @@ -171,7 +171,7 @@ export class MemoryAdapter< } const id = (data as any)[this.id] || this._uId++ - const current = _.extend({}, data, { [this.id]: id }) + const current = { ...data, [this.id]: id } as Result const result = (this.store[id] = current) return _select(result, params, this.id) as Result @@ -189,7 +189,7 @@ export class MemoryAdapter< // eslint-disable-next-line eqeqeq id = oldId == id ? oldId : id - this.store[id] = _.extend({}, data, { [this.id]: id }) + this.store[id] = { ...data, [this.id]: id } as Result return this._get(id, params) } @@ -214,7 +214,7 @@ export class MemoryAdapter< const patchEntry = (entry: Result) => { const currentId = (entry as any)[this.id] - this.store[currentId] = _.extend(this.store[currentId], _.omit(data, this.id)) + Object.assign(this.store[currentId], omit(data, this.id)) return _select(this.store[currentId], params, this.id) } diff --git a/packages/mongodb/src/adapter.ts b/packages/mongodb/src/adapter.ts index b640bf395d..0b2d755a54 100644 --- a/packages/mongodb/src/adapter.ts +++ b/packages/mongodb/src/adapter.ts @@ -13,7 +13,7 @@ import { FindOneAndDeleteOptions } from 'mongodb' import { BadRequest, MethodNotAllowed, NotFound } from '@feathersjs/errors' -import { _ } from '@feathersjs/commons' +import { omit } from '@feathersjs/commons' import { AdapterBase, AdapterParams, @@ -194,7 +194,7 @@ export class MongoDbAdapter< if (this.id === '_id') { // Default Mongo IDs cannot be updated. The Mongo library handles // this automatically. - return _.omit(data, this.id) + return omit(data, this.id) } else if (id !== null) { // If not using the default Mongo _id field set the ID to its // previous value. This prevents orphaned documents. diff --git a/packages/rest-client/src/base.ts b/packages/rest-client/src/base.ts index 6e337d784e..7fb657d8af 100644 --- a/packages/rest-client/src/base.ts +++ b/packages/rest-client/src/base.ts @@ -1,11 +1,11 @@ import qs from 'qs' import { Params, Id, Query, NullableId, ServiceInterface } from '@feathersjs/feathers' import { Unavailable, convert } from '@feathersjs/errors' -import { _, stripSlashes } from '@feathersjs/commons' +import { pick, stripSlashes } from '@feathersjs/commons' function toError(error: Error & { code: string }) { if (error.code === 'ECONNREFUSED') { - throw new Unavailable(error.message, _.pick(error, 'address', 'port', 'config')) + throw new Unavailable(error.message, pick(error, 'address', 'port', 'config')) } throw convert(error) diff --git a/packages/schema/src/json-schema.ts b/packages/schema/src/json-schema.ts index 0d0739b49b..7e1814f192 100644 --- a/packages/schema/src/json-schema.ts +++ b/packages/schema/src/json-schema.ts @@ -1,4 +1,4 @@ -import { _ } from '@feathersjs/commons' +import { omit } from '@feathersjs/commons' import { JSONSchema } from 'json-schema-to-ts' import { JSONSchemaDefinition, Ajv, Validator } from './schema' @@ -99,7 +99,7 @@ export const queryProperty = { - const definition = _.omit(def, 'default') + const definition = omit(def, 'default') return { anyOf: [ definition, From c33380651443f2b622bf11cba47774463b809c26 Mon Sep 17 00:00:00 2001 From: fratzinger <22286818+fratzinger@users.noreply.github.com> Date: Sun, 2 Jun 2024 18:50:14 +0200 Subject: [PATCH 2/3] fix: module.exports hack assign for cjs --- packages/commons/src/index.ts | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 735fa6adad..2e381b8291 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -6,7 +6,7 @@ export function stripSlashes(name: string) { export type KeyValueCallback = (value: any, key: string) => T /** - * If the object is an array, it will iterate over every element. + * If the object is an array, it will iterate over every element. * Otherwise, it will iterate over every key in the object. */ export function each(obj: Record | any[], callback: KeyValueCallback) { @@ -19,9 +19,9 @@ export function each(obj: Record | any[], callback: KeyValueCallbac /** * Check if some values in the object pass the test implemented by the provided function - * + * * returns true if some values pass the test, otherwise false - * + * * returns false if the object is empty */ export function some(value: Record, callback: KeyValueCallback) { @@ -36,9 +36,9 @@ export function some(value: Record, callback: KeyValueCallback) { @@ -67,7 +67,7 @@ export function values(obj: any) { /** * Check if values in the source object are equal to the target object - * + * * Does a shallow comparison */ export function isMatch(target: any, source: any) { @@ -162,9 +162,9 @@ export function createSymbol(name: string) { /** * A set of lodash-y utility functions that use ES6 * - * @deprecated Don't use `import { _ } from '@feathersjs/commons'`. You're importing a bunch of functions. You probably only need a few. + * @deprecated Don't use `import { _ } from '@feathersjs/commons'`. You're importing a bunch of functions. You probably only need a few. * Import them directly instead. For example: `import { merge } from '@feathersjs/commons'`. - * + * * If you really want to import all functions or do not care about cherry picking, you can use `import * as _ from '@feathersjs/commons'`. */ export const _ = { @@ -180,7 +180,13 @@ export const _ = { extend, omit, pick, - merge + merge, + isPromise, + createSymbol } export * from './debug' + +if (typeof module !== 'undefined') { + module.exports = Object.assign(_, module.exports) +} From 83413add0d89811801fc1d904dfe9dd2b08aa90a Mon Sep 17 00:00:00 2001 From: fratzinger <22286818+fratzinger@users.noreply.github.com> Date: Sat, 15 Jun 2024 22:27:58 +0200 Subject: [PATCH 3/3] chore: export default _ --- packages/commons/src/index.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/commons/src/index.ts b/packages/commons/src/index.ts index 2e381b8291..e24349cf75 100644 --- a/packages/commons/src/index.ts +++ b/packages/commons/src/index.ts @@ -185,6 +185,8 @@ export const _ = { createSymbol } +export default _ + export * from './debug' if (typeof module !== 'undefined') {