mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 14:14:17 +00:00
chore: create feature endpoints
This commit is contained in:
@@ -2,6 +2,7 @@ import axios, { AxiosError } from 'axios';
|
|||||||
import { LogLevel } from 'ontime-types';
|
import { LogLevel } from 'ontime-types';
|
||||||
import { generateId, millisToString } from 'ontime-utils';
|
import { generateId, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
import { nowInMillis } from '../utils/time';
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
@@ -34,3 +35,16 @@ export function logAxiosError(prepend: string, error: unknown) {
|
|||||||
text: message,
|
text: message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function invalidateAllCaches() {
|
||||||
|
await ontimeQueryClient.invalidateQueries([
|
||||||
|
'project',
|
||||||
|
'aliases',
|
||||||
|
'userFields',
|
||||||
|
'rundown',
|
||||||
|
'appinfo',
|
||||||
|
'oscSettings',
|
||||||
|
'appSettings',
|
||||||
|
'viewSettings',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosResponse } from 'axios';
|
||||||
import { Alias, OSCSettings, OscSubscription, ProjectData, Settings, UserFields, ViewSettings } from 'ontime-types';
|
import {
|
||||||
|
Alias,
|
||||||
|
DatabaseModel,
|
||||||
|
OntimeRundown,
|
||||||
|
OSCSettings,
|
||||||
|
OscSubscription,
|
||||||
|
ProjectData,
|
||||||
|
Settings,
|
||||||
|
UserFields,
|
||||||
|
ViewSettings,
|
||||||
|
} from 'ontime-types';
|
||||||
|
import { ExcelImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
import { apiRepoLatest } from '../../externals';
|
||||||
import { InfoType } from '../models/Info';
|
import { InfoType } from '../models/Info';
|
||||||
@@ -137,17 +148,26 @@ export const downloadRundown = async () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: should this be extracted to shared code?
|
||||||
|
export type ProjectFileImportOptions = {
|
||||||
|
onlyRundown: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @description HTTP request to upload events db
|
* @description HTTP request to upload events db
|
||||||
* @return {Promise}
|
* @return {Promise}
|
||||||
*/
|
*/
|
||||||
type UploadDataOptions = {
|
export const uploadProjectFile = async (
|
||||||
onlyRundown?: boolean;
|
file: File,
|
||||||
};
|
setProgress: (value: number) => void,
|
||||||
export const uploadData = async (file: File, setProgress: (value: number) => void, options?: UploadDataOptions) => {
|
options?: Partial<ProjectFileImportOptions>,
|
||||||
|
) => {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append('userFile', file);
|
formData.append('userFile', file);
|
||||||
const onlyRundown = options?.onlyRundown || 'false';
|
|
||||||
|
const onlyRundown = Boolean(options?.onlyRundown);
|
||||||
|
console.log('debug here', onlyRundown, options);
|
||||||
|
|
||||||
await axios
|
await axios
|
||||||
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
||||||
headers: {
|
headers: {
|
||||||
@@ -161,6 +181,47 @@ export const uploadData = async (file: File, setProgress: (value: number) => voi
|
|||||||
.then((response) => response.data.id);
|
.then((response) => response.data.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Make patch changes to the objects in the db
|
||||||
|
* @return {Promise}
|
||||||
|
*/
|
||||||
|
export async function patchData(patchDb: Partial<DatabaseModel>) {
|
||||||
|
const response = await axios.patch(`${ontimeURL}/db`, patchDb);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostPreviewExcelResponse = {
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
project: ProjectData;
|
||||||
|
userFields: UserFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Make patch changes to the objects in the db
|
||||||
|
* @return {Promise} - returns parsed rundown and userfields
|
||||||
|
*/
|
||||||
|
export async function postPreviewExcel(file: File, setProgress: (value: number) => void, options?: ExcelImportMap) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('userFile', file);
|
||||||
|
formData.append('options', JSON.stringify(options));
|
||||||
|
|
||||||
|
const response: AxiosResponse<PostPreviewExcelResponse> = await axios.post(
|
||||||
|
`${ontimeURL}/preview-spreadsheet`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
onUploadProgress: (progressEvent) => {
|
||||||
|
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
|
||||||
|
setProgress(complete);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
export type HasUpdate = {
|
export type HasUpdate = {
|
||||||
url: string;
|
url: string;
|
||||||
version: string;
|
version: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Alias, LogOrigin, ProjectData } from 'ontime-types';
|
import { Alias, DatabaseModel, LogOrigin, ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { RequestHandler } from 'express';
|
import { RequestHandler } from 'express';
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
@@ -7,13 +7,15 @@ import { networkInterfaces } from 'os';
|
|||||||
import { fileHandler } from '../utils/parser.js';
|
import { fileHandler } from '../utils/parser.js';
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
||||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
||||||
import { mergeObject } from '../utils/parserUtils.js';
|
|
||||||
import { PlaybackService } from '../services/PlaybackService.js';
|
import { PlaybackService } from '../services/PlaybackService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { isDocker, resolveDbPath } from '../setup.js';
|
import { isDocker, resolveDbPath } from '../setup.js';
|
||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { deleteAllEvents, forceReset } from '../services/rundown-service/RundownService.js';
|
import { deleteAllEvents, notifyChanges } from '../services/rundown-service/RundownService.js';
|
||||||
|
import { deepmerge } from 'ontime-utils';
|
||||||
|
import { runtimeCacheStore } from '../stores/cachingStore.js';
|
||||||
|
import { delayedRundownCacheKey } from '../services/rundown-service/delayedRundown.utils.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/poll'
|
// Create controller for GET request to '/ontime/poll'
|
||||||
// Returns data for current state
|
// Returns data for current state
|
||||||
@@ -43,44 +45,43 @@ export const dbDownload = async (req, res) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO: docs
|
||||||
|
// TODO: cleanup usage
|
||||||
/**
|
/**
|
||||||
* handles file upload
|
* Parses a file and returns the result objects
|
||||||
|
* @param file
|
||||||
|
* @param _req
|
||||||
|
* @param _res
|
||||||
|
* @param options
|
||||||
|
*/
|
||||||
|
async function parseFile(file, _req, _res, options) {
|
||||||
|
if (!fs.existsSync(file)) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
const result = await fileHandler(file, options);
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* parse an uploaded file and apply its parsed objects
|
||||||
* @param file
|
* @param file
|
||||||
* @param req
|
* @param req
|
||||||
* @param res
|
* @param res
|
||||||
* @param [options]
|
* @param [options]
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
const uploadAndParse = async (file, req, res, options) => {
|
const parseAndApply = async (file, _req, res, options) => {
|
||||||
if (!fs.existsSync(file)) {
|
const result = await parseFile(file, _req, res, options);
|
||||||
res.status(500).send({ message: 'Upload failed' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
PlaybackService.stop();
|
||||||
const result = await fileHandler(file);
|
|
||||||
|
|
||||||
if ('error' in result && result.error) {
|
const newRundown = result.rundown || [];
|
||||||
res.status(400).send({ message: result.message });
|
if (options?.onlyRundown === 'true') {
|
||||||
} else if ('data' in result && result.message === 'success') {
|
await DataProvider.setRundown(newRundown);
|
||||||
PlaybackService.stop();
|
} else {
|
||||||
// explicitly write objects
|
await DataProvider.mergeIntoData(result);
|
||||||
if (typeof result !== 'undefined') {
|
|
||||||
const newRundown = result.data.rundown || [];
|
|
||||||
if (options?.onlyRundown === 'true') {
|
|
||||||
await DataProvider.setRundown(newRundown);
|
|
||||||
} else {
|
|
||||||
await DataProvider.mergeIntoData(result.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
forceReset();
|
|
||||||
res.sendStatus(200);
|
|
||||||
} else {
|
|
||||||
res.status(400).send({ message: 'Failed parsing, no data' });
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: `Failed parsing ${error}` });
|
|
||||||
}
|
}
|
||||||
|
notifyChanges({ timer: true, external: true, reset: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -169,7 +170,7 @@ export const postUserFields = async (req, res) => {
|
|||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const persistedData = DataProvider.getUserFields();
|
const persistedData = DataProvider.getUserFields();
|
||||||
const newData = mergeObject(persistedData, req.body);
|
const newData = deepmerge(persistedData, req.body);
|
||||||
await DataProvider.setUserFields(newData);
|
await DataProvider.setUserFields(newData);
|
||||||
res.status(200).send(newData);
|
res.status(200).send(newData);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -319,8 +320,38 @@ export const postOSC = async (req, res) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/db'
|
export async function patchPartialProjectFile(req, res) {
|
||||||
// Returns -
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const patchDb: Partial<DatabaseModel> = {
|
||||||
|
project: req.body?.project,
|
||||||
|
settings: req.body?.settings,
|
||||||
|
viewSettings: req.body?.viewSettings,
|
||||||
|
osc: req.body?.osc,
|
||||||
|
aliases: req.body?.aliases,
|
||||||
|
userFields: req.body?.userFields,
|
||||||
|
rundown: req.body?.rundown,
|
||||||
|
};
|
||||||
|
|
||||||
|
await DataProvider.mergeIntoData(patchDb);
|
||||||
|
if (patchDb.rundown !== undefined) {
|
||||||
|
// it is likely cheaper to invalidate cache than to calculate diff
|
||||||
|
PlaybackService.stop();
|
||||||
|
runtimeCacheStore.invalidate(delayedRundownCacheKey);
|
||||||
|
notifyChanges({ external: true, reset: true });
|
||||||
|
}
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uploads and parses a given file
|
||||||
|
*/
|
||||||
export const dbUpload = async (req, res) => {
|
export const dbUpload = async (req, res) => {
|
||||||
if (!req.file) {
|
if (!req.file) {
|
||||||
res.status(400).send({ message: 'File not found' });
|
res.status(400).send({ message: 'File not found' });
|
||||||
@@ -328,10 +359,39 @@ export const dbUpload = async (req, res) => {
|
|||||||
}
|
}
|
||||||
const options = req.query;
|
const options = req.query;
|
||||||
const file = req.file.path;
|
const file = req.file.path;
|
||||||
await uploadAndParse(file, req, res, options);
|
try {
|
||||||
|
await parseAndApply(file, req, res, options);
|
||||||
|
res.status(200).send();
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: `Failed parsing ${error}` });
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/new'
|
/**
|
||||||
|
* uploads and parses an excel file
|
||||||
|
* @returns parsed result
|
||||||
|
*/
|
||||||
|
export async function previewExcel(req, res) {
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).send({ message: 'File not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = JSON.parse(req.body.options);
|
||||||
|
const file = req.file.path;
|
||||||
|
const data = await parseFile(file, req, res, options);
|
||||||
|
res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meant to create a new project file, it will clear only fields which are specific to a project
|
||||||
|
* @param req
|
||||||
|
* @param res
|
||||||
|
*/
|
||||||
export const postNew: RequestHandler = async (req, res) => {
|
export const postNew: RequestHandler = async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const newProjectData: ProjectData = {
|
const newProjectData: ProjectData = {
|
||||||
|
|||||||
@@ -118,3 +118,18 @@ export const validateOscSubscription = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
export const validatePatchProjectFile = [
|
||||||
|
body('rundown').isArray().optional({ nullable: false }),
|
||||||
|
body('project').isObject().optional({ nullable: false }),
|
||||||
|
body('settings').isObject().optional({ nullable: false }),
|
||||||
|
body('viewSettings').isObject().optional({ nullable: false }),
|
||||||
|
body('aliases').isArray().optional({ nullable: false }),
|
||||||
|
body('userFields').isObject().optional({ nullable: false }),
|
||||||
|
body('osc').isObject().optional({ nullable: false }),
|
||||||
|
(req, res, next) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
getSettings,
|
getSettings,
|
||||||
getUserFields,
|
getUserFields,
|
||||||
getViewSettings,
|
getViewSettings,
|
||||||
|
patchPartialProjectFile,
|
||||||
poll,
|
poll,
|
||||||
postAliases,
|
postAliases,
|
||||||
postNew,
|
postNew,
|
||||||
@@ -17,12 +18,14 @@ import {
|
|||||||
postSettings,
|
postSettings,
|
||||||
postUserFields,
|
postUserFields,
|
||||||
postViewSettings,
|
postViewSettings,
|
||||||
|
previewExcel,
|
||||||
} from '../controllers/ontimeController.js';
|
} from '../controllers/ontimeController.js';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
validateAliases,
|
validateAliases,
|
||||||
validateOSC,
|
validateOSC,
|
||||||
validateOscSubscription,
|
validateOscSubscription,
|
||||||
|
validatePatchProjectFile,
|
||||||
validateSettings,
|
validateSettings,
|
||||||
validateUserFields,
|
validateUserFields,
|
||||||
viewValidator,
|
viewValidator,
|
||||||
@@ -40,6 +43,12 @@ router.get('/db', dbDownload);
|
|||||||
// create route between controller and '/ontime/db' endpoint
|
// create route between controller and '/ontime/db' endpoint
|
||||||
router.post('/db', uploadFile, dbUpload);
|
router.post('/db', uploadFile, dbUpload);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/excel' endpoint
|
||||||
|
router.patch('/db', validatePatchProjectFile, patchPartialProjectFile);
|
||||||
|
|
||||||
|
// create route between controller and '/ontime/preview-spreadsheet' endpoint
|
||||||
|
router.post('/preview-spreadsheet', uploadFile, previewExcel);
|
||||||
|
|
||||||
// create route between controller and '/ontime/settings' endpoint
|
// create route between controller and '/ontime/settings' endpoint
|
||||||
router.get('/settings', getSettings);
|
router.get('/settings', getSettings);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user