Skip to content

Commit

Permalink
fix: #401 original data can be mutated by the TransformModal previews
Browse files Browse the repository at this point in the history
  • Loading branch information
josdejong committed Feb 5, 2024
1 parent ac31a79 commit 337f812
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 2 deletions.
8 changes: 6 additions & 2 deletions src/lib/components/modals/TransformModal.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
} from '$lib/types.js'
import { onEscape } from '$lib/actions/onEscape.js'
import type { Context } from 'svelte-simple-modal'
import { readonlyProxy } from '$lib/utils/readonlyProxy.js'
const debug = createDebug('jsoneditor:TransformModal')
Expand Down Expand Up @@ -58,7 +59,7 @@
export let onTransform: OnPatch
let selectedJson: unknown | undefined
$: selectedJson = getIn(json, rootPath)
$: selectedJson = readonlyProxy(getIn(json, rootPath))
let selectedContent: Content
$: selectedContent = selectedJson ? { json: selectedJson } : { text: '' }
Expand All @@ -76,7 +77,10 @@
let query =
queryLanguageId === state.queryLanguageId && state.query
? state.query
: getSelectedQueryLanguage(queryLanguageId).createQuery(selectedJson, state.queryOptions || {})
: getSelectedQueryLanguage(queryLanguageId).createQuery(
selectedJson,
state.queryOptions || {}
)
let isManual = state.isManual || false
let previewError: string | undefined = undefined
Expand Down
101 changes: 101 additions & 0 deletions src/lib/utils/readonlyProxy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { describe, expect, test } from 'vitest'
import { readonlyProxy } from '$lib/utils/readonlyProxy.js'

describe('readonlyProxy', () => {
const objOriginal = createNestedObject()
const obj = createNestedObject()
const proxy = readonlyProxy(obj)

test('should read a nested property', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(proxy[0].data.value).toEqual(42)
})

test('should allow invoking immutable methods', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(proxy.map((item) => item.id)).toEqual([1, 2])

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(proxy.find((item) => item.id === 1)).toEqual(obj[0])

const log: unknown[] = []
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
proxy.forEach((value, index) => log.push({ value, index }))
expect(log).toEqual([
{ value: obj[0], index: 0 },
{ value: obj[1], index: 1 }
])
})

test('should get all object keys', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(Object.keys(proxy[0])).toEqual(['id', 'data'])
})

test('should not allow setting a nested property', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => (proxy[0].data.value = 'foo')).toThrow(
new TypeError("'set' on proxy: trap returned falsish for property 'value'")
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => (proxy[0].data = 'foo')).toThrow(
new TypeError("'set' on proxy: trap returned falsish for property 'data'")
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => (proxy[0] = null)).toThrow(
new TypeError("'set' on proxy: trap returned falsish for property '0'")
)

expect(obj).toEqual(objOriginal)
})

test('should not allow deleting a property', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => delete proxy[0].data.value).toThrow(
new TypeError("'deleteProperty' on proxy: trap returned falsish for property 'value'")
)

// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => delete proxy[0]).toThrow(
new TypeError("'deleteProperty' on proxy: trap returned falsish for property '0'")
)

expect(obj).toEqual(objOriginal)
})

test('should not allow mutable array methods', () => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
expect(() => proxy.splice(2)).toThrow(
new TypeError("'set' on proxy: trap returned falsish for property 'length'")
)

expect(obj).toEqual(objOriginal)
})

test('should not proxy primitives', () => {
expect(readonlyProxy(true)).toEqual(true)
expect(readonlyProxy(42)).toEqual(42)
expect(readonlyProxy('foo')).toEqual('foo')
expect(readonlyProxy(undefined)).toEqual(undefined)
})
})

function createNestedObject() {
return [
{ id: 1, data: { value: 42 } },
{ id: 2, data: { value: 48 } }
]
}
32 changes: 32 additions & 0 deletions src/lib/utils/readonlyProxy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Create a readonly proxy around an object or array.
*
* Will throw an error when trying to mutate the object or array
*
* Inspired by: https://github.com/kourge/readonly-proxy/
*/
export function readonlyProxy(target: unknown): unknown {
if (!isObject(target)) {
return target
}

return new Proxy(target, {
get(target, property, receiver) {
const value = Reflect.get(target, property, receiver)

return readonlyProxy(value)
},

set() {
return false
},

deleteProperty() {
return false
}
})
}

function isObject(value: unknown): value is object {
return typeof value === 'object' && value !== null
}

0 comments on commit 337f812

Please sign in to comment.