refactor: use strict typing

This commit is contained in:
Carlos Valente
2025-03-21 19:44:16 +01:00
committed by Carlos Valente
parent 4ed38340e0
commit 178640bfc4
39 changed files with 339 additions and 246 deletions
@@ -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 = {
+9 -9
View File
@@ -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)}`);
}
}
+1 -1
View File
@@ -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`;
}
+10
View File
@@ -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),
};
+13 -5
View File
@@ -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);
}
-28
View File
@@ -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