mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-12 02:43:50 +00:00
refactor: use strict typing
This commit is contained in:
committed by
Carlos Valente
parent
4ed38340e0
commit
178640bfc4
@@ -0,0 +1,34 @@
|
||||
import { hasKeys, isArray, isDefined, isNumber, isObject, isString } from '../assert.js';
|
||||
|
||||
describe('assert utilities', () => {
|
||||
it('should assert strings', () => {
|
||||
expect(() => isString('hello')).not.toThrow();
|
||||
expect(() => isString(123)).toThrow('Unexpected payload type: 123');
|
||||
});
|
||||
|
||||
it('should assert numbers', () => {
|
||||
expect(() => isNumber(123)).not.toThrow();
|
||||
expect(() => isNumber('123')).toThrow('Unexpected payload type: 123');
|
||||
});
|
||||
|
||||
it('should assert defined values', () => {
|
||||
expect(() => isDefined('value')).not.toThrow();
|
||||
expect(() => isDefined(undefined)).toThrow('Payload not found');
|
||||
});
|
||||
|
||||
it('should assert objects', () => {
|
||||
expect(() => isObject({})).not.toThrow();
|
||||
expect(() => isObject(null)).toThrow('Unexpected payload type: null');
|
||||
expect(() => isObject([])).toThrow('Unexpected payload type: ');
|
||||
});
|
||||
|
||||
it('should assert objects with specific keys', () => {
|
||||
expect(() => hasKeys({ a: 1, b: 2 }, ['a', 'b'])).not.toThrow();
|
||||
expect(() => hasKeys({ a: 1 }, ['a', 'b'])).toThrow('Unexpected payload type: [object Object]');
|
||||
});
|
||||
|
||||
it('should assert arrays', () => {
|
||||
expect(() => isArray([1, 2, 3])).not.toThrow();
|
||||
expect(() => isArray('not an array')).toThrow('Unexpected payload type: not an array');
|
||||
});
|
||||
});
|
||||
@@ -31,15 +31,16 @@ describe('test parseDatabaseModel() with demo project (valid)', () => {
|
||||
const filteredDemoProject = structuredClone(demoDb);
|
||||
const { data } = parseDatabaseModel(filteredDemoProject);
|
||||
|
||||
delete filteredDemoProject.settings.version;
|
||||
delete data.settings.version;
|
||||
|
||||
it('has 16 events', () => {
|
||||
expect(data.rundowns.demo.order.length).toBe(16);
|
||||
expect(Object.keys(data.rundowns.demo.entries).length).toBe(16);
|
||||
});
|
||||
|
||||
it('is the same as the demo project since all data is valid', () => {
|
||||
// @ts-expect-error -- its ok
|
||||
delete filteredDemoProject.settings.version;
|
||||
// @ts-expect-error -- its ok
|
||||
delete data.settings.version;
|
||||
expect(data).toMatchObject(filteredDemoProject);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isEmptyObject, mergeObject, removeUndefined } from '../parserUtils.js';
|
||||
import { isEmptyObject, removeUndefined } from '../parserUtils.js';
|
||||
|
||||
describe('isEmptyObject()', () => {
|
||||
test('finds an empty object', () => {
|
||||
@@ -11,92 +11,6 @@ describe('isEmptyObject()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeObject()', () => {
|
||||
test('it suppresses undefined keys', () => {
|
||||
const a = {
|
||||
first: 'yes',
|
||||
second: 'yes',
|
||||
};
|
||||
const b = {
|
||||
first: undefined,
|
||||
second: 'no',
|
||||
};
|
||||
const merged = mergeObject(a, b);
|
||||
expect(merged).toStrictEqual({
|
||||
first: 'yes',
|
||||
second: 'no',
|
||||
});
|
||||
});
|
||||
test('it handles falsy values', () => {
|
||||
const a = {
|
||||
first: 'yes',
|
||||
second: 'yes' as string | null,
|
||||
third: 'yes',
|
||||
};
|
||||
const b = {
|
||||
first: 'no',
|
||||
second: null,
|
||||
third: '',
|
||||
};
|
||||
const merged = mergeObject(a, b);
|
||||
expect(merged).toStrictEqual({
|
||||
first: 'no',
|
||||
second: null,
|
||||
third: '',
|
||||
});
|
||||
});
|
||||
test('it only merges fields of the first object', () => {
|
||||
const a = {
|
||||
first: 'yes',
|
||||
second: 'yes',
|
||||
third: 'yes',
|
||||
};
|
||||
const b = {
|
||||
first: 0,
|
||||
second: null,
|
||||
third: '',
|
||||
forth: 'not-this',
|
||||
};
|
||||
// @ts-expect-error -- testing changing type
|
||||
const merged = mergeObject(a, b);
|
||||
expect(merged).toStrictEqual({
|
||||
first: 0,
|
||||
second: null,
|
||||
third: '',
|
||||
});
|
||||
});
|
||||
test('merges nested objects', () => {
|
||||
// Define a sample object with nested properties
|
||||
const a = {
|
||||
name: 'John',
|
||||
address: {
|
||||
city: 'New York',
|
||||
postalCode: '10001',
|
||||
},
|
||||
};
|
||||
|
||||
// Define a partial object with nested properties for merging
|
||||
const b = {
|
||||
name: 'Doe',
|
||||
address: {
|
||||
city: 'San Francisco',
|
||||
state: 'CA',
|
||||
},
|
||||
};
|
||||
|
||||
// @ts-expect-error -- testing missing property
|
||||
const merged = mergeObject(a, b);
|
||||
|
||||
expect(merged.name).toBe('Doe');
|
||||
expect(merged.address.city).toBe('San Francisco');
|
||||
// @ts-expect-error -- its ok, just checking
|
||||
expect(merged.address.state).toBe('CA');
|
||||
expect(merged.address.postalCode).toBe('10001');
|
||||
expect(merged.address).not.toBe(a.address);
|
||||
expect(merged.address).not.toBe(b.address);
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeUndefined()', () => {
|
||||
test('it removes undefined keys from object', () => {
|
||||
const obj = {
|
||||
|
||||
@@ -1,29 +1,31 @@
|
||||
import { is } from './is.js';
|
||||
|
||||
export function isString(value: unknown): asserts value is string {
|
||||
if (typeof value !== 'string') {
|
||||
if (!is.string(value)) {
|
||||
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isDefined<T>(value: T | undefined): asserts value is T {
|
||||
if (value === undefined) {
|
||||
if (!is.defined(value)) {
|
||||
throw new Error('Payload not found');
|
||||
}
|
||||
}
|
||||
|
||||
export function isNumber(value: unknown): asserts value is number {
|
||||
if (typeof value !== 'number') {
|
||||
if (!is.number(value)) {
|
||||
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isObject(value: unknown): asserts value is object {
|
||||
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
||||
if (!is.object(value)) {
|
||||
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
export function isArray(value: unknown): asserts value is unknown[] {
|
||||
if (!Array.isArray(value)) {
|
||||
if (!is.array(value)) {
|
||||
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
@@ -32,9 +34,7 @@ export function hasKeys<T extends object, K extends keyof any>(
|
||||
value: T,
|
||||
keys: K[],
|
||||
): asserts value is T & Record<K, unknown> {
|
||||
for (const key of keys) {
|
||||
if (!(key in value)) {
|
||||
throw new Error(`Key not found: ${String(key)}`);
|
||||
}
|
||||
if (!is.objectWithKeys(value, keys)) {
|
||||
throw new Error(`Unexpected payload type: ${String(value)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export function ensureDirectory(directory: string): void {
|
||||
/**
|
||||
* Ensures that a filename ends with .json extension
|
||||
*/
|
||||
export function ensureJsonExtension(filename: string | undefined): string | undefined {
|
||||
export function ensureJsonExtension(filename: string): string {
|
||||
if (!filename) return filename;
|
||||
return filename.endsWith('.json') ? filename : `${filename}.json`;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
export const is = {
|
||||
string: (value: unknown): value is string => typeof value === 'string',
|
||||
number: (value: unknown): value is number => typeof value === 'number',
|
||||
defined: <T>(value: T | undefined): value is T => value !== undefined,
|
||||
object: (value: unknown): value is object => typeof value === 'object' && value !== null && !Array.isArray(value),
|
||||
objectWithKeys: <T extends object, K extends keyof any>(value: T, keys: K[]): value is T & Record<K, unknown> => {
|
||||
return keys.every((key) => key in value);
|
||||
},
|
||||
array: (value: unknown): value is unknown[] => Array.isArray(value),
|
||||
};
|
||||
@@ -32,6 +32,7 @@ import { makeString } from './parserUtils.js';
|
||||
import { parseProject, parseRundowns, parseSettings, parseUrlPresets, parseViewSettings } from './parserFunctions.js';
|
||||
import { parseExcelDate } from './time.js';
|
||||
import { Merge } from 'ts-essentials';
|
||||
import { is } from './is.js';
|
||||
|
||||
export type ErrorEmitter = (message: string) => void;
|
||||
export const EXCEL_MIME = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||
@@ -57,14 +58,19 @@ export function getCustomFieldData(
|
||||
customFieldImportKeys: Record<keyof CustomFields, string>;
|
||||
} {
|
||||
const customFields = {};
|
||||
const customFieldImportKeys = {};
|
||||
const customFieldImportKeys: Record<string, string> = {};
|
||||
|
||||
for (const ontimeLabel in importMap.custom) {
|
||||
const ontimeKey = customKeyFromLabel(ontimeLabel, existingCustomFields) ?? customFieldLabelToKey(ontimeLabel);
|
||||
if (!ontimeKey) {
|
||||
continue;
|
||||
}
|
||||
const importLabel = importMap.custom[ontimeLabel].toLowerCase();
|
||||
const colour = ontimeKey in existingCustomFields ? existingCustomFields[ontimeKey].colour : '';
|
||||
|
||||
// @ts-expect-error -- we are sure that the key exists
|
||||
customFields[ontimeKey] = {
|
||||
type: 'string',
|
||||
colour,
|
||||
colour: ontimeKey in existingCustomFields ? existingCustomFields[ontimeKey].colour : '',
|
||||
label: ontimeLabel,
|
||||
};
|
||||
customFieldImportKeys[importLabel] = ontimeKey;
|
||||
@@ -92,8 +98,9 @@ export const parseExcel = (
|
||||
const importMap: ImportMap = { ...defaultImportMap, ...options };
|
||||
|
||||
for (const [key, value] of Object.entries(importMap)) {
|
||||
if (typeof value === 'string') {
|
||||
importMap[key] = value.toLocaleLowerCase().trim();
|
||||
if (is.string(value)) {
|
||||
// @ts-expect-error -- we are sure that the key exists
|
||||
importMap[key] = value.toLowerCase().trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +285,7 @@ export const parseExcel = (
|
||||
|
||||
// check if it is an ontime column
|
||||
if (handlers[columnText]) {
|
||||
// @ts-expect-error -- its ok
|
||||
handlers[columnText](rowIndex, j, undefined, undefined);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { unlink } from 'fs';
|
||||
import { deepmerge } from 'ontime-utils';
|
||||
|
||||
/**
|
||||
* @description Ensures variable is string, it skips object types
|
||||
@@ -35,33 +34,6 @@ export const isEmptyObject = (obj: object) => {
|
||||
throw new Error('Variable is not an object');
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Merges two objects, suppressing undefined keys
|
||||
* @param {object} a - any object
|
||||
* @param {object} b - a potential partial object of same time as a
|
||||
*/
|
||||
export function mergeObject<T extends object>(a: T, b: Partial<T>): T {
|
||||
const merged = { ...a };
|
||||
|
||||
for (const key in b) {
|
||||
const aValue = a[key];
|
||||
const bValue = b[key];
|
||||
|
||||
// ignore keys that do not exist in original object
|
||||
if (!Object.hasOwn(merged, key)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof bValue === 'object' && bValue !== null && typeof aValue === 'object' && aValue !== null) {
|
||||
// @ts-expect-error -- not sure how to type this
|
||||
merged[key] = deepmerge(aValue, bValue);
|
||||
} else if (bValue !== undefined) {
|
||||
merged[key] = bValue;
|
||||
}
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Removes undefined
|
||||
* @param {object} obj
|
||||
|
||||
Reference in New Issue
Block a user