mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
custom fields (#744)
--------- Co-authored-by: Carlos Valente <carlosvalente@pm.me>
This commit is contained in:
committed by
GitHub
parent
1a420a1ddd
commit
c1377544a0
@@ -11,6 +11,7 @@ import {
|
|||||||
UserFields,
|
UserFields,
|
||||||
Alias,
|
Alias,
|
||||||
Settings,
|
Settings,
|
||||||
|
CustomFields,
|
||||||
HttpSettings,
|
HttpSettings,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
|
|
||||||
@@ -32,6 +33,16 @@ export class DataProvider {
|
|||||||
return data.project;
|
return data.project;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static async setCustomFields(newData: CustomFields): Promise<CustomFields> {
|
||||||
|
data.customFields = { ...newData };
|
||||||
|
await this.persist();
|
||||||
|
return data.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
|
static getCustomFields(): CustomFields {
|
||||||
|
return data.customFields;
|
||||||
|
}
|
||||||
|
|
||||||
static async setRundown(newData: OntimeRundown) {
|
static async setRundown(newData: OntimeRundown) {
|
||||||
data.rundown = [...newData];
|
data.rundown = [...newData];
|
||||||
await this.persist();
|
await this.persist();
|
||||||
@@ -110,6 +121,7 @@ export class DataProvider {
|
|||||||
data.http = mergedData.http;
|
data.http = mergedData.http;
|
||||||
data.aliases = mergedData.aliases;
|
data.aliases = mergedData.aliases;
|
||||||
data.userFields = mergedData.userFields;
|
data.userFields = mergedData.userFields;
|
||||||
|
data.customFields = mergedData.customFields;
|
||||||
data.rundown = mergedData.rundown;
|
data.rundown = mergedData.rundown;
|
||||||
await this.persist();
|
await this.persist();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { DatabaseModel } from 'ontime-types';
|
|||||||
* @param {object} newData
|
* @param {object} newData
|
||||||
*/
|
*/
|
||||||
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseModel>) {
|
||||||
const { rundown, project, settings, viewSettings, aliases, userFields, osc, http } = newData || {};
|
const { rundown, project, settings, viewSettings, aliases, customFields, userFields, osc, http } = newData || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...existing,
|
...existing,
|
||||||
@@ -15,6 +15,7 @@ export function safeMerge(existing: DatabaseModel, newData: Partial<DatabaseMode
|
|||||||
settings: { ...existing.settings, ...settings },
|
settings: { ...existing.settings, ...settings },
|
||||||
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
viewSettings: { ...existing.viewSettings, ...viewSettings },
|
||||||
aliases: aliases ?? existing.aliases,
|
aliases: aliases ?? existing.aliases,
|
||||||
|
customFields: customFields ?? existing.customFields,
|
||||||
userFields: {
|
userFields: {
|
||||||
...existing.userFields,
|
...existing.userFields,
|
||||||
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
|
...(userFields && Object.fromEntries(Object.entries(userFields).filter(([_, value]) => value !== null))),
|
||||||
|
|||||||
@@ -24,11 +24,26 @@ describe('safeMerge', () => {
|
|||||||
viewSettings: {
|
viewSettings: {
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
endMessage: 'existing endMessage',
|
endMessage: 'existing endMessage',
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
dangerColor: '#ED3333',
|
||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
userFields: {
|
userFields: {
|
||||||
user0: 'existing user0',
|
user0: 'existing user0',
|
||||||
user1: 'existing user1',
|
user1: 'existing user1',
|
||||||
|
user2: 'existing user2',
|
||||||
|
user3: 'existing user3',
|
||||||
|
user4: 'existing user4',
|
||||||
|
user5: 'existing user5',
|
||||||
|
user6: 'existing user6',
|
||||||
|
user7: 'existing user7',
|
||||||
|
user8: 'existing user8',
|
||||||
|
user9: 'existing user9',
|
||||||
|
},
|
||||||
|
customFields: {
|
||||||
|
lighting: { type: 'string', label: 'lighting' },
|
||||||
|
vfx: { type: 'string', label: 'vfx' },
|
||||||
},
|
},
|
||||||
osc: {
|
osc: {
|
||||||
portIn: 8888,
|
portIn: 8888,
|
||||||
@@ -212,4 +227,29 @@ describe('safeMerge', () => {
|
|||||||
const result = safeMerge(existing, newData);
|
const result = safeMerge(existing, newData);
|
||||||
expect(result.userFields).toEqual(expected);
|
expect(result.userFields).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('merges customFields into existing object', () => {
|
||||||
|
const existing = {
|
||||||
|
customFields: {
|
||||||
|
lighting: { type: 'string', label: 'lighting' },
|
||||||
|
sound: { type: 'string', label: 'sound' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
customFields: {
|
||||||
|
switcher: { type: 'string', label: 'switcher' },
|
||||||
|
vfx: { type: 'string', label: 'vfx' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
switcher: { type: 'string', label: 'switcher' },
|
||||||
|
vfx: { type: 'string', label: 'vfx' },
|
||||||
|
};
|
||||||
|
|
||||||
|
//@ts-expect-error -- testing partial merge
|
||||||
|
const result = safeMerge(existing, newData);
|
||||||
|
expect(result.customFields).toEqual(expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { RequestHandler } from 'express';
|
import type { Request, Response, RequestHandler } from 'express';
|
||||||
|
|
||||||
import { ProjectData } from 'ontime-types';
|
import { CustomField, CustomFields, ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { removeUndefined } from '../utils/parserUtils.js';
|
import { removeUndefined } from '../utils/parserUtils.js';
|
||||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
import { failEmptyObjects } from '../utils/routerUtils.js';
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
|
import { createCustomField, editCustomField, removeCustomField } from '../utils/customFields.js';
|
||||||
|
|
||||||
// Create controller for GET request to 'project'
|
// Create controller for GET request to 'project'
|
||||||
export const getProject: RequestHandler = async (req, res) => {
|
export const getProject: RequestHandler = async (req, res) => {
|
||||||
@@ -33,3 +34,47 @@ export const postProject: RequestHandler = async (req, res) => {
|
|||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCustomFields: RequestHandler = async (_req: Request, res: Response<CustomFields>) => {
|
||||||
|
res.json(DataProvider.getCustomFields());
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expects { label: <lable> type: 'string | ..' }
|
||||||
|
export const postCustomField: RequestHandler = async (req: Request, res: Response) => {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newField = req.body as CustomField;
|
||||||
|
const allFields = await createCustomField(newField);
|
||||||
|
res.status(201).send(allFields);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expects { label: <oldLable>, field: { label: <newlable> type: 'string | ..' } }
|
||||||
|
export const putCustomField: RequestHandler = async (req: Request, res: Response) => {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newFields = await editCustomField(req.body.label, req.body.field);
|
||||||
|
res.status(200).send(newFields);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Expects { label: <lable> }
|
||||||
|
export const deleteCustomField: RequestHandler = async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const fieldToDelete = req.params.label;
|
||||||
|
await removeCustomField(fieldToDelete);
|
||||||
|
res.sendStatus(204);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -16,3 +16,37 @@ export const projectSanitiser = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const validateCustomField = [
|
||||||
|
body('label').isString().trim(),
|
||||||
|
body('type').isString().trim(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const validateEditCustomField = [
|
||||||
|
body('label').isString().trim(),
|
||||||
|
body('field.label').optional().isString().trim(),
|
||||||
|
body('field.type').optional().isString().trim(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
export const valdiateDeleteCustomField = [
|
||||||
|
body('label').isString(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ export const dbModel: DatabaseModel = {
|
|||||||
endMessage: '',
|
endMessage: '',
|
||||||
},
|
},
|
||||||
aliases: [],
|
aliases: [],
|
||||||
|
customFields: {},
|
||||||
userFields: {
|
userFields: {
|
||||||
user0: 'user0',
|
user0: 'user0',
|
||||||
user1: 'user1',
|
user1: 'user1',
|
||||||
|
|||||||
@@ -1,6 +1,17 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
import { getProject, postProject } from '../controllers/projectController.js';
|
import {
|
||||||
import { projectSanitiser } from '../controllers/projectController.validate.js';
|
deleteCustomField,
|
||||||
|
getCustomFields,
|
||||||
|
getProject,
|
||||||
|
postCustomField,
|
||||||
|
postProject,
|
||||||
|
putCustomField,
|
||||||
|
} from '../controllers/projectController.js';
|
||||||
|
import {
|
||||||
|
projectSanitiser,
|
||||||
|
validateCustomField,
|
||||||
|
validateEditCustomField,
|
||||||
|
} from '../controllers/projectController.validate.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
@@ -9,3 +20,11 @@ router.get('/', getProject);
|
|||||||
|
|
||||||
// create route between controller and 'POST /project' endpoint
|
// create route between controller and 'POST /project' endpoint
|
||||||
router.post('/', projectSanitiser, postProject);
|
router.post('/', projectSanitiser, postProject);
|
||||||
|
|
||||||
|
router.get('/custom-field', getCustomFields);
|
||||||
|
|
||||||
|
router.post('/custom-field', validateCustomField, postCustomField);
|
||||||
|
|
||||||
|
router.put('/custom-field', validateEditCustomField, putCustomField);
|
||||||
|
|
||||||
|
router.delete('/custom-field/:label', deleteCustomField);
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { createCustomField, editCustomField, removeCustomField } from '../customFields.js';
|
||||||
|
|
||||||
|
describe('createCustomField()', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.mock('../../classes/data-provider/DataProvider.js', () => {
|
||||||
|
return {
|
||||||
|
DataProvider: {
|
||||||
|
...vi.fn().mockImplementation(() => {
|
||||||
|
return {};
|
||||||
|
}),
|
||||||
|
getCustomFields: vi.fn().mockReturnValue({}),
|
||||||
|
setCustomFields: vi.fn().mockImplementation((newData) => {
|
||||||
|
return newData;
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('creates a field from given parameters', async () => {
|
||||||
|
const expected = {
|
||||||
|
lighting: {
|
||||||
|
label: 'lighting',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const customField = await createCustomField({ label: 'lighting', type: 'text' });
|
||||||
|
expect(customField).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('editCustomField()', () => {
|
||||||
|
it('edits a field with a given label', async () => {
|
||||||
|
await createCustomField({ label: 'sound', type: 'text' });
|
||||||
|
|
||||||
|
const expected = {
|
||||||
|
lighting: {
|
||||||
|
label: 'lighting',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
sound: {
|
||||||
|
label: 'sound',
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const customField = await editCustomField('sound', { label: 'sound', type: 'number' });
|
||||||
|
|
||||||
|
expect(customField).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeCustomField()', () => {
|
||||||
|
it('deletes a field with a given label', async () => {
|
||||||
|
const expected = {
|
||||||
|
lighting: {
|
||||||
|
label: 'lighting',
|
||||||
|
type: 'text',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const customField = await removeCustomField('sound');
|
||||||
|
|
||||||
|
expect(customField).toStrictEqual(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import { isAlphanumeric } from 'ontime-utils';
|
||||||
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
|
import { CustomField } from 'ontime-types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitises and creates a custom field in the database
|
||||||
|
* @param field
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const createCustomField = async (field: CustomField) => {
|
||||||
|
if (!isAlphanumeric(field.label)) {
|
||||||
|
throw new Error('Label must be Alphanumeric');
|
||||||
|
}
|
||||||
|
|
||||||
|
const customFields = DataProvider.getCustomFields();
|
||||||
|
if (Object.keys(customFields).find((f) => f === field.label) !== undefined) {
|
||||||
|
throw new Error('Label already exists');
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(customFields, { [field.label]: field });
|
||||||
|
const newCustomFields = await DataProvider.setCustomFields(customFields);
|
||||||
|
|
||||||
|
return newCustomFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits an existing custom field in the database
|
||||||
|
* @param label
|
||||||
|
* @param field
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const editCustomField = async (label: string, field: Partial<CustomField>) => {
|
||||||
|
const existingFields = DataProvider.getCustomFields();
|
||||||
|
if (!(label in existingFields)) {
|
||||||
|
throw new Error('Could not find label');
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingField = existingFields[label];
|
||||||
|
if (!existingField) {
|
||||||
|
throw new Error('Could not find label');
|
||||||
|
}
|
||||||
|
|
||||||
|
existingFields[label] = { ...existingField, ...field };
|
||||||
|
|
||||||
|
const newCustomFields = await DataProvider.setCustomFields(existingFields);
|
||||||
|
return newCustomFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a custom field from the database
|
||||||
|
* @param label
|
||||||
|
*/
|
||||||
|
export const removeCustomField = async (label: string) => {
|
||||||
|
const existingFields = DataProvider.getCustomFields();
|
||||||
|
if (!(label in existingFields)) {
|
||||||
|
throw new Error('Could not find label');
|
||||||
|
}
|
||||||
|
|
||||||
|
delete existingFields[label];
|
||||||
|
|
||||||
|
const newCustomFields = await DataProvider.setCustomFields(existingFields);
|
||||||
|
return newCustomFields;
|
||||||
|
};
|
||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
parseSettings,
|
parseSettings,
|
||||||
parseUserFields,
|
parseUserFields,
|
||||||
parseViewSettings,
|
parseViewSettings,
|
||||||
|
parseCustomFields,
|
||||||
} from './parserFunctions.js';
|
} from './parserFunctions.js';
|
||||||
import { parseExcelDate } from './time.js';
|
import { parseExcelDate } from './time.js';
|
||||||
import { configService } from '../services/ConfigService.js';
|
import { configService } from '../services/ConfigService.js';
|
||||||
@@ -330,6 +331,7 @@ export const parseJson = async (jsonData: Partial<DatabaseModel>): Promise<Datab
|
|||||||
viewSettings: parseViewSettings(jsonData) ?? dbModel.viewSettings,
|
viewSettings: parseViewSettings(jsonData) ?? dbModel.viewSettings,
|
||||||
aliases: parseAliases(jsonData),
|
aliases: parseAliases(jsonData),
|
||||||
userFields: parseUserFields(jsonData),
|
userFields: parseUserFields(jsonData),
|
||||||
|
customFields: parseCustomFields(jsonData),
|
||||||
osc: parseOsc(jsonData) ?? dbModel.osc,
|
osc: parseOsc(jsonData) ?? dbModel.osc,
|
||||||
http: parseHttp(jsonData) ?? dbModel.http,
|
http: parseHttp(jsonData) ?? dbModel.http,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
isOntimeEvent,
|
isOntimeEvent,
|
||||||
isOntimeDelay,
|
isOntimeDelay,
|
||||||
isOntimeBlock,
|
isOntimeBlock,
|
||||||
|
CustomFields,
|
||||||
isOntimeCycle,
|
isOntimeCycle,
|
||||||
HttpSubscription,
|
HttpSubscription,
|
||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
@@ -276,3 +277,23 @@ export const parseUserFields = (data): UserFields => {
|
|||||||
}
|
}
|
||||||
return { ...newUserFields };
|
return { ...newUserFields };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse customFields entry
|
||||||
|
* @param {object} data - data object
|
||||||
|
* @returns {object} - event object data
|
||||||
|
*/
|
||||||
|
export const parseCustomFields = (data): CustomFields => {
|
||||||
|
let newCustomFields: CustomFields = { ...dbModel.customFields };
|
||||||
|
|
||||||
|
if ('customFields' in data) {
|
||||||
|
console.log('Found Custom Fields definition, importing...');
|
||||||
|
try {
|
||||||
|
//TODO: validate
|
||||||
|
newCustomFields = { ...dbModel.customFields, ...data.customFields };
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ...newCustomFields };
|
||||||
|
};
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { OSCSettings } from './core/OscSettings.type.js';
|
|||||||
import { Settings } from './core/Settings.type.js';
|
import { Settings } from './core/Settings.type.js';
|
||||||
import { UserFields } from './core/UserFields.type.js';
|
import { UserFields } from './core/UserFields.type.js';
|
||||||
import { ViewSettings } from './core/Views.type.js';
|
import { ViewSettings } from './core/Views.type.js';
|
||||||
import { HttpSettings } from '../index.js';
|
import { CustomFields, HttpSettings } from '../index.js';
|
||||||
|
|
||||||
export type DatabaseModel = {
|
export type DatabaseModel = {
|
||||||
rundown: OntimeRundown;
|
rundown: OntimeRundown;
|
||||||
@@ -14,6 +14,7 @@ export type DatabaseModel = {
|
|||||||
viewSettings: ViewSettings;
|
viewSettings: ViewSettings;
|
||||||
aliases: Alias[];
|
aliases: Alias[];
|
||||||
userFields: UserFields;
|
userFields: UserFields;
|
||||||
|
customFields: CustomFields;
|
||||||
osc: OSCSettings;
|
osc: OSCSettings;
|
||||||
http: HttpSettings;
|
http: HttpSettings;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export type CustomField = {
|
||||||
|
type: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CustomFields = Record<string, CustomField>;
|
||||||
@@ -30,6 +30,9 @@ export type { Alias } from './definitions/core/Alias.type.js';
|
|||||||
// ---> User Fields
|
// ---> User Fields
|
||||||
export type { UserFields } from './definitions/core/UserFields.type.js';
|
export type { UserFields } from './definitions/core/UserFields.type.js';
|
||||||
|
|
||||||
|
// ---> Custom Fields
|
||||||
|
export type { CustomFields, CustomField } from './definitions/core/CustomFields.type.js';
|
||||||
|
|
||||||
// ---> Integration, Subscription
|
// ---> Integration, Subscription
|
||||||
export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
export type { OSCSettings, OscSubscription } from './definitions/core/OscSettings.type.js';
|
||||||
export type { HttpSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js';
|
export type { HttpSettings, HttpSubscription } from './definitions/core/HttpSettings.type.js';
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export {
|
|||||||
removeSeconds,
|
removeSeconds,
|
||||||
removeTrailingZero,
|
removeTrailingZero,
|
||||||
} from './src/date-utils/timeFormatting.js';
|
} from './src/date-utils/timeFormatting.js';
|
||||||
|
export { isAlphanumeric } from './src/regex-utils/isAlphanumeric.js';
|
||||||
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
export { isColourHex } from './src/regex-utils/isColourHex.js';
|
||||||
|
|
||||||
// time utils
|
// time utils
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* @description Validates a alphanumeric string
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
export const isAlphanumeric = (text: string): boolean => {
|
||||||
|
const regex = /^[a-z0-9]+$/i;
|
||||||
|
return regex.test(text);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user