mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-19 22:24:11 +00:00
refactor: organise API around resources (#798)
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { Alias } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const aliasesPath = `${apiEntryUrl}/aliases`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve aliases
|
||||||
|
*/
|
||||||
|
export async function getAliases(): Promise<Alias[]> {
|
||||||
|
const res = await axios.get(aliasesPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate aliases
|
||||||
|
*/
|
||||||
|
export async function postAliases(data: Alias[]): Promise<Alias[]> {
|
||||||
|
return axios.post(aliasesPath, data);
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import axios, { AxiosError } from 'axios';
|
|
||||||
import { LogLevel } from 'ontime-types';
|
|
||||||
import { generateId, millisToString } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
|
||||||
import { addLog } from '../stores/logger';
|
|
||||||
import { nowInMillis } from '../utils/time';
|
|
||||||
|
|
||||||
export function maybeAxiosError(error: unknown) {
|
|
||||||
if (axios.isAxiosError(error)) {
|
|
||||||
const statusText = (error as AxiosError).response?.statusText ?? '';
|
|
||||||
let data = (error as AxiosError).response?.data ?? '';
|
|
||||||
if (typeof data === 'object') {
|
|
||||||
if ('message' in data) {
|
|
||||||
data = JSON.stringify(data.message);
|
|
||||||
} else {
|
|
||||||
data = JSON.stringify(data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return `${statusText}: ${data}`;
|
|
||||||
} else {
|
|
||||||
if (typeof error !== 'string') {
|
|
||||||
return JSON.stringify(error);
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function logAxiosError(prepend: string, error: unknown) {
|
|
||||||
const message = `${prepend}: ${maybeAxiosError(error)}`;
|
|
||||||
|
|
||||||
addLog({
|
|
||||||
id: generateId(),
|
|
||||||
origin: 'SERVER',
|
|
||||||
time: millisToString(nowInMillis()),
|
|
||||||
level: LogLevel.Error,
|
|
||||||
text: message,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility function invalidates react-query caches
|
|
||||||
*/
|
|
||||||
export async function invalidateAllCaches() {
|
|
||||||
await ontimeQueryClient.invalidateQueries();
|
|
||||||
}
|
|
||||||
+5
-1
@@ -1,4 +1,4 @@
|
|||||||
// REST stuff
|
// keys in tanstack store
|
||||||
export const ALIASES = ['aliases'];
|
export const ALIASES = ['aliases'];
|
||||||
export const APP_INFO = ['appinfo'];
|
export const APP_INFO = ['appinfo'];
|
||||||
export const APP_SETTINGS = ['appSettings'];
|
export const APP_SETTINGS = ['appSettings'];
|
||||||
@@ -12,16 +12,20 @@ export const SHEET_STATE = ['sheetState'];
|
|||||||
export const CUSTOM_FIELDS = ['customFields'];
|
export const CUSTOM_FIELDS = ['customFields'];
|
||||||
export const VIEW_SETTINGS = ['viewSettings'];
|
export const VIEW_SETTINGS = ['viewSettings'];
|
||||||
|
|
||||||
|
// resolve location
|
||||||
const location = window.location;
|
const location = window.location;
|
||||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||||
export const isProduction = import.meta.env.MODE === 'production';
|
export const isProduction = import.meta.env.MODE === 'production';
|
||||||
export const isDev = !isProduction;
|
export const isDev = !isProduction;
|
||||||
|
|
||||||
|
// resolve port
|
||||||
const STATIC_PORT = 4001;
|
const STATIC_PORT = 4001;
|
||||||
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
||||||
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
||||||
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
||||||
|
|
||||||
|
export const apiEntryUrl = `${serverURL}/data`;
|
||||||
|
|
||||||
export const projectDataURL = `${serverURL}/project`;
|
export const projectDataURL = `${serverURL}/project`;
|
||||||
export const rundownURL = `${serverURL}/events`;
|
export const rundownURL = `${serverURL}/events`;
|
||||||
export const ontimeURL = `${serverURL}/ontime`;
|
export const ontimeURL = `${serverURL}/ontime`;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { CustomField, CustomFieldLabel, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const customFieldsPath = `${apiEntryUrl}/custom-fields`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests list of known custom fields
|
||||||
|
*/
|
||||||
|
export async function getCustomFields(): Promise<CustomFields> {
|
||||||
|
const res = await axios.get(customFieldsPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets list of known custom fields
|
||||||
|
*/
|
||||||
|
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
||||||
|
const res = await axios.post(customFieldsPath, { ...newField });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Edits single custom field
|
||||||
|
*/
|
||||||
|
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
||||||
|
const res = await axios.put(`${customFieldsPath}/${label}`, { ...newField });
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes single custom field
|
||||||
|
*/
|
||||||
|
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
||||||
|
const res = await axios.delete(`${customFieldsPath}/${label}`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import {
|
||||||
|
CustomFields,
|
||||||
|
DatabaseModel,
|
||||||
|
GetInfo,
|
||||||
|
MessageResponse,
|
||||||
|
OntimeRundown,
|
||||||
|
ProjectData,
|
||||||
|
ProjectFileListResponse,
|
||||||
|
} from 'ontime-types';
|
||||||
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
import fileDownload from './utils';
|
||||||
|
|
||||||
|
const dbPath = `${apiEntryUrl}/db`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to download db in JSON format
|
||||||
|
*/
|
||||||
|
export async function downloadRundown(fileName?: string) {
|
||||||
|
return fileDownload(
|
||||||
|
dbPath,
|
||||||
|
{ name: fileName ?? 'rundown', type: 'json' },
|
||||||
|
{ type: 'application/json;charset=utf-8;' },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to download db in CSV format
|
||||||
|
*/
|
||||||
|
export async function downloadCSV(fileName?: string) {
|
||||||
|
return fileDownload(dbPath, { name: fileName ?? 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to upload project file
|
||||||
|
*/
|
||||||
|
export async function uploadProjectFile(file: File): Promise<MessageResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('project', file);
|
||||||
|
const response = await axios.post(`${dbPath}/upload`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make patch changes to the objects in the db
|
||||||
|
*/
|
||||||
|
export async function patchData(patchDb: Partial<DatabaseModel>): Promise<AxiosResponse<DatabaseModel>> {
|
||||||
|
return await axios.patch(dbPath, patchDb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to create a project file
|
||||||
|
*/
|
||||||
|
export async function createProject(
|
||||||
|
project: Partial<
|
||||||
|
ProjectData & {
|
||||||
|
filename: string;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
): Promise<MessageResponse> {
|
||||||
|
const res = await axios.post(`${dbPath}/new`, project);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get the list of available project files
|
||||||
|
*/
|
||||||
|
export async function getProjects(): Promise<ProjectFileListResponse> {
|
||||||
|
const res = await axios.get(`${dbPath}/all`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to load a project file
|
||||||
|
*/
|
||||||
|
export async function loadProject(filename: string): Promise<MessageResponse> {
|
||||||
|
const res = await axios.post(`${dbPath}/load`, {
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to duplicate a project file
|
||||||
|
*/
|
||||||
|
export async function duplicateProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${dbPath}/${filename}/duplicate`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.post(decodedUrl, {
|
||||||
|
newFilename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to rename a project file
|
||||||
|
*/
|
||||||
|
export async function renameProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${dbPath}/${filename}/rename`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.put(decodedUrl, {
|
||||||
|
newFilename,
|
||||||
|
});
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete a project file
|
||||||
|
*/
|
||||||
|
export async function deleteProject(filename: string): Promise<MessageResponse> {
|
||||||
|
const url = `${dbPath}/${filename}`;
|
||||||
|
const decodedUrl = decodeURIComponent(url);
|
||||||
|
const res = await axios.delete(decodedUrl);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve application info
|
||||||
|
*/
|
||||||
|
export async function getInfo(): Promise<GetInfo> {
|
||||||
|
const res = await axios.get(`${dbPath}/info`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
type PreviewSpreadsheetResponse = {
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make patch changes to the objects in the db
|
||||||
|
*/
|
||||||
|
export async function importSpreadsheetPreview(file: File, options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('spreadsheet', file);
|
||||||
|
formData.append('options', JSON.stringify(options));
|
||||||
|
|
||||||
|
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
||||||
|
`${dbPath}/spreadsheet/preview`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
}
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { OntimeEvent, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
|
||||||
|
|
||||||
import { rundownURL } from './apiConstants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to fetch all events
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function fetchCachedRundown(): Promise<RundownCached> {
|
|
||||||
const res = await axios.get(`${rundownURL}/cached`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use fetchCachedRundown instead
|
|
||||||
* @description HTTP request to fetch all events
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function fetchRundown(): Promise<OntimeRundown> {
|
|
||||||
const res = await axios.get(rundownURL);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to post new event
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestPostEvent(data: Partial<OntimeRundownEntry>) {
|
|
||||||
return axios.post(rundownURL, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to put new event
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestPutEvent(data: Partial<OntimeRundownEntry>) {
|
|
||||||
return axios.put(rundownURL, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
type BatchEditEntry = {
|
|
||||||
data: Partial<OntimeEvent>;
|
|
||||||
ids: string[];
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to put multiple events
|
|
||||||
* @returns {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestBatchPutEvents(data: BatchEditEntry) {
|
|
||||||
return axios.put(`${rundownURL}/batchEdit`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ReorderEntry = {
|
|
||||||
eventId: string;
|
|
||||||
from: number;
|
|
||||||
to: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to reorder events
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestReorderEvent(data: ReorderEntry) {
|
|
||||||
return axios.patch(`${rundownURL}/reorder`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to request application of delay
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestApplyDelay(eventId: string) {
|
|
||||||
return axios.patch(`${rundownURL}/applydelay/${eventId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export type SwapEntry = {
|
|
||||||
from: string;
|
|
||||||
to: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to swap two events
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestEventSwap(data: SwapEntry) {
|
|
||||||
return axios.patch(`${rundownURL}/swap`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to delete given event
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestDelete(eventId: string) {
|
|
||||||
return axios.delete(`${rundownURL}/${eventId}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to delete all events
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function requestDeleteAll() {
|
|
||||||
return axios.delete(`${rundownURL}/all`);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { apiRepoLatest } from '../../externals';
|
||||||
|
|
||||||
|
export type HasUpdate = {
|
||||||
|
url: string;
|
||||||
|
version: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to get the latest version and url from github
|
||||||
|
*/
|
||||||
|
export async function getLatestVersion(): Promise<HasUpdate> {
|
||||||
|
const res = await axios.get(`${apiRepoLatest}`);
|
||||||
|
return {
|
||||||
|
url: res.data.html_url as string,
|
||||||
|
version: res.data.tag_name as string,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { HttpSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const httpPath = `${apiEntryUrl}/http`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve http settings
|
||||||
|
*/
|
||||||
|
export async function getHTTP(): Promise<HttpSettings> {
|
||||||
|
const res = await axios.get(httpPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate http settings
|
||||||
|
*/
|
||||||
|
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
||||||
|
return axios.post(httpPath, data);
|
||||||
|
}
|
||||||
@@ -1,397 +0,0 @@
|
|||||||
import axios, { AxiosResponse } from 'axios';
|
|
||||||
import {
|
|
||||||
Alias,
|
|
||||||
AuthenticationStatus,
|
|
||||||
CustomField,
|
|
||||||
CustomFieldLabel,
|
|
||||||
CustomFields,
|
|
||||||
DatabaseModel,
|
|
||||||
GetInfo,
|
|
||||||
HttpSettings,
|
|
||||||
MessageResponse,
|
|
||||||
OntimeRundown,
|
|
||||||
OSCSettings,
|
|
||||||
ProjectData,
|
|
||||||
ProjectFileListResponse,
|
|
||||||
Settings,
|
|
||||||
ViewSettings,
|
|
||||||
} from 'ontime-types';
|
|
||||||
import { ImportMap } from 'ontime-utils';
|
|
||||||
|
|
||||||
import { apiRepoLatest } from '../../externals';
|
|
||||||
import fileDownload from '../utils/fileDownload';
|
|
||||||
|
|
||||||
import { ontimeURL, projectDataURL } from './apiConstants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve application settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getSettings(): Promise<Settings> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/settings`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate application settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postSettings(data: Settings) {
|
|
||||||
return axios.post(`${ontimeURL}/settings`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve application info
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getInfo(): Promise<GetInfo> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/info`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve view settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getView(): Promise<ViewSettings> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/views`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate view settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postViewSettings(data: ViewSettings) {
|
|
||||||
return axios.post(`${ontimeURL}/views`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve aliases
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getAliases(): Promise<Alias[]> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/aliases`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate aliases
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postAliases(data: Alias[]) {
|
|
||||||
return axios.post(`${ontimeURL}/aliases`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve osc settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getOSC(): Promise<OSCSettings> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/osc`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate osc settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
|
||||||
return axios.post(`${ontimeURL}/osc`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to retrieve http settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getHTTP(): Promise<HttpSettings> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/http`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate http settings
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postHTTP(data: HttpSettings): Promise<AxiosResponse<HttpSettings>> {
|
|
||||||
return axios.post(`${ontimeURL}/http`, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to download db in CSV format
|
|
||||||
*/
|
|
||||||
export const downloadCSV = (fileName?: string) => {
|
|
||||||
return fileDownload(ontimeURL, { name: fileName ?? 'rundown', type: 'csv' }, { type: 'text/csv;charset=utf-8;' });
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to download db in JSON format
|
|
||||||
*/
|
|
||||||
export const downloadRundown = (fileName?: string) => {
|
|
||||||
return fileDownload(
|
|
||||||
ontimeURL,
|
|
||||||
{ name: fileName ?? 'rundown', type: 'json' },
|
|
||||||
{ type: 'application/json;charset=utf-8;' },
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to upload project file
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function importProjectFile(file: File): Promise<void> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('userFile', file);
|
|
||||||
const response = await axios.post(`${ontimeURL}/db`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should this be extracted to shared code?
|
|
||||||
export type ProjectFileImportOptions = {
|
|
||||||
onlyRundown: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to upload events db
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export const uploadProjectFile = async (
|
|
||||||
file: File,
|
|
||||||
setProgress: (value: number) => void,
|
|
||||||
options?: Partial<ProjectFileImportOptions>,
|
|
||||||
) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('userFile', file);
|
|
||||||
|
|
||||||
const onlyRundown = Boolean(options?.onlyRundown);
|
|
||||||
|
|
||||||
await axios
|
|
||||||
.post(`${ontimeURL}/db?onlyRundown=${onlyRundown}`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
onUploadProgress: (progressEvent) => {
|
|
||||||
const complete = progressEvent?.total ? Math.round((progressEvent.loaded * 100) / progressEvent.total) : 0;
|
|
||||||
setProgress(complete);
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.then((response) => response.data.id);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Make patch changes to the objects in the db
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function patchData(patchDb: Partial<DatabaseModel>): Promise<void> {
|
|
||||||
return await axios.patch(`${ontimeURL}/db`, patchDb);
|
|
||||||
}
|
|
||||||
|
|
||||||
type PreviewSpreadsheetResponse = {
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Make patch changes to the objects in the db
|
|
||||||
* @return {Promise} - returns parsed rundown and customFields
|
|
||||||
*/
|
|
||||||
export async function importSpreadsheetPreview(file: File, options: ImportMap): Promise<PreviewSpreadsheetResponse> {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('userFile', file);
|
|
||||||
formData.append('options', JSON.stringify(options));
|
|
||||||
|
|
||||||
const response: AxiosResponse<PreviewSpreadsheetResponse> = await axios.post(
|
|
||||||
`${ontimeURL}/spreadsheet/preview`,
|
|
||||||
formData,
|
|
||||||
{
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type HasUpdate = {
|
|
||||||
url: string;
|
|
||||||
version: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to get the latest version and url from github
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getLatestVersion(): Promise<HasUpdate> {
|
|
||||||
const res = await axios.get(`${apiRepoLatest}`);
|
|
||||||
return {
|
|
||||||
url: res.data.html_url as string,
|
|
||||||
version: res.data.tag_name as string,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to get the list of available project files
|
|
||||||
*/
|
|
||||||
export async function getProjects(): Promise<ProjectFileListResponse> {
|
|
||||||
const res = await axios.get(`${ontimeURL}/projects`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to load a project file
|
|
||||||
*/
|
|
||||||
export async function loadProject(filename: string): Promise<MessageResponse> {
|
|
||||||
const res = await axios.post(`${ontimeURL}/load-project`, {
|
|
||||||
filename,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to initiate the authentication service with google
|
|
||||||
*/
|
|
||||||
export const requestConnection = async (
|
|
||||||
file: File,
|
|
||||||
sheetId: string,
|
|
||||||
): Promise<{
|
|
||||||
verification_url: string;
|
|
||||||
user_code: string;
|
|
||||||
}> => {
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append('client_secret', file);
|
|
||||||
|
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/connect`, formData, {
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'multipart/form-data',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to verify whether we are authenticated with Google Sheet service
|
|
||||||
*/
|
|
||||||
export const verifyAuthenticationStatus = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
|
||||||
const response = await axios.get(`${ontimeURL}/sheet/connect`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to revoke authentication to google sheet
|
|
||||||
*/
|
|
||||||
export const revokeAuthentication = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/revoke`);
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to upload preview the contents of a google sheet as rundown
|
|
||||||
*/
|
|
||||||
export const previewRundown = async (
|
|
||||||
sheetId: string,
|
|
||||||
options: ImportMap,
|
|
||||||
): Promise<{
|
|
||||||
rundown: OntimeRundown;
|
|
||||||
customFields: CustomFields;
|
|
||||||
}> => {
|
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/read`, { options });
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to upload the rundown to a google sheet
|
|
||||||
*/
|
|
||||||
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
|
||||||
const response = await axios.post(`${ontimeURL}/sheet/${sheetId}/write`, { options });
|
|
||||||
return response.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to rename a project file
|
|
||||||
*/
|
|
||||||
export async function renameProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
|
||||||
const url = `${ontimeURL}/project/${filename}/rename`;
|
|
||||||
const decodedUrl = decodeURIComponent(url);
|
|
||||||
const res = await axios.put(decodedUrl, {
|
|
||||||
newFilename,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to duplicate a project file
|
|
||||||
*/
|
|
||||||
export async function duplicateProject(filename: string, newFilename: string): Promise<MessageResponse> {
|
|
||||||
const url = `${ontimeURL}/project/${filename}/duplicate`;
|
|
||||||
const decodedUrl = decodeURIComponent(url);
|
|
||||||
const res = await axios.post(decodedUrl, {
|
|
||||||
newFilename,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to delete a project file
|
|
||||||
*/
|
|
||||||
export async function deleteProject(filename: string): Promise<MessageResponse> {
|
|
||||||
const url = `${ontimeURL}/project/${filename}`;
|
|
||||||
const decodedUrl = decodeURIComponent(url);
|
|
||||||
const res = await axios.delete(decodedUrl);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to create a project file
|
|
||||||
*/
|
|
||||||
export async function createProject(
|
|
||||||
project: Partial<
|
|
||||||
ProjectData & {
|
|
||||||
filename: string;
|
|
||||||
}
|
|
||||||
>,
|
|
||||||
): Promise<MessageResponse> {
|
|
||||||
const res = await axios.post(`${ontimeURL}/project`, project);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Requests list of known custom fields
|
|
||||||
*/
|
|
||||||
export async function getCustomFields(): Promise<CustomFields> {
|
|
||||||
const res = await axios.get(`${projectDataURL}/custom-field`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sets list of known custom fields
|
|
||||||
*/
|
|
||||||
export async function postCustomField(newField: CustomField): Promise<CustomFields> {
|
|
||||||
const res = await axios.post(`${projectDataURL}/custom-field`, {
|
|
||||||
...newField,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Edits single custom field
|
|
||||||
*/
|
|
||||||
export async function editCustomField(label: CustomFieldLabel, newField: CustomField): Promise<CustomFields> {
|
|
||||||
const res = await axios.put(`${projectDataURL}/custom-field/${label}`, {
|
|
||||||
...newField,
|
|
||||||
});
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes single custom field
|
|
||||||
*/
|
|
||||||
export async function deleteCustomField(label: CustomFieldLabel): Promise<CustomFields> {
|
|
||||||
const res = await axios.delete(`${projectDataURL}/custom-field/${label}`);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const oscPath = `${apiEntryUrl}/osc`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve osc settings
|
||||||
|
*/
|
||||||
|
export async function getOSC(): Promise<OSCSettings> {
|
||||||
|
const res = await axios.get(oscPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate osc settings
|
||||||
|
*/
|
||||||
|
export async function postOSC(data: OSCSettings): Promise<AxiosResponse<OSCSettings>> {
|
||||||
|
return axios.post(oscPath, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const projectPath = `${apiEntryUrl}/project`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to fetch project data
|
||||||
|
*/
|
||||||
|
export async function getProjectData(): Promise<ProjectData> {
|
||||||
|
const res = await axios.get(projectPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate project data
|
||||||
|
*/
|
||||||
|
export async function postProjectData(data: ProjectData): Promise<AxiosResponse<ProjectData>> {
|
||||||
|
return axios.post(projectPath, data);
|
||||||
|
}
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import axios from 'axios';
|
|
||||||
import { ProjectData } from 'ontime-types';
|
|
||||||
|
|
||||||
import { projectDataURL } from './apiConstants';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to fetch project data
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function getProjectData(): Promise<ProjectData> {
|
|
||||||
const res = await axios.get(projectDataURL);
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description HTTP request to mutate project data
|
|
||||||
* @return {Promise}
|
|
||||||
*/
|
|
||||||
export async function postProjectData(data: ProjectData) {
|
|
||||||
return axios.post(projectDataURL, data);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { MessageResponse, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const rundownPath = `${apiEntryUrl}/rundown`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to fetch all events
|
||||||
|
*/
|
||||||
|
export async function fetchNormalisedRundown(): Promise<RundownCached> {
|
||||||
|
const res = await axios.get(`${rundownPath}/normalised`);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to post new event
|
||||||
|
*/
|
||||||
|
export async function requestPostEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
|
return axios.post(rundownPath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to put new event
|
||||||
|
*/
|
||||||
|
export async function requestPutEvent(data: Partial<OntimeRundownEntry>): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
|
return axios.put(rundownPath, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
type BatchEditEntry = {
|
||||||
|
data: Partial<OntimeEvent>;
|
||||||
|
ids: string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to put multiple events
|
||||||
|
*/
|
||||||
|
export async function requestBatchPutEvents(data: BatchEditEntry): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.put(`${rundownPath}/batch`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ReorderEntry = {
|
||||||
|
eventId: string;
|
||||||
|
from: number;
|
||||||
|
to: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to reorder events
|
||||||
|
*/
|
||||||
|
export async function requestReorderEvent(data: ReorderEntry): Promise<AxiosResponse<OntimeRundownEntry>> {
|
||||||
|
return axios.patch(`${rundownPath}/reorder`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SwapEntry = {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to swap two events
|
||||||
|
*/
|
||||||
|
export async function requestEventSwap(data: SwapEntry): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.patch(`${rundownPath}/swap`, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to request application of delay
|
||||||
|
*/
|
||||||
|
export async function requestApplyDelay(eventId: string): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.patch(`${rundownPath}/applydelay/${eventId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete given event
|
||||||
|
*/
|
||||||
|
export async function requestDelete(eventId: string): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.delete(`${rundownPath}/${eventId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to delete all events
|
||||||
|
*/
|
||||||
|
export async function requestDeleteAll(): Promise<AxiosResponse<MessageResponse>> {
|
||||||
|
return axios.delete(`${rundownPath}/all`);
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios, { AxiosResponse } from 'axios';
|
||||||
|
import { Settings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const settingsPath = `${apiEntryUrl}/settings`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve application settings
|
||||||
|
*/
|
||||||
|
export async function getSettings(): Promise<Settings> {
|
||||||
|
const res = await axios.get(settingsPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate application settings
|
||||||
|
*/
|
||||||
|
export async function postSettings(data: Settings): Promise<AxiosResponse<Settings>> {
|
||||||
|
return axios.post(settingsPath, data);
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const sheetsPath = `${apiEntryUrl}/sheets`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to verify whether we are authenticated with Google Sheet service
|
||||||
|
*/
|
||||||
|
export const verifyAuthenticationStatus = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
||||||
|
const response = await axios.get(`${apiEntryUrl}/connect`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to initiate the authentication service with google
|
||||||
|
*/
|
||||||
|
export const requestConnection = async (
|
||||||
|
file: File,
|
||||||
|
sheetId: string,
|
||||||
|
): Promise<{
|
||||||
|
verification_url: string;
|
||||||
|
user_code: string;
|
||||||
|
}> => {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('client_secret', file);
|
||||||
|
|
||||||
|
const response = await axios.post(`${sheetsPath}/${sheetId}/connect`, formData, {
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to revoke authentication to google sheet
|
||||||
|
*/
|
||||||
|
export const revokeAuthentication = async (): Promise<{ authenticated: AuthenticationStatus }> => {
|
||||||
|
const response = await axios.post(`${sheetsPath}/revoke`);
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to upload preview the contents of a google sheet as rundown
|
||||||
|
*/
|
||||||
|
export const previewRundown = async (
|
||||||
|
sheetId: string,
|
||||||
|
options: ImportMap,
|
||||||
|
): Promise<{
|
||||||
|
rundown: OntimeRundown;
|
||||||
|
customFields: CustomFields;
|
||||||
|
}> => {
|
||||||
|
const response = await axios.post(`${sheetsPath}/${sheetId}/read`, { options });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to upload the rundown to a google sheet
|
||||||
|
*/
|
||||||
|
export const uploadRundown = async (sheetId: string, options: ImportMap): Promise<void> => {
|
||||||
|
const response = await axios.post(`${sheetsPath}/${sheetId}/write`, { options });
|
||||||
|
return response.data;
|
||||||
|
};
|
||||||
@@ -1,6 +1,60 @@
|
|||||||
import axios from 'axios';
|
import axios, { AxiosError } from 'axios';
|
||||||
|
import { LogLevel } from 'ontime-types';
|
||||||
|
import { generateId, millisToString } from 'ontime-utils';
|
||||||
|
|
||||||
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
import { makeCSV, makeTable } from '../../features/cuesheet/cuesheetUtils';
|
||||||
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
import { addLog } from '../stores/logger';
|
||||||
|
import { nowInMillis } from '../utils/time';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility unrwap a potential axios error
|
||||||
|
* @param error
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export function maybeAxiosError(error: unknown) {
|
||||||
|
if (axios.isAxiosError(error)) {
|
||||||
|
const statusText = (error as AxiosError).response?.statusText ?? '';
|
||||||
|
let data = (error as AxiosError).response?.data ?? '';
|
||||||
|
if (typeof data === 'object') {
|
||||||
|
if ('message' in data) {
|
||||||
|
data = JSON.stringify(data.message);
|
||||||
|
} else {
|
||||||
|
data = JSON.stringify(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${statusText}: ${data}`;
|
||||||
|
} else {
|
||||||
|
if (typeof error !== 'string') {
|
||||||
|
return JSON.stringify(error);
|
||||||
|
}
|
||||||
|
return error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility unrwaps a potential axios error and sends to logger
|
||||||
|
* @param prepend
|
||||||
|
* @param error
|
||||||
|
*/
|
||||||
|
export function logAxiosError(prepend: string, error: unknown) {
|
||||||
|
const message = `${prepend}: ${maybeAxiosError(error)}`;
|
||||||
|
|
||||||
|
addLog({
|
||||||
|
id: generateId(),
|
||||||
|
origin: 'SERVER',
|
||||||
|
time: millisToString(nowInMillis()),
|
||||||
|
level: LogLevel.Error,
|
||||||
|
text: message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function invalidates react-query caches
|
||||||
|
*/
|
||||||
|
export async function invalidateAllCaches() {
|
||||||
|
await ontimeQueryClient.invalidateQueries();
|
||||||
|
}
|
||||||
|
|
||||||
type FileOptions = {
|
type FileOptions = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -61,3 +115,4 @@ export default async function fileDownload(url: string, fileOptions: FileOptions
|
|||||||
URL.revokeObjectURL(downloadUrl);
|
URL.revokeObjectURL(downloadUrl);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { apiEntryUrl } from './constants';
|
||||||
|
|
||||||
|
const viewSettingsPath = `${apiEntryUrl}/view-settings`;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to retrieve view settings
|
||||||
|
*/
|
||||||
|
export async function getView(): Promise<ViewSettings> {
|
||||||
|
const res = await axios.get(viewSettingsPath);
|
||||||
|
return res.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* HTTP request to mutate view settings
|
||||||
|
*/
|
||||||
|
export async function postViewSettings(data: ViewSettings) {
|
||||||
|
return axios.post(viewSettingsPath, data);
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { ALIASES } from '../api/apiConstants';
|
import { getAliases } from '../api/aliases';
|
||||||
import { getAliases } from '../api/ontimeApi';
|
import { ALIASES } from '../api/constants';
|
||||||
|
|
||||||
export default function useAliases() {
|
export default function useAliases() {
|
||||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { CustomFields } from 'ontime-types';
|
import { CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
import { CUSTOM_FIELDS } from '../api/apiConstants';
|
import { CUSTOM_FIELDS } from '../api/constants';
|
||||||
import { getCustomFields } from '../api/ontimeApi';
|
import { getCustomFields } from '../api/customFields';
|
||||||
|
|
||||||
const placeholder: CustomFields = {};
|
const placeholder: CustomFields = {};
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { HTTP_SETTINGS } from '../api/apiConstants';
|
import { HTTP_SETTINGS } from '../api/constants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { getHTTP, postHTTP } from '../api/http';
|
||||||
import { getHTTP, postHTTP } from '../api/ontimeApi';
|
import { logAxiosError } from '../api/utils';
|
||||||
import { httpPlaceholder } from '../models/Http';
|
import { httpPlaceholder } from '../models/Http';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { GetInfo } from 'ontime-types';
|
import { GetInfo } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { APP_INFO } from '../api/apiConstants';
|
import { APP_INFO } from '../api/constants';
|
||||||
import { getInfo } from '../api/ontimeApi';
|
import { getInfo } from '../api/db';
|
||||||
import { ontimePlaceholderInfo } from '../models/Info';
|
import { ontimePlaceholderInfo } from '../models/Info';
|
||||||
|
|
||||||
export default function useInfo() {
|
export default function useInfo() {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
import { OSC_SETTINGS } from '../api/constants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
import { getOSC, postOSC } from '../api/osc';
|
||||||
import { getOSC, postOSC } from '../api/ontimeApi';
|
import { logAxiosError } from '../api/utils';
|
||||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
import { oscPlaceholderSettings } from '../models/OscSettings';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { PROJECT_DATA } from '../api/apiConstants';
|
import { PROJECT_DATA } from '../api/constants';
|
||||||
import { getProjectData } from '../api/projectDataApi';
|
import { getProjectData } from '../api/project';
|
||||||
import { projectDataPlaceholder } from '../models/ProjectData';
|
import { projectDataPlaceholder } from '../models/ProjectData';
|
||||||
|
|
||||||
export default function useProjectData() {
|
export default function useProjectData() {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { ProjectFileListResponse } from 'ontime-types';
|
import { ProjectFileListResponse } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { PROJECT_LIST } from '../api/apiConstants';
|
import { PROJECT_LIST } from '../api/constants';
|
||||||
import { getProjects } from '../api/ontimeApi';
|
import { getProjects } from '../api/db';
|
||||||
|
|
||||||
const placeholderProjectList: ProjectFileListResponse = {
|
const placeholderProjectList: ProjectFileListResponse = {
|
||||||
files: [],
|
files: [],
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useQuery } from '@tanstack/react-query';
|
|||||||
import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types';
|
import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||||
import { RUNDOWN } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import { fetchCachedRundown } from '../api/eventsApi';
|
import { fetchNormalisedRundown } from '../api/rundown';
|
||||||
|
|
||||||
// revision is -1 so that the remote revision is higher
|
// revision is -1 so that the remote revision is higher
|
||||||
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as NormalisedRundown, revision: -1 };
|
||||||
@@ -12,7 +12,7 @@ const cachedRundownPlaceholder = { order: [] as string[], rundown: {} as Normali
|
|||||||
export default function useRundown() {
|
export default function useRundown() {
|
||||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||||
queryKey: RUNDOWN,
|
queryKey: RUNDOWN,
|
||||||
queryFn: fetchCachedRundown,
|
queryFn: fetchNormalisedRundown,
|
||||||
placeholderData: cachedRundownPlaceholder,
|
placeholderData: cachedRundownPlaceholder,
|
||||||
retry: 5,
|
retry: 5,
|
||||||
retryDelay: (attempt) => attempt * 2500,
|
retryDelay: (attempt) => attempt * 2500,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { APP_SETTINGS } from '../api/apiConstants';
|
import { APP_SETTINGS } from '../api/constants';
|
||||||
import { getSettings } from '../api/ontimeApi';
|
import { getSettings } from '../api/settings';
|
||||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||||
|
|
||||||
export default function useSettings() {
|
export default function useSettings() {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useQuery } from '@tanstack/react-query';
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||||
import { VIEW_SETTINGS } from '../api/apiConstants';
|
import { VIEW_SETTINGS } from '../api/constants';
|
||||||
import { getView } from '../api/ontimeApi';
|
import { getView } from '../api/viewSettings';
|
||||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||||
|
|
||||||
export default function useViewSettings() {
|
export default function useViewSettings() {
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
import { isOntimeEvent, MaybeString, OntimeEvent, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
|
import { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
|
||||||
|
|
||||||
import { RUNDOWN } from '../api/apiConstants';
|
import { RUNDOWN } from '../api/constants';
|
||||||
import { logAxiosError } from '../api/apiUtils';
|
|
||||||
import {
|
import {
|
||||||
ReorderEntry,
|
ReorderEntry,
|
||||||
requestApplyDelay,
|
requestApplyDelay,
|
||||||
@@ -16,7 +15,8 @@ import {
|
|||||||
requestPutEvent,
|
requestPutEvent,
|
||||||
requestReorderEvent,
|
requestReorderEvent,
|
||||||
SwapEntry,
|
SwapEntry,
|
||||||
} from '../api/eventsApi';
|
} from '../api/rundown';
|
||||||
|
import { logAxiosError } from '../api/utils';
|
||||||
import { useEditorSettings } from '../stores/editorSettings';
|
import { useEditorSettings } from '../stores/editorSettings';
|
||||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
import { useMemo, useRef } from 'react';
|
import { useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import { isDev } from '../api/apiConstants';
|
import { isDev } from '../api/constants';
|
||||||
|
|
||||||
type noop = (this: any, ...args: any[]) => any;
|
type noop = (this: any, ...args: any[]) => any;
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Log, RuntimeStore } from 'ontime-types';
|
import { Log, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
import { isProduction, RUNTIME, websocketUrl } from '../api/apiConstants';
|
import { isProduction, RUNTIME, websocketUrl } from '../api/constants';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
import { socketClientName } from '../stores/connectionName';
|
import { socketClientName } from '../stores/connectionName';
|
||||||
import { addLog } from '../stores/logger';
|
import { addLog } from '../stores/logger';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MaybeNumber, Settings, TimeFormat } from 'ontime-types';
|
|||||||
import { formatFromMillis } from 'ontime-utils';
|
import { formatFromMillis } from 'ontime-utils';
|
||||||
|
|
||||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||||
import { APP_SETTINGS } from '../api/apiConstants';
|
import { APP_SETTINGS } from '../api/constants';
|
||||||
import { ontimeQueryClient } from '../queryClient';
|
import { ontimeQueryClient } from '../queryClient';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Button } from '@chakra-ui/react';
|
import { Button } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { getLatestVersion, HasUpdate } from '../../../../common/api/ontimeApi';
|
import { getLatestVersion, HasUpdate } from '../../../../common/api/external';
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
|
|
||||||
import style from '../Panel.module.scss';
|
import style from '../Panel.module.scss';
|
||||||
@@ -52,7 +52,13 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button onClick={versionCheck} variant='ontime-filled' isLoading={isFetching} isDisabled={disableButton} size='sm'>
|
<Button
|
||||||
|
onClick={versionCheck}
|
||||||
|
variant='ontime-filled'
|
||||||
|
isLoading={isFetching}
|
||||||
|
isDisabled={disableButton}
|
||||||
|
size='sm'
|
||||||
|
>
|
||||||
Check for updates
|
Check for updates
|
||||||
</Button>
|
</Button>
|
||||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
<ResolveUpdateMessage updateMessage={updateMessage} />
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
|||||||
import { HttpSettings } from 'ontime-types';
|
import { HttpSettings } from 'ontime-types';
|
||||||
import { generateId } from 'ontime-utils';
|
import { generateId } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
import { useHttpSettings, usePostHttpSettings } from '../../../../common/hooks-query/useHttpSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { startsWithHttp } from '../../../../common/utils/regex';
|
import { startsWithHttp } from '../../../../common/utils/regex';
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
|||||||
import { OSCSettings } from 'ontime-types';
|
import { OSCSettings } from 'ontime-types';
|
||||||
import { generateId } from 'ontime-utils';
|
import { generateId } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
import useOscSettings, { useOscSettingsMutation } from '../../../../common/hooks-query/useOscSettings';
|
||||||
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
import { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||||
import { isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
import { isIPAddress, isOnlyNumbers, startsWithSlash } from '../../../../common/utils/regex';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { serverPort } from '../../../../common/api/apiConstants';
|
import { serverPort } from '../../../../common/api/constants';
|
||||||
import AppLink from '../../../../common/components/app-link/AppLink';
|
import AppLink from '../../../../common/components/app-link/AppLink';
|
||||||
import useInfo from '../../../../common/hooks-query/useInfo';
|
import useInfo from '../../../../common/hooks-query/useInfo';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||||
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { PROJECT_LIST } from '../../../../common/api/constants';
|
||||||
import { createProject } from '../../../../common/api/ontimeApi';
|
import { createProject } from '../../../../common/api/db';
|
||||||
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
import style from './ProjectPanel.module.scss';
|
import style from './ProjectPanel.module.scss';
|
||||||
@@ -25,6 +27,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
const { onClose } = props;
|
const { onClose } = props;
|
||||||
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
@@ -53,6 +56,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
|||||||
...values,
|
...values,
|
||||||
filename,
|
filename,
|
||||||
});
|
});
|
||||||
|
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
||||||
onClose();
|
onClose();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
setError(maybeAxiosError(error));
|
setError(maybeAxiosError(error));
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
|||||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||||
|
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
|
|
||||||
import {
|
import {
|
||||||
deleteProject,
|
deleteProject,
|
||||||
downloadCSV,
|
downloadCSV,
|
||||||
@@ -10,7 +9,8 @@ import {
|
|||||||
duplicateProject,
|
duplicateProject,
|
||||||
loadProject,
|
loadProject,
|
||||||
renameProject,
|
renameProject,
|
||||||
} from '../../../../common/api/ontimeApi';
|
} from '../../../../common/api/db';
|
||||||
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
|
||||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { ChangeEvent, useRef, useState } from 'react';
|
|||||||
import { Button, Input } from '@chakra-ui/react';
|
import { Button, Input } from '@chakra-ui/react';
|
||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
|
|
||||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { uploadProjectFile } from '../../../../common/api/db';
|
||||||
import { importProjectFile } from '../../../../common/api/ontimeApi';
|
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export default function ProjectPanel() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
validateProjectFile(selectedFile);
|
validateProjectFile(selectedFile);
|
||||||
await importProjectFile(selectedFile);
|
await uploadProjectFile(selectedFile);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = maybeAxiosError(error);
|
const errorMessage = maybeAxiosError(error);
|
||||||
setError(`Error uploading file: ${errorMessage}`);
|
setError(`Error uploading file: ${errorMessage}`);
|
||||||
@@ -48,6 +48,10 @@ export default function ProjectPanel() {
|
|||||||
setLoading(null);
|
setLoading(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCloseForm = () => {
|
||||||
|
setIsCreatingProject(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Panel.Header>Project</Panel.Header>
|
<Panel.Header>Project</Panel.Header>
|
||||||
@@ -85,7 +89,7 @@ export default function ProjectPanel() {
|
|||||||
</div>
|
</div>
|
||||||
</Panel.SubHeader>
|
</Panel.SubHeader>
|
||||||
{error && <Panel.Error>{error}</Panel.Error>}
|
{error && <Panel.Error>{error}</Panel.Error>}
|
||||||
{isCreatingProject && <ProjectCreateForm onClose={() => setIsCreatingProject(false)} />}
|
{isCreatingProject && <ProjectCreateForm onClose={handleCloseForm} />}
|
||||||
<ProjectList />
|
<ProjectList />
|
||||||
</Panel.Card>
|
</Panel.Card>
|
||||||
</Panel.Section>
|
</Panel.Section>
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import { Button, Input } from '@chakra-ui/react';
|
|||||||
import { CustomField } from 'ontime-types';
|
import { CustomField } from 'ontime-types';
|
||||||
import { isAlphanumeric } from 'ontime-utils';
|
import { isAlphanumeric } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
import SwatchSelect from '../../../../common/components/input/colour-input/SwatchSelect';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@ import { Alert, AlertDescription, AlertIcon, Button } from '@chakra-ui/react';
|
|||||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||||
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
import { CustomField, CustomFieldLabel } from 'ontime-types';
|
||||||
|
|
||||||
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/ontimeApi';
|
import { deleteCustomField, editCustomField, postCustomField } from '../../../../common/api/customFields';
|
||||||
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
import ExternalLink from '../../../../common/components/external-link/ExternalLink';
|
||||||
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
import useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import { IoCloudOutline } from '@react-icons/all-files/io5/IoCloudOutline';
|
|||||||
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
import { IoDownloadOutline } from '@react-icons/all-files/io5/IoDownloadOutline';
|
||||||
import { ImportMap, unpackError } from 'ontime-utils';
|
import { ImportMap, unpackError } from 'ontime-utils';
|
||||||
|
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { importSpreadsheetPreview } from '../../../../common/api/db';
|
||||||
import { importSpreadsheetPreview } from '../../../../common/api/ontimeApi';
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
import { validateSpreadsheetImport } from '../../../../common/utils/uploadUtils';
|
import { validateSpreadsheetImport } from '../../../../common/utils/uploadUtils';
|
||||||
import * as Panel from '../PanelUtils';
|
import * as Panel from '../PanelUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,16 @@ import { useQueryClient } from '@tanstack/react-query';
|
|||||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||||
import { ImportMap } from 'ontime-utils';
|
import { ImportMap } from 'ontime-utils';
|
||||||
|
|
||||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/apiConstants';
|
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
import { patchData } from '../../../../common/api/db';
|
||||||
import {
|
import {
|
||||||
patchData,
|
|
||||||
previewRundown,
|
previewRundown,
|
||||||
requestConnection,
|
requestConnection,
|
||||||
revokeAuthentication,
|
revokeAuthentication,
|
||||||
uploadRundown,
|
uploadRundown,
|
||||||
verifyAuthenticationStatus,
|
verifyAuthenticationStatus,
|
||||||
} from '../../../../common/api/ontimeApi';
|
} from '../../../../common/api/sheets';
|
||||||
|
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||||
|
|
||||||
import { useSheetStore } from './useSheetStore';
|
import { useSheetStore } from './useSheetStore';
|
||||||
|
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { IoOpenOutline } from '@react-icons/all-files/io5/IoOpenOutline';
|
|||||||
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
import { IoRemove } from '@react-icons/all-files/io5/IoRemove';
|
||||||
import { Alias } from 'ontime-types';
|
import { Alias } from 'ontime-types';
|
||||||
|
|
||||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
import { postAliases } from '../../../common/api/aliases';
|
||||||
import { postAliases } from '../../../common/api/ontimeApi';
|
import { logAxiosError } from '../../../common/api/utils';
|
||||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||||
import useAliases from '../../../common/hooks-query/useAliases';
|
import useAliases from '../../../common/hooks-query/useAliases';
|
||||||
import { useEmitLog } from '../../../common/stores/logger';
|
import { useEmitLog } from '../../../common/stores/logger';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { Input, Select } from '@chakra-ui/react';
|
import { Input, Select } from '@chakra-ui/react';
|
||||||
import type { Settings } from 'ontime-types';
|
import type { Settings } from 'ontime-types';
|
||||||
|
|
||||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
import { postSettings } from '../../../common/api/settings';
|
||||||
import { postSettings } from '../../../common/api/ontimeApi';
|
import { logAxiosError } from '../../../common/api/utils';
|
||||||
import useSettings from '../../../common/hooks-query/useSettings';
|
import useSettings from '../../../common/hooks-query/useSettings';
|
||||||
import { isOnlyNumbers } from '../../../common/utils/regex';
|
import { isOnlyNumbers } from '../../../common/utils/regex';
|
||||||
import ModalLoader from '../modal-loader/ModalLoader';
|
import ModalLoader from '../modal-loader/ModalLoader';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { Input, Textarea } from '@chakra-ui/react';
|
import { Input, Textarea } from '@chakra-ui/react';
|
||||||
import { ProjectData } from 'ontime-types';
|
import { ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
import { postProjectData } from '../../../common/api/project';
|
||||||
import { postProjectData } from '../../../common/api/projectDataApi';
|
import { logAxiosError } from '../../../common/api/utils';
|
||||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||||
import ModalLoader from '../modal-loader/ModalLoader';
|
import ModalLoader from '../modal-loader/ModalLoader';
|
||||||
import { inputProps } from '../modalHelper';
|
import { inputProps } from '../modalHelper';
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
|||||||
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input, Switch } from '@chakra-ui/react';
|
import { Alert, AlertDescription, AlertIcon, AlertTitle, Input, Switch } from '@chakra-ui/react';
|
||||||
import { ViewSettings } from 'ontime-types';
|
import { ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
import { logAxiosError } from '../../../common/api/utils';
|
||||||
import { postViewSettings } from '../../../common/api/ontimeApi';
|
import { postViewSettings } from '../../../common/api/viewSettings';
|
||||||
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
|
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
|
||||||
import useInfo from '../../../common/hooks-query/useInfo';
|
import useInfo from '../../../common/hooks-query/useInfo';
|
||||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
|||||||
import { isOntimeEvent, OntimeEvent, RundownCached } from 'ontime-types';
|
import { isOntimeEvent, OntimeEvent, RundownCached } from 'ontime-types';
|
||||||
import { create } from 'zustand';
|
import { create } from 'zustand';
|
||||||
|
|
||||||
import { RUNDOWN } from '../../common/api/apiConstants';
|
import { RUNDOWN } from '../../common/api/constants';
|
||||||
import { ontimeQueryClient } from '../../common/queryClient';
|
import { ontimeQueryClient } from '../../common/queryClient';
|
||||||
import { isMacOS } from '../../common/utils/deviceUtils';
|
import { isMacOS } from '../../common/utils/deviceUtils';
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { AnimatePresence, motion } from 'framer-motion';
|
|||||||
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
import ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { Settings, ViewSettings } from 'ontime-types';
|
import { Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
import { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
import { OntimeEvent, OntimeRundownEntry, Playback, Settings, SupportedEvent, ViewSettings } from 'ontime-types';
|
||||||
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
import { millisToString, removeLeadingZero } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
import { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useEffect, useMemo, useState } from 'react';
|
|||||||
import { useSearchParams } from 'react-router-dom';
|
import { useSearchParams } from 'react-router-dom';
|
||||||
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
import { CustomFields, Message, OntimeEvent, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
import { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
import { Playback, TimerMessage, TimerType, ViewSettings } from 'ontime-types';
|
||||||
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
import { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useSearchParams } from 'react-router-dom';
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
import { CustomFields, Message, OntimeEvent, ProjectData, Settings, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import Schedule from '../../../common/components/schedule/Schedule';
|
import Schedule from '../../../common/components/schedule/Schedule';
|
||||||
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
import { ScheduleProvider } from '../../../common/components/schedule/ScheduleContext';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { OntimeEvent, OntimeRundown, Settings, ViewSettings } from 'ontime-
|
|||||||
import { isOntimeEvent, Playback } from 'ontime-types';
|
import { isOntimeEvent, Playback } from 'ontime-types';
|
||||||
import { millisToString, removeSeconds } from 'ontime-utils';
|
import { millisToString, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
import { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
} from 'ontime-types';
|
} from 'ontime-types';
|
||||||
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
import { millisToString, removeLeadingZero, removeSeconds } from 'ontime-utils';
|
||||||
|
|
||||||
import { overrideStylesURL } from '../../../common/api/apiConstants';
|
import { overrideStylesURL } from '../../../common/api/constants';
|
||||||
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
import MultiPartProgressBar from '../../../common/components/multi-part-progress-bar/MultiPartProgressBar';
|
||||||
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
import NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { LogOrigin, OSCSettings } from 'ontime-types';
|
|||||||
import { Server } from 'node-osc';
|
import { Server } from 'node-osc';
|
||||||
|
|
||||||
import { IAdapter } from './IAdapter.js';
|
import { IAdapter } from './IAdapter.js';
|
||||||
import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js';
|
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
import { objectFromPath } from './utils/parse.js';
|
import { objectFromPath } from './utils/parse.js';
|
||||||
|
import { type ChangeOptions, dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||||
|
|
||||||
export class OscServer implements IAdapter {
|
export class OscServer implements IAdapter {
|
||||||
private readonly osc: Server;
|
private readonly osc: Server;
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ import type { Server } from 'http';
|
|||||||
import getRandomName from '../utils/getRandomName.js';
|
import getRandomName from '../utils/getRandomName.js';
|
||||||
import { IAdapter } from './IAdapter.js';
|
import { IAdapter } from './IAdapter.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
|
||||||
import { logger } from '../classes/Logger.js';
|
import { logger } from '../classes/Logger.js';
|
||||||
|
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||||
|
|
||||||
let instance: SocketServer | null = null;
|
let instance: SocketServer | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import type { Alias, ErrorResponse } from 'ontime-types';
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failIsNotArray } from '../../utils/routerUtils.js';
|
||||||
|
|
||||||
|
export async function getAliases(_req: Request, res: Response<Alias[]>) {
|
||||||
|
const aliases = DataProvider.getAliases();
|
||||||
|
res.status(200).send(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postAliases(req: Request, res: Response<Alias[] | ErrorResponse>) {
|
||||||
|
if (failIsNotArray(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const newAliases: Alias[] = [];
|
||||||
|
req.body.forEach((a) => {
|
||||||
|
newAliases.push({
|
||||||
|
enabled: a.enabled,
|
||||||
|
alias: a.alias,
|
||||||
|
pathAndParams: a.pathAndParams,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await DataProvider.setAliases(newAliases);
|
||||||
|
res.status(200).send(newAliases);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { getAliases, postAliases } from './aliases.controller.js';
|
||||||
|
import { validateAliases } from './aliases.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getAliases);
|
||||||
|
router.post('/', validateAliases, postAliases);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates object for POST /ontime/aliases
|
||||||
|
*/
|
||||||
|
export const validateAliases = [
|
||||||
|
body().isArray(),
|
||||||
|
body('*.enabled').isBoolean(),
|
||||||
|
body('*.alias').isString().trim(),
|
||||||
|
body('*.pathAndParams').isString().trim(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { CustomField, CustomFields } from 'ontime-types';
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createCustomField,
|
||||||
|
editCustomField,
|
||||||
|
getCustomFields as getCustomFieldsFromCache,
|
||||||
|
removeCustomField,
|
||||||
|
} from '../../services/rundown-service/rundownCache.js';
|
||||||
|
|
||||||
|
export async function getCustomFields(_req: Request, res: Response<CustomFields>) {
|
||||||
|
const customFields = getCustomFieldsFromCache();
|
||||||
|
res.json(customFields);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects { label: <label> type: 'string | ..' }
|
||||||
|
export async function postCustomField(req: Request, res: Response) {
|
||||||
|
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: <oldLabel>, field: { label: <newLabel> type: 'string | ..' } }
|
||||||
|
export async function putCustomField(req: Request, res: Response) {
|
||||||
|
try {
|
||||||
|
const oldLabel = req.params.label;
|
||||||
|
const { colour, type, label } = req.body;
|
||||||
|
const newFields = await editCustomField(oldLabel, { label, colour, type });
|
||||||
|
res.status(200).send(newFields);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expects { label: <label> }
|
||||||
|
export async function deleteCustomField(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() });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { deleteCustomField, getCustomFields, postCustomField, putCustomField } from './customFields.controller.js';
|
||||||
|
import { validateCustomField, validateDeleteCustomField, validateEditCustomField } from './customFields.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getCustomFields);
|
||||||
|
|
||||||
|
router.post('/', validateCustomField, postCustomField);
|
||||||
|
|
||||||
|
router.put('/:label', validateEditCustomField, putCustomField);
|
||||||
|
|
||||||
|
router.delete('/:label', validateDeleteCustomField, deleteCustomField);
|
||||||
-29
@@ -3,24 +3,6 @@ import { isAlphanumeric } from 'ontime-utils';
|
|||||||
import { Request, Response, NextFunction } from 'express';
|
import { Request, Response, NextFunction } from 'express';
|
||||||
import { body, param, validationResult } from 'express-validator';
|
import { body, param, validationResult } from 'express-validator';
|
||||||
|
|
||||||
import { ensureJsonExtension } from '../utils/fileManagement.js';
|
|
||||||
|
|
||||||
export const projectSanitiser = [
|
|
||||||
body('title').optional().isString().trim(),
|
|
||||||
body('description').optional().isString().trim(),
|
|
||||||
body('publicUrl').optional().isString().trim(),
|
|
||||||
body('publicInfo').optional().isString().trim(),
|
|
||||||
body('backstageUrl').optional().isString().trim(),
|
|
||||||
body('backstageInfo').optional().isString().trim(),
|
|
||||||
body('endMessage').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 validateCustomField = [
|
export const validateCustomField = [
|
||||||
body('label')
|
body('label')
|
||||||
.exists()
|
.exists()
|
||||||
@@ -61,14 +43,3 @@ export const validateDeleteCustomField = [
|
|||||||
next();
|
next();
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const sanitizeProjectFilename = (req: Request, _res: Response, next: NextFunction) => {
|
|
||||||
const { filename, newFilename } = req.body;
|
|
||||||
const { filename: projectName } = req.params;
|
|
||||||
|
|
||||||
req.body.filename = ensureJsonExtension(filename);
|
|
||||||
req.body.newFilename = ensureJsonExtension(newFilename);
|
|
||||||
req.params.filename = ensureJsonExtension(projectName);
|
|
||||||
|
|
||||||
next();
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,285 @@
|
|||||||
|
import {
|
||||||
|
DatabaseModel,
|
||||||
|
ErrorResponse,
|
||||||
|
GetInfo,
|
||||||
|
MessageResponse,
|
||||||
|
ProjectData,
|
||||||
|
ProjectFileListResponse,
|
||||||
|
} from 'ontime-types';
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import fs from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
import { resolveDbPath, resolveProjectsDirectory } from '../../setup/index.js';
|
||||||
|
|
||||||
|
import * as projectService from '../../services/project-service/ProjectService.js';
|
||||||
|
import { runtimeService } from '../../services/runtime-service/RuntimeService.js';
|
||||||
|
import { setRundown } from '../../services/rundown-service/RundownService.js';
|
||||||
|
import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||||
|
import { generateUniqueFileName } from '../../utils/generateUniqueFilename.js';
|
||||||
|
import { appStateService } from '../../services/app-state-service/AppStateService.js';
|
||||||
|
import { handleMaybeExcel } from '../../utils/parser.js';
|
||||||
|
|
||||||
|
export async function patchPartialProjectFile(req: Request, res: Response<DatabaseModel | ErrorResponse>) {
|
||||||
|
// all fields are optional in validation
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
res.status(400).send({ message: 'No field found to patch' });
|
||||||
|
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,
|
||||||
|
customFields: req.body?.customFields,
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeRundown = req.body?.rundown;
|
||||||
|
await DataProvider.mergeIntoData(patchDb);
|
||||||
|
if (maybeRundown !== undefined) {
|
||||||
|
// it is likely cheaper to invalidate cache than to calculate diff
|
||||||
|
runtimeService.stop();
|
||||||
|
await setRundown(maybeRundown);
|
||||||
|
}
|
||||||
|
const newData = DataProvider.getData();
|
||||||
|
res.status(200).send(newData);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new project file.
|
||||||
|
* Receives the project filename (`filename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful creation,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export async function createProjectFile(req: Request, res: Response<{ filename: string } | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const originalFilename = ensureJsonExtension(req.body.title || 'Untitled');
|
||||||
|
const filename = generateUniqueFileName(resolveProjectsDirectory, originalFilename);
|
||||||
|
const errors = projectService.validateProjectFiles({ newFilename: filename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: 'Project with title already exists' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const newProjectData: ProjectData = {
|
||||||
|
title: req.body?.title ?? '',
|
||||||
|
description: req.body?.description ?? '',
|
||||||
|
publicUrl: req.body?.publicUrl ?? '',
|
||||||
|
publicInfo: req.body?.publicInfo ?? '',
|
||||||
|
backstageUrl: req.body?.backstageUrl ?? '',
|
||||||
|
backstageInfo: req.body?.backstageInfo ?? '',
|
||||||
|
};
|
||||||
|
|
||||||
|
projectService.createProjectFile(filename, newProjectData);
|
||||||
|
|
||||||
|
res.status(200).send({
|
||||||
|
filename,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function projectDownload(_req: Request, res: Response) {
|
||||||
|
const { title } = DataProvider.getProjectData();
|
||||||
|
const fileTitle = title || 'ontime data';
|
||||||
|
|
||||||
|
res.download(resolveDbPath, `${fileTitle}.json`, (err) => {
|
||||||
|
if (err) {
|
||||||
|
res.status(500).send({
|
||||||
|
message: `Could not download the file: ${err}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uploads, parses and applies the data from a given file
|
||||||
|
*/
|
||||||
|
export async function postProjectFile(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).send({ message: 'File not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const options = req.query;
|
||||||
|
const filePath = req.file.path;
|
||||||
|
await projectService.applyProjectFile(filePath, options);
|
||||||
|
res.status(201).send({ message: 'ok' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: `Failed parsing ${error}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves and lists all project files from the uploads directory.
|
||||||
|
*/
|
||||||
|
export async function listProjects(_req: Request, res: Response<ProjectFileListResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const data = await projectService.getProjectList();
|
||||||
|
res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Receives a `filename` from the request body and loads the project file from the uploads directory.
|
||||||
|
*/
|
||||||
|
export async function loadProject(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const filename = req.body.filename;
|
||||||
|
const filePath = join(resolveProjectsDirectory, filename);
|
||||||
|
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
return res.status(404).send({ message: 'File not found' });
|
||||||
|
}
|
||||||
|
await projectService.applyProjectFile(filePath);
|
||||||
|
res.status(201).send({
|
||||||
|
message: `Loaded project ${filename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates a project file.
|
||||||
|
* Receives the original project filename (`filename`) from the request parameters
|
||||||
|
* and the filename for the duplicate (`newFilename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 201 status with a success message upon successful duplication,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export async function duplicateProjectFile(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
const { newFilename } = req.body;
|
||||||
|
|
||||||
|
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectService.duplicateProjectFile(filename, newFilename);
|
||||||
|
|
||||||
|
res.status(201).send({
|
||||||
|
message: `Duplicated project ${filename} to ${newFilename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renames a project file.
|
||||||
|
* Receives the current filename (`filename`) from the request parameters
|
||||||
|
* and the new filename (`newFilename`) from the request body.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
||||||
|
* @param {Response} res - The express response object. Sends a 201 status with a success message upon successful renaming,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export async function renameProjectFile(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const { newFilename } = req.body;
|
||||||
|
const { filename } = req.params;
|
||||||
|
|
||||||
|
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename the file
|
||||||
|
await projectService.renameProjectFile(filename, newFilename);
|
||||||
|
|
||||||
|
res.status(201).send({
|
||||||
|
message: `Renamed project ${filename} to ${newFilename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an existing project file.
|
||||||
|
* Receives the project filename (`filename`) from the request parameters.
|
||||||
|
*
|
||||||
|
* @param {Request} req - The express request object. Expects `filename` in the request parameters.
|
||||||
|
* @param {Response} res - The express response object. Sends a 204 status with a success message upon successful deletion,
|
||||||
|
* a 403 status if attempting to delete the currently loaded project,
|
||||||
|
* a 409 status if there are validation errors,
|
||||||
|
* or a 500 status with an error message in case of an exception.
|
||||||
|
*/
|
||||||
|
export async function deleteProjectFile(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
const { filename } = req.params;
|
||||||
|
|
||||||
|
const { lastLoadedProject } = await appStateService.get();
|
||||||
|
|
||||||
|
if (lastLoadedProject === filename) {
|
||||||
|
return res.status(403).send({ message: 'Cannot delete currently loaded project' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const errors = projectService.validateProjectFiles({ filename });
|
||||||
|
|
||||||
|
if (errors.length) {
|
||||||
|
return res.status(409).send({ message: errors.join(', ') });
|
||||||
|
}
|
||||||
|
|
||||||
|
await projectService.deleteProjectFile(filename);
|
||||||
|
|
||||||
|
res.status(204).send({
|
||||||
|
message: `Deleted project ${filename}`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getInfo(_req: Request, res: Response<GetInfo>) {
|
||||||
|
const info = await projectService.getInfo();
|
||||||
|
res.status(200).send(info);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* uploads and parses an excel spreadsheet
|
||||||
|
* @returns parsed result
|
||||||
|
*/
|
||||||
|
export async function previewSpreadsheet(req: Request, res: Response) {
|
||||||
|
if (!req.file) {
|
||||||
|
res.status(400).send({ message: 'File not found' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const filePath = req.file.path;
|
||||||
|
if (!fs.existsSync(filePath)) {
|
||||||
|
throw new Error('Upload failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const options = JSON.parse(req.body.options);
|
||||||
|
const data = handleMaybeExcel(filePath, options);
|
||||||
|
res.status(200).send(data);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(500).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { Request } from 'express';
|
||||||
|
import multer, { FileFilterCallback } from 'multer';
|
||||||
|
|
||||||
|
import { EXCEL_MIME, JSON_MIME } from '../../utils/parser.js';
|
||||||
|
import { storage } from '../../utils/upload.js';
|
||||||
|
|
||||||
|
const filterProjectFile = (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
|
||||||
|
if (file.mimetype.includes(JSON_MIME)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(null, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const filterSpreadsheet = (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
|
||||||
|
if (file.mimetype.includes(EXCEL_MIME)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(null, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Build multer uploader for a single file
|
||||||
|
export const uploadProjectFile = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: filterProjectFile,
|
||||||
|
}).single('project');
|
||||||
|
|
||||||
|
export const uploadSpreadsheet = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: filterSpreadsheet,
|
||||||
|
}).single('spreadsheet');
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
createProjectFile,
|
||||||
|
projectDownload,
|
||||||
|
deleteProjectFile,
|
||||||
|
getInfo,
|
||||||
|
listProjects,
|
||||||
|
patchPartialProjectFile,
|
||||||
|
previewSpreadsheet,
|
||||||
|
loadProject,
|
||||||
|
duplicateProjectFile,
|
||||||
|
renameProjectFile,
|
||||||
|
postProjectFile,
|
||||||
|
} from './db.controller.js';
|
||||||
|
import { uploadProjectFile, uploadSpreadsheet } from './db.middleware.js';
|
||||||
|
import {
|
||||||
|
projectSanitiser,
|
||||||
|
sanitizeProjectFilename,
|
||||||
|
validateLoadProjectFile,
|
||||||
|
validatePatchProjectFile,
|
||||||
|
validateProjectDuplicate,
|
||||||
|
validateProjectRename,
|
||||||
|
} from './db.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/download', projectDownload);
|
||||||
|
router.post('/upload', uploadProjectFile, postProjectFile);
|
||||||
|
|
||||||
|
router.patch('/', validatePatchProjectFile, patchPartialProjectFile);
|
||||||
|
router.post('/new', projectSanitiser, createProjectFile);
|
||||||
|
|
||||||
|
router.get('/all', listProjects);
|
||||||
|
|
||||||
|
router.post('/load', validateLoadProjectFile, sanitizeProjectFilename, loadProject);
|
||||||
|
router.post('/:filename/duplicate', validateProjectDuplicate, sanitizeProjectFilename, duplicateProjectFile);
|
||||||
|
router.put('/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
||||||
|
router.delete('/:filename', sanitizeProjectFilename, deleteProjectFile);
|
||||||
|
|
||||||
|
router.get('/info', getInfo);
|
||||||
|
|
||||||
|
// TODO: validate import map
|
||||||
|
router.post('/spreadsheet/preview', uploadSpreadsheet, previewSpreadsheet);
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
|
||||||
|
import { ensureJsonExtension } from '../../utils/fileManagement.js';
|
||||||
|
|
||||||
|
export const projectSanitiser = [
|
||||||
|
body('title').optional().isString().trim(),
|
||||||
|
body('description').optional().isString().trim(),
|
||||||
|
body('publicUrl').optional().isString().trim(),
|
||||||
|
body('publicInfo').optional().isString().trim(),
|
||||||
|
body('backstageUrl').optional().isString().trim(),
|
||||||
|
body('backstageInfo').optional().isString().trim(),
|
||||||
|
body('endMessage').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 sanitizeProjectFilename = (req: Request, _res: Response, next: NextFunction) => {
|
||||||
|
const { filename, newFilename } = req.body;
|
||||||
|
const { filename: projectName } = req.params;
|
||||||
|
|
||||||
|
req.body.filename = ensureJsonExtension(filename);
|
||||||
|
req.body.newFilename = ensureJsonExtension(newFilename);
|
||||||
|
req.params.filename = ensureJsonExtension(projectName);
|
||||||
|
|
||||||
|
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('customFields').isObject().optional({ nullable: false }),
|
||||||
|
body('osc').isObject().optional({ nullable: false }),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filename for loading a project file.
|
||||||
|
*/
|
||||||
|
export const validateLoadProjectFile = [
|
||||||
|
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filenames for duplicating a project.
|
||||||
|
*/
|
||||||
|
export const validateProjectDuplicate = [
|
||||||
|
body('newFilename')
|
||||||
|
.exists()
|
||||||
|
.withMessage('New project filename is required')
|
||||||
|
.isString()
|
||||||
|
.withMessage('New project filename must be a string')
|
||||||
|
.isLength({ min: 1, max: 255 })
|
||||||
|
.withMessage('New project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates the filenames for renaming a project.
|
||||||
|
*/
|
||||||
|
export const validateProjectRename = [
|
||||||
|
body('newFilename')
|
||||||
|
.exists()
|
||||||
|
.withMessage('Duplicate project filename is required')
|
||||||
|
.isString()
|
||||||
|
.withMessage('Duplicate project filename must be a string')
|
||||||
|
.isLength({ min: 1, max: 255 })
|
||||||
|
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
return res.status(422).json({ errors: errors.array() });
|
||||||
|
}
|
||||||
|
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ErrorResponse, HttpSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
import { httpIntegration } from '../../services/integration-service/HttpIntegration.js';
|
||||||
|
|
||||||
|
export async function getHTTP(_req: Request, res: Response<HttpSettings>) {
|
||||||
|
const http = DataProvider.getHttp();
|
||||||
|
res.status(200).send(http);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postHTTP(req: Request, res: Response<HttpSettings | ErrorResponse>) {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const httpSettings = req.body;
|
||||||
|
|
||||||
|
httpIntegration.init(httpSettings);
|
||||||
|
// we persist the data after init to avoid persisting invalid data
|
||||||
|
const result = await DataProvider.setHttp(httpSettings);
|
||||||
|
res.send(result).status(200);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { validateHTTP } from './http.validation.js';
|
||||||
|
import { getHTTP, postHTTP } from './http.controller.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getHTTP);
|
||||||
|
router.post('/', validateHTTP, postHTTP);
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
import { sanitiseHttpSubscriptions } from '../../utils/parserFunctions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates object for POST /ontime/http
|
||||||
|
*/
|
||||||
|
export const validateHTTP = [
|
||||||
|
body('enabledOut').exists().isBoolean(),
|
||||||
|
body('subscriptions')
|
||||||
|
.exists()
|
||||||
|
.isArray()
|
||||||
|
.custom((value) => sanitiseHttpSubscriptions(value)),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { router as aliasesRouter } from './aliases/aliases.router.js';
|
||||||
|
import { router as customFieldsRouter } from './custom-fields/customFields.router.js';
|
||||||
|
import { router as dbRouter } from './db/db.router.js';
|
||||||
|
import { router as httpRouter } from './http/http.router.js';
|
||||||
|
import { router as oscRouter } from './osc/osc.router.js';
|
||||||
|
import { router as projectRouter } from './project/project.router.js';
|
||||||
|
import { router as rundownRouter } from './rundown/rundown.router.js';
|
||||||
|
import { router as settingsRouter } from './settings/settings.router.js';
|
||||||
|
import { router as sheetsRouter } from './sheets/sheets.router.js';
|
||||||
|
import { router as viewSettingsRouter } from './view-settings/viewSettings.router.js';
|
||||||
|
|
||||||
|
export const appRouter = express.Router();
|
||||||
|
|
||||||
|
appRouter.use('/aliases', aliasesRouter);
|
||||||
|
appRouter.use('/custom-fields', customFieldsRouter);
|
||||||
|
appRouter.use('/db', dbRouter);
|
||||||
|
appRouter.use('/http', httpRouter);
|
||||||
|
appRouter.use('/osc', oscRouter);
|
||||||
|
appRouter.use('/project', projectRouter);
|
||||||
|
appRouter.use('/rundown', rundownRouter);
|
||||||
|
appRouter.use('/settings', settingsRouter);
|
||||||
|
appRouter.use('/sheets', sheetsRouter);
|
||||||
|
appRouter.use('/view-settings', viewSettingsRouter);
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import type { ErrorResponse, OSCSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
import { oscIntegration } from '../../services/integration-service/OscIntegration.js';
|
||||||
|
|
||||||
|
export async function getOSC(_req: Request, res: Response<OSCSettings>) {
|
||||||
|
const osc = DataProvider.getOsc();
|
||||||
|
res.status(200).send(osc);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postOSC(req: Request, res: Response<OSCSettings | ErrorResponse>) {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const oscSettings = req.body;
|
||||||
|
|
||||||
|
oscIntegration.init(oscSettings);
|
||||||
|
// we persist the data after init to avoid persisting invalid data
|
||||||
|
const result = await DataProvider.setOsc(oscSettings);
|
||||||
|
res.send(result).status(200);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { getOSC, postOSC } from './osc.controller.js';
|
||||||
|
import { validateOSC } from './osc.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getOSC);
|
||||||
|
router.post('/', validateOSC, postOSC);
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
import { sanitiseOscSubscriptions } from '../../utils/parserFunctions.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates object for POST /ontime/osc
|
||||||
|
*/
|
||||||
|
export const validateOSC = [
|
||||||
|
body('portIn').exists().isPort(),
|
||||||
|
body('portOut').exists().isPort(),
|
||||||
|
body('targetIP').exists().isIP(),
|
||||||
|
body('enabledIn').exists().isBoolean(),
|
||||||
|
body('enabledOut').exists().isBoolean(),
|
||||||
|
body('subscriptions')
|
||||||
|
.exists()
|
||||||
|
.isArray()
|
||||||
|
.custom((value) => sanitiseOscSubscriptions(value)),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { ErrorResponse, ProjectData } from 'ontime-types';
|
||||||
|
|
||||||
|
import type { Request, Response } from 'express';
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { removeUndefined } from '../../utils/parserUtils.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
|
||||||
|
export async function getProjectData(_req: Request, res: Response<ProjectData>) {
|
||||||
|
res.json(DataProvider.getProjectData());
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postProjectData(req: Request, res: Response<ProjectData | ErrorResponse>) {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newEvent: Partial<ProjectData> = removeUndefined({
|
||||||
|
title: req.body?.title,
|
||||||
|
description: req.body?.description,
|
||||||
|
publicUrl: req.body?.publicUrl,
|
||||||
|
publicInfo: req.body?.publicInfo,
|
||||||
|
backstageUrl: req.body?.backstageUrl,
|
||||||
|
backstageInfo: req.body?.backstageInfo,
|
||||||
|
endMessage: req.body?.endMessage,
|
||||||
|
});
|
||||||
|
const newData = await DataProvider.setProjectData(newEvent);
|
||||||
|
res.status(200).send(newData);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { getProjectData, postProjectData } from './project.controller.js';
|
||||||
|
import { projectSanitiser } from './project.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getProjectData);
|
||||||
|
router.post('/', projectSanitiser, postProjectData);
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
|
||||||
|
export const projectSanitiser = [
|
||||||
|
body('title').optional().isString().trim(),
|
||||||
|
body('description').optional().isString().trim(),
|
||||||
|
body('publicUrl').optional().isString().trim(),
|
||||||
|
body('publicInfo').optional().isString().trim(),
|
||||||
|
body('backstageUrl').optional().isString().trim(),
|
||||||
|
body('backstageInfo').optional().isString().trim(),
|
||||||
|
body('endMessage').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();
|
||||||
|
},
|
||||||
|
];
|
||||||
+37
-51
@@ -1,8 +1,8 @@
|
|||||||
import { RundownCached } from 'ontime-types';
|
import { ErrorResponse, MessageResponse, OntimeRundown, OntimeRundownEntry, RundownCached } from 'ontime-types';
|
||||||
|
|
||||||
import { Request, Response, RequestHandler } from 'express';
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
applyDelay,
|
applyDelay,
|
||||||
@@ -12,26 +12,20 @@ import {
|
|||||||
editEvent,
|
editEvent,
|
||||||
reorderEvent,
|
reorderEvent,
|
||||||
swapEvents,
|
swapEvents,
|
||||||
} from '../services/rundown-service/RundownService.js';
|
} from '../../services/rundown-service/RundownService.js';
|
||||||
import { getNormalisedRundown, getRundown } from '../services/rundown-service/rundownUtils.js';
|
import { getNormalisedRundown, getRundown } from '../../services/rundown-service/rundownUtils.js';
|
||||||
|
|
||||||
// Create controller for GET request to '/events'
|
export async function rundownGetAll(_req: Request, res: Response<OntimeRundown>) {
|
||||||
// Returns -
|
|
||||||
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
|
||||||
const rundown = getRundown();
|
const rundown = getRundown();
|
||||||
res.json(rundown);
|
res.json(rundown);
|
||||||
};
|
}
|
||||||
|
|
||||||
// Create controller for GET request to '/events/cached'
|
export async function rundownGetNormalised(_req: Request, res: Response<RundownCached>) {
|
||||||
// Returns -
|
|
||||||
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
|
||||||
const cachedRundown = getNormalisedRundown();
|
const cachedRundown = getNormalisedRundown();
|
||||||
res.json(cachedRundown);
|
res.json(cachedRundown);
|
||||||
};
|
}
|
||||||
|
|
||||||
// Create controller for POST request to '/events/'
|
export async function rundownPost(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||||
// Returns -
|
|
||||||
export const rundownPost: RequestHandler = async (req, res) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -42,11 +36,9 @@ export const rundownPost: RequestHandler = async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Create controller for PUT request to '/events/'
|
export async function rundownPut(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||||
// Returns -
|
|
||||||
export const rundownPut: RequestHandler = async (req, res) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -57,9 +49,9 @@ export const rundownPut: RequestHandler = async (req, res) => {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const rundownBatchPut: RequestHandler = async (req, res) => {
|
export async function rundownBatchPut(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return res.status(404);
|
return res.status(404);
|
||||||
}
|
}
|
||||||
@@ -67,13 +59,13 @@ export const rundownBatchPut: RequestHandler = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { data, ids } = req.body;
|
const { data, ids } = req.body;
|
||||||
await batchEditEvents(ids, data);
|
await batchEditEvents(ids, data);
|
||||||
res.status(200);
|
res.status(200).send({ message: 'Batch edit successful' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send(error);
|
res.status(400).send(error);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const rundownReorder: RequestHandler = async (req, res) => {
|
export async function rundownReorder(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -81,13 +73,13 @@ export const rundownReorder: RequestHandler = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { eventId, from, to } = req.body;
|
const { eventId, from, to } = req.body;
|
||||||
const event = await reorderEvent(eventId, from, to);
|
const event = await reorderEvent(eventId, from, to);
|
||||||
res.status(200).send(event);
|
res.status(200).send(event.newEvent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
export const rundownSwap: RequestHandler = async (req, res) => {
|
export async function rundownSwap(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
if (failEmptyObjects(req.body, res)) {
|
if (failEmptyObjects(req.body, res)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -95,41 +87,35 @@ export const rundownSwap: RequestHandler = async (req, res) => {
|
|||||||
try {
|
try {
|
||||||
const { from, to } = req.body;
|
const { from, to } = req.body;
|
||||||
await swapEvents(from, to);
|
await swapEvents(from, to);
|
||||||
res.sendStatus(200);
|
res.status(200).send({ message: 'Swap successful' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Create controller for PATCH request to '/events/applydelay/:eventId'
|
export async function rundownApplyDelay(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
// Returns -
|
|
||||||
export const rundownApplyDelay: RequestHandler = async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
await applyDelay(req.params.eventId);
|
await applyDelay(req.params.eventId);
|
||||||
res.sendStatus(200);
|
res.status(200).send({ message: 'Delay applied' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
// Create controller for DELETE request to '/events/:eventId'
|
export async function rundownDelete(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
// Returns -
|
|
||||||
export const deleteEventById: RequestHandler = async (req, res) => {
|
|
||||||
try {
|
|
||||||
await deleteEvent(req.params.eventId);
|
|
||||||
res.sendStatus(204);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: error.toString() });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for DELETE request to '/events/'
|
|
||||||
// Returns -
|
|
||||||
export const rundownDelete: RequestHandler = async (req, res) => {
|
|
||||||
try {
|
try {
|
||||||
await deleteAllEvents();
|
await deleteAllEvents();
|
||||||
res.sendStatus(204);
|
res.status(204).send({ message: 'All events deleted' });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).send({ message: error.toString() });
|
res.status(400).send({ message: error.toString() });
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export async function deleteEventById(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||||
|
try {
|
||||||
|
await deleteEvent(req.params.eventId);
|
||||||
|
res.status(204).send({ message: 'Event deleted' });
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: error.toString() });
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-20
@@ -1,16 +1,17 @@
|
|||||||
import express from 'express';
|
import express from 'express';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
deleteEventById,
|
deleteEventById,
|
||||||
rundownApplyDelay,
|
rundownApplyDelay,
|
||||||
|
rundownBatchPut,
|
||||||
rundownDelete,
|
rundownDelete,
|
||||||
rundownGetAll,
|
rundownGetAll,
|
||||||
rundownGetCached,
|
rundownGetNormalised,
|
||||||
rundownPost,
|
rundownPost,
|
||||||
rundownPut,
|
rundownPut,
|
||||||
rundownReorder,
|
rundownReorder,
|
||||||
rundownSwap,
|
rundownSwap,
|
||||||
rundownBatchPut,
|
} from './rundown.controller.js';
|
||||||
} from '../controllers/rundownController.js';
|
|
||||||
import {
|
import {
|
||||||
paramsMustHaveEventId,
|
paramsMustHaveEventId,
|
||||||
rundownBatchPutValidator,
|
rundownBatchPutValidator,
|
||||||
@@ -18,34 +19,21 @@ import {
|
|||||||
rundownPutValidator,
|
rundownPutValidator,
|
||||||
rundownReorderValidator,
|
rundownReorderValidator,
|
||||||
rundownSwapValidator,
|
rundownSwapValidator,
|
||||||
} from '../controllers/rundownController.validate.js';
|
} from './rundown.validation.js';
|
||||||
|
|
||||||
export const router = express.Router();
|
export const router = express.Router();
|
||||||
|
|
||||||
// create route between controller and '/events/cached' endpoint
|
router.get('/', rundownGetAll); // not used in Ontime frontend
|
||||||
router.get('/cached', rundownGetCached);
|
router.get('/normalised', rundownGetNormalised);
|
||||||
|
|
||||||
// create route between controller and '/events/' endpoint
|
|
||||||
router.get('/', rundownGetAll);
|
|
||||||
|
|
||||||
// create route between controller and '/events/' endpoint
|
|
||||||
router.post('/', rundownPostValidator, rundownPost);
|
router.post('/', rundownPostValidator, rundownPost);
|
||||||
|
|
||||||
// create route between controller and '/events/' endpoint
|
|
||||||
router.put('/', rundownPutValidator, rundownPut);
|
router.put('/', rundownPutValidator, rundownPut);
|
||||||
|
router.put('/batch', rundownBatchPutValidator, rundownBatchPut);
|
||||||
|
|
||||||
router.put('/batchEdit', rundownBatchPutValidator, rundownBatchPut);
|
|
||||||
|
|
||||||
// create route between controller and '/events/reorder' endpoint
|
|
||||||
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
router.patch('/reorder/', rundownReorderValidator, rundownReorder);
|
||||||
|
|
||||||
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
router.patch('/swap', rundownSwapValidator, rundownSwap);
|
||||||
|
|
||||||
// create route between controller and '/events/applydelay/:eventId' endpoint
|
|
||||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||||
|
|
||||||
// create route between controller and '/events/all' endpoint
|
|
||||||
router.delete('/all', rundownDelete);
|
router.delete('/all', rundownDelete);
|
||||||
|
|
||||||
// create route between controller and '/events/:eventId' endpoint
|
|
||||||
router.delete('/:eventId', paramsMustHaveEventId, deleteEventById);
|
router.delete('/:eventId', paramsMustHaveEventId, deleteEventById);
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { ErrorResponse, Settings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
import { extractPin } from '../../services/project-service/ProjectService.js';
|
||||||
|
import { isDocker } from '../../setup/index.js';
|
||||||
|
|
||||||
|
export async function getSettings(_req: Request, res: Response<Settings>) {
|
||||||
|
const settings = DataProvider.getSettings();
|
||||||
|
res.status(200).send(settings);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postSettings(req: Request, res: Response<Settings | ErrorResponse>) {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const settings = DataProvider.getSettings();
|
||||||
|
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
|
||||||
|
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
|
||||||
|
const serverPort = Number(req.body?.serverPort);
|
||||||
|
if (isNaN(serverPort)) {
|
||||||
|
return res.status(400).send({ message: `Invalid value found for server port: ${req.body?.serverPort}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChangedPort = settings.serverPort !== serverPort;
|
||||||
|
|
||||||
|
if (isDocker && hasChangedPort) {
|
||||||
|
return res.status(403).json({ message: 'Can`t change port when running inside docker' });
|
||||||
|
}
|
||||||
|
|
||||||
|
let timeFormat = settings.timeFormat;
|
||||||
|
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
|
||||||
|
timeFormat = req.body.timeFormat;
|
||||||
|
}
|
||||||
|
|
||||||
|
const language = req.body?.language || 'en';
|
||||||
|
|
||||||
|
const newData = {
|
||||||
|
...settings,
|
||||||
|
editorKey,
|
||||||
|
operatorKey,
|
||||||
|
timeFormat,
|
||||||
|
language,
|
||||||
|
serverPort,
|
||||||
|
};
|
||||||
|
await DataProvider.setSettings(newData);
|
||||||
|
res.status(200).send(newData);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import express from 'express';
|
||||||
|
import { getSettings, postSettings } from './settings.controller.js';
|
||||||
|
import { validateSettings } from './settings.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getSettings);
|
||||||
|
router.post('/', validateSettings, postSettings);
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { body, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates object for POST /ontime/settings
|
||||||
|
*/
|
||||||
|
export const validateSettings = [
|
||||||
|
body('editorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
||||||
|
body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
||||||
|
body('timeFormat').isString().isIn(['12', '24']),
|
||||||
|
body('language').isString(),
|
||||||
|
body('serverPort').isPort().optional(),
|
||||||
|
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
+2
-2
@@ -6,7 +6,7 @@
|
|||||||
import { Request, Response } from 'express';
|
import { Request, Response } from 'express';
|
||||||
import { readFileSync } from 'fs';
|
import { readFileSync } from 'fs';
|
||||||
|
|
||||||
import { deleteFile } from '../utils/parserUtils.js';
|
import { deleteFile } from '../../utils/parserUtils.js';
|
||||||
import {
|
import {
|
||||||
revoke,
|
revoke,
|
||||||
handleClientSecret,
|
handleClientSecret,
|
||||||
@@ -14,7 +14,7 @@ import {
|
|||||||
hasAuth,
|
hasAuth,
|
||||||
download,
|
download,
|
||||||
upload,
|
upload,
|
||||||
} from '../services/sheet-service/SheetService.js';
|
} from '../../services/sheet-service/SheetService.js';
|
||||||
|
|
||||||
export async function requestConnection(req: Request, res: Response) {
|
export async function requestConnection(req: Request, res: Response) {
|
||||||
const { sheetId } = req.params;
|
const { sheetId } = req.params;
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
import { Request } from 'express';
|
||||||
|
import multer, { FileFilterCallback } from 'multer';
|
||||||
|
|
||||||
|
import { JSON_MIME } from '../../utils/parser.js';
|
||||||
|
import { storage } from '../../utils/upload.js';
|
||||||
|
|
||||||
|
const filterClientSecret = (_req: Request, file: Express.Multer.File, cb: FileFilterCallback) => {
|
||||||
|
if (file.mimetype.includes(JSON_MIME)) {
|
||||||
|
cb(null, true);
|
||||||
|
} else {
|
||||||
|
cb(null, false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const uploadClientSecret = multer({
|
||||||
|
storage,
|
||||||
|
fileFilter: filterClientSecret,
|
||||||
|
}).single('client_secret');
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
/**
|
||||||
|
* This is a feature specific router for integration with google sheets
|
||||||
|
*/
|
||||||
|
|
||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
readFromSheet,
|
||||||
|
requestConnection,
|
||||||
|
revokeAuthentication,
|
||||||
|
verifyAuthentication,
|
||||||
|
writeToSheet,
|
||||||
|
} from './sheets.controller.js';
|
||||||
|
import { uploadClientSecret } from './sheets.middleware.js';
|
||||||
|
import { validateRequestConnection, validateSheetOptions } from './sheets.validation.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/connect', verifyAuthentication);
|
||||||
|
router.post('/:sheetId/connect', uploadClientSecret, validateRequestConnection, requestConnection);
|
||||||
|
|
||||||
|
router.post('/revoke', revokeAuthentication);
|
||||||
|
|
||||||
|
router.post('/:sheetId/read', validateSheetOptions, readFromSheet);
|
||||||
|
router.post('/:sheetId/write', validateSheetOptions, writeToSheet);
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import type { ErrorResponse, ViewSettings } from 'ontime-types';
|
||||||
|
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
|
||||||
|
import { DataProvider } from '../../classes/data-provider/DataProvider.js';
|
||||||
|
import { failEmptyObjects } from '../../utils/routerUtils.js';
|
||||||
|
|
||||||
|
export async function getViewSettings(_req: Request, res: Response<ViewSettings>) {
|
||||||
|
const views = DataProvider.getViewSettings();
|
||||||
|
res.status(200).send(views);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function postViewSettings(req: Request, res: Response<ViewSettings | ErrorResponse>) {
|
||||||
|
if (failEmptyObjects(req.body, res)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const newData = {
|
||||||
|
overrideStyles: req.body.overrideStyles,
|
||||||
|
endMessage: req.body?.endMessage || '',
|
||||||
|
normalColor: req.body.normalColor,
|
||||||
|
warningColor: req.body.warningColor,
|
||||||
|
warningThreshold: req.body.warningThreshold,
|
||||||
|
dangerColor: req.body.dangerColor,
|
||||||
|
dangerThreshold: req.body.dangerThreshold,
|
||||||
|
};
|
||||||
|
await DataProvider.setViewSettings(newData);
|
||||||
|
res.status(200).send(newData);
|
||||||
|
} catch (error) {
|
||||||
|
res.status(400).send({ message: String(error) });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import express from 'express';
|
||||||
|
|
||||||
|
import { validateViewSettings } from './viewSettings.validation.js';
|
||||||
|
import { getViewSettings, postViewSettings } from './viewSettings.controller.js';
|
||||||
|
|
||||||
|
export const router = express.Router();
|
||||||
|
|
||||||
|
router.get('/', getViewSettings);
|
||||||
|
router.post('/', validateViewSettings, postViewSettings);
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { check, validationResult } from 'express-validator';
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @description Validates object for POST /ontime/views
|
||||||
|
*/
|
||||||
|
export const validateViewSettings = [
|
||||||
|
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
|
||||||
|
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
|
||||||
|
check('normalColor').isString().trim().withMessage('normalColor value must be string'),
|
||||||
|
check('warningColor').isString().trim().withMessage('warningColor value must be string'),
|
||||||
|
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
||||||
|
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
||||||
|
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
||||||
|
(req: Request, res: Response, next: NextFunction) => {
|
||||||
|
const errors = validationResult(req);
|
||||||
|
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
||||||
|
next();
|
||||||
|
},
|
||||||
|
];
|
||||||
+2
-1
@@ -7,10 +7,11 @@ import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
|
|||||||
import { messageService } from '../services/message-service/MessageService.js';
|
import { messageService } from '../services/message-service/MessageService.js';
|
||||||
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
import { parse, updateEvent } from './integrationController.config.js';
|
|
||||||
import { extraTimerService } from '../services/extra-timer-service/ExtraTimerService.js';
|
import { extraTimerService } from '../services/extra-timer-service/ExtraTimerService.js';
|
||||||
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
|
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
|
||||||
|
|
||||||
|
import { parse, updateEvent } from './integration.utils.js';
|
||||||
|
|
||||||
export type ChangeOptions = {
|
export type ChangeOptions = {
|
||||||
eventId: string;
|
eventId: string;
|
||||||
property: string;
|
property: string;
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* API Router
|
||||||
|
* User to handle all requests which affect runtime
|
||||||
|
* It is a mirror implementation of OSC and Websocket Adapters
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { ErrorResponse, LogOrigin, RuntimeStore } from 'ontime-types';
|
||||||
|
|
||||||
|
import express, { type Request, type Response } from 'express';
|
||||||
|
|
||||||
|
import { logger } from '../classes/Logger.js';
|
||||||
|
import { objectFromPath } from '../adapters/utils/parse.js';
|
||||||
|
|
||||||
|
import { dispatchFromAdapter } from './integration.controller.js';
|
||||||
|
import { unpackError } from 'ontime-utils';
|
||||||
|
import { eventStore } from '../stores/EventStore.js';
|
||||||
|
|
||||||
|
export const integrationRouter = express.Router();
|
||||||
|
|
||||||
|
const helloMessage = 'You have reached Ontime API server';
|
||||||
|
|
||||||
|
integrationRouter.get('/', (_req: Request, res: Response<{ message: string }>) => {
|
||||||
|
res.status(200).json({ message: helloMessage });
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* All calls are sent to the dispatcher
|
||||||
|
*/
|
||||||
|
integrationRouter.get('/*', (req: Request, res: Response) => {
|
||||||
|
let action = req.path.substring(1);
|
||||||
|
|
||||||
|
if (!action) {
|
||||||
|
return res.status(400).json({ error: 'No action found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const actionArray = action.split('/');
|
||||||
|
const params = { payload: req.query as object } as { payload: object | null };
|
||||||
|
|
||||||
|
if (actionArray.length > 1) {
|
||||||
|
action = actionArray.shift() || '';
|
||||||
|
params.payload = objectFromPath(actionArray, params.payload);
|
||||||
|
}
|
||||||
|
|
||||||
|
const reply = dispatchFromAdapter(action, params, 'http');
|
||||||
|
res.status(202).json(reply);
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = unpackError(error);
|
||||||
|
logger.error(LogOrigin.Rx, `HTTP IN: ${errorMessage}`);
|
||||||
|
res.status(500).send({ message: errorMessage });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
integrationRouter.get('/poll', (_req: Request, res: Response<Partial<RuntimeStore> | ErrorResponse>) => {
|
||||||
|
try {
|
||||||
|
const state = eventStore.poll();
|
||||||
|
res.status(200).send(state);
|
||||||
|
} catch (error) {
|
||||||
|
const message = unpackError(error);
|
||||||
|
res.status(500).send({
|
||||||
|
message: `Could not get sync data: ${message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
+1
@@ -1,4 +1,5 @@
|
|||||||
import { OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
import { OntimeEvent, isKeyOfType, isOntimeEvent } from 'ontime-types';
|
||||||
|
|
||||||
import { editEvent } from '../services/rundown-service/RundownService.js';
|
import { editEvent } from '../services/rundown-service/RundownService.js';
|
||||||
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
||||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||||
@@ -19,11 +19,9 @@ import {
|
|||||||
} from './setup/index.js';
|
} from './setup/index.js';
|
||||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||||
|
|
||||||
// Import Routes
|
// Import Routers
|
||||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
import { appRouter } from './api-data/index.js';
|
||||||
import { router as projectRouter } from './routes/projectRouter.js';
|
import { integrationRouter } from './api-integration/integration.router.js';
|
||||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
|
||||||
import { router as apiRouter } from './routes/apiRouter.js';
|
|
||||||
|
|
||||||
// Import adapters
|
// Import adapters
|
||||||
import { OscServer } from './adapters/OscAdapter.js';
|
import { OscServer } from './adapters/OscAdapter.js';
|
||||||
@@ -70,10 +68,8 @@ app.use(express.urlencoded({ extended: true }));
|
|||||||
app.use(express.json({ limit: '1mb' }));
|
app.use(express.json({ limit: '1mb' }));
|
||||||
|
|
||||||
// Implement route endpoints
|
// Implement route endpoints
|
||||||
app.use('/events', rundownRouter);
|
app.use('/data', appRouter); // router for application data
|
||||||
app.use('/project', projectRouter);
|
app.use('/api', integrationRouter); // router for integrations
|
||||||
app.use('/ontime', ontimeRouter);
|
|
||||||
app.use('/api', apiRouter);
|
|
||||||
|
|
||||||
// serve static - css
|
// serve static - css
|
||||||
app.use('/external/styles', express.static(resolveStylesDirectory));
|
app.use('/external/styles', express.static(resolveStylesDirectory));
|
||||||
|
|||||||
@@ -1,477 +0,0 @@
|
|||||||
import type {
|
|
||||||
Alias,
|
|
||||||
DatabaseModel,
|
|
||||||
GetInfo,
|
|
||||||
HttpSettings,
|
|
||||||
ProjectData,
|
|
||||||
ErrorResponse,
|
|
||||||
ProjectFileListResponse,
|
|
||||||
OSCSettings,
|
|
||||||
RuntimeStore,
|
|
||||||
Settings,
|
|
||||||
ViewSettings,
|
|
||||||
} from 'ontime-types';
|
|
||||||
|
|
||||||
import { RequestHandler, Request, Response } from 'express';
|
|
||||||
import fs from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
|
||||||
import { failEmptyObjects, failIsNotArray } from '../utils/routerUtils.js';
|
|
||||||
import { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
|
||||||
import { eventStore } from '../stores/EventStore.js';
|
|
||||||
import { isDocker, resolveDbPath, resolveProjectsDirectory, uploadsFolderPath } from '../setup/index.js';
|
|
||||||
import { oscIntegration } from '../services/integration-service/OscIntegration.js';
|
|
||||||
import { httpIntegration } from '../services/integration-service/HttpIntegration.js';
|
|
||||||
import { setRundown } from '../services/rundown-service/RundownService.js';
|
|
||||||
import { appStateService } from '../services/app-state-service/AppStateService.js';
|
|
||||||
import type { OntimeError } from '../utils/backend.types.js';
|
|
||||||
import { generateUniqueFileName } from '../utils/generateUniqueFilename.js';
|
|
||||||
|
|
||||||
import * as projectService from '../services/project-service/ProjectService.js';
|
|
||||||
import { extractPin } from '../services/project-service/ProjectService.js';
|
|
||||||
import { handleMaybeExcel } from '../utils/parser.js';
|
|
||||||
import { ensureJsonExtension } from '../utils/fileManagement.js';
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/poll'
|
|
||||||
// Returns data for current state
|
|
||||||
export const poll = async (_req: Request, res: Response<Partial<RuntimeStore> | ErrorResponse>) => {
|
|
||||||
try {
|
|
||||||
const state = eventStore.poll();
|
|
||||||
res.status(200).send(state);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({
|
|
||||||
message: `Could not get sync data: ${error}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/db'
|
|
||||||
// Returns -
|
|
||||||
export const dbDownload = async (_req: Request, res: Response) => {
|
|
||||||
const { title } = DataProvider.getProjectData();
|
|
||||||
const fileTitle = title || 'ontime data';
|
|
||||||
|
|
||||||
res.download(resolveDbPath, `${fileTitle}.json`, (err) => {
|
|
||||||
if (err) {
|
|
||||||
res.status(500).send({
|
|
||||||
message: `Could not download the file: ${err}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/info'
|
|
||||||
// Returns -
|
|
||||||
export const getInfo = async (_req: Request, res: Response<GetInfo>) => {
|
|
||||||
const info = await projectService.getInfo();
|
|
||||||
res.status(200).send(info);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/aliases'
|
|
||||||
// Returns -
|
|
||||||
export const getAliases = async (_req: Request, res: Response<Alias[]>) => {
|
|
||||||
const aliases = DataProvider.getAliases();
|
|
||||||
res.status(200).send(aliases);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/aliases'
|
|
||||||
// Returns ACK message
|
|
||||||
export const postAliases = async (req: Request, res: Response<Alias[] | ErrorResponse>) => {
|
|
||||||
if (failIsNotArray(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const newAliases: Alias[] = [];
|
|
||||||
req.body.forEach((a) => {
|
|
||||||
newAliases.push({
|
|
||||||
enabled: a.enabled,
|
|
||||||
alias: a.alias,
|
|
||||||
pathAndParams: a.pathAndParams,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
await DataProvider.setAliases(newAliases);
|
|
||||||
res.status(200).send(newAliases);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/settings'
|
|
||||||
// Returns -
|
|
||||||
export const getSettings = async (_req: Request, res: Response<Settings>) => {
|
|
||||||
const settings = DataProvider.getSettings();
|
|
||||||
res.status(200).send(settings);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/settings'
|
|
||||||
// Returns ACK message
|
|
||||||
export const postSettings = async (req: Request, res: Response) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const settings = DataProvider.getSettings();
|
|
||||||
const editorKey = extractPin(req.body?.editorKey, settings.editorKey);
|
|
||||||
const operatorKey = extractPin(req.body?.operatorKey, settings.operatorKey);
|
|
||||||
const serverPort = Number(req.body?.serverPort);
|
|
||||||
if (isNaN(serverPort)) {
|
|
||||||
return res.status(400).send(`Invalid value found for server port: ${req.body?.serverPort}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasChangedPort = settings.serverPort !== serverPort;
|
|
||||||
|
|
||||||
if (isDocker && hasChangedPort) {
|
|
||||||
return res.status(403).json({ message: 'Can`t change port when running inside docker' });
|
|
||||||
}
|
|
||||||
|
|
||||||
let timeFormat = settings.timeFormat;
|
|
||||||
if (req.body?.timeFormat === '12' || req.body?.timeFormat === '24') {
|
|
||||||
timeFormat = req.body.timeFormat;
|
|
||||||
}
|
|
||||||
|
|
||||||
const language = req.body?.language || 'en';
|
|
||||||
|
|
||||||
const newData = {
|
|
||||||
...settings,
|
|
||||||
editorKey,
|
|
||||||
operatorKey,
|
|
||||||
timeFormat,
|
|
||||||
language,
|
|
||||||
serverPort,
|
|
||||||
};
|
|
||||||
await DataProvider.setSettings(newData);
|
|
||||||
res.status(200).send(newData);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Get view Settings
|
|
||||||
*/
|
|
||||||
export const getViewSettings = async (_req: Request, res: Response<ViewSettings>) => {
|
|
||||||
const views = DataProvider.getViewSettings();
|
|
||||||
res.status(200).send(views);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Change view Settings
|
|
||||||
*/
|
|
||||||
export const postViewSettings = async (req: Request, res: Response<ViewSettings | ErrorResponse>) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newData = {
|
|
||||||
overrideStyles: req.body.overrideStyles,
|
|
||||||
endMessage: req.body?.endMessage || '',
|
|
||||||
normalColor: req.body.normalColor,
|
|
||||||
warningColor: req.body.warningColor,
|
|
||||||
warningThreshold: req.body.warningThreshold,
|
|
||||||
dangerColor: req.body.dangerColor,
|
|
||||||
dangerThreshold: req.body.dangerThreshold,
|
|
||||||
};
|
|
||||||
await DataProvider.setViewSettings(newData);
|
|
||||||
res.status(200).send(newData);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/osc'
|
|
||||||
// Returns -
|
|
||||||
export const getOSC = async (_req: Request, res: Response<OSCSettings>) => {
|
|
||||||
const osc = DataProvider.getOsc();
|
|
||||||
res.status(200).send(osc);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/osc'
|
|
||||||
// Returns ACK message
|
|
||||||
export const postOSC = async (req: Request, res: Response<OSCSettings | OntimeError>) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const oscSettings = req.body;
|
|
||||||
|
|
||||||
oscIntegration.init(oscSettings);
|
|
||||||
// we persist the data after init to avoid persisting invalid data
|
|
||||||
const result = await DataProvider.setOsc(oscSettings);
|
|
||||||
res.send(result).status(200);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for GET request to '/ontime/http'
|
|
||||||
export const getHTTP = async (_req: Request, res: Response<HttpSettings>) => {
|
|
||||||
const http = DataProvider.getHttp();
|
|
||||||
res.status(200).send(http);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to '/ontime/http'
|
|
||||||
export const postHTTP = async (req: Request, res: Response<HttpSettings | OntimeError>) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const httpSettings = req.body;
|
|
||||||
|
|
||||||
httpIntegration.init(httpSettings);
|
|
||||||
// we persist the data after init to avoid persisting invalid data
|
|
||||||
const result = await DataProvider.setHttp(httpSettings);
|
|
||||||
res.send(result).status(200);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function patchPartialProjectFile(req: Request, res: Response) {
|
|
||||||
// all fields are optional in validation
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
res.status(400).send({ message: 'No field found to patch' });
|
|
||||||
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,
|
|
||||||
customFields: req.body?.customFields,
|
|
||||||
};
|
|
||||||
|
|
||||||
const maybeRundown = req.body?.rundown;
|
|
||||||
await DataProvider.mergeIntoData(patchDb);
|
|
||||||
if (maybeRundown !== undefined) {
|
|
||||||
// it is likely cheaper to invalidate cache than to calculate diff
|
|
||||||
runtimeService.stop();
|
|
||||||
await setRundown(maybeRundown);
|
|
||||||
}
|
|
||||||
res.status(200).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* uploads, parses and applies the data from a given file
|
|
||||||
*/
|
|
||||||
export const uploadProjectFile = async (req: Request, res: Response) => {
|
|
||||||
if (!req.file) {
|
|
||||||
res.status(400).send({ message: 'File not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const options = req.query;
|
|
||||||
const filePath = req.file.path;
|
|
||||||
await projectService.applyProjectFile(filePath, options);
|
|
||||||
res.status(200).send();
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: `Failed parsing ${error}` });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* uploads and parses an excel spreadsheet
|
|
||||||
* @returns parsed result
|
|
||||||
*/
|
|
||||||
export async function previewSpreadsheet(req: Request, res: Response) {
|
|
||||||
if (!req.file) {
|
|
||||||
res.status(400).send({ message: 'File not found' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const filePath = req.file.path;
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
throw new Error('Upload failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = JSON.parse(req.body.options);
|
|
||||||
const data = handleMaybeExcel(filePath, options);
|
|
||||||
res.status(200).send(data);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Retrieves and lists all project files from the uploads directory.
|
|
||||||
* @param _req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const listProjects: RequestHandler = async (_req, res: Response<ProjectFileListResponse | ErrorResponse>) => {
|
|
||||||
try {
|
|
||||||
const data = await projectService.getProjectList();
|
|
||||||
res.status(200).send(data);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Receives a `filename` from the request body and loads the project file from the uploads directory.
|
|
||||||
* @param req
|
|
||||||
* @param res
|
|
||||||
*/
|
|
||||||
export const loadProject: RequestHandler = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const filename = req.body.filename;
|
|
||||||
const filePath = join(resolveProjectsDirectory, filename);
|
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
|
||||||
return res.status(404).send({ message: 'File not found' });
|
|
||||||
}
|
|
||||||
await projectService.applyProjectFile(filePath);
|
|
||||||
res.status(200).send({
|
|
||||||
message: `Loaded project ${filename}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Duplicates a project file.
|
|
||||||
* Receives the original project filename (`filename`) from the request parameters
|
|
||||||
* and the filename for the duplicate (`newFilename`) from the request body.
|
|
||||||
*
|
|
||||||
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
|
||||||
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful duplication,
|
|
||||||
* a 409 status if there are validation errors,
|
|
||||||
* or a 500 status with an error message in case of an exception.
|
|
||||||
*/
|
|
||||||
export const duplicateProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { filename } = req.params;
|
|
||||||
const { newFilename } = req.body;
|
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectService.duplicateProjectFile(filename, newFilename);
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
message: `Duplicated project ${filename} to ${newFilename}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Renames a project file.
|
|
||||||
* Receives the current filename (`filename`) from the request parameters
|
|
||||||
* and the new filename (`newFilename`) from the request body.
|
|
||||||
*
|
|
||||||
* @param {Request} req - The express request object. Expects `filename` in the request parameters and `newFilename` in the request body.
|
|
||||||
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful renaming,
|
|
||||||
* a 409 status if there are validation errors,
|
|
||||||
* or a 500 status with an error message in case of an exception.
|
|
||||||
*/
|
|
||||||
export const renameProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { newFilename } = req.body;
|
|
||||||
const { filename } = req.params;
|
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename, newFilename });
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename the file
|
|
||||||
await projectService.renameProjectFile(filename, newFilename);
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
message: `Renamed project ${filename} to ${newFilename}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new project file.
|
|
||||||
* Receives the project filename (`filename`) from the request body.
|
|
||||||
*
|
|
||||||
* @param {Request} req - The express request object. Expects `filename` in the request body.
|
|
||||||
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful creation,
|
|
||||||
* a 409 status if there are validation errors,
|
|
||||||
* or a 500 status with an error message in case of an exception.
|
|
||||||
*/
|
|
||||||
export const createProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const originalFilename = ensureJsonExtension(req.body.title || 'Untitled');
|
|
||||||
const filename = generateUniqueFileName(uploadsFolderPath, originalFilename);
|
|
||||||
const errors = projectService.validateProjectFiles({ newFilename: filename });
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return res.status(409).send({ message: 'Project with title already exists' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const newProjectData: ProjectData = {
|
|
||||||
title: req.body?.title ?? '',
|
|
||||||
description: req.body?.description ?? '',
|
|
||||||
publicUrl: req.body?.publicUrl ?? '',
|
|
||||||
publicInfo: req.body?.publicInfo ?? '',
|
|
||||||
backstageUrl: req.body?.backstageUrl ?? '',
|
|
||||||
backstageInfo: req.body?.backstageInfo ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
projectService.createProjectFile(filename, newProjectData);
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
filename,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes an existing project file.
|
|
||||||
* Receives the project filename (`filename`) from the request parameters.
|
|
||||||
*
|
|
||||||
* @param {Request} req - The express request object. Expects `filename` in the request parameters.
|
|
||||||
* @param {Response} res - The express response object. Sends a 200 status with a success message upon successful deletion,
|
|
||||||
* a 403 status if attempting to delete the currently loaded project,
|
|
||||||
* a 409 status if there are validation errors,
|
|
||||||
* or a 500 status with an error message in case of an exception.
|
|
||||||
*/
|
|
||||||
export const deleteProjectFile: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const { filename } = req.params;
|
|
||||||
|
|
||||||
const { lastLoadedProject } = await appStateService.get();
|
|
||||||
|
|
||||||
if (lastLoadedProject === filename) {
|
|
||||||
return res.status(403).send({ message: 'Cannot delete currently loaded project' });
|
|
||||||
}
|
|
||||||
|
|
||||||
const errors = projectService.validateProjectFiles({ filename });
|
|
||||||
|
|
||||||
if (errors.length) {
|
|
||||||
return res.status(409).send({ message: errors.join(', ') });
|
|
||||||
}
|
|
||||||
|
|
||||||
await projectService.deleteProjectFile(filename);
|
|
||||||
|
|
||||||
res.status(200).send({
|
|
||||||
message: `Deleted project ${filename}`,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
res.status(500).send({ message: String(error) });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
import { body, check, validationResult } from 'express-validator';
|
|
||||||
import { Request, Response, NextFunction } from 'express';
|
|
||||||
|
|
||||||
import { sanitiseHttpSubscriptions, sanitiseOscSubscriptions } from '../utils/parserFunctions.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/views
|
|
||||||
*/
|
|
||||||
export const viewValidator = [
|
|
||||||
check('overrideStyles').isBoolean().withMessage('overrideStyles value must be boolean'),
|
|
||||||
check('endMessage').isString().trim().withMessage('endMessage value must be string'),
|
|
||||||
check('normalColor').isString().trim().withMessage('normalColor value must be string'),
|
|
||||||
check('warningColor').isString().trim().withMessage('warningColor value must be string'),
|
|
||||||
check('dangerColor').isString().trim().withMessage('dangerColor value must be string'),
|
|
||||||
check('warningThreshold').isNumeric().withMessage('warningThreshold value must be a number'),
|
|
||||||
check('dangerThreshold').isNumeric().withMessage('dangerThreshold value must a number'),
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/aliases
|
|
||||||
*/
|
|
||||||
export const validateAliases = [
|
|
||||||
body().isArray(),
|
|
||||||
body('*.enabled').isBoolean(),
|
|
||||||
body('*.alias').isString().trim(),
|
|
||||||
body('*.pathAndParams').isString().trim(),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/settings
|
|
||||||
*/
|
|
||||||
export const validateSettings = [
|
|
||||||
body('editorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
|
||||||
body('operatorKey').isString().isLength({ min: 0, max: 4 }).optional({ nullable: true }),
|
|
||||||
body('timeFormat').isString().isIn(['12', '24']),
|
|
||||||
body('language').isString(),
|
|
||||||
body('serverPort').isPort().optional(),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/osc
|
|
||||||
*/
|
|
||||||
export const validateOSC = [
|
|
||||||
body('portIn').exists().isPort(),
|
|
||||||
body('portOut').exists().isPort(),
|
|
||||||
body('targetIP').exists().isIP(),
|
|
||||||
body('enabledIn').exists().isBoolean(),
|
|
||||||
body('enabledOut').exists().isBoolean(),
|
|
||||||
body('subscriptions')
|
|
||||||
.exists()
|
|
||||||
.isArray()
|
|
||||||
.custom((value) => sanitiseOscSubscriptions(value)),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates object for POST /ontime/http
|
|
||||||
*/
|
|
||||||
export const validateHTTP = [
|
|
||||||
body('enabledOut').exists().isBoolean(),
|
|
||||||
body('subscriptions')
|
|
||||||
.exists()
|
|
||||||
.isArray()
|
|
||||||
.custom((value) => sanitiseHttpSubscriptions(value)),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
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('customFields').isObject().optional({ nullable: false }),
|
|
||||||
body('osc').isObject().optional({ nullable: false }),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) return res.status(422).json({ errors: errors.array() });
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates the filename for loading a project file.
|
|
||||||
*/
|
|
||||||
export const validateLoadProjectFile = [
|
|
||||||
body('filename').exists().withMessage('Filename is required').isString().withMessage('Filename must be a string'),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(422).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates the filenames for duplicating a project.
|
|
||||||
*/
|
|
||||||
export const validateProjectDuplicate = [
|
|
||||||
body('newFilename')
|
|
||||||
.exists()
|
|
||||||
.withMessage('New project filename is required')
|
|
||||||
.isString()
|
|
||||||
.withMessage('New project filename must be a string')
|
|
||||||
.isLength({ min: 1, max: 255 })
|
|
||||||
.withMessage('New project filename must be between 1 and 255 characters'),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(422).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @description Validates the filenames for renaming a project.
|
|
||||||
*/
|
|
||||||
export const validateProjectRename = [
|
|
||||||
body('newFilename')
|
|
||||||
.exists()
|
|
||||||
.withMessage('Duplicate project filename is required')
|
|
||||||
.isString()
|
|
||||||
.withMessage('Duplicate project filename must be a string')
|
|
||||||
.isLength({ min: 1, max: 255 })
|
|
||||||
.withMessage('Duplicate project filename must be between 1 and 255 characters'),
|
|
||||||
|
|
||||||
(req: Request, res: Response, next: NextFunction) => {
|
|
||||||
const errors = validationResult(req);
|
|
||||||
if (!errors.isEmpty()) {
|
|
||||||
return res.status(422).json({ errors: errors.array() });
|
|
||||||
}
|
|
||||||
|
|
||||||
next();
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import type { Request, Response, RequestHandler } from 'express';
|
|
||||||
|
|
||||||
import { CustomField, CustomFields, ProjectData } from 'ontime-types';
|
|
||||||
|
|
||||||
import { removeUndefined } from '../utils/parserUtils.js';
|
|
||||||
import { failEmptyObjects } from '../utils/routerUtils.js';
|
|
||||||
import { DataProvider } from '../classes/data-provider/DataProvider.js';
|
|
||||||
import {
|
|
||||||
createCustomField,
|
|
||||||
editCustomField,
|
|
||||||
getCustomFields as getCustomFieldsFromCache,
|
|
||||||
removeCustomField,
|
|
||||||
} from '../services/rundown-service/rundownCache.js';
|
|
||||||
|
|
||||||
// Create controller for GET request to 'project'
|
|
||||||
export const getProject: RequestHandler = async (req, res) => {
|
|
||||||
res.json(DataProvider.getProjectData());
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create controller for POST request to 'project'
|
|
||||||
export const postProject: RequestHandler = async (req, res) => {
|
|
||||||
if (failEmptyObjects(req.body, res)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newEvent: Partial<ProjectData> = removeUndefined({
|
|
||||||
title: req.body?.title,
|
|
||||||
description: req.body?.description,
|
|
||||||
publicUrl: req.body?.publicUrl,
|
|
||||||
publicInfo: req.body?.publicInfo,
|
|
||||||
backstageUrl: req.body?.backstageUrl,
|
|
||||||
backstageInfo: req.body?.backstageInfo,
|
|
||||||
endMessage: req.body?.endMessage,
|
|
||||||
});
|
|
||||||
const newData = await DataProvider.setProjectData(newEvent);
|
|
||||||
res.status(200).send(newData);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: error.toString() });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getCustomFields: RequestHandler = async (_req: Request, res: Response<CustomFields>) => {
|
|
||||||
const customFields = getCustomFieldsFromCache();
|
|
||||||
res.json(customFields);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expects { label: <label> type: 'string | ..' }
|
|
||||||
export const postCustomField: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
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: <oldLabel>, field: { label: <newLabel> type: 'string | ..' } }
|
|
||||||
export const putCustomField: RequestHandler = async (req: Request, res: Response) => {
|
|
||||||
try {
|
|
||||||
const oldLabel = req.params.label;
|
|
||||||
const { colour, type, label } = req.body;
|
|
||||||
const newFields = await editCustomField(oldLabel, { label, colour, type });
|
|
||||||
res.status(200).send(newFields);
|
|
||||||
} catch (error) {
|
|
||||||
res.status(400).send({ message: error.toString() });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Expects { label: <label> }
|
|
||||||
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() });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
/**
|
|
||||||
* API Router
|
|
||||||
* User to handle all requests which affect runtime
|
|
||||||
* It is a mirror implementation of OSC and Websocket Adapters
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { LogOrigin } from 'ontime-types';
|
|
||||||
|
|
||||||
import express from 'express';
|
|
||||||
|
|
||||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
|
||||||
import { logger } from '../classes/Logger.js';
|
|
||||||
import { objectFromPath } from '../adapters/utils/parse.js';
|
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
|
|
||||||
const helloMessage = 'You have reached Ontime API server';
|
|
||||||
|
|
||||||
// create route between controller and '/api/' endpoint
|
|
||||||
router.get('/', (_req, res) => {
|
|
||||||
res.status(200).json({ message: helloMessage });
|
|
||||||
});
|
|
||||||
|
|
||||||
// any GET request in /api is sent to the integration controller
|
|
||||||
router.get('/*', (req, res) => {
|
|
||||||
let action = req.path.substring(1);
|
|
||||||
const actionArray = action.split('/');
|
|
||||||
|
|
||||||
const params = { payload: req.query as object };
|
|
||||||
|
|
||||||
if (actionArray.length > 1) {
|
|
||||||
action = actionArray.shift();
|
|
||||||
params.payload = objectFromPath(actionArray, params.payload);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const reply = dispatchFromAdapter(action, params, 'http');
|
|
||||||
res.status(202).json(reply);
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(LogOrigin.Rx, `HTTP IN: ${error}`);
|
|
||||||
res.status(500).json({ error: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
import express from 'express';
|
|
||||||
import { uploadClientSecret, uploadFile } from '../utils/upload.js';
|
|
||||||
import {
|
|
||||||
dbDownload,
|
|
||||||
uploadProjectFile,
|
|
||||||
getAliases,
|
|
||||||
getInfo,
|
|
||||||
getOSC,
|
|
||||||
getHTTP,
|
|
||||||
getSettings,
|
|
||||||
getViewSettings,
|
|
||||||
patchPartialProjectFile,
|
|
||||||
poll,
|
|
||||||
postAliases,
|
|
||||||
postOSC,
|
|
||||||
postSettings,
|
|
||||||
postViewSettings,
|
|
||||||
previewSpreadsheet,
|
|
||||||
postHTTP,
|
|
||||||
duplicateProjectFile,
|
|
||||||
listProjects,
|
|
||||||
loadProject,
|
|
||||||
renameProjectFile,
|
|
||||||
createProjectFile,
|
|
||||||
deleteProjectFile,
|
|
||||||
} from '../controllers/ontimeController.js';
|
|
||||||
|
|
||||||
import {
|
|
||||||
validateAliases,
|
|
||||||
validateOSC,
|
|
||||||
validatePatchProjectFile,
|
|
||||||
validateSettings,
|
|
||||||
viewValidator,
|
|
||||||
validateHTTP,
|
|
||||||
validateProjectDuplicate,
|
|
||||||
validateLoadProjectFile,
|
|
||||||
validateProjectRename,
|
|
||||||
} from '../controllers/ontimeController.validate.js';
|
|
||||||
import { projectSanitiser, sanitizeProjectFilename } from '../controllers/projectController.validate.js';
|
|
||||||
import {
|
|
||||||
revokeAuthentication,
|
|
||||||
readFromSheet,
|
|
||||||
requestConnection,
|
|
||||||
verifyAuthentication,
|
|
||||||
writeToSheet,
|
|
||||||
} from '../controllers/sheetsController.js';
|
|
||||||
import { validateRequestConnection, validateSheetOptions } from '../controllers/sheetController.validate.js';
|
|
||||||
|
|
||||||
export const router = express.Router();
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/sync' endpoint
|
|
||||||
router.get('/poll', poll);
|
|
||||||
|
|
||||||
// TODO: should db be the root endpoint for /ontime/data
|
|
||||||
// create route between controller and '/ontime/db' endpoint
|
|
||||||
router.get('/db', dbDownload);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/db' endpoint
|
|
||||||
router.post('/db', uploadFile, uploadProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/db' endpoint
|
|
||||||
router.patch('/db', validatePatchProjectFile, patchPartialProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/spreadsheet/preview' endpoint
|
|
||||||
// TODO: validate import map
|
|
||||||
router.post('/spreadsheet/preview', uploadFile, previewSpreadsheet);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/settings' endpoint
|
|
||||||
router.get('/settings', getSettings);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/settings' endpoint
|
|
||||||
router.post('/settings', validateSettings, postSettings);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/views' endpoint
|
|
||||||
router.get('/views', getViewSettings);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/views' endpoint
|
|
||||||
router.post('/views', viewValidator, postViewSettings);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/aliases' endpoint
|
|
||||||
router.get('/aliases', getAliases);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/aliases' endpoint
|
|
||||||
router.post('/aliases', validateAliases, postAliases);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/info' endpoint
|
|
||||||
router.get('/info', getInfo);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/osc' endpoint
|
|
||||||
router.get('/osc', getOSC);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/osc' endpoint
|
|
||||||
router.post('/osc', validateOSC, postOSC);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/http' endpoint
|
|
||||||
router.get('/http', getHTTP);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/http' endpoint
|
|
||||||
router.post('/http', validateHTTP, postHTTP);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/projects' endpoint
|
|
||||||
router.get('/projects', listProjects);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/load-project' endpoint
|
|
||||||
router.post('/load-project', validateLoadProjectFile, sanitizeProjectFilename, loadProject);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/project/:filename/duplicate' endpoint
|
|
||||||
router.post('/project/:filename/duplicate', validateProjectDuplicate, sanitizeProjectFilename, duplicateProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/project/:filename/rename' endpoint
|
|
||||||
router.put('/project/:filename/rename', validateProjectRename, sanitizeProjectFilename, renameProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/project' endpoint
|
|
||||||
router.post('/project', projectSanitiser, createProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/ontime/project/:filename' endpoint
|
|
||||||
router.delete('/project/:filename', sanitizeProjectFilename, deleteProjectFile);
|
|
||||||
|
|
||||||
// create route between controller and '/sheet/:sheetId/connect' endpoint
|
|
||||||
router.post('/sheet/:sheetId/connect', uploadClientSecret, validateRequestConnection, requestConnection);
|
|
||||||
|
|
||||||
router.get('/sheet/connect', verifyAuthentication);
|
|
||||||
|
|
||||||
router.post('/sheet/revoke', revokeAuthentication);
|
|
||||||
|
|
||||||
router.post('/sheet/:sheetId/read', validateSheetOptions, readFromSheet);
|
|
||||||
|
|
||||||
router.post('/sheet/:sheetId/write', validateSheetOptions, writeToSheet);
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user