You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

451 lines
13 KiB

// 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;
}