Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(url-state-provider): new way of encoding and decoding query string #712

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/light-vans-suffer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@cloudoperators/juno-url-state-provider": minor
---

Exports new `encodeV2` and `decodeV2` utilities to encode js object to url query parameters in a standard way as well as to decode query parameters back to javascript object.
53 changes: 52 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 4 additions & 3 deletions packages/url-state-provider/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,16 @@
"lz-string": "^1.4.4",
"typescript": "^5.5.4",
"vite": "^5.4.8",
"vitest": "^2.1.1",
"vite-plugin-dts": "^4.0.3"
"vite-plugin-dts": "^4.0.3",
"vitest": "^2.1.1"
},
"babel": {
"presets": [
"@babel/preset-env"
]
},
"dependencies": {
"juri": "^1.0.3"
"juri": "^1.0.3",
"query-string": "^9.1.1"
}
}
2 changes: 2 additions & 0 deletions packages/url-state-provider/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -324,3 +324,5 @@ export {
decode,
encode,
}

export { encode as encodeV2, decode as decodeV2 } from "./v2"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question:
I understand why you did this in the context of a minor update. But why not make it a new major version instead and have the encode and decode exports be the V2 versions?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason I did not created a major version just yet is because the new solution would run alongside the existing one so technically this is not a break change at the moment but after all apps are fully converted to the new URL structure then we can remove the old solution and release a major version as a breaking change and rename encodeV2, decodeV2 exports.
Moreover if we release a new major version we’ll have to make sure that any important change in the previous version(e.g vulnerabilities fix) will have to be separately released to the previous version as patch.

13 changes: 13 additions & 0 deletions packages/url-state-provider/src/v2/decode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import decode from "./decode"
import testCases from "./testCases"

describe("decode", () => {
it.each(testCases)("[$id] should successfully decode given input", ({ encoded: input, decoded: output }) => {
expect(decode(input)).toMatchObject(output)
})
})
16 changes: 16 additions & 0 deletions packages/url-state-provider/src/v2/decode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import queryString from "query-string"
import { DecodedObject } from "./types"

const decode = (string: string): DecodedObject =>
queryString.parse(string, {
arrayFormat: "comma",
parseBooleans: true,
parseNumbers: true,
})

export default decode
28 changes: 28 additions & 0 deletions packages/url-state-provider/src/v2/encode.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import encode from "./encode"
import testCases from "./testCases"

describe("encode", () => {
it.each(testCases)("[$id] should successfully encode given input", ({ decoded: input, encoded: output }) => {
expect(encode(input)).toBe(output)
})

it.each`
description | input
${"not an object"} | ${null}
${"not an object"} | ${undefined}
${"not an object"} | ${true}
${"not an object"} | ${false}
${"not an object"} | ${1}
${"not an object"} | ${"string"}
${"not an object"} | ${/regexp/}
${"an array"} | ${[1, 2, 3]}
${"a valid object but the value of each key does not conform to required type"} | ${{ a: "b", c: { d: "e" } }}
`("should throw an error when input is $description", ({ input }) => {
expect(() => encode(input)).toThrowError("Invalid object to encode")
})
})
49 changes: 49 additions & 0 deletions packages/url-state-provider/src/v2/encode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import queryString from "query-string"
import { ObjectToEncode, Primitive } from "./types"

const isPrimitive = (value: Primitive | Primitive[]) => {
return (
typeof value === "string" ||
typeof value === "number" ||
typeof value === "boolean" ||
value === null ||
value === undefined ||
value instanceof RegExp
)
}

const validateObjectToEncode = (object: ObjectToEncode) => {
if (object === null || typeof object !== "object" || object instanceof RegExp || Array.isArray(object)) {
return false
}

for (const key in object) {
const value = object[key]
if (!isPrimitive(value) && !Array.isArray(value)) {
return false
}
if (Array.isArray(value) && !value.every(isPrimitive)) {
return false
}
}

return true
}

const encode = (object: ObjectToEncode) => {
if (!validateObjectToEncode(object)) {
throw new TypeError(`Invalid object to encode`)
}

return queryString.stringify(object, {
arrayFormat: "comma",
sort: false,
})
}

export default encode
9 changes: 9 additions & 0 deletions packages/url-state-provider/src/v2/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import encode from "./encode"
import decode from "./decode"

export { encode, decode }
35 changes: 35 additions & 0 deletions packages/url-state-provider/src/v2/testCases.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

import { DecodedObject } from "./types"

type TestCase = {
id: number
encoded: string
decoded: DecodedObject
}

const testCases: TestCase[] = [
{
encoded: "aString=someId1&aNumber=2&aBoolean=true&anArray=1,true,something",
decoded: { aString: "someId1", aNumber: 2, aBoolean: true, anArray: [1, true, "something"] },
},
{
encoded: "aStringWithSpaces=some%20id%20and",
decoded: { aStringWithSpaces: "some id and" },
},
{
encoded: "regularExpression=%2Fw3schools%2Fi",
decoded: { regularExpression: /w3schools/i },
},
{
encoded: "aStringWithSpecialCharacter=A%26B",
decoded: { aStringWithSpecialCharacter: "A&B" },
},
]
//assign 'id' to each test case to better identify which one is failing
.map((item, idx) => ({ id: idx + 1, ...item }))

export default testCases
13 changes: 13 additions & 0 deletions packages/url-state-provider/src/v2/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Juno contributors
* SPDX-License-Identifier: Apache-2.0
*/

export type Primitive = string | number | boolean | null | undefined | RegExp

type LimitedNestedObject = {
[key: string]: Primitive | Primitive[]
}

export type ObjectToEncode = LimitedNestedObject
export type DecodedObject = LimitedNestedObject
Loading