// TODO: this file is located in the client src folder because // otherwise, Create React App will refuse to compile it. // Putting it in the server folder or in its own folder makes more sense. // This file represents the API interface for Mudbase's back-end. // Each endpoint is described by its endpoint address, // a request structure, a response structure and // a checking function which determines request validity. export enum ItemType { Song = 0, Artist, Album, Tag } export interface ArtistDetails { artistId: number, name: string, storeLinks?: string[], } export function isArtistDetails(q: any): q is ArtistDetails { return 'artistId' in q; } export interface AlbumDetails { albumId: number, name: string, storeLinks?: string[], } export function isAlbumDetails(q: any): q is ArtistDetails { return 'albumId' in q; } export interface TagDetails { tagId: number, name: string, parent?: TagDetails, storeLinks?: string[], } export function isTagDetails(q: any): q is TagDetails { return 'tagId' in q; } export interface RankingDetails { rankingId: number, type: ItemType, // The item type being ranked rankedId: number, // The item being ranked context: ArtistDetails | TagDetails, value: number, // The ranking (higher = better) } export function isRankingDetails(q: any): q is RankingDetails { return 'rankingId' in q; } export interface SongDetails { songId: number, title: string, artists?: ArtistDetails[], albums?: AlbumDetails[], tags?: TagDetails[], storeLinks?: string[], rankings?: RankingDetails[], } export function isSongDetails(q: any): q is SongDetails { return 'songId' in q; } // Query for items (POST). export const QueryEndpoint = '/query'; export enum QueryElemOp { And = "AND", Or = "OR", } export enum QueryFilterOp { Eq = "EQ", Ne = "NE", In = "IN", NotIn = "NOTIN", Like = "LIKE", } export enum QueryElemProperty { songTitle = "songTitle", songId = "songId", artistName = "artistName", artistId = "artistId", albumName = "albumName", albumId = "albumId", tagId = "tagId", songStoreLinks = "songStoreLinks", //Note: treated as a JSON string for filter operations artistStoreLinks = "artistStoreLinks", //Note: treated as a JSON string for filter operations albumStoreLinks = "albumStoreLinks", //Note: treated as a JSON string for filter operations } export enum OrderByType { Name = 'name', } export enum QueryResponseType { Details = 'details', // Returns detailed result items. Ids = 'ids', // Returns IDs only. Count = 'count', // Returns an item count only. } export interface QueryElem { prop?: QueryElemProperty, propOperand?: any, propOperator?: QueryFilterOp, children?: QueryElem[] childrenOperator?: QueryElemOp, } export interface Ordering { orderBy: { type: OrderByType, } ascending: boolean, } export interface Query extends QueryElem { } export interface QueryRequest { query: Query, offsetsLimits: OffsetsLimits, ordering: Ordering, responseType: QueryResponseType } export interface QueryResponse { songs: SongDetails[] | number[] | number, // Details | IDs | count, depending on QueryResponseType artists: ArtistDetails[] | number[] | number, tags: TagDetails[] | number[] | number, albums: AlbumDetails[] | number[] | number, } // Note: use -1 as an infinity limit. export interface OffsetsLimits { songOffset?: number, songLimit?: number, artistOffset?: number, artistLimit?: number, tagOffset?: number, tagLimit?: number, albumOffset?: number, albumLimit?: number, } export function checkQueryElem(elem: any): boolean { if (elem.childrenOperator && elem.children) { elem.children.forEach((child: any) => { if (!checkQueryElem(child)) { return false; } }); } return (elem.childrenOperator && elem.children) || ("prop" in elem && "propOperand" in elem && "propOperator" in elem) || Object.keys(elem).length === 0; } export function checkQueryRequest(req: any): boolean { return 'query' in req && 'offsetsLimits' in req && 'ordering' in req && 'responseType' in req && checkQueryElem(req.query); } // Get song details (GET). export const SongDetailsEndpoint = '/song/:id'; export interface SongDetailsRequest { } export interface SongDetailsResponse { title: string, storeLinks: string[], artistIds: number[], albumIds: number[], tagIds: number[], } export function checkSongDetailsRequest(req: any): boolean { return true; } // Get artist details (GET). export const ArtistDetailsEndpoint = '/artist/:id'; export interface ArtistDetailsRequest { } export interface ArtistDetailsResponse { name: string, tagIds: number[], storeLinks: string[], } export function checkArtistDetailsRequest(req: any): boolean { return true; } // Create a new song (POST). export const CreateSongEndpoint = '/song'; export interface CreateSongRequest { title: string; artistIds?: number[]; albumIds?: number[]; tagIds?: number[]; storeLinks?: string[]; } export interface CreateSongResponse { id: number; } export function checkCreateSongRequest(req: any): boolean { return "body" in req && "title" in req.body; } // Modify an existing song (PUT). export const ModifySongEndpoint = '/song/:id'; export interface ModifySongRequest { title?: string; artistIds?: number[]; albumIds?: number[]; tagIds?: number[]; storeLinks?: string[]; } export interface ModifySongResponse { } export function checkModifySongRequest(req: any): boolean { return true; } // Create a new album (POST). export const CreateAlbumEndpoint = '/album'; export interface CreateAlbumRequest { name: string; tagIds?: number[]; artistIds?: number[]; storeLinks?: string[]; } export interface CreateAlbumResponse { id: number; } export function checkCreateAlbumRequest(req: any): boolean { return "body" in req && "name" in req.body; } // Modify an existing album (PUT). export const ModifyAlbumEndpoint = '/album/:id'; export interface ModifyAlbumRequest { name?: string; tagIds?: number[]; artistIds?: number[]; storeLinks?: string[]; } export interface ModifyAlbumResponse { } export function checkModifyAlbumRequest(req: any): boolean { return true; } // Get album details (GET). export const AlbumDetailsEndpoint = '/album/:id'; export interface AlbumDetailsRequest { } export interface AlbumDetailsResponse { name: string; tagIds: number[]; artistIds: number[]; songIds: number[]; storeLinks: string[]; } export function checkAlbumDetailsRequest(req: any): boolean { return true; } // Create a new artist (POST). export const CreateArtistEndpoint = '/artist'; export interface CreateArtistRequest { name: string; tagIds?: number[]; storeLinks?: string[]; } export interface CreateArtistResponse { id: number; } export function checkCreateArtistRequest(req: any): boolean { return "body" in req && "name" in req.body; } // Modify an existing artist (PUT). export const ModifyArtistEndpoint = '/artist/:id'; export interface ModifyArtistRequest { name?: string, tagIds?: number[]; storeLinks?: string[], } export interface ModifyArtistResponse { } export function checkModifyArtistRequest(req: any): boolean { return true; } // Create a new tag (POST). export const CreateTagEndpoint = '/tag'; export interface CreateTagRequest { name: string; parentId?: number; } export interface CreateTagResponse { id: number; } export function checkCreateTagRequest(req: any): boolean { return "body" in req && "name" in req.body; } // Modify an existing tag (PUT). export const ModifyTagEndpoint = '/tag/:id'; export interface ModifyTagRequest { name?: string, parentId?: number; } export interface ModifyTagResponse { } export function checkModifyTagRequest(req: any): boolean { return true; } // Get tag details (GET). export const TagDetailsEndpoint = '/tag/:id'; export interface TagDetailsRequest { } export interface TagDetailsResponse { name: string, parentId?: number, } export function checkTagDetailsRequest(req: any): boolean { return true; } // Delete tag (DELETE). export const DeleteTagEndpoint = '/tag/:id'; export interface DeleteTagRequest { } export interface DeleteTagResponse { } export function checkDeleteTagRequest(req: any): boolean { return true; } // Merge tag (POST). export const MergeTagEndpoint = '/tag/:id/merge/:toId'; export interface MergeTagRequest { } export interface MergeTagResponse { } export function checkMergeTagRequest(req: any): boolean { return true; } // Register a user (POST). // TODO: add e-mail verification. export const RegisterUserEndpoint = '/register'; export interface RegisterUserRequest { email: string, password: string, } export interface RegisterUserResponse { } export function checkPassword(password: string): boolean { const result = (password.length < 32) && (password.length >= 8) && password.split("").every(char => char.charCodeAt(0) <= 127) && // is ASCII (/[a-z]/g.test(password)) && // has lowercase (/[A-Z]/g.test(password)) && // has uppercase (/[0-9]/g.test(password)) && // has number (/[!@#$%^&*()_+/]/g.test(password)) // has special character; console.log("Password check for ", password, ": ", result); return result; } export function checkEmail(email: string): boolean { const re = /^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; const result = re.test(String(email).toLowerCase()); console.log("Email check for ", email, ": ", result); return result; } export function checkRegisterUserRequest(req: any): boolean { return "body" in req && "email" in req.body && "password" in req.body && checkEmail(req.body.email) && checkPassword(req.body.password); } // Note: Login is handled by Passport.js, so it is not explicitly written here. export const LoginEndpoint = "/login"; export const LogoutEndpoint = "/logout"; export enum IntegrationType { SpotifyClientCredentials = "SpotifyClientCredentials", YoutubeWebScraper = "YoutubeWebScraper", } export interface SpotifyClientCredentialsDetails { clientId: string, } export interface SpotifyClientCredentialsSecretDetails { clientSecret: string, } export interface YoutubeMusicWebScraperDetails {} export interface YoutubeMusicWebScraperSecretDetails {} export type IntegrationDetails = SpotifyClientCredentialsDetails | YoutubeMusicWebScraperDetails; export type IntegrationSecretDetails = SpotifyClientCredentialsSecretDetails | YoutubeMusicWebScraperSecretDetails; // Create a new integration (POST). export const CreateIntegrationEndpoint = '/integration'; export interface CreateIntegrationRequest { name: string, type: IntegrationType, details: IntegrationDetails, secretDetails: IntegrationSecretDetails, } export interface CreateIntegrationResponse { id: number; } export function checkCreateIntegrationRequest(req: any): boolean { return "body" in req && "name" in req.body && "type" in req.body && "details" in req.body && "secretDetails" in req.body && (req.body.type in IntegrationType); } // Modify an existing integration (PUT). export const ModifyIntegrationEndpoint = '/integration/:id'; export interface ModifyIntegrationRequest { name?: string, type?: IntegrationType, details?: IntegrationDetails, secretDetails?: IntegrationSecretDetails, } export interface ModifyIntegrationResponse { } export function checkModifyIntegrationRequest(req: any): boolean { if("type" in req.body && !(req.body.type in IntegrationType)) return false; return true; } // Get integration details (GET). export const IntegrationDetailsEndpoint = '/integration/:id'; export interface IntegrationDetailsRequest { } export interface IntegrationDetailsResponse { name: string, type: IntegrationType, details: IntegrationDetails, } export function checkIntegrationDetailsRequest(req: any): boolean { return true; } // List integrations (GET). export const ListIntegrationsEndpoint = '/integration'; export interface ListIntegrationsRequest { } export interface ListIntegrationsItem extends IntegrationDetailsResponse { id: number } export type ListIntegrationsResponse = ListIntegrationsItem[]; export function checkListIntegrationsRequest(req: any): boolean { return true; } // Delete integration (DELETE). export const DeleteIntegrationEndpoint = '/integration/:id'; export interface DeleteIntegrationRequest { } export interface DeleteIntegrationResponse { } export function checkDeleteIntegrationRequest(req: any): boolean { return true; }