From 5bc286145d3f53a292b8be2a213acc64c8027799 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sun, 20 Jul 2025 07:14:54 +0200 Subject: [PATCH] refactor: extract regex utilities --- .../composite/CustomFieldForm.tsx | 4 +- .../import-map/ImportMapForm.tsx | 4 +- .../custom-fields/customFields.parser.ts | 4 +- .../custom-fields/customFields.validation.ts | 6 +-- .../server/src/api-data/excel/excel.parser.ts | 4 +- packages/utils/index.ts | 2 +- .../utils/src/regex-utils/checkRegex.test.ts | 45 +++++++++++++++++++ packages/utils/src/regex-utils/checkRegex.ts | 25 +++++++++++ .../src/regex-utils/isAlphanumeric.test.ts | 17 ------- .../utils/src/regex-utils/isAlphanumeric.ts | 17 ------- 10 files changed, 82 insertions(+), 46 deletions(-) create mode 100644 packages/utils/src/regex-utils/checkRegex.test.ts create mode 100644 packages/utils/src/regex-utils/checkRegex.ts delete mode 100644 packages/utils/src/regex-utils/isAlphanumeric.test.ts delete mode 100644 packages/utils/src/regex-utils/isAlphanumeric.ts diff --git a/apps/client/src/features/app-settings/panel/manage-panel/composite/CustomFieldForm.tsx b/apps/client/src/features/app-settings/panel/manage-panel/composite/CustomFieldForm.tsx index 3cf2eb2b8..223110776 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/composite/CustomFieldForm.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/composite/CustomFieldForm.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useForm } from 'react-hook-form'; import { CustomField } from 'ontime-types'; -import { customFieldLabelToKey, isAlphanumericWithSpace } from 'ontime-utils'; +import { checkRegex, customFieldLabelToKey } from 'ontime-utils'; import { maybeAxiosError } from '../../../../../common/api/utils'; import Button from '../../../../../common/components/buttons/Button'; @@ -117,7 +117,7 @@ export default function CustomFieldForm({ onChange: () => setValue('key', customFieldLabelToKey(getValues('label')) ?? 'N/A'), validate: (value) => { if (value.trim().length === 0) return 'Required field'; - if (!isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed'; + if (!checkRegex.isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed'; if (!isEditMode) { if (isEditMode && Object.keys(data).includes(value)) return 'Custom fields must be unique'; } diff --git a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/ImportMapForm.tsx b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/ImportMapForm.tsx index f3d04e317..33ad5acef 100644 --- a/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/ImportMapForm.tsx +++ b/apps/client/src/features/app-settings/panel/manage-panel/sources-panel/import-map/ImportMapForm.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { useFieldArray, useForm } from 'react-hook-form'; import { IoAdd, IoTrash } from 'react-icons/io5'; -import { ImportMap, isAlphanumericWithSpace } from 'ontime-utils'; +import { checkRegex, ImportMap } from 'ontime-utils'; import Button from '../../../../../../common/components/buttons/Button'; import IconButton from '../../../../../../common/components/buttons/IconButton'; @@ -200,7 +200,7 @@ export default function ImportMapForm({ placeholder='Name of the field as shown in Ontime' {...register(`custom.${index}.ontimeName`, { validate: (value) => { - if (!isAlphanumericWithSpace(value)) + if (!checkRegex.isAlphanumericWithSpace(value)) return 'Only alphanumeric characters and space are allowed'; return true; }, diff --git a/apps/server/src/api-data/custom-fields/customFields.parser.ts b/apps/server/src/api-data/custom-fields/customFields.parser.ts index b8a873033..be176ffc4 100644 --- a/apps/server/src/api-data/custom-fields/customFields.parser.ts +++ b/apps/server/src/api-data/custom-fields/customFields.parser.ts @@ -1,5 +1,5 @@ import { DatabaseModel, CustomFields, CustomField } from 'ontime-types'; -import { isAlphanumericWithSpace, customFieldLabelToKey } from 'ontime-utils'; +import { checkRegex, customFieldLabelToKey } from 'ontime-utils'; import type { ErrorEmitter } from '../../utils/parserUtils.js'; @@ -29,7 +29,7 @@ export function sanitiseCustomFields(data: object): CustomFields { continue; } - if (!isAlphanumericWithSpace(field.label)) { + if (!checkRegex.isAlphanumericWithSpace(field.label)) { continue; } diff --git a/apps/server/src/api-data/custom-fields/customFields.validation.ts b/apps/server/src/api-data/custom-fields/customFields.validation.ts index c4f775ffb..47b6021eb 100644 --- a/apps/server/src/api-data/custom-fields/customFields.validation.ts +++ b/apps/server/src/api-data/custom-fields/customFields.validation.ts @@ -1,4 +1,4 @@ -import { isAlphanumericWithSpace } from 'ontime-utils'; +import { checkRegex } from 'ontime-utils'; import { body, param } from 'express-validator'; import { requestValidationFunction } from '../validation-utils/validationFunction.js'; @@ -9,7 +9,7 @@ export const validateCustomField = [ .trim() .notEmpty() .custom((value) => { - return isAlphanumericWithSpace(value); + return checkRegex.isAlphanumericWithSpace(value); }), body('type').isIn(['text', 'image']), body('colour').isString().trim(), @@ -24,7 +24,7 @@ export const validateEditCustomField = [ .trim() .notEmpty() .custom((value) => { - return isAlphanumericWithSpace(value); + return checkRegex.isAlphanumericWithSpace(value); }), body('type').isIn(['text', 'image']), body('colour').isString().trim(), diff --git a/apps/server/src/api-data/excel/excel.parser.ts b/apps/server/src/api-data/excel/excel.parser.ts index 2aa4b8055..5cfc3a129 100644 --- a/apps/server/src/api-data/excel/excel.parser.ts +++ b/apps/server/src/api-data/excel/excel.parser.ts @@ -17,7 +17,7 @@ import { validateTimerType, validateEndAction, customFieldLabelToKey, - isAlphanumericWithSpace, + checkRegex, } from 'ontime-utils'; import { Merge } from 'ts-essentials'; @@ -325,7 +325,7 @@ export function getCustomFieldData( for (const ontimeLabel in importMap.custom) { // if the label is not valid, we skip the import - if (!isAlphanumericWithSpace(ontimeLabel)) { + if (!checkRegex.isAlphanumericWithSpace(ontimeLabel)) { continue; } diff --git a/packages/utils/index.ts b/packages/utils/index.ts index 274999a5e..8b1fe3883 100644 --- a/packages/utils/index.ts +++ b/packages/utils/index.ts @@ -50,7 +50,7 @@ export { removeTrailingZero, } from './src/date-utils/timeFormatting.js'; export { parseUserTime } from './src/date-utils/parseUserTime.js'; -export { isAlphanumeric, isAlphanumericWithSpace } from './src/regex-utils/isAlphanumeric.js'; +export { checkRegex, regex } from './src/regex-utils/checkRegex.js'; export { isColourHex } from './src/regex-utils/isColourHex.js'; export { splitWhitespace } from './src/regex-utils/splitWhitespace.js'; diff --git a/packages/utils/src/regex-utils/checkRegex.test.ts b/packages/utils/src/regex-utils/checkRegex.test.ts new file mode 100644 index 000000000..ca926d76f --- /dev/null +++ b/packages/utils/src/regex-utils/checkRegex.test.ts @@ -0,0 +1,45 @@ +import { checkRegex } from './checkRegex'; + +describe('checkRegex()', () => { + it('isOnlyNumbers', () => { + expect(checkRegex.isOnlyNumbers('12345')).toBe(true); + expect(checkRegex.isOnlyNumbers('123a')).toBe(false); + }); + + it('isIPAddress', () => { + expect(checkRegex.isIPAddress('192.168.0.1')).toBe(true); + expect(checkRegex.isIPAddress('999.999.999.999')).toBe(false); + }); + + it('startsWithHttp', () => { + expect(checkRegex.startsWithHttp('http://example.com')).toBe(true); + expect(checkRegex.startsWithHttp('https://example.com')).toBe(true); + expect(checkRegex.startsWithHttp('ftp://example.com')).toBe(false); + }); + + it('startsWithSlash', () => { + expect(checkRegex.startsWithSlash('/path')).toBe(true); + expect(checkRegex.startsWithSlash('path')).toBe(false); + }); + + it('isAlphanumericWithSpace', () => { + expect(checkRegex.isAlphanumericWithSpace('abc 123')).toBe(true); + expect(checkRegex.isAlphanumericWithSpace('abc-123')).toBe(false); + }); + + it('isASCII', () => { + expect(checkRegex.isASCII('Hello, World!')).toBe(true); + expect(checkRegex.isASCII('こんにちは')).toBe(false); + }); + + it('isASCIIorEmpty', () => { + expect(checkRegex.isASCIIorEmpty('')).toBe(true); + expect(checkRegex.isASCIIorEmpty('Hello')).toBe(true); + expect(checkRegex.isASCIIorEmpty('こんにちは')).toBe(false); + }); + + it('isNotEmpty', () => { + expect(checkRegex.isNotEmpty('Hello')).toBe(true); + expect(checkRegex.isNotEmpty('')).toBe(false); + }); +}); diff --git a/packages/utils/src/regex-utils/checkRegex.ts b/packages/utils/src/regex-utils/checkRegex.ts new file mode 100644 index 000000000..2fc6cee5f --- /dev/null +++ b/packages/utils/src/regex-utils/checkRegex.ts @@ -0,0 +1,25 @@ +export const regex = { + isOnlyNumbers: /^\d+$/, + isIPAddress: /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)\.?\b){4}$/, + startsWithHttp: /^https?:\/\//, + startsWithSlash: /^\//, + isAlphanumeric: /^[a-z0-9]+$/i, + isAlphanumericWithSpace: /^[a-z0-9_ ]+$/i, + isASCII: /^[ -~]+$/, // https://catonmat.net/my-favorite-regex + isASCIIorEmpty: /^$|^[ -~]+$/, // https://catonmat.net/my-favorite-regex + isNotEmpty: /\S/, + isUrlSafe: /^[a-zA-Z0-9_-]*$/, // https://stackoverflow.com/questions/24419067/validate-a-string-to-be-url-safe-using-regex +}; + +export const checkRegex = { + isOnlyNumbers: (text: string): boolean => regex.isOnlyNumbers.test(text), + isIPAddress: (text: string): boolean => regex.isIPAddress.test(text), + startsWithHttp: (text: string): boolean => regex.startsWithHttp.test(text), + startsWithSlash: (text: string): boolean => regex.startsWithSlash.test(text), + isAlphanumeric: (text: string): boolean => regex.isAlphanumeric.test(text), + isAlphanumericWithSpace: (text: string): boolean => regex.isAlphanumericWithSpace.test(text), + isASCII: (text: string): boolean => regex.isASCII.test(text), + isASCIIorEmpty: (text: string): boolean => regex.isASCIIorEmpty.test(text), + isNotEmpty: (text: string): boolean => regex.isNotEmpty.test(text), + isUrlSafe: (text: string): boolean => regex.isUrlSafe.test(text), +}; diff --git a/packages/utils/src/regex-utils/isAlphanumeric.test.ts b/packages/utils/src/regex-utils/isAlphanumeric.test.ts deleted file mode 100644 index 2d7f47deb..000000000 --- a/packages/utils/src/regex-utils/isAlphanumeric.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { isAlphanumeric } from './isAlphanumeric'; - -describe('test isAlphanumeric() function', () => { - it('it OK strings', () => { - const ts = ['abcdefghijklmnopqrstuvwxyz', '0123456798', '123asd']; - for (const s of ts) { - expect(isAlphanumeric(s)).toBe(true); - } - }); - - it('it bad strings', () => { - const ts = ['!abcd1234', 'åøæ', '*']; - for (const s of ts) { - expect(isAlphanumeric(s)).toBe(false); - } - }); -}); diff --git a/packages/utils/src/regex-utils/isAlphanumeric.ts b/packages/utils/src/regex-utils/isAlphanumeric.ts deleted file mode 100644 index 64b375b93..000000000 --- a/packages/utils/src/regex-utils/isAlphanumeric.ts +++ /dev/null @@ -1,17 +0,0 @@ -/** - * @description Validates a alphanumeric string - * @returns {boolean} - */ -export const isAlphanumeric = (text: string): boolean => { - const regex = /^[a-z0-9]+$/i; - return regex.test(text); -}; - -/** - * @description Validates a alphanumeric string allow space - * @returns {boolean} - */ -export const isAlphanumericWithSpace = (text: string): boolean => { - const regex = /^[a-z0-9_ ]+$/i; - return regex.test(text); -};