Skip to content

Commit b790fa8

Browse files
committed
feat(cli): implement nested object value manipulation functions
Added `setNestedValue` and `getNestedValue` functions to safely set and retrieve values in nested objects using dot notation, while preventing prototype pollution. Updated imports in relevant files to utilize these new utility functions, removing redundant implementations.
1 parent 227bd84 commit b790fa8

5 files changed

Lines changed: 61 additions & 52 deletions

File tree

examples/cli/src/input/prompt.ts

Lines changed: 3 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type { DataPortSchemaObject, DataPortSchemaNonBoolean } from "@workglow/util";
7+
import type { DataPortSchemaNonBoolean, DataPortSchemaObject } from "@workglow/util";
8+
import { getNestedValue } from "../util";
89
import { deepMerge } from "./resolve-input";
910

1011
export interface PromptFieldDescriptor {
@@ -79,11 +80,7 @@ function evaluateConditionalRequired(
7980
break;
8081
}
8182

82-
if (
83-
typeof constraint === "object" &&
84-
constraint !== null &&
85-
"const" in constraint
86-
) {
83+
if (typeof constraint === "object" && constraint !== null && "const" in constraint) {
8784
if (inputValue !== (constraint as { const: unknown }).const) {
8885
matches = false;
8986
break;
@@ -184,18 +181,6 @@ function collectMissingFields(
184181
}
185182
}
186183

187-
function getNestedValue(obj: Record<string, unknown>, key: string): unknown {
188-
const parts = key.split(".");
189-
let current: unknown = obj;
190-
for (const part of parts) {
191-
if (current === null || current === undefined || typeof current !== "object") {
192-
return undefined;
193-
}
194-
current = (current as Record<string, unknown>)[part];
195-
}
196-
return current;
197-
}
198-
199184
function formatKeyAsLabel(key: string): string {
200185
return key
201186
.replace(/_/g, " ")

examples/cli/src/input/schema-flags.ts

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type { DataPortSchemaObject, DataPortSchemaNonBoolean } from "@workglow/util";
7+
import type { DataPortSchemaNonBoolean, DataPortSchemaObject } from "@workglow/util";
8+
import { setNestedValue } from "../util";
89

910
type SchemaProperty = DataPortSchemaNonBoolean;
1011

@@ -104,20 +105,6 @@ function coerceValue(
104105
}
105106
}
106107

107-
function setNestedValue(obj: Record<string, unknown>, key: string, value: unknown): void {
108-
const parts = key.split(".");
109-
let current: Record<string, unknown> = obj;
110-
111-
for (let i = 0; i < parts.length - 1; i++) {
112-
if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
113-
current[parts[i]] = {};
114-
}
115-
current = current[parts[i]] as Record<string, unknown>;
116-
}
117-
118-
current[parts[parts.length - 1]] = value;
119-
}
120-
121108
/**
122109
* Parse config flags from argv based on a config schema.
123110
*

examples/cli/src/ui/SchemaPromptApp.tsx

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,18 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import React, { useState, useCallback, useRef } from "react";
7+
import { ConfirmInput, Select, TextInput } from "@inkjs/ui";
88
import { Box, Text, useInput } from "ink";
9-
import { TextInput, Select, ConfirmInput } from "@inkjs/ui";
9+
import React, { useCallback, useRef, useState } from "react";
1010
import type { PromptFieldDescriptor } from "../input/prompt";
11+
import { setNestedValue } from "../util";
1112

1213
interface SchemaPromptAppProps {
1314
readonly fields: readonly PromptFieldDescriptor[];
1415
readonly onComplete: (values: Record<string, unknown>) => void;
1516
readonly onCancel: () => void;
1617
}
1718

18-
function setNestedValue(obj: Record<string, unknown>, key: string, value: unknown): void {
19-
const parts = key.split(".");
20-
let current: Record<string, unknown> = obj;
21-
22-
for (let i = 0; i < parts.length - 1; i++) {
23-
if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
24-
current[parts[i]] = {};
25-
}
26-
current = current[parts[i]] as Record<string, unknown>;
27-
}
28-
29-
current[parts[parts.length - 1]] = value;
30-
}
31-
3219
function coercePromptValue(raw: string, field: PromptFieldDescriptor): unknown {
3320
switch (field.type) {
3421
case "number":
@@ -110,7 +97,11 @@ function isTextField(field: PromptFieldDescriptor): boolean {
11097
return field.type !== "enum" && field.type !== "boolean";
11198
}
11299

113-
export function SchemaPromptApp({ fields, onComplete, onCancel }: SchemaPromptAppProps): React.ReactElement {
100+
export function SchemaPromptApp({
101+
fields,
102+
onComplete,
103+
onCancel,
104+
}: SchemaPromptAppProps): React.ReactElement {
114105
const [focusedIndex, setFocusedIndex] = useState(0);
115106
const valuesRef = useRef<Record<string, unknown>>({});
116107
const rawValuesRef = useRef<Record<string, string>>({});
@@ -290,7 +281,11 @@ function FieldWidget({
290281
if (field.type === "enum" && field.enumValues) {
291282
const options = field.enumValues.map((v) => ({ label: v, value: v }));
292283
return (
293-
<Select options={options} defaultValue={previousValue} onChange={(value) => onSubmit(value)} />
284+
<Select
285+
options={options}
286+
defaultValue={previousValue}
287+
onChange={(value) => onSubmit(value)}
288+
/>
294289
);
295290
}
296291

examples/cli/src/util.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,48 @@
66

77
import { writeFile } from "fs/promises";
88

9+
const PROTOTYPE_POLLUTION_KEYS = new Set(["__proto__", "constructor", "prototype"]);
10+
11+
/**
12+
* Set a value on an object using dot-notation key (e.g. "a.b.c").
13+
* Creates intermediate objects as needed. Skips keys that could cause prototype pollution.
14+
*/
15+
export function setNestedValue(obj: Record<string, unknown>, key: string, value: unknown): void {
16+
const parts = key.split(".");
17+
for (const part of parts) {
18+
if (PROTOTYPE_POLLUTION_KEYS.has(part)) return;
19+
}
20+
let current: Record<string, unknown> = obj;
21+
22+
for (let i = 0; i < parts.length - 1; i++) {
23+
if (!(parts[i] in current) || typeof current[parts[i]] !== "object") {
24+
current[parts[i]] = {};
25+
}
26+
current = current[parts[i]] as Record<string, unknown>;
27+
}
28+
29+
current[parts[parts.length - 1]] = value;
30+
}
31+
32+
/**
33+
* Get a value from an object using dot-notation key (e.g. "a.b.c").
34+
* Returns undefined if any segment is missing or if the key would touch prototype-pollution-sensitive properties.
35+
*/
36+
export function getNestedValue(obj: Record<string, unknown>, key: string): unknown {
37+
const parts = key.split(".");
38+
for (const part of parts) {
39+
if (PROTOTYPE_POLLUTION_KEYS.has(part)) return undefined;
40+
}
41+
let current: unknown = obj;
42+
for (const part of parts) {
43+
if (current === null || current === undefined || typeof current !== "object") {
44+
return undefined;
45+
}
46+
current = (current as Record<string, unknown>)[part];
47+
}
48+
return current;
49+
}
50+
951
/**
1052
* Read all of stdin as a string.
1153
*/

packages/storage/src/vector/PostgresVectorStorage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7+
import { cosineSimilarity } from "@workglow/util";
78
import type {
89
DataPortSchemaObject,
910
FromSchema,
1011
TypedArray,
1112
TypedArraySchemaOptions,
1213
} from "@workglow/util";
13-
import { cosineSimilarity } from "@workglow/util";
1414
import type { Pool } from "pg";
1515
import { PostgresTabularStorage } from "../tabular/PostgresTabularStorage";
1616
import {

0 commit comments

Comments
 (0)