diff --git a/packages/utils/source/chunk.ts b/packages/utils/source/chunk.ts index 83c5729d0c..843a53d171 100644 --- a/packages/utils/source/chunk.ts +++ b/packages/utils/source/chunk.ts @@ -1,5 +1,3 @@ -import { slice } from "./slice.js"; - export const chunk = (iterable: T[], chunkSize: number): T[][] => { const iterableLength: number = iterable.length; @@ -12,7 +10,7 @@ export const chunk = (iterable: T[], chunkSize: number): T[][] => { const result: T[][] = Array.from({ length: Math.ceil(iterableLength / chunkSize) }); while (index < iterableLength) { - result[resIndex++] = slice(iterable, index, (index += chunkSize)); + result[resIndex++] = iterable.slice(index, (index += chunkSize)); } return result; diff --git a/packages/utils/source/get-path-segments.test.ts b/packages/utils/source/get-path-segments.test.ts index 54f3930f53..d9263a911b 100644 --- a/packages/utils/source/get-path-segments.test.ts +++ b/packages/utils/source/get-path-segments.test.ts @@ -2,10 +2,6 @@ import { describe } from "@mainsail/test-runner"; import { getPathSegments } from "./get-path-segments"; describe("getPathSegments", async ({ assert, it, nock, loader }) => { - it("should return an array as is", () => { - assert.equal(getPathSegments(["a", "b"]), ["a", "b"]); - }); - it("should return the path as array", () => { assert.equal(getPathSegments("a.b"), ["a", "b"]); }); diff --git a/packages/utils/source/get-path-segments.ts b/packages/utils/source/get-path-segments.ts index dfe6190a74..f096b46fc1 100644 --- a/packages/utils/source/get-path-segments.ts +++ b/packages/utils/source/get-path-segments.ts @@ -1,7 +1,7 @@ const disallowedKeys = new Set(["__proto__", "prototype", "constructor"]); -export const getPathSegments = (value: string | string[]): string[] => { - const segments: string[] = Array.isArray(value) ? value : value.split("."); +export const getPathSegments = (value: string): string[] => { + const segments: string[] = value.split("."); return segments.some((segment: string) => disallowedKeys.has(segment)) ? [] : segments; }; diff --git a/packages/utils/source/get.ts b/packages/utils/source/get.ts index c38d15d0e9..a06e571011 100644 --- a/packages/utils/source/get.ts +++ b/packages/utils/source/get.ts @@ -3,7 +3,7 @@ import { isEnumerable } from "./is-enumerable.js"; import { isObject } from "./is-object.js"; import { isString } from "./is-string.js"; -export const get = (object: T, path: string | string[], defaultValue?: V): V | undefined => { +export const get = (object: T, path: string, defaultValue?: V): V | undefined => { if (!isObject(object) || !isString(path)) { return defaultValue; } diff --git a/packages/utils/source/group-by.ts b/packages/utils/source/group-by.ts index a1a140a05d..2d79a72ab8 100644 --- a/packages/utils/source/group-by.ts +++ b/packages/utils/source/group-by.ts @@ -1,4 +1,4 @@ -import type { FunctionReturning } from "./internal/index.js"; +import type { FunctionReturning } from "./types.js"; export const groupBy = ( iterable: T[], diff --git a/packages/utils/source/has.ts b/packages/utils/source/has.ts index 23eba9fc3f..e6ed728646 100644 --- a/packages/utils/source/has.ts +++ b/packages/utils/source/has.ts @@ -2,7 +2,7 @@ import { getPathSegments } from "./get-path-segments.js"; import { isObject } from "./is-object.js"; import { isString } from "./is-string.js"; -export const has = (object: T, path: string | string[]): boolean => { +export const has = (object: T, path: string): boolean => { if (!isObject(object) || !isString(path)) { return false; } diff --git a/packages/utils/source/index.test.ts b/packages/utils/source/index.test.ts new file mode 100644 index 0000000000..524d3a5014 --- /dev/null +++ b/packages/utils/source/index.test.ts @@ -0,0 +1,61 @@ +import { describe } from "@mainsail/test-runner"; +import * as utils from "./index"; + +describe("index", ({ assert, it }) => { + it("should export all functions and classes", () => { + for (const name of [ + "ByteBuffer", + "HttpError", + "Lock", + "camelCase", + "chunk", + "cloneDeep", + "comparator", + "ensureError", + "expandTilde", + "formatEcdsaSignature", + "get", + "getPathSegments", + "groupBy", + "has", + "isArray", + "isBlacklisted", + "isEmpty", + "isEnumerable", + "isObject", + "isString", + "isWhitelisted", + "map", + "mapArray", + "mapObject", + "merge", + "minBy", + "orderBy", + "pascalCase", + "pluralize", + "prettyBytes", + "prettyTime", + "randomNumber", + "set", + "setTimeoutAsync", + "shuffle", + "sleep", + "take", + "unset", + "validatorSetPack", + "validatorSetUnpack", + ]) { + assert.function(utils[name]); + } + }); + + it("should export all namespaces", () => { + for (const name of ["IpAddress", "assert", "dotenv", "http", "semver"]) { + assert.object(utils[name]); + } + }); + + it("should not export anything else", () => { + assert.equal(Object.keys(utils).length, 45); + }); +}); diff --git a/packages/utils/source/index.ts b/packages/utils/source/index.ts index 4d52914f93..9a8d196e81 100644 --- a/packages/utils/source/index.ts +++ b/packages/utils/source/index.ts @@ -11,7 +11,6 @@ export * from "./get.js"; export * from "./group-by.js"; export * from "./has.js"; export * from "./http.js"; -export * from "./internal/index.js"; export * as IpAddress from "./ip-address.js"; export * from "./is-array.js"; export * from "./is-blacklisted.js"; @@ -38,5 +37,6 @@ export * from "./shuffle.js"; export * from "./sleep.js"; export * from "./string-case.js"; export * from "./take.js"; +export * from "./types.js"; export * from "./unset.js"; export * from "./validator-set-pack.js"; diff --git a/packages/utils/source/internal/index.ts b/packages/utils/source/internal/index.ts deleted file mode 100644 index a0c4ebf250..0000000000 --- a/packages/utils/source/internal/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./types.js"; diff --git a/packages/utils/source/is-blacklisted.test.ts b/packages/utils/source/is-blacklisted.test.ts index 4c008d6d63..2652f4d955 100644 --- a/packages/utils/source/is-blacklisted.test.ts +++ b/packages/utils/source/is-blacklisted.test.ts @@ -39,4 +39,9 @@ describe("isBlacklisted", ({ assert, it }) => { assert.true(isBlacklisted(["*:104"], "2001:3984:3989::104")); assert.false(isBlacklisted(["*:104"], "2001:3984:3989::105")); }); + + it("should skip invalid patterns instead of throwing", () => { + assert.false(isBlacklisted([""], "127.0.0.1")); + assert.true(isBlacklisted(["", "127.*"], "127.0.0.1")); + }); }); diff --git a/packages/utils/source/is-blacklisted.ts b/packages/utils/source/is-blacklisted.ts index 84b45b4967..03c0e0b6ee 100644 --- a/packages/utils/source/is-blacklisted.ts +++ b/packages/utils/source/is-blacklisted.ts @@ -1,6 +1,5 @@ import mm from "micromatch"; -// @TODO review the implementation export const isBlacklisted = (blacklist: string[], remoteAddress: string): boolean => { if (!Array.isArray(blacklist) || blacklist.length === 0) { return false; diff --git a/packages/utils/source/is-empty.test.ts b/packages/utils/source/is-empty.test.ts index 9f0fe2f9c3..90f24795cf 100644 --- a/packages/utils/source/is-empty.test.ts +++ b/packages/utils/source/is-empty.test.ts @@ -33,4 +33,18 @@ describe("at", async ({ assert, it, nock, loader }) => { it("should return false if the value contains something", () => { assert.false(isEmpty([1])); }); + + it("should return false for non-empty strings, maps and sets", () => { + assert.false(isEmpty("a")); + assert.false(isEmpty(new Map([["a", 1]]))); + assert.false(isEmpty(new Set([1]))); + assert.false(isEmpty({ a: 1 })); + }); + + it("should return false for truthy primitives and functions", () => { + assert.false(isEmpty(1)); + assert.false(isEmpty(true)); + assert.false(isEmpty(() => {})); + assert.false(isEmpty(Symbol("a"))); + }); }); diff --git a/packages/utils/source/is-whitelisted.test.ts b/packages/utils/source/is-whitelisted.test.ts index 6bb2324e8c..80a06b486d 100644 --- a/packages/utils/source/is-whitelisted.test.ts +++ b/packages/utils/source/is-whitelisted.test.ts @@ -34,4 +34,9 @@ describe("isWhitelisted", ({ assert, it }) => { assert.true(isWhitelisted(["*.127"], "1.1.1.127")); assert.false(isWhitelisted(["*.127"], "1.1.1.128")); }); + + it("should skip invalid patterns instead of throwing", () => { + assert.false(isWhitelisted([""], "127.0.0.1")); + assert.true(isWhitelisted(["", "127.*"], "127.0.0.1")); + }); }); diff --git a/packages/utils/source/is-whitelisted.ts b/packages/utils/source/is-whitelisted.ts index d1f2f29a79..966175e261 100644 --- a/packages/utils/source/is-whitelisted.ts +++ b/packages/utils/source/is-whitelisted.ts @@ -1,6 +1,5 @@ import mm from "micromatch"; -// @TODO review the implementation export const isWhitelisted = (whitelist: string[], remoteAddress: string): boolean => { if (!Array.isArray(whitelist) || whitelist.length === 0) { return true; diff --git a/packages/utils/source/lock.test.ts b/packages/utils/source/lock.test.ts index 4aa711de83..82294cad4d 100644 --- a/packages/utils/source/lock.test.ts +++ b/packages/utils/source/lock.test.ts @@ -85,4 +85,41 @@ describe("Lock", ({ assert, it }) => { assert.equal(await Promise.all(promises), [1, 3, 3]); }); + + it("should let a non-exclusive execution run after a rejected exclusive one", async () => { + const lock = new Lock(); + const error = new Error("boom"); + + const exclusive = lock.runExclusive(async () => { + throw error; + }); + const nonExclusive = lock.runNonExclusive(async () => "ok"); + + await assert.rejects(() => exclusive, "boom"); + assert.equal(await nonExclusive, "ok"); + }); + + it("should let an exclusive execution run after a rejected exclusive one", async () => { + const lock = new Lock(); + + const first = lock.runExclusive(async () => { + throw new Error("boom"); + }); + const second = lock.runExclusive(async () => "ok"); + + await assert.rejects(() => first, "boom"); + assert.equal(await second, "ok"); + }); + + it("should let an exclusive execution run after a rejected non-exclusive one", async () => { + const lock = new Lock(); + + const nonExclusive = lock.runNonExclusive(async () => { + throw new Error("boom"); + }); + const exclusive = lock.runExclusive(async () => "ok"); + + await assert.rejects(() => nonExclusive, "boom"); + assert.equal(await exclusive, "ok"); + }); }); diff --git a/packages/utils/source/min-by.ts b/packages/utils/source/min-by.ts index 66546d715a..674bfa409a 100644 --- a/packages/utils/source/min-by.ts +++ b/packages/utils/source/min-by.ts @@ -1,4 +1,4 @@ -import type { FunctionReturning } from "./internal/index.js"; +import type { FunctionReturning } from "./types.js"; import { mapArray } from "./map-array.js"; diff --git a/packages/utils/source/order-by.ts b/packages/utils/source/order-by.ts index c319ac111d..19ed4564b3 100644 --- a/packages/utils/source/order-by.ts +++ b/packages/utils/source/order-by.ts @@ -2,7 +2,7 @@ import type { ISortByObjectSorter } from "fast-sort"; import { sort } from "fast-sort"; -import type { Iteratee } from "./internal/index.js"; +import type { Iteratee } from "./types.js"; import { map } from "./map.js"; diff --git a/packages/utils/source/set.ts b/packages/utils/source/set.ts index 3e4fc2ee96..06ecd42a44 100644 --- a/packages/utils/source/set.ts +++ b/packages/utils/source/set.ts @@ -2,7 +2,7 @@ import { getPathSegments } from "./get-path-segments.js"; import { isObject } from "./is-object.js"; import { isString } from "./is-string.js"; -export const set = (object: T, path: string | string[], value: unknown): boolean => { +export const set = (object: T, path: string, value: unknown): boolean => { if (!isObject(object) || !isString(path)) { return false; } diff --git a/packages/utils/source/slice.ts b/packages/utils/source/slice.ts deleted file mode 100644 index 90d2329de6..0000000000 --- a/packages/utils/source/slice.ts +++ /dev/null @@ -1,27 +0,0 @@ -// Taken from https://raw.githubusercontent.com/lodash/lodash/4.17.15-post/lodash.js - -export const slice = (array: T[], start: number, end: number): T[] => { - let index = -1; - let length = array.length; - - if (start < 0) { - start = -start > length ? 0 : length + start; - } - - end = end > length ? length : end; - - if (end < 0) { - end += length; - } - - length = start > end ? 0 : (end - start) >>> 0; - start >>>= 0; - - const result: T[] = Array.from({ length }); - - while (++index < length) { - result[index] = array[index + start]; - } - - return result; -}; diff --git a/packages/utils/source/take.ts b/packages/utils/source/take.ts index 598bc8ac0c..9d9d9a49f1 100644 --- a/packages/utils/source/take.ts +++ b/packages/utils/source/take.ts @@ -1,3 +1 @@ -import { slice } from "./slice.js"; - -export const take = (iterable: T[], amount = 1): T[] => slice(iterable, 0, amount); +export const take = (iterable: T[], amount = 1): T[] => iterable.slice(0, amount); diff --git a/packages/utils/source/internal/types.ts b/packages/utils/source/types.ts similarity index 100% rename from packages/utils/source/internal/types.ts rename to packages/utils/source/types.ts diff --git a/packages/utils/source/unset.test.ts b/packages/utils/source/unset.test.ts index 4f1db79bc0..1030443096 100644 --- a/packages/utils/source/unset.test.ts +++ b/packages/utils/source/unset.test.ts @@ -25,4 +25,19 @@ describe("unset", async ({ assert, it, nock, loader }) => { assert.equal(object, { a: { b: {} } }); }); + + it("should return false if an intermediate segment is not an object", () => { + const object = { a: 1 }; + + assert.false(unset(object, "a.b")); + assert.equal(object, { a: 1 }); + }); + + it("should return false if the path contains dangerous segments", () => { + const object = { a: { b: 1 } }; + + assert.false(unset(object, "__proto__.a")); + assert.false(unset(object, "a.constructor")); + assert.equal(object, { a: { b: 1 } }); + }); }); diff --git a/packages/utils/source/unset.ts b/packages/utils/source/unset.ts index 59337f8571..9d96aca924 100644 --- a/packages/utils/source/unset.ts +++ b/packages/utils/source/unset.ts @@ -2,7 +2,7 @@ import { getPathSegments } from "./get-path-segments.js"; import { isObject } from "./is-object.js"; import { isString } from "./is-string.js"; -export const unset = (object: T, path: string | string[]): boolean => { +export const unset = (object: T, path: string): boolean => { if (!isObject(object) || !isString(path)) { return false; }