diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index c3fefbe..d1160c9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -23,12 +23,12 @@ jobs: # Working around https://github.com/npm/cli/issues/4828 # - run: npm ci - run: npm install --no-package-lock - + - name: Run client tests run: | cd inspector/client npm test - + - run: npm run build publish: diff --git a/client/jest.config.cjs b/client/jest.config.cjs index bf6a1fc..3830e79 100644 --- a/client/jest.config.cjs +++ b/client/jest.config.cjs @@ -1,32 +1,37 @@ module.exports = { - preset: 'ts-jest', - testEnvironment: 'jsdom', + preset: "ts-jest", + testEnvironment: "jsdom", moduleNameMapper: { - '^@/(.*)$': '/src/$1', - '^../components/DynamicJsonForm$': '/src/utils/__mocks__/DynamicJsonForm.ts', - '^../../components/DynamicJsonForm$': '/src/utils/__mocks__/DynamicJsonForm.ts' + "^@/(.*)$": "/src/$1", + "^../components/DynamicJsonForm$": + "/src/utils/__mocks__/DynamicJsonForm.ts", + "^../../components/DynamicJsonForm$": + "/src/utils/__mocks__/DynamicJsonForm.ts", }, transform: { - '^.+\\.tsx?$': ['ts-jest', { - useESM: true, - jsx: 'react-jsx', - tsconfig: 'tsconfig.jest.json' - }] + "^.+\\.tsx?$": [ + "ts-jest", + { + useESM: true, + jsx: "react-jsx", + tsconfig: "tsconfig.jest.json", + }, + ], }, - extensionsToTreatAsEsm: ['.ts', '.tsx'], - testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$', + extensionsToTreatAsEsm: [".ts", ".tsx"], + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", // Exclude directories and files that don't need to be tested testPathIgnorePatterns: [ - '/node_modules/', - '/dist/', - '/bin/', - '\\.config\\.(js|ts|cjs|mjs)$' + "/node_modules/", + "/dist/", + "/bin/", + "\\.config\\.(js|ts|cjs|mjs)$", ], // Exclude the same patterns from coverage reports coveragePathIgnorePatterns: [ - '/node_modules/', - '/dist/', - '/bin/', - '\\.config\\.(js|ts|cjs|mjs)$' - ] + "/node_modules/", + "/dist/", + "/bin/", + "\\.config\\.(js|ts|cjs|mjs)$", + ], }; diff --git a/client/src/components/DynamicJsonForm.tsx b/client/src/components/DynamicJsonForm.tsx index 784cfc0..9e865f9 100644 --- a/client/src/components/DynamicJsonForm.tsx +++ b/client/src/components/DynamicJsonForm.tsx @@ -28,25 +28,25 @@ interface DynamicJsonFormProps { maxDepth?: number; } - const DynamicJsonForm = ({ schema, value, onChange, maxDepth = 3, }: DynamicJsonFormProps) => { - const [isJsonMode, setIsJsonMode] = useState(false); const [jsonError, setJsonError] = useState(); // Add state for storing raw JSON value const [rawJsonValue, setRawJsonValue] = useState( - JSON.stringify(value ?? generateDefaultValue(schema), null, 2) + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), ); // Update rawJsonValue when value prop changes useEffect(() => { if (!isJsonMode) { - setRawJsonValue(JSON.stringify(value ?? generateDefaultValue(schema), null, 2)); + setRawJsonValue( + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), + ); } }, [value, schema, isJsonMode]); @@ -64,7 +64,9 @@ const DynamicJsonForm = ({ } } else { // Update raw JSON value when switching to JSON mode - setRawJsonValue(JSON.stringify(value ?? generateDefaultValue(schema), null, 2)); + setRawJsonValue( + JSON.stringify(value ?? generateDefaultValue(schema), null, 2), + ); setIsJsonMode(true); } }; @@ -131,8 +133,8 @@ const DynamicJsonForm = ({ ); case "object": { // Handle case where we have a value but no schema properties - const objectValue = currentValue as JsonObject || {}; - + const objectValue = (currentValue as JsonObject) || {}; + // If we have schema properties, use them to render fields if (propSchema.properties) { return ( @@ -150,7 +152,7 @@ const DynamicJsonForm = ({ ))} ); - } + } // If we have a value but no schema properties, render fields based on the value else if (Object.keys(objectValue).length > 0) { return ( @@ -161,7 +163,7 @@ const DynamicJsonForm = ({ + onChange={(e) => handleFieldChange([...path, key], e.target.value) } /> @@ -254,11 +256,7 @@ const DynamicJsonForm = ({ return (
-
@@ -281,23 +279,28 @@ const DynamicJsonForm = ({ }} error={jsonError} /> - ) : ( - // If schema type is object but value is not an object or is empty, and we have actual JSON data, - // render a simple representation of the JSON data - schema.type === "object" && - (typeof value !== "object" || value === null || Object.keys(value).length === 0) && - rawJsonValue && + ) : // If schema type is object but value is not an object or is empty, and we have actual JSON data, + // render a simple representation of the JSON data + schema.type === "object" && + (typeof value !== "object" || + value === null || + Object.keys(value).length === 0) && + rawJsonValue && rawJsonValue !== "{}" ? ( -
-

Form view not available for this JSON structure. Using simplified view:

-
-              {rawJsonValue}
-            
-

Use JSON mode for full editing capabilities.

-
- ) : ( - renderFormFields(schema, value) - ) +
+

+ Form view not available for this JSON structure. Using simplified + view: +

+
+            {rawJsonValue}
+          
+

+ Use JSON mode for full editing capabilities. +

+
+ ) : ( + renderFormFields(schema, value) )}
); diff --git a/client/src/components/JsonEditor.tsx b/client/src/components/JsonEditor.tsx index 9109088..87a6367 100644 --- a/client/src/components/JsonEditor.tsx +++ b/client/src/components/JsonEditor.tsx @@ -11,10 +11,16 @@ interface JsonEditorProps { error?: string; } -const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps) => { +const JsonEditor = ({ + value, + onChange, + error: externalError, +}: JsonEditorProps) => { const [editorContent, setEditorContent] = useState(value); - const [internalError, setInternalError] = useState(undefined); - + const [internalError, setInternalError] = useState( + undefined, + ); + useEffect(() => { setEditorContent(value); }, [value]); @@ -22,8 +28,8 @@ const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps) const formatJson = (json: string): string => { try { // Handle empty arrays and objects specifically - if (json.trim() === '[]') return '[]'; - if (json.trim() === '{}') return '{}'; + if (json.trim() === "[]") return "[]"; + if (json.trim() === "{}") return "{}"; return JSON.stringify(JSON.parse(json), null, 2); } catch { return json; @@ -52,17 +58,15 @@ const JsonEditor = ({ value, onChange, error: externalError }: JsonEditorProps) return (
-
- {displayError &&

{displayError}

} + {displayError && ( +

{displayError}

+ )}
); }; diff --git a/client/src/utils/__tests__/jsonPathUtils.test.ts b/client/src/utils/__tests__/jsonPathUtils.test.ts index bca9661..b6ef7e9 100644 --- a/client/src/utils/__tests__/jsonPathUtils.test.ts +++ b/client/src/utils/__tests__/jsonPathUtils.test.ts @@ -1,119 +1,126 @@ -import { updateValueAtPath, getValueAtPath } from '../jsonPathUtils'; -import { JsonValue } from '../../components/DynamicJsonForm'; +import { updateValueAtPath, getValueAtPath } from "../jsonPathUtils"; +import { JsonValue } from "../../components/DynamicJsonForm"; -describe('updateValueAtPath', () => { +describe("updateValueAtPath", () => { // Basic functionality tests - test('returns the new value when path is empty', () => { - expect(updateValueAtPath({ foo: 'bar' }, [], 'newValue')).toBe('newValue'); + test("returns the new value when path is empty", () => { + expect(updateValueAtPath({ foo: "bar" }, [], "newValue")).toBe("newValue"); }); - test('initializes an empty object when input is null/undefined and path starts with a string', () => { - expect(updateValueAtPath(null as any, ['foo'], 'bar')).toEqual({ foo: 'bar' }); - expect(updateValueAtPath(undefined as any, ['foo'], 'bar')).toEqual({ foo: 'bar' }); + test("initializes an empty object when input is null/undefined and path starts with a string", () => { + expect(updateValueAtPath(null as any, ["foo"], "bar")).toEqual({ + foo: "bar", + }); + expect(updateValueAtPath(undefined as any, ["foo"], "bar")).toEqual({ + foo: "bar", + }); }); - test('initializes an empty array when input is null/undefined and path starts with a number', () => { - expect(updateValueAtPath(null as any, ['0'], 'bar')).toEqual(['bar']); - expect(updateValueAtPath(undefined as any, ['0'], 'bar')).toEqual(['bar']); + test("initializes an empty array when input is null/undefined and path starts with a number", () => { + expect(updateValueAtPath(null as any, ["0"], "bar")).toEqual(["bar"]); + expect(updateValueAtPath(undefined as any, ["0"], "bar")).toEqual(["bar"]); }); // Object update tests - test('updates a simple object property', () => { - const obj = { name: 'John', age: 30 }; - expect(updateValueAtPath(obj, ['age'], 31)).toEqual({ name: 'John', age: 31 }); + test("updates a simple object property", () => { + const obj = { name: "John", age: 30 }; + expect(updateValueAtPath(obj, ["age"], 31)).toEqual({ + name: "John", + age: 31, + }); }); - test('updates a nested object property', () => { - const obj = { user: { name: 'John', address: { city: 'New York' } } }; - expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual( - { user: { name: 'John', address: { city: 'Boston' } } } - ); + test("updates a nested object property", () => { + const obj = { user: { name: "John", address: { city: "New York" } } }; + expect( + updateValueAtPath(obj, ["user", "address", "city"], "Boston"), + ).toEqual({ user: { name: "John", address: { city: "Boston" } } }); }); - test('creates missing object properties', () => { - const obj = { user: { name: 'John' } }; - expect(updateValueAtPath(obj, ['user', 'address', 'city'], 'Boston')).toEqual( - { user: { name: 'John', address: { city: 'Boston' } } } - ); + test("creates missing object properties", () => { + const obj = { user: { name: "John" } }; + expect( + updateValueAtPath(obj, ["user", "address", "city"], "Boston"), + ).toEqual({ user: { name: "John", address: { city: "Boston" } } }); }); // Array update tests - test('updates an array item', () => { + test("updates an array item", () => { const arr = [1, 2, 3, 4]; - expect(updateValueAtPath(arr, ['2'], 5)).toEqual([1, 2, 5, 4]); + expect(updateValueAtPath(arr, ["2"], 5)).toEqual([1, 2, 5, 4]); }); - test('extends an array when index is out of bounds', () => { + test("extends an array when index is out of bounds", () => { const arr = [1, 2, 3]; - const result = updateValueAtPath(arr, ['5'], 'new') as JsonValue[]; - + const result = updateValueAtPath(arr, ["5"], "new") as JsonValue[]; + // Check overall array structure - expect(result).toEqual([1, 2, 3, null, null, 'new']); - + expect(result).toEqual([1, 2, 3, null, null, "new"]); + // Explicitly verify that indices 3 and 4 contain null, not undefined expect(result[3]).toBe(null); expect(result[4]).toBe(null); - + // Verify these aren't "holes" in the array (important distinction) expect(3 in result).toBe(true); expect(4 in result).toBe(true); - + // Verify the array has the correct length expect(result.length).toBe(6); - + // Verify the array doesn't have holes by checking every index exists expect(result.every((_, index: number) => index in result)).toBe(true); }); - test('updates a nested array item', () => { - const obj = { users: [{ name: 'John' }, { name: 'Jane' }] }; - expect(updateValueAtPath(obj, ['users', '1', 'name'], 'Janet')).toEqual( - { users: [{ name: 'John' }, { name: 'Janet' }] } - ); + test("updates a nested array item", () => { + const obj = { users: [{ name: "John" }, { name: "Jane" }] }; + expect(updateValueAtPath(obj, ["users", "1", "name"], "Janet")).toEqual({ + users: [{ name: "John" }, { name: "Janet" }], + }); }); // Error handling tests - test('returns original value when trying to update a primitive with a path', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(); - const result = updateValueAtPath('string', ['foo'], 'bar'); - expect(result).toBe('string'); + test("returns original value when trying to update a primitive with a path", () => { + const spy = jest.spyOn(console, "error").mockImplementation(); + const result = updateValueAtPath("string", ["foo"], "bar"); + expect(result).toBe("string"); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); - test('returns original array when index is invalid', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(); + test("returns original array when index is invalid", () => { + const spy = jest.spyOn(console, "error").mockImplementation(); const arr = [1, 2, 3]; - expect(updateValueAtPath(arr, ['invalid'], 4)).toEqual(arr); + expect(updateValueAtPath(arr, ["invalid"], 4)).toEqual(arr); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); - test('returns original array when index is negative', () => { - const spy = jest.spyOn(console, 'error').mockImplementation(); + test("returns original array when index is negative", () => { + const spy = jest.spyOn(console, "error").mockImplementation(); const arr = [1, 2, 3]; - expect(updateValueAtPath(arr, ['-1'], 4)).toEqual(arr); + expect(updateValueAtPath(arr, ["-1"], 4)).toEqual(arr); expect(spy).toHaveBeenCalled(); spy.mockRestore(); }); - test('handles sparse arrays correctly by filling holes with null', () => { + test("handles sparse arrays correctly by filling holes with null", () => { // Create a sparse array by deleting an element const sparseArr = [1, 2, 3]; delete sparseArr[1]; // Now sparseArr is [1, <1 empty item>, 3] - + // Update a value beyond the array length - const result = updateValueAtPath(sparseArr, ['5'], 'new') as JsonValue[]; - + const result = updateValueAtPath(sparseArr, ["5"], "new") as JsonValue[]; + // Check overall array structure - expect(result).toEqual([1, null, 3, null, null, 'new']); - + expect(result).toEqual([1, null, 3, null, null, "new"]); + // Explicitly verify that index 1 (the hole) contains null, not undefined expect(result[1]).toBe(null); - + // Verify this isn't a hole in the array expect(1 in result).toBe(true); - + // Verify all indices contain null (not undefined) expect(result[1]).not.toBe(undefined); expect(result[3]).toBe(null); @@ -121,49 +128,53 @@ describe('updateValueAtPath', () => { }); }); -describe('getValueAtPath', () => { - test('returns the original value when path is empty', () => { - const obj = { foo: 'bar' }; +describe("getValueAtPath", () => { + test("returns the original value when path is empty", () => { + const obj = { foo: "bar" }; expect(getValueAtPath(obj, [])).toBe(obj); }); - test('returns the value at a simple path', () => { - const obj = { name: 'John', age: 30 }; - expect(getValueAtPath(obj, ['name'])).toBe('John'); + test("returns the value at a simple path", () => { + const obj = { name: "John", age: 30 }; + expect(getValueAtPath(obj, ["name"])).toBe("John"); }); - test('returns the value at a nested path', () => { - const obj = { user: { name: 'John', address: { city: 'New York' } } }; - expect(getValueAtPath(obj, ['user', 'address', 'city'])).toBe('New York'); + test("returns the value at a nested path", () => { + const obj = { user: { name: "John", address: { city: "New York" } } }; + expect(getValueAtPath(obj, ["user", "address", "city"])).toBe("New York"); }); - test('returns default value when path does not exist', () => { - const obj = { user: { name: 'John' } }; - expect(getValueAtPath(obj, ['user', 'address', 'city'], 'Unknown')).toBe('Unknown'); + test("returns default value when path does not exist", () => { + const obj = { user: { name: "John" } }; + expect(getValueAtPath(obj, ["user", "address", "city"], "Unknown")).toBe( + "Unknown", + ); }); - test('returns default value when input is null/undefined', () => { - expect(getValueAtPath(null as any, ['foo'], 'default')).toBe('default'); - expect(getValueAtPath(undefined as any, ['foo'], 'default')).toBe('default'); + test("returns default value when input is null/undefined", () => { + expect(getValueAtPath(null as any, ["foo"], "default")).toBe("default"); + expect(getValueAtPath(undefined as any, ["foo"], "default")).toBe( + "default", + ); }); - test('handles array indices correctly', () => { - const arr = ['a', 'b', 'c']; - expect(getValueAtPath(arr, ['1'])).toBe('b'); + test("handles array indices correctly", () => { + const arr = ["a", "b", "c"]; + expect(getValueAtPath(arr, ["1"])).toBe("b"); }); - test('returns default value for out of bounds array indices', () => { - const arr = ['a', 'b', 'c']; - expect(getValueAtPath(arr, ['5'], 'default')).toBe('default'); + test("returns default value for out of bounds array indices", () => { + const arr = ["a", "b", "c"]; + expect(getValueAtPath(arr, ["5"], "default")).toBe("default"); }); - test('returns default value for invalid array indices', () => { - const arr = ['a', 'b', 'c']; - expect(getValueAtPath(arr, ['invalid'], 'default')).toBe('default'); + test("returns default value for invalid array indices", () => { + const arr = ["a", "b", "c"]; + expect(getValueAtPath(arr, ["invalid"], "default")).toBe("default"); }); - test('navigates through mixed object and array paths', () => { - const obj = { users: [{ name: 'John' }, { name: 'Jane' }] }; - expect(getValueAtPath(obj, ['users', '1', 'name'])).toBe('Jane'); + test("navigates through mixed object and array paths", () => { + const obj = { users: [{ name: "John" }, { name: "Jane" }] }; + expect(getValueAtPath(obj, ["users", "1", "name"])).toBe("Jane"); }); }); diff --git a/client/src/utils/__tests__/schemaUtils.test.ts b/client/src/utils/__tests__/schemaUtils.test.ts index beb267e..0a10af4 100644 --- a/client/src/utils/__tests__/schemaUtils.test.ts +++ b/client/src/utils/__tests__/schemaUtils.test.ts @@ -1,135 +1,141 @@ -import { generateDefaultValue, formatFieldLabel, validateValueAgainstSchema } from '../schemaUtils'; -import { JsonSchemaType } from '../../components/DynamicJsonForm'; +import { + generateDefaultValue, + formatFieldLabel, + validateValueAgainstSchema, +} from "../schemaUtils"; +import { JsonSchemaType } from "../../components/DynamicJsonForm"; -describe('generateDefaultValue', () => { - test('generates default string', () => { - expect(generateDefaultValue({ type: 'string' })).toBe(''); +describe("generateDefaultValue", () => { + test("generates default string", () => { + expect(generateDefaultValue({ type: "string" })).toBe(""); }); - test('generates default number', () => { - expect(generateDefaultValue({ type: 'number' })).toBe(0); + test("generates default number", () => { + expect(generateDefaultValue({ type: "number" })).toBe(0); }); - test('generates default integer', () => { - expect(generateDefaultValue({ type: 'integer' })).toBe(0); + test("generates default integer", () => { + expect(generateDefaultValue({ type: "integer" })).toBe(0); }); - test('generates default boolean', () => { - expect(generateDefaultValue({ type: 'boolean' })).toBe(false); + test("generates default boolean", () => { + expect(generateDefaultValue({ type: "boolean" })).toBe(false); }); - test('generates default array', () => { - expect(generateDefaultValue({ type: 'array' })).toEqual([]); + test("generates default array", () => { + expect(generateDefaultValue({ type: "array" })).toEqual([]); }); - test('generates default empty object', () => { - expect(generateDefaultValue({ type: 'object' })).toEqual({}); + test("generates default empty object", () => { + expect(generateDefaultValue({ type: "object" })).toEqual({}); }); - test('generates default null for unknown types', () => { - expect(generateDefaultValue({ type: 'unknown' as any })).toBe(null); + test("generates default null for unknown types", () => { + expect(generateDefaultValue({ type: "unknown" as any })).toBe(null); }); - test('generates object with properties', () => { + test("generates object with properties", () => { const schema: JsonSchemaType = { - type: 'object', + type: "object", properties: { - name: { type: 'string' }, - age: { type: 'number' }, - isActive: { type: 'boolean' } - } + name: { type: "string" }, + age: { type: "number" }, + isActive: { type: "boolean" }, + }, }; expect(generateDefaultValue(schema)).toEqual({ - name: '', + name: "", age: 0, - isActive: false + isActive: false, }); }); - test('handles nested objects', () => { + test("handles nested objects", () => { const schema: JsonSchemaType = { - type: 'object', + type: "object", properties: { user: { - type: 'object', + type: "object", properties: { - name: { type: 'string' }, + name: { type: "string" }, address: { - type: 'object', + type: "object", properties: { - city: { type: 'string' } - } - } - } - } - } + city: { type: "string" }, + }, + }, + }, + }, + }, }; expect(generateDefaultValue(schema)).toEqual({ user: { - name: '', + name: "", address: { - city: '' - } - } + city: "", + }, + }, }); }); }); -describe('formatFieldLabel', () => { - test('formats camelCase', () => { - expect(formatFieldLabel('firstName')).toBe('First Name'); +describe("formatFieldLabel", () => { + test("formats camelCase", () => { + expect(formatFieldLabel("firstName")).toBe("First Name"); }); - test('formats snake_case', () => { - expect(formatFieldLabel('first_name')).toBe('First name'); + test("formats snake_case", () => { + expect(formatFieldLabel("first_name")).toBe("First name"); }); - test('formats single word', () => { - expect(formatFieldLabel('name')).toBe('Name'); + test("formats single word", () => { + expect(formatFieldLabel("name")).toBe("Name"); }); - test('formats mixed case with underscores', () => { - expect(formatFieldLabel('user_firstName')).toBe('User first Name'); + test("formats mixed case with underscores", () => { + expect(formatFieldLabel("user_firstName")).toBe("User first Name"); }); - test('handles empty string', () => { - expect(formatFieldLabel('')).toBe(''); + test("handles empty string", () => { + expect(formatFieldLabel("")).toBe(""); }); }); -describe('validateValueAgainstSchema', () => { - test('validates string type', () => { - expect(validateValueAgainstSchema('test', { type: 'string' })).toBe(true); - expect(validateValueAgainstSchema(123, { type: 'string' })).toBe(false); +describe("validateValueAgainstSchema", () => { + test("validates string type", () => { + expect(validateValueAgainstSchema("test", { type: "string" })).toBe(true); + expect(validateValueAgainstSchema(123, { type: "string" })).toBe(false); }); - test('validates number type', () => { - expect(validateValueAgainstSchema(123, { type: 'number' })).toBe(true); - expect(validateValueAgainstSchema('test', { type: 'number' })).toBe(false); + test("validates number type", () => { + expect(validateValueAgainstSchema(123, { type: "number" })).toBe(true); + expect(validateValueAgainstSchema("test", { type: "number" })).toBe(false); }); - test('validates integer type', () => { - expect(validateValueAgainstSchema(123, { type: 'integer' })).toBe(true); - expect(validateValueAgainstSchema('test', { type: 'integer' })).toBe(false); + test("validates integer type", () => { + expect(validateValueAgainstSchema(123, { type: "integer" })).toBe(true); + expect(validateValueAgainstSchema("test", { type: "integer" })).toBe(false); }); - test('validates boolean type', () => { - expect(validateValueAgainstSchema(true, { type: 'boolean' })).toBe(true); - expect(validateValueAgainstSchema('test', { type: 'boolean' })).toBe(false); + test("validates boolean type", () => { + expect(validateValueAgainstSchema(true, { type: "boolean" })).toBe(true); + expect(validateValueAgainstSchema("test", { type: "boolean" })).toBe(false); }); - test('validates array type', () => { - expect(validateValueAgainstSchema([], { type: 'array' })).toBe(true); - expect(validateValueAgainstSchema({}, { type: 'array' })).toBe(false); + test("validates array type", () => { + expect(validateValueAgainstSchema([], { type: "array" })).toBe(true); + expect(validateValueAgainstSchema({}, { type: "array" })).toBe(false); }); - test('validates object type', () => { - expect(validateValueAgainstSchema({}, { type: 'object' })).toBe(true); - expect(validateValueAgainstSchema([], { type: 'object' })).toBe(false); - expect(validateValueAgainstSchema('test', { type: 'object' })).toBe(false); + test("validates object type", () => { + expect(validateValueAgainstSchema({}, { type: "object" })).toBe(true); + expect(validateValueAgainstSchema([], { type: "object" })).toBe(false); + expect(validateValueAgainstSchema("test", { type: "object" })).toBe(false); }); - test('returns true for unknown types', () => { - expect(validateValueAgainstSchema('anything', { type: 'unknown' as any })).toBe(true); + test("returns true for unknown types", () => { + expect( + validateValueAgainstSchema("anything", { type: "unknown" as any }), + ).toBe(true); }); }); diff --git a/client/src/utils/jsonPathUtils.ts b/client/src/utils/jsonPathUtils.ts index ef5a18e..25b1e0a 100644 --- a/client/src/utils/jsonPathUtils.ts +++ b/client/src/utils/jsonPathUtils.ts @@ -10,9 +10,9 @@ export type JsonObject = { [key: string]: JsonValue }; * @returns A new JSON value with the updated path */ export function updateValueAtPath( - obj: JsonValue, - path: string[], - value: JsonValue + obj: JsonValue, + path: string[], + value: JsonValue, ): JsonValue { if (path.length === 0) return value; @@ -24,16 +24,16 @@ export function updateValueAtPath( // Handle arrays if (Array.isArray(obj)) { return updateArray(obj, path, value); - } + } // Handle objects else if (typeof obj === "object" && obj !== null) { return updateObject(obj as JsonObject, path, value); - } + } // Cannot update primitives else { console.error( `Cannot update path ${path.join(".")} in non-object/array value:`, - obj + obj, ); return obj; } @@ -43,9 +43,9 @@ export function updateValueAtPath( * Updates an array at a specific path */ function updateArray( - array: JsonValue[], - path: string[], - value: JsonValue + array: JsonValue[], + path: string[], + value: JsonValue, ): JsonValue[] { const [index, ...restPath] = path; const arrayIndex = Number(index); @@ -82,7 +82,11 @@ function updateArray( if (restPath.length === 0) { newArray[arrayIndex] = value; } else { - newArray[arrayIndex] = updateValueAtPath(newArray[arrayIndex], restPath, value); + newArray[arrayIndex] = updateValueAtPath( + newArray[arrayIndex], + restPath, + value, + ); } return newArray; } @@ -91,9 +95,9 @@ function updateArray( * Updates an object at a specific path */ function updateObject( - obj: JsonObject, - path: string[], - value: JsonValue + obj: JsonObject, + path: string[], + value: JsonValue, ): JsonObject { const [key, ...restPath] = path; @@ -125,18 +129,18 @@ function updateObject( * @returns The value at the path, or defaultValue if not found */ export function getValueAtPath( - obj: JsonValue, - path: string[], - defaultValue: JsonValue = null + obj: JsonValue, + path: string[], + defaultValue: JsonValue = null, ): JsonValue { if (path.length === 0) return obj; - + const [first, ...rest] = path; - + if (obj === null || obj === undefined) { return defaultValue; } - + if (Array.isArray(obj)) { const index = Number(first); if (isNaN(index) || index < 0 || index >= obj.length) { @@ -144,13 +148,13 @@ export function getValueAtPath( } return getValueAtPath(obj[index], rest, defaultValue); } - + if (typeof obj === "object" && obj !== null) { if (!(first in obj)) { return defaultValue; } return getValueAtPath((obj as JsonObject)[first], rest, defaultValue); } - + return defaultValue; } diff --git a/client/src/utils/schemaUtils.ts b/client/src/utils/schemaUtils.ts index feee4de..b3c8e48 100644 --- a/client/src/utils/schemaUtils.ts +++ b/client/src/utils/schemaUtils.ts @@ -50,8 +50,8 @@ export function formatFieldLabel(key: string): string { * @returns True if valid, false otherwise */ export function validateValueAgainstSchema( - value: JsonValue, - schema: JsonSchemaType + value: JsonValue, + schema: JsonSchemaType, ): boolean { // Basic type validation switch (schema.type) { @@ -65,7 +65,9 @@ export function validateValueAgainstSchema( case "array": return Array.isArray(value); case "object": - return typeof value === "object" && value !== null && !Array.isArray(value); + return ( + typeof value === "object" && value !== null && !Array.isArray(value) + ); default: return true; }