mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-06 07:53:54 +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 APP_INFO = ['appinfo'];
|
||||
export const APP_SETTINGS = ['appSettings'];
|
||||
@@ -12,16 +12,20 @@ export const SHEET_STATE = ['sheetState'];
|
||||
export const CUSTOM_FIELDS = ['customFields'];
|
||||
export const VIEW_SETTINGS = ['viewSettings'];
|
||||
|
||||
// resolve location
|
||||
const location = window.location;
|
||||
const socketProtocol = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
export const isProduction = import.meta.env.MODE === 'production';
|
||||
export const isDev = !isProduction;
|
||||
|
||||
// resolve port
|
||||
const STATIC_PORT = 4001;
|
||||
export const serverPort = isProduction ? location.port : STATIC_PORT;
|
||||
export const serverURL = `${location.protocol}//${location.hostname}:${serverPort}`;
|
||||
export const websocketUrl = `${socketProtocol}://${location.hostname}:${serverPort}/ws`;
|
||||
|
||||
export const apiEntryUrl = `${serverURL}/data`;
|
||||
|
||||
export const projectDataURL = `${serverURL}/project`;
|
||||
export const rundownURL = `${serverURL}/events`;
|
||||
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 { 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 = {
|
||||
name: string;
|
||||
@@ -61,3 +115,4 @@ export default async function fileDownload(url: string, fileOptions: FileOptions
|
||||
URL.revokeObjectURL(downloadUrl);
|
||||
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 { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { ALIASES } from '../api/apiConstants';
|
||||
import { getAliases } from '../api/ontimeApi';
|
||||
import { getAliases } from '../api/aliases';
|
||||
import { ALIASES } from '../api/constants';
|
||||
|
||||
export default function useAliases() {
|
||||
const { data, status, isFetching, isError, refetch } = useQuery({
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { CustomFields } from 'ontime-types';
|
||||
|
||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||
import { CUSTOM_FIELDS } from '../api/apiConstants';
|
||||
import { getCustomFields } from '../api/ontimeApi';
|
||||
import { CUSTOM_FIELDS } from '../api/constants';
|
||||
import { getCustomFields } from '../api/customFields';
|
||||
|
||||
const placeholder: CustomFields = {};
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { HTTP_SETTINGS } from '../api/apiConstants';
|
||||
import { logAxiosError } from '../api/apiUtils';
|
||||
import { getHTTP, postHTTP } from '../api/ontimeApi';
|
||||
import { HTTP_SETTINGS } from '../api/constants';
|
||||
import { getHTTP, postHTTP } from '../api/http';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { httpPlaceholder } from '../models/Http';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { GetInfo } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { APP_INFO } from '../api/apiConstants';
|
||||
import { getInfo } from '../api/ontimeApi';
|
||||
import { APP_INFO } from '../api/constants';
|
||||
import { getInfo } from '../api/db';
|
||||
import { ontimePlaceholderInfo } from '../models/Info';
|
||||
|
||||
export default function useInfo() {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { OSC_SETTINGS } from '../api/apiConstants';
|
||||
import { logAxiosError } from '../api/apiUtils';
|
||||
import { getOSC, postOSC } from '../api/ontimeApi';
|
||||
import { OSC_SETTINGS } from '../api/constants';
|
||||
import { getOSC, postOSC } from '../api/osc';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { oscPlaceholderSettings } from '../models/OscSettings';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_DATA } from '../api/apiConstants';
|
||||
import { getProjectData } from '../api/projectDataApi';
|
||||
import { PROJECT_DATA } from '../api/constants';
|
||||
import { getProjectData } from '../api/project';
|
||||
import { projectDataPlaceholder } from '../models/ProjectData';
|
||||
|
||||
export default function useProjectData() {
|
||||
|
||||
@@ -2,8 +2,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { ProjectFileListResponse } from 'ontime-types';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { PROJECT_LIST } from '../api/apiConstants';
|
||||
import { getProjects } from '../api/ontimeApi';
|
||||
import { PROJECT_LIST } from '../api/constants';
|
||||
import { getProjects } from '../api/db';
|
||||
|
||||
const placeholderProjectList: ProjectFileListResponse = {
|
||||
files: [],
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useQuery } from '@tanstack/react-query';
|
||||
import { NormalisedRundown, OntimeRundown, RundownCached } from 'ontime-types';
|
||||
|
||||
import { queryRefetchInterval } from '../../ontimeConfig';
|
||||
import { RUNDOWN } from '../api/apiConstants';
|
||||
import { fetchCachedRundown } from '../api/eventsApi';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import { fetchNormalisedRundown } from '../api/rundown';
|
||||
|
||||
// revision is -1 so that the remote revision is higher
|
||||
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() {
|
||||
const { data, status, isError, refetch, isFetching } = useQuery<RundownCached>({
|
||||
queryKey: RUNDOWN,
|
||||
queryFn: fetchCachedRundown,
|
||||
queryFn: fetchNormalisedRundown,
|
||||
placeholderData: cachedRundownPlaceholder,
|
||||
retry: 5,
|
||||
retryDelay: (attempt) => attempt * 2500,
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { getSettings } from '../api/ontimeApi';
|
||||
import { APP_SETTINGS } from '../api/constants';
|
||||
import { getSettings } from '../api/settings';
|
||||
import { ontimePlaceholderSettings } from '../models/OntimeSettings';
|
||||
|
||||
export default function useSettings() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { queryRefetchIntervalSlow } from '../../ontimeConfig';
|
||||
import { VIEW_SETTINGS } from '../api/apiConstants';
|
||||
import { getView } from '../api/ontimeApi';
|
||||
import { VIEW_SETTINGS } from '../api/constants';
|
||||
import { getView } from '../api/viewSettings';
|
||||
import { viewsSettingsPlaceholder } from '../models/ViewSettings.type';
|
||||
|
||||
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 { getLinkedTimes, getPreviousEventNormal, reorderArray, swapEventData } from 'ontime-utils';
|
||||
|
||||
import { RUNDOWN } from '../api/apiConstants';
|
||||
import { logAxiosError } from '../api/apiUtils';
|
||||
import { RUNDOWN } from '../api/constants';
|
||||
import {
|
||||
ReorderEntry,
|
||||
requestApplyDelay,
|
||||
@@ -16,7 +15,8 @@ import {
|
||||
requestPutEvent,
|
||||
requestReorderEvent,
|
||||
SwapEntry,
|
||||
} from '../api/eventsApi';
|
||||
} from '../api/rundown';
|
||||
import { logAxiosError } from '../api/utils';
|
||||
import { useEditorSettings } from '../stores/editorSettings';
|
||||
import { forgivingStringToMillis } from '../utils/dateConfig';
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
import { useMemo, useRef } from 'react';
|
||||
|
||||
import { isDev } from '../api/apiConstants';
|
||||
import { isDev } from '../api/constants';
|
||||
|
||||
type noop = (this: any, ...args: any[]) => any;
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { socketClientName } from '../stores/connectionName';
|
||||
import { addLog } from '../stores/logger';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MaybeNumber, Settings, TimeFormat } from 'ontime-types';
|
||||
import { formatFromMillis } from 'ontime-utils';
|
||||
|
||||
import { FORMAT_12, FORMAT_24 } from '../../viewerConfig';
|
||||
import { APP_SETTINGS } from '../api/apiConstants';
|
||||
import { APP_SETTINGS } from '../api/constants';
|
||||
import { ontimeQueryClient } from '../queryClient';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from '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 style from '../Panel.module.scss';
|
||||
@@ -52,7 +52,13 @@ export default function CheckUpdatesButton(props: CheckUpdatesButtonProps) {
|
||||
|
||||
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
|
||||
</Button>
|
||||
<ResolveUpdateMessage updateMessage={updateMessage} />
|
||||
|
||||
@@ -5,7 +5,7 @@ import { IoTrash } from '@react-icons/all-files/io5/IoTrash';
|
||||
import { HttpSettings } from 'ontime-types';
|
||||
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 { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||
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 { 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 { isKeyEscape } from '../../../../common/utils/keyEvent';
|
||||
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 useInfo from '../../../../common/hooks-query/useInfo';
|
||||
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { Button, Input, Textarea } from '@chakra-ui/react';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import { createProject } from '../../../../common/api/ontimeApi';
|
||||
import { PROJECT_LIST } from '../../../../common/api/constants';
|
||||
import { createProject } from '../../../../common/api/db';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
import style from './ProjectPanel.module.scss';
|
||||
@@ -25,6 +27,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
const { onClose } = props;
|
||||
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
handleSubmit,
|
||||
@@ -53,6 +56,7 @@ export default function ProjectCreateForm(props: ProjectCreateFromProps) {
|
||||
...values,
|
||||
filename,
|
||||
});
|
||||
await queryClient.invalidateQueries({ queryKey: PROJECT_LIST });
|
||||
onClose();
|
||||
} catch (error) {
|
||||
setError(maybeAxiosError(error));
|
||||
|
||||
@@ -2,7 +2,6 @@ import { useState } from 'react';
|
||||
import { IconButton, Menu, MenuButton, MenuItem, MenuList } from '@chakra-ui/react';
|
||||
import { IoEllipsisHorizontal } from '@react-icons/all-files/io5/IoEllipsisHorizontal';
|
||||
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import {
|
||||
deleteProject,
|
||||
downloadCSV,
|
||||
@@ -10,7 +9,8 @@ import {
|
||||
duplicateProject,
|
||||
loadProject,
|
||||
renameProject,
|
||||
} from '../../../../common/api/ontimeApi';
|
||||
} from '../../../../common/api/db';
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||
|
||||
import ProjectForm, { ProjectFormValues } from './ProjectForm';
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@ import { ChangeEvent, useRef, useState } from 'react';
|
||||
import { Button, Input } from '@chakra-ui/react';
|
||||
import { IoAdd } from '@react-icons/all-files/io5/IoAdd';
|
||||
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import { importProjectFile } from '../../../../common/api/ontimeApi';
|
||||
import { uploadProjectFile } from '../../../../common/api/db';
|
||||
import { invalidateAllCaches, maybeAxiosError } from '../../../../common/api/utils';
|
||||
import { validateProjectFile } from '../../../../common/utils/uploadUtils';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
@@ -37,7 +37,7 @@ export default function ProjectPanel() {
|
||||
|
||||
try {
|
||||
validateProjectFile(selectedFile);
|
||||
await importProjectFile(selectedFile);
|
||||
await uploadProjectFile(selectedFile);
|
||||
} catch (error) {
|
||||
const errorMessage = maybeAxiosError(error);
|
||||
setError(`Error uploading file: ${errorMessage}`);
|
||||
@@ -48,6 +48,10 @@ export default function ProjectPanel() {
|
||||
setLoading(null);
|
||||
};
|
||||
|
||||
const handleCloseForm = () => {
|
||||
setIsCreatingProject(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Panel.Header>Project</Panel.Header>
|
||||
@@ -85,7 +89,7 @@ export default function ProjectPanel() {
|
||||
</div>
|
||||
</Panel.SubHeader>
|
||||
{error && <Panel.Error>{error}</Panel.Error>}
|
||||
{isCreatingProject && <ProjectCreateForm onClose={() => setIsCreatingProject(false)} />}
|
||||
{isCreatingProject && <ProjectCreateForm onClose={handleCloseForm} />}
|
||||
<ProjectList />
|
||||
</Panel.Card>
|
||||
</Panel.Section>
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ import { Button, Input } from '@chakra-ui/react';
|
||||
import { CustomField } from 'ontime-types';
|
||||
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 * 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 { 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 useCustomFields from '../../../../common/hooks-query/useCustomFields';
|
||||
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 { ImportMap, unpackError } from 'ontime-utils';
|
||||
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import { importSpreadsheetPreview } from '../../../../common/api/ontimeApi';
|
||||
import { importSpreadsheetPreview } from '../../../../common/api/db';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
import { validateSpreadsheetImport } from '../../../../common/utils/uploadUtils';
|
||||
import * as Panel from '../PanelUtils';
|
||||
|
||||
|
||||
@@ -2,16 +2,16 @@ import { useQueryClient } from '@tanstack/react-query';
|
||||
import { AuthenticationStatus, CustomFields, OntimeRundown } from 'ontime-types';
|
||||
import { ImportMap } from 'ontime-utils';
|
||||
|
||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/apiConstants';
|
||||
import { maybeAxiosError } from '../../../../common/api/apiUtils';
|
||||
import { CUSTOM_FIELDS, RUNDOWN } from '../../../../common/api/constants';
|
||||
import { patchData } from '../../../../common/api/db';
|
||||
import {
|
||||
patchData,
|
||||
previewRundown,
|
||||
requestConnection,
|
||||
revokeAuthentication,
|
||||
uploadRundown,
|
||||
verifyAuthenticationStatus,
|
||||
} from '../../../../common/api/ontimeApi';
|
||||
} from '../../../../common/api/sheets';
|
||||
import { maybeAxiosError } from '../../../../common/api/utils';
|
||||
|
||||
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 { Alias } from 'ontime-types';
|
||||
|
||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
||||
import { postAliases } from '../../../common/api/ontimeApi';
|
||||
import { postAliases } from '../../../common/api/aliases';
|
||||
import { logAxiosError } from '../../../common/api/utils';
|
||||
import TooltipActionBtn from '../../../common/components/buttons/TooltipActionBtn';
|
||||
import useAliases from '../../../common/hooks-query/useAliases';
|
||||
import { useEmitLog } from '../../../common/stores/logger';
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
||||
import { Input, Select } from '@chakra-ui/react';
|
||||
import type { Settings } from 'ontime-types';
|
||||
|
||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
||||
import { postSettings } from '../../../common/api/ontimeApi';
|
||||
import { postSettings } from '../../../common/api/settings';
|
||||
import { logAxiosError } from '../../../common/api/utils';
|
||||
import useSettings from '../../../common/hooks-query/useSettings';
|
||||
import { isOnlyNumbers } from '../../../common/utils/regex';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
|
||||
@@ -3,8 +3,8 @@ import { useForm } from 'react-hook-form';
|
||||
import { Input, Textarea } from '@chakra-ui/react';
|
||||
import { ProjectData } from 'ontime-types';
|
||||
|
||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
||||
import { postProjectData } from '../../../common/api/projectDataApi';
|
||||
import { postProjectData } from '../../../common/api/project';
|
||||
import { logAxiosError } from '../../../common/api/utils';
|
||||
import useProjectData from '../../../common/hooks-query/useProjectData';
|
||||
import ModalLoader from '../modal-loader/ModalLoader';
|
||||
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 { ViewSettings } from 'ontime-types';
|
||||
|
||||
import { logAxiosError } from '../../../common/api/apiUtils';
|
||||
import { postViewSettings } from '../../../common/api/ontimeApi';
|
||||
import { logAxiosError } from '../../../common/api/utils';
|
||||
import { postViewSettings } from '../../../common/api/viewSettings';
|
||||
import { PopoverPickerRHF } from '../../../common/components/input/popover-picker/PopoverPicker';
|
||||
import useInfo from '../../../common/hooks-query/useInfo';
|
||||
import useViewSettings from '../../../common/hooks-query/useViewSettings';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { MouseEvent } from 'react';
|
||||
import { isOntimeEvent, OntimeEvent, RundownCached } from 'ontime-types';
|
||||
import { create } from 'zustand';
|
||||
|
||||
import { RUNDOWN } from '../../common/api/apiConstants';
|
||||
import { RUNDOWN } from '../../common/api/constants';
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
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 { 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 ProgressBar from '../../../common/components/progress-bar/ProgressBar';
|
||||
import Schedule from '../../../common/components/schedule/Schedule';
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useEffect } from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
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 { getClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
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 { 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 { getCountdownOptions } from '../../../common/components/view-params-editor/constants';
|
||||
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 { 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 { getLowerThirdOptions } from '../../../common/components/view-params-editor/constants';
|
||||
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 { 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 { MINIMAL_TIMER_OPTIONS } from '../../../common/components/view-params-editor/constants';
|
||||
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 { 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 Schedule from '../../../common/components/schedule/Schedule';
|
||||
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 { 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 { getStudioClockOptions } from '../../../common/components/view-params-editor/constants';
|
||||
import ViewParamsEditor from '../../../common/components/view-params-editor/ViewParamsEditor';
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
} from 'ontime-types';
|
||||
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 NavigationMenu from '../../../common/components/navigation-menu/NavigationMenu';
|
||||
import TitleCard from '../../../common/components/title-card/TitleCard';
|
||||
|
||||
@@ -3,9 +3,9 @@ import { LogOrigin, OSCSettings } from 'ontime-types';
|
||||
import { Server } from 'node-osc';
|
||||
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { dispatchFromAdapter, type ChangeOptions } from '../controllers/integrationController.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { objectFromPath } from './utils/parse.js';
|
||||
import { type ChangeOptions, dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||
|
||||
export class OscServer implements IAdapter {
|
||||
private readonly osc: Server;
|
||||
|
||||
@@ -22,8 +22,8 @@ import type { Server } from 'http';
|
||||
import getRandomName from '../utils/getRandomName.js';
|
||||
import { IAdapter } from './IAdapter.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { dispatchFromAdapter } from '../controllers/integrationController.js';
|
||||
import { logger } from '../classes/Logger.js';
|
||||
import { dispatchFromAdapter } from '../api-integration/integration.controller.js';
|
||||
|
||||
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 { 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 = [
|
||||
body('label')
|
||||
.exists()
|
||||
@@ -61,14 +43,3 @@ export const validateDeleteCustomField = [
|
||||
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 {
|
||||
addEvent,
|
||||
applyDelay,
|
||||
@@ -12,26 +12,20 @@ import {
|
||||
editEvent,
|
||||
reorderEvent,
|
||||
swapEvents,
|
||||
} from '../services/rundown-service/RundownService.js';
|
||||
import { getNormalisedRundown, getRundown } from '../services/rundown-service/rundownUtils.js';
|
||||
} from '../../services/rundown-service/RundownService.js';
|
||||
import { getNormalisedRundown, getRundown } from '../../services/rundown-service/rundownUtils.js';
|
||||
|
||||
// Create controller for GET request to '/events'
|
||||
// Returns -
|
||||
export const rundownGetAll: RequestHandler = async (_req, res) => {
|
||||
export async function rundownGetAll(_req: Request, res: Response<OntimeRundown>) {
|
||||
const rundown = getRundown();
|
||||
res.json(rundown);
|
||||
};
|
||||
}
|
||||
|
||||
// Create controller for GET request to '/events/cached'
|
||||
// Returns -
|
||||
export const rundownGetCached: RequestHandler = async (_req: Request, res: Response<RundownCached>) => {
|
||||
export async function rundownGetNormalised(_req: Request, res: Response<RundownCached>) {
|
||||
const cachedRundown = getNormalisedRundown();
|
||||
res.json(cachedRundown);
|
||||
};
|
||||
}
|
||||
|
||||
// Create controller for POST request to '/events/'
|
||||
// Returns -
|
||||
export const rundownPost: RequestHandler = async (req, res) => {
|
||||
export async function rundownPost(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
@@ -42,11 +36,9 @@ export const rundownPost: RequestHandler = async (req, res) => {
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create controller for PUT request to '/events/'
|
||||
// Returns -
|
||||
export const rundownPut: RequestHandler = async (req, res) => {
|
||||
export async function rundownPut(req: Request, res: Response<OntimeRundownEntry | ErrorResponse>) {
|
||||
if (failEmptyObjects(req.body, res)) {
|
||||
return;
|
||||
}
|
||||
@@ -57,9 +49,9 @@ export const rundownPut: RequestHandler = async (req, res) => {
|
||||
} catch (error) {
|
||||
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)) {
|
||||
return res.status(404);
|
||||
}
|
||||
@@ -67,13 +59,13 @@ export const rundownBatchPut: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { data, ids } = req.body;
|
||||
await batchEditEvents(ids, data);
|
||||
res.status(200);
|
||||
res.status(200).send({ message: 'Batch edit successful' });
|
||||
} catch (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)) {
|
||||
return;
|
||||
}
|
||||
@@ -81,13 +73,13 @@ export const rundownReorder: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { eventId, from, to } = req.body;
|
||||
const event = await reorderEvent(eventId, from, to);
|
||||
res.status(200).send(event);
|
||||
res.status(200).send(event.newEvent);
|
||||
} catch (error) {
|
||||
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)) {
|
||||
return;
|
||||
}
|
||||
@@ -95,41 +87,35 @@ export const rundownSwap: RequestHandler = async (req, res) => {
|
||||
try {
|
||||
const { from, to } = req.body;
|
||||
await swapEvents(from, to);
|
||||
res.sendStatus(200);
|
||||
res.status(200).send({ message: 'Swap successful' });
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create controller for PATCH request to '/events/applydelay/:eventId'
|
||||
// Returns -
|
||||
export const rundownApplyDelay: RequestHandler = async (req, res) => {
|
||||
export async function rundownApplyDelay(req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||
try {
|
||||
await applyDelay(req.params.eventId);
|
||||
res.sendStatus(200);
|
||||
res.status(200).send({ message: 'Delay applied' });
|
||||
} catch (error) {
|
||||
res.status(400).send({ message: error.toString() });
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Create controller for DELETE request to '/events/:eventId'
|
||||
// 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) => {
|
||||
export async function rundownDelete(_req: Request, res: Response<MessageResponse | ErrorResponse>) {
|
||||
try {
|
||||
await deleteAllEvents();
|
||||
res.sendStatus(204);
|
||||
res.status(204).send({ message: 'All events deleted' });
|
||||
} catch (error) {
|
||||
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 {
|
||||
deleteEventById,
|
||||
rundownApplyDelay,
|
||||
rundownBatchPut,
|
||||
rundownDelete,
|
||||
rundownGetAll,
|
||||
rundownGetCached,
|
||||
rundownGetNormalised,
|
||||
rundownPost,
|
||||
rundownPut,
|
||||
rundownReorder,
|
||||
rundownSwap,
|
||||
rundownBatchPut,
|
||||
} from '../controllers/rundownController.js';
|
||||
} from './rundown.controller.js';
|
||||
import {
|
||||
paramsMustHaveEventId,
|
||||
rundownBatchPutValidator,
|
||||
@@ -18,34 +19,21 @@ import {
|
||||
rundownPutValidator,
|
||||
rundownReorderValidator,
|
||||
rundownSwapValidator,
|
||||
} from '../controllers/rundownController.validate.js';
|
||||
} from './rundown.validation.js';
|
||||
|
||||
export const router = express.Router();
|
||||
|
||||
// create route between controller and '/events/cached' endpoint
|
||||
router.get('/cached', rundownGetCached);
|
||||
router.get('/', rundownGetAll); // not used in Ontime frontend
|
||||
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);
|
||||
|
||||
// create route between controller and '/events/' endpoint
|
||||
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('/swap', rundownSwapValidator, rundownSwap);
|
||||
|
||||
// create route between controller and '/events/applydelay/:eventId' endpoint
|
||||
router.patch('/applydelay/:eventId', paramsMustHaveEventId, rundownApplyDelay);
|
||||
|
||||
// create route between controller and '/events/all' endpoint
|
||||
router.delete('/all', rundownDelete);
|
||||
|
||||
// create route between controller and '/events/:eventId' endpoint
|
||||
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 { readFileSync } from 'fs';
|
||||
|
||||
import { deleteFile } from '../utils/parserUtils.js';
|
||||
import { deleteFile } from '../../utils/parserUtils.js';
|
||||
import {
|
||||
revoke,
|
||||
handleClientSecret,
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
hasAuth,
|
||||
download,
|
||||
upload,
|
||||
} from '../services/sheet-service/SheetService.js';
|
||||
} from '../../services/sheet-service/SheetService.js';
|
||||
|
||||
export async function requestConnection(req: Request, res: Response) {
|
||||
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 { runtimeService } from '../services/runtime-service/RuntimeService.js';
|
||||
import { eventStore } from '../stores/EventStore.js';
|
||||
import { parse, updateEvent } from './integrationController.config.js';
|
||||
import { extraTimerService } from '../services/extra-timer-service/ExtraTimerService.js';
|
||||
import { validateMessage, validateTimerMessage } from '../services/message-service/messageUtils.js';
|
||||
|
||||
import { parse, updateEvent } from './integration.utils.js';
|
||||
|
||||
export type ChangeOptions = {
|
||||
eventId: 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 { editEvent } from '../services/rundown-service/RundownService.js';
|
||||
import { getEventWithId } from '../services/rundown-service/rundownUtils.js';
|
||||
import { coerceString, coerceNumber, coerceBoolean, coerceColour } from '../utils/coerceType.js';
|
||||
@@ -19,11 +19,9 @@ import {
|
||||
} from './setup/index.js';
|
||||
import { ONTIME_VERSION } from './ONTIME_VERSION.js';
|
||||
|
||||
// Import Routes
|
||||
import { router as rundownRouter } from './routes/rundownRouter.js';
|
||||
import { router as projectRouter } from './routes/projectRouter.js';
|
||||
import { router as ontimeRouter } from './routes/ontimeRouter.js';
|
||||
import { router as apiRouter } from './routes/apiRouter.js';
|
||||
// Import Routers
|
||||
import { appRouter } from './api-data/index.js';
|
||||
import { integrationRouter } from './api-integration/integration.router.js';
|
||||
|
||||
// Import adapters
|
||||
import { OscServer } from './adapters/OscAdapter.js';
|
||||
@@ -70,10 +68,8 @@ app.use(express.urlencoded({ extended: true }));
|
||||
app.use(express.json({ limit: '1mb' }));
|
||||
|
||||
// Implement route endpoints
|
||||
app.use('/events', rundownRouter);
|
||||
app.use('/project', projectRouter);
|
||||
app.use('/ontime', ontimeRouter);
|
||||
app.use('/api', apiRouter);
|
||||
app.use('/data', appRouter); // router for application data
|
||||
app.use('/api', integrationRouter); // router for integrations
|
||||
|
||||
// serve static - css
|
||||
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