Further fixes.

editsong
Sander Vocke 5 years ago
parent 35cd904a63
commit 8fcc67022d
  1. 2
      client/src/api/api.ts
  2. 10
      client/src/api/endpoints/data.ts
  3. 1
      client/src/api/types/resources.ts
  4. 4
      client/src/components/MainWindow.tsx
  5. 31
      client/src/components/common/FileUploadButton.tsx
  6. 12
      client/src/components/windows/manage/ManageWindow.tsx
  7. 124
      client/src/components/windows/manage_data/ManageData.tsx
  8. 65
      client/src/components/windows/manage_importexport/ManageImportExport.tsx
  9. 32
      client/src/lib/backend/data.tsx
  10. 30
      client/src/lib/integration/useIntegrations.tsx
  11. 22335
      data/20201204_initial_GPM_import.mudbase.json
  12. 6
      server/app.ts
  13. 4
      server/db/Album.ts
  14. 84
      server/db/Data.ts
  15. 15
      server/endpoints/Data.ts
  16. 110
      server/migrations/20200828124218_init_db.ts
  17. 6
      server/server.ts

@ -9,6 +9,6 @@
export * from './types/resources';
export * from './endpoints/auth';
export * from './endpoints/importexport';
export * from './endpoints/data';
export * from './endpoints/resources';
export * from './endpoints/query';

@ -9,7 +9,7 @@
import { AlbumWithRefsWithId, ArtistWithRefsWithId, isAlbumWithRefs, isArtistWithRefs, isTagWithRefs, isTrackBaseWithRefs, isTrackWithRefs, TagWithRefsWithId, TrackWithRefsWithId } from "../types/resources";
// generated.
export interface DBImportExportFormat {
export interface DBDataFormat {
tracks: TrackWithRefsWithId[],
albums: AlbumWithRefsWithId[],
artists: ArtistWithRefsWithId[],
@ -18,11 +18,11 @@ export interface DBImportExportFormat {
// Get a full export of a user's database (GET).
export const DBExportEndpoint = "/export";
export type DBExportResponse = DBImportExportFormat;
export type DBExportResponse = DBDataFormat;
// Fully replace the user's database by an import (POST).
export const DBImportEndpoint = "/import";
export type DBImportRequest = DBImportExportFormat;
export type DBImportRequest = DBDataFormat;
export type DBImportResponse = void;
export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
return 'tracks' in v &&
@ -42,3 +42,7 @@ export const checkDBImportRequest: (v: any) => boolean = (v: any) => {
return prev && isTagWithRefs(cur);
}, true);
}
// Wipe this user's database (POST).
export const DBWipeEndpoint = "/wipe";
export type DBWipeResponse = void;

@ -189,7 +189,6 @@ export function isTagBaseWithRefs(q: any): q is TagBaseWithRefs {
return isTagBase(q);
}
export function isTagWithRefs(q: any): q is TagWithRefs {
console.log("Check", q)
return isTagBaseWithRefs(q) && "name" in q;
}

@ -97,9 +97,9 @@ export default function MainWindow(props: any) {
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.Links} />
</PrivateRoute>
<PrivateRoute path="/manage/importexport">
<PrivateRoute path="/manage/Data">
<AppBar selectedTab={AppBarTab.Manage} />
<ManageWindow selectedWindow={ManageWhat.ImportExport} />
<ManageWindow selectedWindow={ManageWhat.Data} />
</PrivateRoute>
<PrivateRoute exact path="/manage">
<Redirect to={"/manage/tags"} />

@ -0,0 +1,31 @@
import { Button } from '@material-ui/core';
import React from 'react';
export default function FileUploadButton(props: any) {
const hiddenFileInput = React.useRef<null | any>(null);
const { onGetFile, ...restProps } = props;
const handleClick = (event: any) => {
if (hiddenFileInput) {
hiddenFileInput.current.click();
}
}
const handleChange = (event: any) => {
const fileUploaded = event.target.files[0];
onGetFile(fileUploaded);
};
return (
<>
<Button onClick={handleClick} {...restProps}>
{props.children}
</Button>
<input type="file"
ref={hiddenFileInput}
onChange={handleChange}
style={{ display: 'none' }}
/>
</>
);
};

@ -10,12 +10,12 @@ import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import SaveIcon from '@material-ui/icons/Save';
import ManageLinksWindow from '../manage_links/ManageLinksWindow';
import ManageTagsWindow from '../manage_tags/ManageTagsWindow';
import ManageImportExportWindow from '../manage_importexport/ManageImportExport';
import ManageDataWindow from '../manage_data/ManageData';
export enum ManageWhat {
Tags = 0,
Links,
ImportExport,
Data,
}
export default function ManageWindow(props: {
@ -56,14 +56,14 @@ export default function ManageWindow(props: {
onClick={() => history.push('/manage/links')}
/>
<NavButton
label="Import/Export"
label="Data"
icon={<SaveIcon />}
selected={props.selectedWindow === ManageWhat.ImportExport}
onClick={() => history.push('/manage/importexport')}
selected={props.selectedWindow === ManageWhat.Data}
onClick={() => history.push('/manage/data')}
/>
</Box>
{props.selectedWindow === ManageWhat.Tags && <ManageTagsWindow/>}
{props.selectedWindow === ManageWhat.Links && <ManageLinksWindow/>}
{props.selectedWindow === ManageWhat.ImportExport && <ManageImportExportWindow/>}
{props.selectedWindow === ManageWhat.Data && <ManageDataWindow/>}
</Box >
}

@ -0,0 +1,124 @@
import React, { ReactFragment, useReducer, useState } from 'react';
import { WindowState } from "../Windows";
import { Box, Paper, Typography, TextField, Button, Dialog, CircularProgress } from "@material-ui/core";
import SaveIcon from '@material-ui/icons/Save';
import GetAppIcon from '@material-ui/icons/GetApp';
import PublishIcon from '@material-ui/icons/Publish';
import DeleteForeverIcon from '@material-ui/icons/DeleteForever';
import { Alert } from '@material-ui/lab';
import * as serverApi from '../../../api/api';
import { getDBExportLink, importDB, wipeDB } from '../../../lib/backend/data';
import FileUploadButton from '../../common/FileUploadButton';
import { DBImportRequest } from '../../../api/api';
export interface ManageDataWindowState extends WindowState {
dummy: boolean
}
export enum ManageDataWindowActions {
SetDummy = "SetDummy",
}
export function ManageDataWindowReducer(state: ManageDataWindowState, action: any) {
switch (action.type) {
case ManageDataWindowActions.SetDummy: {
return state;
}
default:
throw new Error("Unimplemented ManageDataWindow state update.")
}
}
export default function ManageDataWindow(props: {}) {
const [state, dispatch] = useReducer(ManageDataWindowReducer, {
dummy: true,
});
return <ManageDataWindowControlled state={state} dispatch={dispatch} />
}
export function ManageDataWindowControlled(props: {
state: ManageDataWindowState,
dispatch: (action: any) => void,
}) {
let [alert, setAlert] = useState<ReactFragment>(<></>);
let handleImport = async (jsonData: DBImportRequest) => {
try {
setAlert(<CircularProgress/>)
await importDB(jsonData);
} catch (e) {
setAlert(<Alert severity="error">Failed to import database.</Alert>)
return;
}
setAlert(<Alert severity="success">Successfully imported database.</Alert>)
}
let handleWipe = async () => {
try {
setAlert(<CircularProgress/>)
await wipeDB();
} catch (e) {
setAlert(<Alert severity="error">Failed to wipe database.</Alert>)
return;
}
setAlert(<Alert severity="success">Successfully wiped database.</Alert>)
}
return <>
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<SaveIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<Typography variant="h4">Manage Data</Typography>
<Box mt={2} />
<Typography>
An exported database contains all your artists, albums, tracks and tags.<br />
It is represented as a JSON structure.
</Typography>
<Box mt={2} />
<Alert severity="warning">Imported items will not be merged with existing ones. If you wish to replace your database, wipe it first.</Alert>
{alert}
<Box display="flex" flexDirection="column" alignItems="left">
<Box mt={2}>
<a href={getDBExportLink()}>
<Button variant="outlined">
<GetAppIcon />
Export
</Button>
</a>
</Box>
<Box mt={2} >
<FileUploadButton variant="outlined"
onGetFile={(file: any) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e: any) => {
let json: DBImportRequest = JSON.parse(e.target.result);
handleImport(json);
}
}}>
<PublishIcon />
Import
</FileUploadButton>
</Box>
<Box mt={2} >
<Button variant="outlined"
onClick={handleWipe}
>
<DeleteForeverIcon />
Wipe
</Button>
</Box>
</Box>
</Box>
</Box>
</>
}

@ -1,65 +0,0 @@
import React, { useReducer, useState } from 'react';
import { WindowState } from "../Windows";
import { Box, Paper, Typography, TextField, Button } from "@material-ui/core";
import SaveIcon from '@material-ui/icons/Save';
import { Alert } from '@material-ui/lab';
import * as serverApi from '../../../api/api';
export interface ManageImportExportWindowState extends WindowState {
dummy: boolean
}
export enum ManageImportExportWindowActions {
SetDummy = "SetDummy",
}
export function ManageImportExportWindowReducer(state: ManageImportExportWindowState, action: any) {
switch (action.type) {
case ManageImportExportWindowActions.SetDummy: {
return state;
}
default:
throw new Error("Unimplemented ManageImportExportWindow state update.")
}
}
export default function ManageImportExportWindow(props: {}) {
const [state, dispatch] = useReducer(ManageImportExportWindowReducer, {
dummy: true,
});
return <ManageImportExportWindowControlled state={state} dispatch={dispatch} />
}
export function ManageImportExportWindowControlled(props: {
state: ManageImportExportWindowState,
dispatch: (action: any) => void,
}) {
return <>
<Box width="100%" justifyContent="center" display="flex" flexWrap="wrap">
<Box
m={1}
mt={4}
width="80%"
>
<SaveIcon style={{ fontSize: 80 }} />
</Box>
<Box
m={1}
mt={4}
width="80%"
>
<Typography variant="h4">Import / Export</Typography>
<Box mt={2} />
<Typography>
An exported database contains all your artists, albums, tracks and tags.<br />
It is represented as a JSON structure.
</Typography>
<Box mt={2} />
<Alert severity="warning">Upon importing a previously exported database, your database will be completely replaced!</Alert>
<Box mt={2} />
<a href={(process.env.REACT_APP_BACKEND || "") + serverApi.DBExportEndpoint}>
<Button variant="outlined">Export</Button>
</a>
</Box>
</Box>
</>
}

@ -0,0 +1,32 @@
import { DBExportEndpoint, DBImportEndpoint, DBImportRequest, DBWipeEndpoint } from "../../api/api";
import backendRequest from "./request";
export function getDBExportLink() {
return (process.env.REACT_APP_BACKEND || "") + DBExportEndpoint;
}
export async function wipeDB() {
const requestOpts = {
method: 'POST'
};
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + DBWipeEndpoint, requestOpts)
if (!response.ok) {
throw new Error("Response to DB wipe not OK: " + JSON.stringify(response));
}
return;
}
export async function importDB(jsonData: DBImportRequest) {
const requestOpts = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(jsonData),
};
const response = await backendRequest((process.env.REACT_APP_BACKEND || "") + DBImportEndpoint, requestOpts)
if (!response.ok) {
throw new Error("Response to DB import not OK: " + JSON.stringify(response));
}
}

@ -128,20 +128,22 @@ function useProvideIntegrations(): Integrations {
const [state, dispatch] = useReducer(IntegrationsReducer, [])
let updateFromUpstream = async () => {
return await backend.getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => {
dispatch({
type: IntegrationsActions.Set,
value: integrations.map((i: any) => {
return {
integration: new (IntegrationClasses[i.type])(i.id),
properties: { ...i },
id: i.id,
}
})
});
})
.catch((e) => handleNotLoggedIn(auth, e));
try {
return await backend.getIntegrations()
.then((integrations: serverApi.ListIntegrationsResponse) => {
dispatch({
type: IntegrationsActions.Set,
value: integrations.map((i: any) => {
return {
integration: new (IntegrationClasses[i.type])(i.id),
properties: { ...i },
id: i.id,
}
})
});
})
.catch((e) => handleNotLoggedIn(auth, e));
} catch(e) {}
}
let addIntegration = async (v: serverApi.PostIntegrationRequest) => {

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ const bodyParser = require('body-parser');
import * as api from '../client/src/api/api';
import Knex from 'knex';
import { importExportEndpoints } from './endpoints/ImportExport';
import { DataEndpoints } from './endpoints/Data';
import { queryEndpoints } from './endpoints/Query';
import { artistEndpoints } from './endpoints/Artist';
import { albumEndpoints } from './endpoints/Album';
@ -34,7 +34,7 @@ const invokeHandler = (handler: endpointTypes.EndpointHandler, knex: Knex) => {
}
const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
app.use(bodyParser.json());
app.use(bodyParser.json({ limit: "10mb" }));
app.use(bodyParser.urlencoded({ extended: true }));
// Set up auth. See: https://github.com/passport/express-4.x-local-example.git
@ -121,7 +121,7 @@ const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
integrationEndpoints,
userEndpoints,
queryEndpoints,
importExportEndpoints,
DataEndpoints,
].forEach((endpoints: [string, string, boolean, endpointTypes.EndpointHandler][]) => {
endpoints.forEach((endpoint: [string, string, boolean, endpointTypes.EndpointHandler]) => {
let [url, method, authenticated, handler] = endpoint;

@ -76,6 +76,8 @@ export async function getAlbum(id: number, userId: number, knex: Knex):
// Returns the id of the created album.
export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Knex): Promise<number> {
return await knex.transaction(async (trx) => {
console.log("create album", album);
// Start retrieving artists.
const artistIdsPromise: Promise<number[]> =
trx.select('id')
@ -103,6 +105,8 @@ export async function createAlbum(userId: number, album: AlbumWithRefs, knex: Kn
// Wait for the requests to finish.
var [artists, tags, tracks] = await Promise.all([artistIdsPromise, tagIdsPromise, trackIdsPromise]);;
console.log("Got refs")
// Check that we found all artists and tags we need.
if ((!_.isEqual(artists.sort(), (album.artistIds || []).sort())) ||
(!_.isEqual(tags.sort(), (album.tagIds || []).sort())) ||

@ -7,7 +7,7 @@ import { createTag } from "./Tag";
import { createAlbum } from "./Album";
import { createTrack } from "./Track";
export async function exportDB(userId: number, knex: Knex): Promise<api.DBImportExportFormat> {
export async function exportDB(userId: number, knex: Knex): Promise<api.DBDataFormat> {
// First, retrieve all the objects without taking linking tables into account.
// Fetch the links separately.
@ -141,7 +141,7 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBImport
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.tagIds.push(tagId);
})
artistsAlbums.forEach((v: [number, number]) => {
let [artistId, albumId] = v;
let [albumId, artistId] = v;
artists.find((t: ArtistWithRefsWithId) => t.id === artistId)?.albumIds.push(albumId);
albums.find((t: AlbumWithRefsWithId) => t.id === albumId)?.artistIds.push(artistId);
})
@ -154,37 +154,53 @@ export async function exportDB(userId: number, knex: Knex): Promise<api.DBImport
}
}
export async function importDB(userId: number, db: api.DBImportExportFormat, knex: Knex): Promise<void> {
export async function importDB(userId: number, db: api.DBDataFormat, knex: Knex): Promise<void> {
// Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {};
let artistIdMaps: Record<number, number> = {};
let albumIdMaps: Record<number, number> = {};
let trackIdMaps: Record<number, number> = {};
// Insert items one by one, remapping the IDs as we go.
for(const tag of db.tags) {
let _tag = {
...tag,
parentId: tag.parentId ? tagIdMaps[tag.parentId] : null,
}
tagIdMaps[tag.id] = await createTag(userId, _tag, knex);
}
for(const artist of db.artists) {
artistIdMaps[artist.id] = await createArtist(userId, {
...artist,
tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]),
trackIds: [],
albumIds: [],
}, knex);
}
for(const album of db.albums) {
albumIdMaps[album.id] = await createAlbum(userId, {
...album,
tagIds: album.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: album.artistIds.map((id: number) => artistIdMaps[id]),
trackIds: [],
}, knex);
}
for(const track of db.tracks) {
trackIdMaps[track.id] = await createTrack(userId, {
...track,
tagIds: track.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: track.artistIds.map((id: number) => artistIdMaps[id]),
albumId: track.albumId ? albumIdMaps[track.albumId] : null,
}, knex);
}
}
export async function wipeDB(userId: number, knex: Knex) {
return await knex.transaction(async (trx) => {
// Store the ID mappings in this record.
let tagIdMaps: Record<number, number> = {};
let artistIdMaps: Record<number, number> = {};
let albumIdMaps: Record<number, number> = {};
let trackIdMaps: Record<number, number> = {};
// Insert items one by one, remapping the IDs as we go.
await Promise.all(db.tags.map((tag: TagWithRefsWithId) => async () => {
tagIdMaps[tag.id] = await createTag(userId, tag, knex);
}));
await Promise.all(db.artists.map((artist: ArtistWithRefsWithId) => async () => {
artistIdMaps[artist.id] = await createArtist(userId, {
...artist,
tagIds: artist.tagIds.map((id: number) => tagIdMaps[id]),
}, knex);
}))
await Promise.all(db.albums.map((album: AlbumWithRefsWithId) => async () => {
albumIdMaps[album.id] = await createAlbum(userId, {
...album,
tagIds: album.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: album.artistIds.map((id: number) => artistIdMaps[id]),
}, knex);
}))
await Promise.all(db.tracks.map((track: TrackWithRefsWithId) => async () => {
trackIdMaps[track.id] = await createTrack(userId, {
...track,
tagIds: track.tagIds.map((id: number) => tagIdMaps[id]),
artistIds: track.artistIds.map((id: number) => artistIdMaps[id]),
albumId: track.albumId ? albumIdMaps[track.albumId] : null,
}, knex);
}))
});
await Promise.all([
trx('tracks').where({ 'user': userId }).del(),
trx('artists').where({ 'user': userId }).del(),
trx('albums').where({ 'user': userId }).del(),
trx('tags').where({ 'user': userId }).del(),
])
})
}

@ -1,5 +1,5 @@
import Knex from "knex";
import { exportDB, importDB } from "../db/ImportExport";
import { exportDB, importDB, wipeDB } from "../db/Data";
import { EndpointError, EndpointHandler, handleErrorsInEndpoint } from "./types";
import * as api from '../../client/src/api/api';
import { DBExportEndpoint } from "../../client/src/api/api";
@ -39,7 +39,18 @@ export const DBImport: EndpointHandler = async (req: any, res: any, knex: Knex)
}
}
export const importExportEndpoints: [ string, string, boolean, EndpointHandler ][] = [
export const DBWipe: EndpointHandler = async (req: any, res: any, knex: Knex) => {
console.log("User ", req.user.id, ": Wipe DB");
try {
await wipeDB(req.user.id, knex);
res.status(200).send();
} catch (e) {
handleErrorsInEndpoint(e)
}
}
export const DataEndpoints: [ string, string, boolean, EndpointHandler ][] = [
[ api.DBExportEndpoint, 'get', true, DBExport ],
[ api.DBImportEndpoint, 'post', true, DBImport ],
[ api.DBWipeEndpoint, 'post', true, DBWipe ],
];

@ -9,8 +9,19 @@ export async function up(knex: Knex): Promise<void> {
table.increments('id');
table.string('name');
table.string('storeLinks')
table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.integer('album').unsigned().defaultTo(null).references('albums.id');
table.integer('user')
.unsigned()
.notNullable()
.defaultTo(1)
.references('users.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('album')
.unsigned()
.defaultTo(null)
.references('albums.id')
.onDelete('SET NULL')
.onUpdate('CASCADE')
}
)
@ -21,7 +32,13 @@ export async function up(knex: Knex): Promise<void> {
table.increments('id');
table.string('name');
table.string('storeLinks');
table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.integer('user')
.unsigned()
.notNullable()
.defaultTo(1)
.references('users.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
}
)
@ -32,7 +49,13 @@ export async function up(knex: Knex): Promise<void> {
table.increments('id');
table.string('name');
table.string('storeLinks');
table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.integer('user')
.unsigned()
.notNullable()
.defaultTo(1)
.references('users.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
}
)
@ -42,8 +65,17 @@ export async function up(knex: Knex): Promise<void> {
(table: any) => {
table.increments('id');
table.string('name');
table.integer('parentId');
table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.integer('parentId')
.references('tags.id')
.onDelete('SET NULL')
.onUpdate('CASCADE')
table.integer('user')
.unsigned()
.notNullable()
.defaultTo(1)
.references('users.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
}
)
@ -62,12 +94,20 @@ export async function up(knex: Knex): Promise<void> {
'integrations',
(table: any) => {
table.increments('id');
table.integer('user').unsigned().notNullable().defaultTo(1).references('users.id');
table.string('name').notNullable(); // Uniquely identifies this integration configuration for the user.
table.string('type').notNullable(); // Enumerates different supported integration types (e.g. Spotify)
table.integer('user')
.unsigned()
.notNullable()
.defaultTo(1)
.references('users.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.string('name')
.notNullable(); // Uniquely identifies this integration configuration for the user.
table.string('type')
.notNullable(); // Enumerates different supported integration types (e.g. Spotify)
table.string('details'); // Stores anything that might be needed for the integration to work.
table.string('secretDetails'); // Stores anything that might be needed for the integration to work and which
// should never leave the server.
// should never leave the server.
}
)
@ -76,8 +116,14 @@ export async function up(knex: Knex): Promise<void> {
'tracks_artists',
(table: any) => {
table.increments('id');
table.integer('trackId').references('tracks.id');
table.integer('artistId').references('artists.id');
table.integer('trackId')
.references('tracks.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('artistId')
.references('artists.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.unique(['trackId', 'artistId'])
}
)
@ -87,8 +133,14 @@ export async function up(knex: Knex): Promise<void> {
'tracks_tags',
(table: any) => {
table.increments('id');
table.integer('trackId').references('tracks.id');
table.integer('tagId').references('tags.id');
table.integer('trackId')
.references('tracks.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('tagId')
.references('tags.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.unique(['trackId', 'tagId'])
}
)
@ -98,8 +150,14 @@ export async function up(knex: Knex): Promise<void> {
'artists_tags',
(table: any) => {
table.increments('id');
table.integer('artistId').references('artists.id');
table.integer('tagId').references('tags.id');
table.integer('artistId')
.references('artists.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('tagId')
.references('tags.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.unique(['artistId', 'tagId'])
}
)
@ -109,8 +167,14 @@ export async function up(knex: Knex): Promise<void> {
'albums_tags',
(table: any) => {
table.increments('id');
table.integer('tagId').references('tags.id');
table.integer('albumId').references('albums.id');
table.integer('tagId')
.references('tags.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('albumId')
.references('albums.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.unique(['albumId', 'tagId'])
}
)
@ -120,8 +184,14 @@ export async function up(knex: Knex): Promise<void> {
'artists_albums',
(table: any) => {
table.increments('id');
table.integer('artistId').references('artists.id');
table.integer('albumId').references('albums.id');
table.integer('artistId')
.references('artists.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.integer('albumId')
.references('albums.id')
.onDelete('CASCADE')
.onUpdate('CASCADE')
table.unique(['artistId', 'albumId'])
}
)

@ -6,7 +6,9 @@ import { SetupApp } from './app';
const app = express();
const knex = get_knex();
knex.migrate.latest().then(() => {
(async () => {
await knex.raw('PRAGMA foreign_keys = ON');
await knex.migrate.latest();
const port = process.env.PORT || 5000;
const apiBase = process.env.API || "";
const frontEndPrefix = process.env.FRONTEND_PREFIX || undefined;
@ -20,6 +22,6 @@ knex.migrate.latest().then(() => {
}
app.listen(port, () => console.log(`Listening on port ${port}`));
})
})()
export { }
Loading…
Cancel
Save