mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-30 19:39:10 +00:00
refactor: parse import fields
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
import { EndAction, TimerType } from 'ontime-types';
|
import { EndAction, OntimeEvent, TimerType } from 'ontime-types';
|
||||||
|
|
||||||
import { dbModel } from '../../models/dataModel.js';
|
import { dbModel } from '../../models/dataModel.js';
|
||||||
import { parseExcel, parseJson, validateEvent } from '../parser.js';
|
import { parseExcel, parseJson, validateEvent } from '../parser.js';
|
||||||
@@ -587,16 +587,16 @@ describe('test parseExcel function', () => {
|
|||||||
'Is Public? (x)',
|
'Is Public? (x)',
|
||||||
'Skip? (x)',
|
'Skip? (x)',
|
||||||
'Notes',
|
'Notes',
|
||||||
'User0:test0',
|
'test0',
|
||||||
'User1:test1',
|
'test1',
|
||||||
'User2:test2',
|
'test2',
|
||||||
'User3:test3',
|
'test3',
|
||||||
'User4:test4',
|
'test4',
|
||||||
'User5:test5',
|
'test5',
|
||||||
'User6:test6',
|
'test6',
|
||||||
'user7:test7',
|
'test7',
|
||||||
'user8:test8',
|
'test8',
|
||||||
'user9:test9',
|
'test9',
|
||||||
'Colour',
|
'Colour',
|
||||||
'cue',
|
'cue',
|
||||||
],
|
],
|
||||||
@@ -651,6 +651,19 @@ describe('test parseExcel function', () => {
|
|||||||
[],
|
[],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const partialOptions = {
|
||||||
|
user0: 'test0',
|
||||||
|
user1: 'test1',
|
||||||
|
user2: 'test2',
|
||||||
|
user3: 'test3',
|
||||||
|
user4: 'test4',
|
||||||
|
user5: 'test5',
|
||||||
|
user6: 'test6',
|
||||||
|
user7: 'test7',
|
||||||
|
user8: 'test8',
|
||||||
|
user9: 'test9',
|
||||||
|
};
|
||||||
|
|
||||||
const expectedParsedProjectData = {
|
const expectedParsedProjectData = {
|
||||||
title: 'Test Event',
|
title: 'Test Event',
|
||||||
description: 'test description',
|
description: 'test description',
|
||||||
@@ -706,7 +719,7 @@ describe('test parseExcel function', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const parsedData = await parseExcel(testdata);
|
const parsedData = parseExcel(testdata, partialOptions);
|
||||||
expect(parsedData.project).toStrictEqual(expectedParsedProjectData);
|
expect(parsedData.project).toStrictEqual(expectedParsedProjectData);
|
||||||
expect(parsedData.rundown).toBeDefined();
|
expect(parsedData.rundown).toBeDefined();
|
||||||
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
expect(parsedData.rundown[0]).toMatchObject(expectedParsedRundown[0]);
|
||||||
@@ -835,7 +848,16 @@ describe('test views import', () => {
|
|||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
viewSettings: {},
|
viewSettings: {
|
||||||
|
normalColor: '#ffffffcc',
|
||||||
|
warningColor: '#FFAB33',
|
||||||
|
warningThreshold: 120000,
|
||||||
|
dangerColor: '#ED3333',
|
||||||
|
dangerThreshold: 60000,
|
||||||
|
endMessage: '',
|
||||||
|
overrideStyles: false,
|
||||||
|
notAthing: true,
|
||||||
|
},
|
||||||
views: {
|
views: {
|
||||||
overrideStyles: true,
|
overrideStyles: true,
|
||||||
},
|
},
|
||||||
@@ -849,7 +871,7 @@ describe('test views import', () => {
|
|||||||
endMessage: '',
|
endMessage: '',
|
||||||
overrideStyles: false,
|
overrideStyles: false,
|
||||||
};
|
};
|
||||||
const parsed = parseViewSettings(testData, false);
|
const parsed = parseViewSettings(testData);
|
||||||
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -861,16 +883,7 @@ describe('test views import', () => {
|
|||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
const expectedParsedViewSettings = {
|
const parsed = parseViewSettings(testData);
|
||||||
normalColor: '#ffffffcc',
|
expect(parsed).toStrictEqual({});
|
||||||
warningColor: '#FFAB33',
|
|
||||||
warningThreshold: 120000,
|
|
||||||
dangerColor: '#ED3333',
|
|
||||||
dangerThreshold: 60000,
|
|
||||||
endMessage: '',
|
|
||||||
overrideStyles: false,
|
|
||||||
};
|
|
||||||
const parsed = parseViewSettings(testData, true);
|
|
||||||
expect(parsed).toStrictEqual(expectedParsedViewSettings);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ describe('mergeObject()', () => {
|
|||||||
third: '',
|
third: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
test.skip('it only merges fields of the first object', () => {
|
test('it only merges fields of the first object', () => {
|
||||||
const a = {
|
const a = {
|
||||||
first: 'yes',
|
first: 'yes',
|
||||||
second: 'yes',
|
second: 'yes',
|
||||||
@@ -64,6 +64,35 @@ describe('mergeObject()', () => {
|
|||||||
third: '',
|
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',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
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()', () => {
|
describe('removeUndefined()', () => {
|
||||||
|
|||||||
+159
-173
@@ -1,18 +1,18 @@
|
|||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
import {
|
||||||
// @ts-nocheck -- not ready to fully type
|
generateId,
|
||||||
|
calculateDuration,
|
||||||
|
isExcelImportMap,
|
||||||
|
type ExcelImportMap,
|
||||||
|
defaultExcelImportMap,
|
||||||
|
validateEndAction,
|
||||||
|
validateTimerType,
|
||||||
|
type ExcelImportOptions,
|
||||||
|
} from 'ontime-utils';
|
||||||
|
import { DatabaseModel, OntimeEvent, OntimeRundown, SupportedEvent, ProjectData, UserFields } from 'ontime-types';
|
||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
import xlsx from 'node-xlsx';
|
import xlsx from 'node-xlsx';
|
||||||
import { generateId, calculateDuration } from 'ontime-utils';
|
|
||||||
import {
|
|
||||||
DatabaseModel,
|
|
||||||
EndAction,
|
|
||||||
OntimeEvent,
|
|
||||||
OntimeRundown,
|
|
||||||
SupportedEvent,
|
|
||||||
TimerType,
|
|
||||||
UserFields,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { event as eventDef } from '../models/eventsDefinition.js';
|
import { event as eventDef } from '../models/eventsDefinition.js';
|
||||||
import { dbModel } from '../models/dataModel.js';
|
import { dbModel } from '../models/dataModel.js';
|
||||||
import { deleteFile, makeString } from './parserUtils.js';
|
import { deleteFile, makeString } from './parserUtils.js';
|
||||||
@@ -33,27 +33,55 @@ export const JSON_MIME = 'application/json';
|
|||||||
/**
|
/**
|
||||||
* @description Excel array parser
|
* @description Excel array parser
|
||||||
* @param {array} excelData - array with excel sheet
|
* @param {array} excelData - array with excel sheet
|
||||||
|
* @param {ExcelImportOptions} options - an object that contains the import map
|
||||||
* @returns {object} - parsed object
|
* @returns {object} - parsed object
|
||||||
*/
|
*/
|
||||||
export const parseExcel = async (excelData) => {
|
export const parseExcel = (excelData: unknown[][], options?: Partial<ExcelImportMap>) => {
|
||||||
|
const importMap: ExcelImportMap = { ...defaultExcelImportMap, ...options };
|
||||||
const projectData: Partial<ProjectData> = {
|
const projectData: Partial<ProjectData> = {
|
||||||
title: '',
|
title: '',
|
||||||
description: '',
|
description: '',
|
||||||
publicUrl: '',
|
publicUrl: '',
|
||||||
|
publicInfo: '',
|
||||||
backstageUrl: '',
|
backstageUrl: '',
|
||||||
|
backstageInfo: '',
|
||||||
|
};
|
||||||
|
const customUserFields: Partial<UserFields> = {
|
||||||
|
user0: importMap.user0,
|
||||||
|
user1: importMap.user1,
|
||||||
|
user2: importMap.user2,
|
||||||
|
user3: importMap.user3,
|
||||||
|
user4: importMap.user4,
|
||||||
|
user5: importMap.user5,
|
||||||
|
user6: importMap.user6,
|
||||||
|
user7: importMap.user7,
|
||||||
|
user8: importMap.user8,
|
||||||
|
user9: importMap.user9,
|
||||||
};
|
};
|
||||||
const customUserFields: Partial<UserFields> = {};
|
|
||||||
const rundown: OntimeRundown = [];
|
const rundown: OntimeRundown = [];
|
||||||
let timeStartIndex: number | null = null;
|
|
||||||
let timeEndIndex: number | null = null;
|
// title stuff: strings
|
||||||
let titleIndex: number | null = null;
|
let titleIndex: number | null = null;
|
||||||
let cueIndex: number | null = null;
|
let cueIndex: number | null = null;
|
||||||
let presenterIndex: number | null = null;
|
let presenterIndex: number | null = null;
|
||||||
let subtitleIndex: number | null = null;
|
let subtitleIndex: number | null = null;
|
||||||
let isPublicIndex: number | null = null;
|
|
||||||
let skipIndex: number | null = null;
|
|
||||||
let notesIndex: number | null = null;
|
let notesIndex: number | null = null;
|
||||||
let colourIndex: number | null = null;
|
let colourIndex: number | null = null;
|
||||||
|
|
||||||
|
// options: booleans
|
||||||
|
let isPublicIndex: number | null = null;
|
||||||
|
let skipIndex: number | null = null;
|
||||||
|
|
||||||
|
// times: numbers
|
||||||
|
// TODO: handle duration
|
||||||
|
let timeStartIndex: number | null = null;
|
||||||
|
let timeEndIndex: number | null = null;
|
||||||
|
|
||||||
|
// options: enum properties
|
||||||
|
let endActionIndex: number | null = null;
|
||||||
|
let timerTypeIndex: number | null = null;
|
||||||
|
|
||||||
|
// user fields: strings
|
||||||
let user0Index: number | null = null;
|
let user0Index: number | null = null;
|
||||||
let user1Index: number | null = null;
|
let user1Index: number | null = null;
|
||||||
let user2Index: number | null = null;
|
let user2Index: number | null = null;
|
||||||
@@ -64,12 +92,11 @@ export const parseExcel = async (excelData) => {
|
|||||||
let user7Index: number | null = null;
|
let user7Index: number | null = null;
|
||||||
let user8Index: number | null = null;
|
let user8Index: number | null = null;
|
||||||
let user9Index: number | null = null;
|
let user9Index: number | null = null;
|
||||||
let endActionIndex: number | null = null;
|
|
||||||
let timerTypeIndex: number | null = null;
|
|
||||||
|
|
||||||
excelData
|
excelData
|
||||||
.filter((e) => e.length > 0)
|
.filter((e) => e.length > 0)
|
||||||
.forEach((row) => {
|
.forEach((row) => {
|
||||||
|
// these fields contain the data to its right
|
||||||
let eventTitleNext = false;
|
let eventTitleNext = false;
|
||||||
let projectTitleNext = false;
|
let projectTitleNext = false;
|
||||||
let publicUrlNext = false;
|
let publicUrlNext = false;
|
||||||
@@ -80,24 +107,24 @@ export const parseExcel = async (excelData) => {
|
|||||||
const event: Partial<OntimeEvent> = {};
|
const event: Partial<OntimeEvent> = {};
|
||||||
|
|
||||||
row.forEach((column, j) => {
|
row.forEach((column, j) => {
|
||||||
// check flags
|
// 1. we check if we have set a flag for a known field
|
||||||
if (eventTitleNext) {
|
if (eventTitleNext) {
|
||||||
projectData.title = column;
|
projectData.title = makeString(column, '');
|
||||||
eventTitleNext = false;
|
eventTitleNext = false;
|
||||||
} else if (projectTitleNext) {
|
} else if (projectTitleNext) {
|
||||||
projectData.description = column;
|
projectData.description = makeString(column, '');
|
||||||
projectTitleNext = false;
|
projectTitleNext = false;
|
||||||
} else if (publicUrlNext) {
|
} else if (publicUrlNext) {
|
||||||
projectData.publicUrl = column;
|
projectData.publicUrl = makeString(column, '');
|
||||||
publicUrlNext = false;
|
publicUrlNext = false;
|
||||||
} else if (publicInfoNext) {
|
} else if (publicInfoNext) {
|
||||||
projectData.publicInfo = column;
|
projectData.publicInfo = makeString(column, '');
|
||||||
publicInfoNext = false;
|
publicInfoNext = false;
|
||||||
} else if (backstageUrlNext) {
|
} else if (backstageUrlNext) {
|
||||||
projectData.backstageUrl = column;
|
projectData.backstageUrl = makeString(column, '');
|
||||||
backstageUrlNext = false;
|
backstageUrlNext = false;
|
||||||
} else if (backstageInfoNext) {
|
} else if (backstageInfoNext) {
|
||||||
projectData.backstageInfo = column;
|
projectData.backstageInfo = makeString(column, '');
|
||||||
backstageInfoNext = false;
|
backstageInfoNext = false;
|
||||||
} else if (j === timeStartIndex) {
|
} else if (j === timeStartIndex) {
|
||||||
event.timeStart = parseExcelDate(column);
|
event.timeStart = parseExcelDate(column);
|
||||||
@@ -118,155 +145,127 @@ export const parseExcel = async (excelData) => {
|
|||||||
} else if (j === notesIndex) {
|
} else if (j === notesIndex) {
|
||||||
event.note = makeString(column, '');
|
event.note = makeString(column, '');
|
||||||
} else if (j === endActionIndex) {
|
} else if (j === endActionIndex) {
|
||||||
if (column === '') {
|
event.endAction = validateEndAction(column);
|
||||||
event.endAction = EndAction.None;
|
|
||||||
} else {
|
|
||||||
event.endAction = column;
|
|
||||||
}
|
|
||||||
} else if (j === timerTypeIndex) {
|
} else if (j === timerTypeIndex) {
|
||||||
if (column === '') {
|
event.timerType = validateTimerType(column);
|
||||||
event.timerType = TimerType.CountDown;
|
|
||||||
} else {
|
|
||||||
event.timerType = column;
|
|
||||||
}
|
|
||||||
} else if (j === colourIndex) {
|
} else if (j === colourIndex) {
|
||||||
event.colour = column;
|
event.colour = makeString(column, '');
|
||||||
} else if (j === user0Index) {
|
} else if (j === user0Index) {
|
||||||
event.user0 = column;
|
event.user0 = makeString(column, '');
|
||||||
} else if (j === user1Index) {
|
} else if (j === user1Index) {
|
||||||
event.user1 = column;
|
event.user1 = makeString(column, '');
|
||||||
} else if (j === user2Index) {
|
} else if (j === user2Index) {
|
||||||
event.user2 = column;
|
event.user2 = makeString(column, '');
|
||||||
} else if (j === user3Index) {
|
} else if (j === user3Index) {
|
||||||
event.user3 = column;
|
event.user3 = makeString(column, '');
|
||||||
} else if (j === user4Index) {
|
} else if (j === user4Index) {
|
||||||
event.user4 = column;
|
event.user4 = makeString(column, '');
|
||||||
} else if (j === user5Index) {
|
} else if (j === user5Index) {
|
||||||
event.user5 = column;
|
event.user5 = makeString(column, '');
|
||||||
} else if (j === user6Index) {
|
} else if (j === user6Index) {
|
||||||
event.user6 = column;
|
event.user6 = makeString(column, '');
|
||||||
} else if (j === user7Index) {
|
} else if (j === user7Index) {
|
||||||
event.user7 = column;
|
event.user7 = makeString(column, '');
|
||||||
} else if (j === user8Index) {
|
} else if (j === user8Index) {
|
||||||
event.user8 = column;
|
event.user8 = makeString(column, '');
|
||||||
} else if (j === user9Index) {
|
} else if (j === user9Index) {
|
||||||
event.user9 = column;
|
event.user9 = makeString(column, '');
|
||||||
} else {
|
} else {
|
||||||
|
// 2. if there is no flag, lets see if we know the field type
|
||||||
if (typeof column === 'string') {
|
if (typeof column === 'string') {
|
||||||
const col = column.toLowerCase();
|
const col = column.toLowerCase();
|
||||||
|
|
||||||
// look for keywords
|
// look for keywords
|
||||||
// need to make sure it is a string first
|
// need to make sure it is a string first
|
||||||
switch (col) {
|
switch (col) {
|
||||||
case 'project name':
|
case importMap.projectName:
|
||||||
eventTitleNext = true;
|
eventTitleNext = true;
|
||||||
break;
|
break;
|
||||||
case 'project description':
|
case importMap.projectDescription:
|
||||||
projectTitleNext = true;
|
projectTitleNext = true;
|
||||||
break;
|
break;
|
||||||
case 'public url':
|
case importMap.publicUrl:
|
||||||
publicUrlNext = true;
|
publicUrlNext = true;
|
||||||
break;
|
break;
|
||||||
case 'public info':
|
case importMap.publicInfo:
|
||||||
publicInfoNext = true;
|
publicInfoNext = true;
|
||||||
break;
|
break;
|
||||||
case 'backstage url':
|
case importMap.backstageUrl:
|
||||||
backstageUrlNext = true;
|
backstageUrlNext = true;
|
||||||
break;
|
break;
|
||||||
case 'backstage info':
|
case importMap.backstageInfo:
|
||||||
backstageInfoNext = true;
|
backstageInfoNext = true;
|
||||||
break;
|
break;
|
||||||
case 'time start':
|
case importMap.timeStart:
|
||||||
case 'start':
|
|
||||||
timeStartIndex = j;
|
timeStartIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'time end':
|
case importMap.timeEnd:
|
||||||
case 'end':
|
|
||||||
case 'finish':
|
|
||||||
timeEndIndex = j;
|
timeEndIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'cue':
|
case importMap.cue:
|
||||||
case 'page':
|
|
||||||
cueIndex = j;
|
cueIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'event title':
|
case importMap.title:
|
||||||
case 'title':
|
|
||||||
titleIndex = j;
|
titleIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'presenter name':
|
case importMap.presenter:
|
||||||
case 'speaker':
|
|
||||||
case 'presenter':
|
|
||||||
presenterIndex = j;
|
presenterIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'event subtitle':
|
case importMap.subtitle:
|
||||||
case 'subtitle':
|
|
||||||
subtitleIndex = j;
|
subtitleIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'is public? (x)':
|
case importMap.isPublic:
|
||||||
case 'is public':
|
|
||||||
case 'public':
|
|
||||||
isPublicIndex = j;
|
isPublicIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'skip? (x)':
|
case importMap.skip:
|
||||||
case 'skip?':
|
|
||||||
case 'skip':
|
|
||||||
skipIndex = j;
|
skipIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'note':
|
case importMap.note:
|
||||||
case 'notes':
|
|
||||||
notesIndex = j;
|
notesIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'colour':
|
case importMap.colour:
|
||||||
case 'color':
|
|
||||||
colourIndex = j;
|
colourIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'end action':
|
case importMap.endAction:
|
||||||
endActionIndex = j;
|
endActionIndex = j;
|
||||||
break;
|
break;
|
||||||
case 'timer type':
|
case importMap.timerType:
|
||||||
timerTypeIndex = j;
|
timerTypeIndex = j;
|
||||||
break;
|
break;
|
||||||
default:
|
case importMap.user0:
|
||||||
// look for user defined
|
user0Index = j;
|
||||||
if (col.startsWith('user')) {
|
|
||||||
const index = column.charAt(4);
|
|
||||||
// name is the bit after the :
|
|
||||||
const [, name] = column.split(':');
|
|
||||||
if (typeof name !== 'undefined') {
|
|
||||||
if (index === '0') {
|
|
||||||
customUserFields.user0 = name;
|
|
||||||
user0Index = j;
|
|
||||||
} else if (index === '1') {
|
|
||||||
customUserFields.user1 = name;
|
|
||||||
user1Index = j;
|
|
||||||
} else if (index === '2') {
|
|
||||||
customUserFields.user2 = name;
|
|
||||||
user2Index = j;
|
|
||||||
} else if (index === '3') {
|
|
||||||
customUserFields.user3 = name;
|
|
||||||
user3Index = j;
|
|
||||||
} else if (index === '4') {
|
|
||||||
customUserFields.user4 = name;
|
|
||||||
user4Index = j;
|
|
||||||
} else if (index === '5') {
|
|
||||||
customUserFields.user5 = name;
|
|
||||||
user5Index = j;
|
|
||||||
} else if (index === '6') {
|
|
||||||
customUserFields.user6 = name;
|
|
||||||
user6Index = j;
|
|
||||||
} else if (index === '7') {
|
|
||||||
customUserFields.user7 = name;
|
|
||||||
user7Index = j;
|
|
||||||
} else if (index === '8') {
|
|
||||||
customUserFields.user8 = name;
|
|
||||||
user8Index = j;
|
|
||||||
} else if (index === '9') {
|
|
||||||
customUserFields.user9 = name;
|
|
||||||
user9Index = j;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
|
case importMap.user1:
|
||||||
|
user1Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user2:
|
||||||
|
user2Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user3:
|
||||||
|
user3Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user4:
|
||||||
|
user4Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user5:
|
||||||
|
user5Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user6:
|
||||||
|
user6Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user7:
|
||||||
|
user7Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user8:
|
||||||
|
user8Index = j;
|
||||||
|
break;
|
||||||
|
case importMap.user9:
|
||||||
|
user9Index = j;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
// we don't know how to handle this column
|
||||||
|
// just ignore it
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,17 +284,16 @@ export const parseExcel = async (excelData) => {
|
|||||||
app: 'ontime',
|
app: 'ontime',
|
||||||
version: 2,
|
version: 2,
|
||||||
},
|
},
|
||||||
userFields: { ...dbModel.userFields, ...customUserFields },
|
userFields: customUserFields,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description JSON parser function for v1 of data system
|
* @description JSON parser function for ontime project file
|
||||||
* @param {object} jsonData - json data JSON object to be parsed
|
* @param {object} jsonData - project file to be parsed
|
||||||
* @param {boolean} [enforce=false] - flag, tells to create an object anyway
|
|
||||||
* @returns {object} - parsed object
|
* @returns {object} - parsed object
|
||||||
*/
|
*/
|
||||||
export const parseJson = async (jsonData, enforce = false): Promise<DatabaseModel | null> => {
|
export const parseJson = async (jsonData): Promise<DatabaseModel | null> => {
|
||||||
if (!jsonData || typeof jsonData !== 'object') {
|
if (!jsonData || typeof jsonData !== 'object') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -306,17 +304,17 @@ export const parseJson = async (jsonData, enforce = false): Promise<DatabaseMode
|
|||||||
// parse Events
|
// parse Events
|
||||||
returnData.rundown = parseRundown(jsonData);
|
returnData.rundown = parseRundown(jsonData);
|
||||||
// parse Event
|
// parse Event
|
||||||
returnData.project = parseProject(jsonData, enforce);
|
returnData.project = parseProject(jsonData) ?? dbModel.project;
|
||||||
// Settings handled partially
|
// Settings handled partially
|
||||||
returnData.settings = parseSettings(jsonData, enforce);
|
returnData.settings = parseSettings(jsonData) ?? dbModel.settings;
|
||||||
// View settings handled partially
|
// View settings handled partially
|
||||||
returnData.viewSettings = parseViewSettings(jsonData, enforce);
|
returnData.viewSettings = parseViewSettings(jsonData) ?? dbModel.viewSettings;
|
||||||
// Import Aliases if any
|
// Import Aliases if any
|
||||||
returnData.aliases = parseAliases(jsonData);
|
returnData.aliases = parseAliases(jsonData);
|
||||||
// Import user fields if any
|
// Import user fields if any
|
||||||
returnData.userFields = parseUserFields(jsonData);
|
returnData.userFields = parseUserFields(jsonData);
|
||||||
// Import OSC settings if any
|
// Import OSC settings if any
|
||||||
returnData.osc = parseOsc(jsonData, enforce);
|
returnData.osc = parseOsc(jsonData) ?? dbModel.osc;
|
||||||
// Import HTTP settings if any
|
// Import HTTP settings if any
|
||||||
// returnData.http = parseHttp(jsonData, enforce);
|
// returnData.http = parseHttp(jsonData, enforce);
|
||||||
|
|
||||||
@@ -379,68 +377,56 @@ export const validateEvent = (eventArgs: Partial<OntimeEvent>, cueFallback: stri
|
|||||||
return event;
|
return event;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResponseOK = { data: Partial<DatabaseModel>; message: 'success' };
|
type ResponseOK = {
|
||||||
type ResponseError = { error: true; message: string };
|
data: Partial<DatabaseModel>;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Middleware function that checks file type and calls relevant parser
|
* @description Middleware function that checks file type and calls relevant parser
|
||||||
* @param {string} file - reference to file
|
* @param {string} file - reference to file
|
||||||
|
* @param options - import options
|
||||||
* @return {object} - parse result message
|
* @return {object} - parse result message
|
||||||
*/
|
*/
|
||||||
export const fileHandler = async (file): Promise<ResponseOK | ResponseError> => {
|
export const fileHandler = async (file: string, options: ExcelImportOptions): Promise<Partial<ResponseOK>> => {
|
||||||
let res: Partial<ResponseOK | ResponseError> = {};
|
const res: Partial<ResponseOK> = {};
|
||||||
|
|
||||||
// check which file type are we dealing with
|
// check which file type are we dealing with
|
||||||
if (file.endsWith('.xlsx')) {
|
if (file.endsWith('.xlsx')) {
|
||||||
try {
|
// we need to check that the options are applicable
|
||||||
const excelData = xlsx
|
if (!isExcelImportMap(options)) {
|
||||||
.parse(file, { cellDates: true })
|
throw new Error('Got incorrect options to excel import', JSON.parse(options));
|
||||||
.find(({ name }) => name.toLowerCase() === 'ontime' || name.toLowerCase() === 'event schedule');
|
}
|
||||||
|
|
||||||
// we only look at worksheets called ontime or event schedule
|
const excelData = xlsx
|
||||||
if (excelData?.data) {
|
.parse(file, { cellDates: true })
|
||||||
const dataFromExcel = await parseExcel(excelData.data);
|
.find(({ name }) => name.toLowerCase() === options.worksheet);
|
||||||
res.data = {};
|
|
||||||
res.data.rundown = parseRundown(dataFromExcel);
|
if (excelData?.data) {
|
||||||
res.data.project = parseProject(dataFromExcel, true);
|
const dataFromExcel = parseExcel(excelData.data, options);
|
||||||
res.data.userFields = parseUserFields(dataFromExcel);
|
// we run the parsed data through an extra step to ensure the objects shape
|
||||||
res.message = 'success';
|
res.data = {};
|
||||||
} else {
|
res.data.rundown = parseRundown(dataFromExcel);
|
||||||
const errorMessage = 'No sheet found named "ontime" or "event schedule"';
|
res.data.project = parseProject(dataFromExcel);
|
||||||
res = {
|
res.data.userFields = parseUserFields(dataFromExcel);
|
||||||
error: true,
|
return res;
|
||||||
message: errorMessage,
|
} else {
|
||||||
};
|
throw new Error(`Could not find data to import, maybe the worksheet name is incorrect: ${options.worksheet}`);
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res = { error: true, message: `Error parsing file: ${error}` };
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (file.endsWith('.json')) {
|
if (file.endsWith('.json')) {
|
||||||
// if json check version
|
// if json check version
|
||||||
const rawdata = fs.readFileSync(file);
|
const rawdata = fs.readFileSync(file).toString();
|
||||||
let uploadedJson = null;
|
let uploadedJson = null;
|
||||||
|
|
||||||
try {
|
uploadedJson = JSON.parse(rawdata);
|
||||||
uploadedJson = JSON.parse(rawdata);
|
if (uploadedJson.settings.version !== 2) {
|
||||||
} catch (error) {
|
throw new Error(`Project version unknown ${uploadedJson.settings.version}`);
|
||||||
return { error: true, message: 'Error parsing JSON file' };
|
|
||||||
}
|
}
|
||||||
|
res.data = await parseJson(uploadedJson);
|
||||||
|
|
||||||
if (uploadedJson.settings.version === 2) {
|
// delete file
|
||||||
try {
|
await deleteFile(file);
|
||||||
res.data = await parseJson(uploadedJson);
|
return res;
|
||||||
res.message = 'success';
|
|
||||||
} catch (error) {
|
|
||||||
res = { error: true, message: `Error parsing file: ${error}` };
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res = { error: true, message: 'Error parsing file, version unknown' };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// delete file
|
|
||||||
await deleteFile(file);
|
|
||||||
return res;
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,10 +88,9 @@ export const parseRundown = (data): OntimeRundown => {
|
|||||||
/**
|
/**
|
||||||
* Parse event portion of an entry
|
* Parse event portion of an entry
|
||||||
* @param {object} data - data object
|
* @param {object} data - data object
|
||||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
|
||||||
* @returns {object} - event object data
|
* @returns {object} - event object data
|
||||||
*/
|
*/
|
||||||
export const parseProject = (data, enforce): ProjectData => {
|
export const parseProject = (data): ProjectData => {
|
||||||
let newProjectData: Partial<ProjectData> = {};
|
let newProjectData: Partial<ProjectData> = {};
|
||||||
// we are adding this here to aid transition, should be removed once enough time has past that users have fully migrated
|
// we are adding this here to aid transition, should be removed once enough time has past that users have fully migrated
|
||||||
// TODO: Remove eventually
|
// TODO: Remove eventually
|
||||||
@@ -109,9 +108,6 @@ export const parseProject = (data, enforce): ProjectData => {
|
|||||||
backstageUrl: project.backstageUrl || dbModel.project.backstageUrl,
|
backstageUrl: project.backstageUrl || dbModel.project.backstageUrl,
|
||||||
backstageInfo: project.backstageInfo || dbModel.project.backstageInfo,
|
backstageInfo: project.backstageInfo || dbModel.project.backstageInfo,
|
||||||
};
|
};
|
||||||
} else if (enforce) {
|
|
||||||
newProjectData = { ...dbModel.project };
|
|
||||||
console.log('Created project object in db');
|
|
||||||
}
|
}
|
||||||
return newProjectData as ProjectData;
|
return newProjectData as ProjectData;
|
||||||
};
|
};
|
||||||
@@ -119,10 +115,9 @@ export const parseProject = (data, enforce): ProjectData => {
|
|||||||
/**
|
/**
|
||||||
* Parse settings portion of an entry
|
* Parse settings portion of an entry
|
||||||
* @param {object} data - data object
|
* @param {object} data - data object
|
||||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
|
||||||
* @returns {object} - event object data
|
* @returns {object} - event object data
|
||||||
*/
|
*/
|
||||||
export const parseSettings = (data, enforce): Settings => {
|
export const parseSettings = (data): Settings => {
|
||||||
let newSettings: Partial<Settings> = {};
|
let newSettings: Partial<Settings> = {};
|
||||||
if ('settings' in data) {
|
if ('settings' in data) {
|
||||||
console.log('Found settings definition, importing...');
|
console.log('Found settings definition, importing...');
|
||||||
@@ -146,9 +141,6 @@ export const parseSettings = (data, enforce): Settings => {
|
|||||||
...settings,
|
...settings,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else if (enforce) {
|
|
||||||
newSettings = dbModel.settings;
|
|
||||||
console.log('Created settings object in db');
|
|
||||||
}
|
}
|
||||||
return newSettings as Settings;
|
return newSettings as Settings;
|
||||||
};
|
};
|
||||||
@@ -156,10 +148,9 @@ export const parseSettings = (data, enforce): Settings => {
|
|||||||
/**
|
/**
|
||||||
* Parse settings portion of an entry
|
* Parse settings portion of an entry
|
||||||
* @param {object} data - data object
|
* @param {object} data - data object
|
||||||
* @param {boolean} enforce - whether to create a definition if one is missing
|
|
||||||
* @returns {object} - event object data
|
* @returns {object} - event object data
|
||||||
*/
|
*/
|
||||||
export const parseViewSettings = (data, enforce): ViewSettings => {
|
export const parseViewSettings = (data): ViewSettings => {
|
||||||
let newViews: Partial<ViewSettings> = {};
|
let newViews: Partial<ViewSettings> = {};
|
||||||
if ('viewSettings' in data) {
|
if ('viewSettings' in data) {
|
||||||
console.log('Found view definition, importing...');
|
console.log('Found view definition, importing...');
|
||||||
@@ -175,13 +166,7 @@ export const parseViewSettings = (data, enforce): ViewSettings => {
|
|||||||
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
|
endMessage: v.endMessage ?? dbModel.viewSettings.endMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
// write to db
|
newViews = { ...viewSettings };
|
||||||
newViews = {
|
|
||||||
...viewSettings,
|
|
||||||
};
|
|
||||||
} else if (enforce) {
|
|
||||||
newViews = dbModel.viewSettings;
|
|
||||||
console.log('Created viewSettings object in db');
|
|
||||||
}
|
}
|
||||||
return newViews as ViewSettings;
|
return newViews as ViewSettings;
|
||||||
};
|
};
|
||||||
@@ -224,16 +209,11 @@ export const validateOscObject = (data: OscSubscription): boolean => {
|
|||||||
/**
|
/**
|
||||||
* Parse osc portion of an entry
|
* Parse osc portion of an entry
|
||||||
*/
|
*/
|
||||||
export const parseOsc = (
|
export const parseOsc = (data: { osc?: Partial<OSCSettings> }): OSCSettings => {
|
||||||
data: {
|
|
||||||
osc?: Partial<OSCSettings>;
|
|
||||||
},
|
|
||||||
enforce: boolean,
|
|
||||||
): OSCSettings | Record<string, never> => {
|
|
||||||
if ('osc' in data) {
|
if ('osc' in data) {
|
||||||
console.log('Found OSC definition, importing...');
|
console.log('Found OSC definition, importing...');
|
||||||
|
|
||||||
const loadedConfig = data?.osc || {};
|
const loadedConfig = data.osc || {};
|
||||||
const validatedSubscriptions = validateOscObject(loadedConfig.subscriptions)
|
const validatedSubscriptions = validateOscObject(loadedConfig.subscriptions)
|
||||||
? loadedConfig.subscriptions
|
? loadedConfig.subscriptions
|
||||||
: dbModel.osc.subscriptions;
|
: dbModel.osc.subscriptions;
|
||||||
@@ -246,10 +226,7 @@ export const parseOsc = (
|
|||||||
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
|
enabledOut: loadedConfig.enabledOut ?? dbModel.osc.enabledOut,
|
||||||
subscriptions: validatedSubscriptions,
|
subscriptions: validatedSubscriptions,
|
||||||
};
|
};
|
||||||
} else if (enforce) {
|
}
|
||||||
console.log('Created OSC object in db');
|
|
||||||
return { ...dbModel.osc };
|
|
||||||
} else return {};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
import { deepmerge } from 'ontime-utils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Ensures variable is string, it skips object types
|
* @description Ensures variable is string, it skips object types
|
||||||
@@ -52,16 +53,30 @@ export const isEmptyObject = (obj: object) => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Merges two objects, suppressing undefined keys
|
* @description Merges two objects, suppressing undefined keys
|
||||||
* @param {object} a
|
* @param {object} a - any object
|
||||||
* @param {object} b
|
* @param {object} b - a potential partial object of same time as a
|
||||||
*/
|
*/
|
||||||
export const mergeObject = (a, b) => {
|
export function mergeObject<T extends Record<string, any>>(a: T, b: Partial<Record<keyof T, any>>): T {
|
||||||
const merged = {};
|
const merged = { ...a };
|
||||||
Object.keys({ ...a, ...b }).map((key) => {
|
|
||||||
merged[key] = typeof b[key] === 'undefined' ? a[key] : b[key];
|
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 -- library side, ignore for now
|
||||||
|
merged[key] = deepmerge(aValue, bValue);
|
||||||
|
} else if (bValue !== undefined) {
|
||||||
|
merged[key] = bValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
return merged;
|
return merged;
|
||||||
};
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description Removes undefined
|
* @description Removes undefined
|
||||||
|
|||||||
@@ -23,6 +23,12 @@ export { deepmerge } from './src/externals/deepmerge.js';
|
|||||||
|
|
||||||
// generic utilities
|
// generic utilities
|
||||||
export { isNumeric } from './src/types/types.js';
|
export { isNumeric } from './src/types/types.js';
|
||||||
|
|
||||||
|
// model validation
|
||||||
|
export { validateEndAction, validateTimerType } from './src/validate-events/validateEvent.js';
|
||||||
|
|
||||||
|
// feature business logic
|
||||||
|
|
||||||
// feature business logic - excel import
|
// feature business logic - excel import
|
||||||
export {
|
export {
|
||||||
type ExcelImportMap,
|
type ExcelImportMap,
|
||||||
|
|||||||
Reference in New Issue
Block a user