1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-03 04:35:31 +00:00

Add array support to query parameters validation handlers

This commit is contained in:
Evren Ozkan 2025-03-31 09:55:57 +00:00
parent 5fc1ef2d1b
commit 332ec39ef6
14 changed files with 461 additions and 129 deletions
backend
src/baserow/contrib/integrations/local_baserow
tests/baserow/contrib/integrations/local_baserow/service_types
changelog/entries/unreleased/feature
e2e-tests/tests/builder
web-frontend

View file

@ -172,7 +172,7 @@ class LocalBaserowTableServiceFilterableMixin:
if service_filter.value_is_formula:
try:
resolved_value = str(
resolved_value = ensure_string(
resolve_formula(
service_filter.value,
formula_runtime_function_registry,

View file

@ -11,6 +11,7 @@ from baserow.contrib.database.api.rows.serializers import RowSerializer
from baserow.contrib.database.rows.handler import RowHandler
from baserow.contrib.database.table.handler import TableHandler
from baserow.contrib.database.views.models import SORT_ORDER_ASC, SORT_ORDER_DESC
from baserow.contrib.database.views.view_filters import MultipleSelectHasViewFilterType
from baserow.contrib.integrations.local_baserow.models import LocalBaserowListRows
from baserow.contrib.integrations.local_baserow.service_types import (
LocalBaserowListRowsUserServiceType,
@ -1162,3 +1163,63 @@ def test_extract_properties(path, expected):
result = service_type.extract_properties(path)
assert result == expected
@pytest.mark.django_db(transaction=True)
def test_search_on_multiple_select_with_list(data_fixture):
user = data_fixture.create_user()
workspace = data_fixture.create_workspace(user=user)
database = data_fixture.create_database_application(workspace=workspace)
page = data_fixture.create_builder_page(
user=user, path="/page/", query_params=[{"name": "foobar", "type": "numeric"}]
)
service_type = service_type_registry.get("local_baserow_list_rows")
integration = data_fixture.create_local_baserow_integration(
application=page.builder, user=user
)
table = data_fixture.create_database_table(database=database)
view = data_fixture.create_grid_view(user, table=table)
option_field = data_fixture.create_multiple_select_field(
table=table, name="option_field", order=1, primary=True
)
options = [
data_fixture.create_select_option(
field=option_field, value="A", color="AAA", order=0
),
data_fixture.create_select_option(
field=option_field, value="B", color="BBB", order=0
),
data_fixture.create_select_option(
field=option_field, value="C", color="CCC", order=0
),
]
table_rows = data_fixture.create_rows_in_table(
table=table,
rows=[[(options[0].id,)], [(options[1].id,)], [(options[1].id,)]],
fields=[option_field],
)
service = data_fixture.create_local_baserow_list_rows_service(
integration=integration,
view=view,
table=view.table,
search_query="",
filter_type="OR",
)
data_fixture.create_local_baserow_table_service_filter(
service=service,
field=option_field,
value="get('foobar')",
order=0,
type=MultipleSelectHasViewFilterType.type,
value_is_formula=True,
)
dispatch_context = FakeDispatchContext(context={"foobar": [options[0].id]})
dispatch_data = service_type.dispatch_data(
service, {"table": table}, dispatch_context
)
assert len(dispatch_data["results"]) == 1
assert dispatch_data["results"][0]._primary_field_id == options[0].field_id

View file

@ -0,0 +1,8 @@
{
"type": "feature",
"message": "Add support for list type values for query parameters.",
"domain": "builder",
"issue_number": 3433,
"bullet_points": [],
"created_at": "2025-02-20"
}

View file

@ -1,11 +1,11 @@
import {expect, test} from "../baserowTest";
import { expect, test } from "../baserowTest";
test.describe("Builder page test suite", () => {
test.beforeEach(async ({builderPagePage}) => {
test.beforeEach(async ({ builderPagePage }) => {
await builderPagePage.goto();
});
test("Can create a page", async ({page}) => {
test("Can create a page", async ({ page }) => {
await page.getByText("New page").click();
await page.getByText("Create page").waitFor();
await page
@ -25,12 +25,12 @@ test.describe("Builder page test suite", () => {
).toBeVisible();
});
test("Can open page settings", async ({page}) => {
test("Can open page settings", async ({ page }) => {
await page.getByText("Page settings").click();
await expect(page.locator(".box__title").getByText("Page")).toBeVisible();
});
test("Can change page settings", async ({page}) => {
test("Can change page settings", async ({ page }) => {
await page.getByText("Page settings").click();
await page
@ -57,9 +57,9 @@ test.describe("Builder page test suite", () => {
).toBeVisible();
});
test("Can create an element from empty page", async ({page}) => {
test("Can create an element from empty page", async ({ page }) => {
await page.getByText("Click to create an element").click();
await page.getByText("Heading", {exact: true}).click();
await page.getByText("Heading", { exact: true }).click();
await expect(
page.locator(".modal__box").getByText("Add new element")
@ -69,13 +69,13 @@ test.describe("Builder page test suite", () => {
).toBeVisible();
});
test("Can create an element from element menu", async ({page}) => {
test("Can create an element from element menu", async ({ page }) => {
await page.locator(".header").getByText("Elements").click();
await page
.locator(".elements-context")
.getByText("Element", {exact: true})
.getByText("Element", { exact: true })
.click();
await page.getByText("Heading", {exact: true}).click();
await page.getByText("Heading", { exact: true }).click();
await expect(
page.locator(".modal__box").getByText("Add new element")
@ -85,8 +85,7 @@ test.describe("Builder page test suite", () => {
).toBeVisible();
});
test("Can add query parameter to page setting", async ({page}) => {
test("Can add query parameter to page setting", async ({ page }) => {
await page.getByText("Page settings").click();
await page
@ -94,13 +93,33 @@ test.describe("Builder page test suite", () => {
.getByPlaceholder("Enter a name...")
.fill("New page name");
await page.getByRole('button', {name: 'Add query string parameter'}).click();
await page
.getByRole("button", { name: "Add query string parameter" })
.click();
await page
.getByRole("button", { name: "Add another query string parameter" })
.click();
await page
.locator(".page-settings-query-params .form-input__wrapper")
.getByRole('textbox')
.getByRole("textbox")
.nth(1)
.fill("my_param");
await page
.locator(".page-settings-query-params .form-input__wrapper")
.getByRole("textbox")
.nth(0)
.fill("my_param2");
await page.locator("a").filter({ hasText: "Text" }).first().click();
await page
.locator("form")
.getByRole("list")
.locator("a")
.filter({ hasText: "Numeric" })
.click();
await page.locator(".button").getByText("Save").click();
await expect(
page.getByText("The page settings have been updated.")
@ -108,20 +127,33 @@ test.describe("Builder page test suite", () => {
await page.getByTitle("Close").click();
await expect(page.locator(".box__title").getByText("Page")).toBeHidden();
await page.getByText('Click to create an element').click();
await page.locator('.add-element-card__element-type-icon-link').click();
await page.getByLabel('my_param=').fill("foo")
await page.getByRole('complementary').getByRole('textbox').click();
await page.getByRole('complementary').getByRole('textbox').locator('div').first().fill('linkim');
await page.locator('a').filter({hasText: 'Make a choice'}).click();
await page.locator('a').filter({hasText: '?my_param=*'}).click();
await page.getByText("Click to create an element").click();
await page.locator(".add-element-card__element-type-icon-link").click();
await page.getByLabel("my_param=").fill("foo");
await page.getByLabel("my_param2=").fill("15");
await page.getByRole("complementary").getByRole("textbox").click();
await page
.getByRole("complementary")
.getByRole("textbox")
.locator("div")
.first()
.fill("linkim");
await page.locator("a").filter({ hasText: "Make a choice" }).click();
await page
.locator("a")
.filter({ hasText: "New page name /default/page?" })
.click();
// click empty place to close tooltip from prev. step
await page.click('body')
await page.getByRole('textbox').nth(2).click();
await page.getByText('my_param', { exact: true }).first().click();
await page.click('body')
await expect(page.getByRole('link', {name: 'linkim'})).toHaveAttribute(
'href', /\?my_param=foo/);
await page.click("body");
await page.getByRole("textbox").nth(4).click();
await page.getByText("my_param", { exact: true }).first().click();
await page.locator('[id="__layout"]').getByRole("textbox").nth(3).click();
await page.getByText("my_param2", { exact: true }).first().click();
await page.click("body");
await expect(page.getByRole("link", { name: "linkim" })).toHaveAttribute(
"href",
/\?my_param2=15&my_param=foo/
);
debugger;
});
});

View file

@ -61,6 +61,7 @@ import {
ensureString,
ensureStringOrInteger,
ensureArray,
ensurePositiveInteger,
} from '@baserow/modules/core/utils/validator'
import { CHOICE_OPTION_TYPES } from '@baserow/modules/builder/enums'
@ -94,20 +95,36 @@ export default {
return ensureString(this.resolveFormula(this.element.placeholder))
},
defaultValueResolved() {
let converter = ensureString
if (
this.optionsResolved.find(
({ value }) => value !== undefined && value !== null // We skip null values
) &&
Number.isInteger(this.optionsResolved[0].value)
) {
converter = (v) => ensurePositiveInteger(v, { allowNull: true })
}
if (this.element.multiple) {
const existingValues = this.optionsResolved.map(({ value }) => value)
return ensureArray(this.resolveFormula(this.element.default_value))
.map(ensureStringOrInteger)
.filter((value) => existingValues.includes(value))
try {
const existingValues = this.optionsResolved.map(({ value }) => value)
return ensureArray(this.resolveFormula(this.element.default_value))
.map(converter)
.filter((value) => existingValues.includes(value))
} catch {
return []
}
} else {
// Always return a string if we have a default value, otherwise
// set the value to null as single select fields will only skip
// field preparation if the value is null.
const resolvedSingleValue = ensureStringOrInteger(
this.resolveFormula(this.element.default_value)
)
return resolvedSingleValue === '' ? null : resolvedSingleValue
try {
// Always return a string if we have a default value, otherwise
// set the value to null as single select fields will only skip
// field preparation if the value is null.
const resolvedSingleValue = converter(
this.resolveFormula(this.element.default_value)
)
return resolvedSingleValue === '' ? null : resolvedSingleValue
} catch {
return null
}
}
},
canHaveOptions() {

View file

@ -35,21 +35,22 @@
class="preview-navigation-bar__query-separator"
>
{{ index === 0 ? '?' : '&' }}
</span>
<PreviewNavigationBarQueryParam
:key="`param-${queryParam.key}`"
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
:validation-fn="queryParam.validationFn"
:default-value="pageParameters[queryParam.name]"
:name="queryParam.name"
@change="
actionSetParameter({
page,
name: queryParam.name,
value: $event,
})
"
/>
<label :for="queryParam.name">{{ queryParam.name }}=</label>
<PreviewNavigationBarInput
:id="queryParam.name"
:key="`param-${queryParam.key}`"
:class="`preview-navigation-bar__query-parameter-input--${queryParam.type}`"
:validation-fn="queryParam.validationFn"
:default-value="pageParameters[queryParam.name]"
@change="
actionSetParameter({
page,
name: queryParam.name,
value: $event,
})
"
/></span>
</template>
</div>
<div />
@ -61,15 +62,13 @@ import { splitPath } from '@baserow/modules/builder/utils/path'
import PreviewNavigationBarInput from '@baserow/modules/builder/components/page/PreviewNavigationBarInput'
import UserSelector from '@baserow/modules/builder/components/page/UserSelector'
import { mapActions } from 'vuex'
import { PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS } from '@baserow/modules/builder/enums'
import PreviewNavigationBarQueryParam from '@baserow/modules/builder/components/page/PreviewNavigationBarQueryParam.vue'
import {
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
} from '@baserow/modules/builder/enums'
export default {
components: {
PreviewNavigationBarInput,
UserSelector,
PreviewNavigationBarQueryParam,
},
components: { PreviewNavigationBarInput, UserSelector },
props: {
page: {
type: Object,
@ -84,7 +83,7 @@ export default {
return this.page.query_params.map((queryParam, idx) => ({
...queryParam,
key: `query-param-${queryParam.name}-${idx}`,
validationFn: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS[queryParam.type],
validationFn: QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[queryParam.type],
}))
},
splitPath() {

View file

@ -9,10 +9,12 @@
</template>
<script>
import _ from 'lodash'
export default {
props: {
defaultValue: {
type: [String, Number],
type: [String, Number, Array],
required: false,
default: '',
},
@ -45,7 +47,9 @@ export default {
},
watch: {
defaultValue(newValue) {
this.inputValue = newValue
if (!_.isEqual(this.inputValue, newValue)) {
this.inputValue = newValue
}
},
},
}

View file

@ -1,48 +0,0 @@
<template>
<div class="preview-navigation-bar-param">
<label :for="name" class="preview-navigation-bar-param__label">
{{ name }}=</label
><input
:id="name"
v-model="inputValue"
class="preview-navigation-bar-input"
/>
</div>
</template>
<script>
export default {
props: {
defaultValue: {
type: [String, Number],
required: false,
default: '',
},
name: {
type: String,
required: true,
},
},
data() {
return {
value: this.defaultValue,
}
},
computed: {
inputValue: {
get() {
return this.value
},
set(inputValue) {
this.value = inputValue
this.$emit('change', this.value)
},
},
},
watch: {
defaultValue(newValue) {
this.value = newValue
},
},
}
</script>

View file

@ -6,7 +6,7 @@ import { defaultValueForParameterType } from '@baserow/modules/builder/utils/par
import { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
import {
PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS,
QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS,
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
} from '@baserow/modules/builder/enums'
import { extractSubSchema } from '@baserow/modules/core/utils/schema'
@ -311,7 +311,7 @@ export class PageParameterDataProviderType extends DataProviderType {
await Promise.all(
pageParams.map(({ name, type }) => {
const validators = queryParamNames.includes(name)
? QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS
? QUERY_PARAM_TYPE_HANDLER_FUNCTIONS
: PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS
let value
try {

View file

@ -1497,17 +1497,45 @@ export class ChoiceElementType extends FormElementType {
return element.multiple ? 'array' : 'string'
}
/**
* Returns the first option for this element.
* @param {Object} element the element we want the option for.
* @returns the first option value.
*/
_getFirstOptionValue(element) {
switch (element.option_type) {
case CHOICE_OPTION_TYPES.MANUAL:
return element.options.find(({ value }) => value)
case CHOICE_OPTION_TYPES.FORMULAS: {
const formulaValues = ensureArray(
this.resolveFormula(this.element.formula_value)
)
if (formulaValues.length === 0) {
return null
}
return ensureStringOrInteger(formulaValues[0])
}
default:
return []
}
}
getInitialFormDataValue(element, applicationContext) {
try {
const firstValue = this._getFirstOptionValue(element)
let converter = ensureStringOrInteger
if (firstValue ?? Number.isInteger(firstValue)) {
converter = (v) => ensurePositiveInteger(v, { allowNull: true })
}
if (element.multiple) {
return ensureArray(
this.resolveFormula(element.default_value, {
element,
...applicationContext,
})
).map(ensureStringOrInteger)
).map(converter)
} else {
return ensureStringOrInteger(
return converter(
this.resolveFormula(element.default_value, {
element,
...applicationContext,

View file

@ -2,6 +2,8 @@ import {
ensureString,
ensureNonEmptyString,
ensurePositiveInteger,
ensureArray,
ensureNumeric,
} from '@baserow/modules/core/utils/validator'
import {
DataSourceDataProviderType,
@ -23,9 +25,20 @@ export const PAGE_PARAM_TYPE_VALIDATION_FUNCTIONS = {
numeric: ensurePositiveInteger,
text: ensureNonEmptyString,
}
export const QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS = {
numeric: (n) => ensurePositiveInteger(n, { allowNull: true }),
text: ensureString,
export const QUERY_PARAM_TYPE_HANDLER_FUNCTIONS = {
numeric: (input) => {
const value = ensureArray(input, { allowEmpty: true }).map((i) =>
ensureNumeric(i, { allowNull: true })
)
return value.length === 0 ? null : value.length === 1 ? value[0] : value
},
text: (input) => {
const value = ensureArray(input, {
allowEmpty: true,
}).map((i) => ensureString(i, { allowEmpty: true }))
return value.length === 0 ? null : value.length === 1 ? value[0] : value
},
}
export const ALLOWED_LINK_PROTOCOLS = [

View file

@ -27,7 +27,7 @@ import {
userSourceCookieTokenName,
setToken,
} from '@baserow/modules/core/utils/auth'
import { QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS } from '@baserow/modules/builder/enums'
import { QUERY_PARAM_TYPE_HANDLER_FUNCTIONS } from '@baserow/modules/builder/enums'
const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
await store.dispatch('userSourceUser/logoff', {
@ -41,6 +41,7 @@ const logOffAndReturnToLogin = async ({ builder, store, redirect }) => {
}
export default {
name: 'PublicPage',
components: { PageContent, Toasts },
provide() {
return {
@ -350,12 +351,11 @@ export default {
// update the page's query parameters in the store
Promise.all(
this.currentPage.query_params.map(({ name, type }) => {
if (!newQuery[name]) return null
let value
try {
value = QUERY_PARAM_TYPE_VALIDATION_FUNCTIONS[type](
newQuery[name]
)
if (newQuery[name]) {
value = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS[type](newQuery[name])
}
} catch {
// Skip setting the parameter if the user-provided value
// doesn't pass our parameter `type` validation.

View file

@ -4,10 +4,35 @@ import { trueValues, falseValues } from '@baserow/modules/core/utils/constants'
import { DATE_FORMATS } from '@baserow/modules/builder/enums'
import moment from '@baserow/modules/core/moment'
/**
* Ensures that the value is a Numeral or can be converted to a numeric value.
* @param {number|string} value - The value to ensure as a number.
* @param allowNull {boolean} - Whether to allow null or empty values.
* @returns {number|null} The value as a Number if conversion is successful.
* @throws {Error} If the value is not a valid number or convertible to an number.
*/
export const ensureNumeric = (value, { allowNull = false } = {}) => {
if (allowNull && (value === null || value === '')) {
return null
}
if (Number.isFinite(value)) {
return value
}
if (typeof value === 'string' || value instanceof String) {
if (/^([-+])?(\d+(\.\d+)?)$/.test(value)) {
return Number(value)
}
}
throw new Error(
`Value '${value}' is not a valid number or convertible to a number.`
)
}
/**
* Ensures that the value is an integer or can be converted to an integer.
* @param {number|string} value - The value to ensure as an integer.
* @returns {number} The value as an integer if conversion is successful.
* @param allowNull {boolean} - Whether to allow null or empty values.
* @returns {number|null} The value as an integer if conversion is successful, null otherwise.
* @throws {Error} If the value is not a valid integer or convertible to an integer.
*/
export const ensureInteger = (value) => {
@ -34,7 +59,7 @@ export const ensureInteger = (value) => {
* @throws {Error} If the value is not a valid non-negative integer
*/
export const ensurePositiveInteger = (value, { allowNull = false } = {}) => {
if (allowNull && value === null) {
if (allowNull && (value === null || value === '')) {
return null
}
const validInteger = ensureInteger(value)

View file

@ -3,11 +3,15 @@ import {
ensureDateTime,
ensureInteger,
ensureString,
ensureNumeric,
ensureStringOrInteger,
ensurePositiveInteger,
} from '@baserow/modules/core/utils/validator'
import { expect } from '@jest/globals'
import { DATE_FORMATS } from '@baserow/modules/builder/enums'
import {
DATE_FORMATS,
QUERY_PARAM_TYPE_HANDLER_FUNCTIONS,
} from '@baserow/modules/builder/enums'
describe('ensureInteger', () => {
it('should return the value as an integer if it is already an integer', () => {
@ -29,6 +33,130 @@ describe('ensureInteger', () => {
})
})
describe('ensurePositiveInteger', () => {
// Valid positive integers
test('returns the same value for positive integers', () => {
expect(ensurePositiveInteger(5)).toBe(5)
expect(ensurePositiveInteger(0)).toBe(0)
expect(ensurePositiveInteger(1000)).toBe(1000)
})
// String conversions
test('converts string representations of positive integers', () => {
expect(ensurePositiveInteger('42')).toBe(42)
expect(ensurePositiveInteger('0')).toBe(0)
expect(ensurePositiveInteger('+123')).toBe(123)
})
// Null handling
test('returns null when value is null and allowNull is true', () => {
expect(ensurePositiveInteger(null, { allowNull: true })).toBeNull()
})
test('returns null when value is empty string and allowNull is true', () => {
expect(ensurePositiveInteger('', { allowNull: true })).toBeNull()
})
// Error cases
test('throws error for negative integers', () => {
expect(() => ensurePositiveInteger(-5)).toThrow(
'Value is not a positive integer.'
)
expect(() => ensurePositiveInteger('-10')).toThrow(
'Value is not a positive integer.'
)
})
test('throws error for non-integer values', () => {
expect(() => ensurePositiveInteger('abc')).toThrow('not a valid integer')
expect(() => ensurePositiveInteger({})).toThrow('not a valid integer')
expect(() => ensurePositiveInteger([])).toThrow('not a valid integer')
expect(() => ensurePositiveInteger(3.14)).toThrow('not a valid integer')
expect(() => ensurePositiveInteger('3.14')).toThrow('not a valid integer')
})
test('throws error for null when allowNull is false', () => {
expect(() => ensurePositiveInteger(null)).toThrow('not a valid integer')
})
test('throws error for empty string when allowNull is false', () => {
expect(() => ensurePositiveInteger('')).toThrow('not a valid integer')
})
// Default behavior
test('allowNull defaults to false', () => {
expect(() => ensurePositiveInteger(null)).toThrow()
expect(() => ensurePositiveInteger('')).toThrow()
})
})
describe('ensureNumeric', () => {
// Test valid numeric inputs
test('returns the same value for valid numbers', () => {
expect(ensureNumeric(42)).toBe(42)
expect(ensureNumeric(0)).toBe(0)
expect(ensureNumeric(-10)).toBe(-10)
expect(ensureNumeric(3.14)).toBe(3.14)
})
// Test string conversion
test('converts valid numeric strings to numbers', () => {
expect(ensureNumeric('42')).toBe(42)
expect(ensureNumeric('0')).toBe(0)
expect(ensureNumeric('-10')).toBe(-10)
expect(ensureNumeric('3.14')).toBe(3.14)
expect(ensureNumeric('+42')).toBe(42)
})
// Test null handling
test('handles null values based on allowNull option', () => {
expect(() => ensureNumeric(null)).toThrow()
expect(ensureNumeric(null, { allowNull: true })).toBeNull()
})
// Test empty string handling
test('handles empty strings based on allowNull option', () => {
expect(() => ensureNumeric('')).toThrow()
expect(ensureNumeric('', { allowNull: true })).toBeNull()
})
// Test invalid inputs
test('throws error for non-numeric strings', () => {
expect(() => ensureNumeric('abc')).toThrow()
expect(() => ensureNumeric('123abc')).toThrow()
expect(() => ensureNumeric('abc123')).toThrow()
expect(() => ensureNumeric('12.34.56')).toThrow()
expect(() => ensureNumeric('Infinity')).toThrow()
expect(() => ensureNumeric('-Infinity')).toThrow()
})
test('throws error for objects and arrays', () => {
expect(() => ensureNumeric({})).toThrow()
expect(() => ensureNumeric([])).toThrow()
expect(() => ensureNumeric([1, 2, 3])).toThrow()
})
test('throws error for boolean values', () => {
expect(() => ensureNumeric(true)).toThrow()
expect(() => ensureNumeric(false)).toThrow()
})
test('throws error for undefined', () => {
expect(() => ensureNumeric(undefined)).toThrow()
expect(() => ensureNumeric(undefined, { allowNull: true })).toThrow()
})
// Test error message
test('provides descriptive error message', () => {
try {
ensureNumeric('not-a-number')
} catch (error) {
expect(error.message).toContain('not-a-number')
expect(error.message).toContain('not a valid number')
}
})
})
describe('ensureString', () => {
it('should return an empty string if the value is falsy', () => {
expect(ensureString(null)).toBe('')
@ -185,3 +313,68 @@ describe('ensureDateTime', () => {
).toBe(date)
})
})
describe('QUERY_PARAM_TYPE_HANDLER_FUNCTIONS', () => {
describe('numeric handler', () => {
const numericHandler = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS.numeric
it('should return null for empty input', () => {
expect(numericHandler('')).toBe(null)
expect(numericHandler(null)).toBe(null)
expect(numericHandler(undefined)).toBe(null)
})
it('should handle single numeric string', () => {
expect(numericHandler('5')).toBe(5)
expect(numericHandler('42')).toBe(42)
expect(numericHandler('1.5')).toBe(1.5)
expect(numericHandler('-1')).toBe(-1)
})
it('should handle comma-separated numeric strings', () => {
expect(numericHandler('1,2,3')).toEqual([1, 2, 3])
expect(numericHandler('42,123')).toEqual([42, 123])
})
it('should filter out invalid values from arrays', () => {
expect(numericHandler('1,2,')).toEqual([1, 2, null])
expect(numericHandler('1,2')).toEqual([1, 2])
expect(numericHandler(',1,2')).toEqual([null, 1, 2])
expect(numericHandler('1,,2')).toEqual([1, null, 2])
})
it('should throw error for invalid numeric values', () => {
expect(() => numericHandler('1,abc,3')).toThrow()
})
})
describe('text handler', () => {
const textHandler = QUERY_PARAM_TYPE_HANDLER_FUNCTIONS.text
it('should return null for empty input', () => {
expect(textHandler('')).toBe(null)
expect(textHandler(null)).toBe(null)
expect(textHandler(undefined)).toBe(null)
})
it('should handle single text value', () => {
expect(textHandler('hello')).toBe('hello')
expect(textHandler('test123')).toBe('test123')
})
it('should handle comma-separated text values', () => {
expect(textHandler('hello,world')).toEqual(['hello', 'world'])
expect(textHandler('a,b,c')).toEqual(['a', 'b', 'c'])
})
it('should filter out empty values from arrays', () => {
expect(textHandler('test,')).toEqual(['test', ''])
expect(textHandler('test,,value')).toEqual(['test', '', 'value'])
})
it('should handle numeric values as strings', () => {
expect(textHandler('123')).toBe('123')
expect(textHandler('test,123,abc')).toEqual(['test', '123', 'abc'])
})
})
})