diff --git a/package-lock.json b/package-lock.json index aa873be..82ab414 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,6 @@ "version": "0.0.0-dev", "license": "MIT", "dependencies": { - "dset": "^3.1.3", "fast-decode-uri-component": "^1.0.1" }, "devDependencies": { @@ -852,14 +851,6 @@ "node": ">=6.0.0" } }, - "node_modules/dset": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.3.tgz", - "integrity": "sha512-20TuZZHCEZ2O71q9/+8BwKwZ0QtD9D8ObhrihJPr+vLLYlSuAU3/zL4cSlgbfeoGHTjCSJBa7NGcrF9/Bx/WJQ==", - "engines": { - "node": ">=4" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", diff --git a/package.json b/package.json index a5e5655..7314740 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,6 @@ "typescript-eslint": "^7.7.1" }, "dependencies": { - "dset": "^3.1.3", "fast-decode-uri-component": "^1.0.1" } } diff --git a/src/object-util.ts b/src/object-util.ts index 7c25fbc..6bb3726 100644 --- a/src/object-util.ts +++ b/src/object-util.ts @@ -11,6 +11,49 @@ export function getDeepValue(obj: unknown, keys: PropertyKey[]): unknown { return obj; } +type KeyableObject = Record; + +export function setDeepValue( + obj: unknown, + keys: PropertyKey[], + val: unknown +): void { + const len = keys.length; + const lastKey = len - 1; + let k; + let curr = obj as KeyableObject; + let currVal; + let nextKey; + + for (let i = 0; i < len; i++) { + k = keys[i]; + + if (k === '__proto__' || k === 'constructor' || k === 'prototype') { + break; + } + + if (i === lastKey) { + curr[k] = val; + } else { + currVal = curr[k]; + if (typeof currVal === 'object' && currVal !== null) { + curr = currVal as KeyableObject; + } else { + nextKey = keys[i + 1]; + if ( + typeof nextKey === 'string' && + ((nextKey as unknown as number) * 0 !== 0 || + nextKey.indexOf('.') > -1) + ) { + curr = curr[k] = {}; + } else { + curr = curr[k] = [] as unknown as KeyableObject; + } + } + } + } +} + const MAX_DEPTH = 20; const strBracketPair = '[]'; const strBracketLeft = '['; diff --git a/src/parse.ts b/src/parse.ts index 16d0284..594f320 100644 --- a/src/parse.ts +++ b/src/parse.ts @@ -5,8 +5,7 @@ import { defaultOptions } from './shared.js'; import fastDecode from 'fast-decode-uri-component'; -import {dset} from 'dset'; -import {getDeepValue} from './object-util.js'; +import {getDeepValue, setDeepValue} from './object-util.js'; export type ParsedQuery = Record; export type ParseOptions = Partial; @@ -147,7 +146,7 @@ export function parse(input: string, options?: ParseOptions): ParsedQuery { if (currentValue === undefined || !arrayRepeat) { if (hasNestedKey) { - dset(result, keyPath as string[], newValue); + setDeepValue(result, keyPath, newValue); } else { result[lastKeyPathPart] = newValue; } @@ -157,7 +156,7 @@ export function parse(input: string, options?: ParseOptions): ParsedQuery { (currentValue as unknown[]).push(newValue); } else { if (hasNestedKey) { - dset(result, keyPath as string[], [currentValue, newValue]); + setDeepValue(result, keyPath, [currentValue, newValue]); } else { result[lastKeyPathPart] = [currentValue, newValue]; } diff --git a/src/test/object-util_test.ts b/src/test/object-util_test.ts index e26f4e5..22d68a4 100644 --- a/src/test/object-util_test.ts +++ b/src/test/object-util_test.ts @@ -1,6 +1,6 @@ import * as assert from 'node:assert/strict'; import {test} from 'node:test'; -import {getDeepValue, getNestedValues} from '../object-util.js'; +import {getDeepValue, getNestedValues, setDeepValue} from '../object-util.js'; test('getDeepValue', async (t) => { await t.test('retrieves by single key', () => { @@ -51,6 +51,90 @@ test('getDeepValue', async (t) => { }); }); +const disallowedKeys = ['__proto__', 'constructor', 'prototype']; + +test('setDeepValue', async (t) => { + for (const key of disallowedKeys) { + await t.test(`cannot set ${key}`, () => { + const obj = {}; + setDeepValue(obj, [key], 123); + assert.deepEqual(obj, {}); + }); + + await t.test('cannot set proto deeply', () => { + const obj = {foo: {}}; + setDeepValue(obj, ['foo', key], 123); + assert.deepEqual(obj, {foo: {}}); + }); + } + + await t.test('sets top level key', () => { + const obj: Record = {}; + setDeepValue(obj, ['foo'], 303); + assert.deepEqual(obj, {foo: 303}); + }); + + await t.test('sets deep key', () => { + const obj: Record = {foo: {}}; + setDeepValue(obj, ['foo', 'bar'], 303); + assert.deepEqual(obj, {foo: {bar: 303}}); + }); + + await t.test('replaces null object value with object', () => { + const obj: Record = {foo: null}; + setDeepValue(obj, ['foo', 'bar'], 303); + assert.deepEqual(obj, { + foo: { + bar: 303 + } + }); + }); + + await t.test('replaces null array value with array', () => { + const obj: Record = {foo: null}; + setDeepValue(obj, ['foo', 0], 303); + assert.deepEqual(obj, { + foo: [303] + }); + }); + + await t.test('creates new objects for object values', () => { + const obj: Record = {}; + setDeepValue(obj, ['foo', 'bar'], 303); + assert.deepEqual(obj, { + foo: { + bar: 303 + } + }); + }); + + await t.test('creates new arrays for array values', () => { + const obj: Record = {}; + setDeepValue(obj, ['foo', 0], 303); + assert.deepEqual(obj, { + foo: [303] + }); + }); + + await t.test('creates new arrays with string indices', () => { + const obj: Record = {}; + setDeepValue(obj, ['foo', '0'], 303); + assert.deepEqual(obj, { + foo: [303] + }); + }); + + await t.test('treats decimal strings as regular keys', () => { + const obj: Record = {}; + setDeepValue(obj, ['foo', '10.0'], 303); + assert.deepEqual(obj, { + foo: { + '10.0': 303 + } + }); + }); +}); + test('getNestedValues', async (t) => { await t.test('shallow values', () => { const obj = {