Merge pull request 'Docker-based deployment scripts.' (#12) from deploy_docker into master

Reviewed-on: #12
pull/16/head
Sander Vocke 5 years ago
commit 7b1b8e0fee
  1. 4
      .dockerignore
  2. 25
      README
  3. 14706
      client/package-lock.json
  4. 12
      client/package.json
  5. 2
      client/src/App.tsx
  6. 2
      client/src/components/QueryBrowseWindow.tsx
  7. 52
      deploy/Dockerfile
  8. 10
      deploy/build.sh
  9. 12
      deploy/run.sh
  10. 13
      package.json
  11. 28
      server/app.ts
  12. 23
      server/knex/get_knex.ts
  13. 3
      server/knex/knex.ts
  14. 35
      server/knexfile.ts
  15. 4056
      server/package-lock.json
  16. 9
      server/package.json
  17. 16
      server/server.ts
  18. 2
      server/test/integration/flows/AlbumFlow.js
  19. 2
      server/test/integration/flows/ArtistFlow.js
  20. 2
      server/test/integration/flows/QueryFlow.js
  21. 2
      server/test/integration/flows/SongFlow.js
  22. 2
      server/test/integration/flows/TagFlow.js
  23. 2298
      server/yarn.lock

@ -0,0 +1,4 @@
node_modules
client/node_modules
server/node_modules
server/dev.sqlite3

@ -1,13 +1,18 @@
Started from: https://www.freecodecamp.org/news/how-to-make-create-react-app-work-with-a-node-backend-api-7c5c48acb1b0/
# MuDBase
MuDBase is a self-hosted database for music.
TODO:
It is made for those who use streaming services such as Spotify for keeping track of their music library and tastes, but don't want to rely solely on them for storing this information.
It is also for power users who would like to organize their music tastes with more powerful tools than only a storage of playlists, albums, artists and songs. MuDBase will offer more rich power features such as tagging and ranking music, and a powerful music querying system.
- Ranking system
- Have "ranking contexts". These can be stored in the database.
- Per artist (this removes need for "per album", which can be a subset)
- Per tag
- Per playlist
- Have a linking table between contexts <-> artists/songs. This linking table should include an optional ranking score.
- The ranking score allows ranking songs per query or per query element. It is a floating point so we can always insert stuff in between.
- Visually, the system shows ranked items in a table and unranked items in another. User can drag to rank.
# Deployment
The easiest deployment is by getting it off of Docker Hub.
The following env variables need to be set for MuDBase to work:
- MUDBASE_DB_CONFIG: This env variable should contain a JSON struct with MuDBase's database configuration. The JSON should hold a valid Knex.js config struct. In principle, all dialects supported by Knex are supported (postgresql, mysql, mssql, sqlite3, oracledb), but only sqlite3 and postgresql are tested.
# Architecture
A Node.js Express back-end with a React front-end.
Started from: https://www.freecodecamp.org/news/how-to-make-create-react-app-work-with-a-node-backend-api-7c5c48acb1b0/

14706
client/package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -10,22 +10,24 @@
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/jest": "^24.0.0",
"@types/node": "^12.0.0",
"@types/react": "^16.9.0",
"@types/node": "^12.12.54",
"@types/react": "^16.9.49",
"@types/react-dom": "^16.9.0",
"@types/react-router": "^5.1.8",
"@types/react-router-dom": "^5.1.5",
"jsurl": "^0.1.5",
"lodash": "^4.17.19",
"material-table": "^1.64.0",
"lodash": "^4.17.20",
"material-table": "^1.69.0",
"react": "^16.13.1",
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3",
"react-dom": "^16.13.1",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"typescript": "~3.7.2"
},
"scripts": {
"start": "BROWSER=none react-scripts start",
"dev": "BROWSER=none react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"

@ -7,7 +7,7 @@ import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';
import {
BrowserRouter as Router,
HashRouter as Router,
Switch,
Route,
useHistory,

@ -159,7 +159,7 @@ export default function QueryBrowseWindow(props: IProps) {
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(request)
};
fetch(serverApi.QueryEndpoint, requestOpts)
fetch((process.env.REACT_APP_BACKEND || "") + serverApi.QueryEndpoint, requestOpts)
.then((response: any) => response.json())
.then((json: any) => {
const match = _.isEqual(q, props.query) && _.isEqual(r, props.resultOrder) && _.isEqual(t, props.typesIncluded);

@ -0,0 +1,52 @@
# Note: this Dockerfile is written to be executed with the whole source
# as its context.
# TODO remove client source
# TODO material UI (and other libs) from CSN
FROM node:12-alpine as frontend_builder
# Make a static build of the front-end only.
WORKDIR /usr/src/
COPY client/package*.json ./
RUN npm install --only=production
COPY client/ ./
# Prime the front-end build to find the back-end at /api.
ENV REACT_APP_BACKEND "/api"
RUN npm run-script build
FROM node:12-alpine
# Some useful tools for servicing
RUN apk update && apk upgrade && apk add bash
# Install back-end dependencies
WORKDIR /opt/mudbase/server
COPY server/package*.json ./
RUN npm install --only=production
# Install back-end source code
WORKDIR /opt/mudbase/server
COPY server/ ./
# Install static front-end build
COPY --from=frontend_builder /usr/src/build /opt/mudbase/frontend
# Install the API where the back-end expects
# to find it.
# TODO: can we move it somewhere else?
COPY client/src/api.ts /opt/mudbase/client/src/api.ts
# Install the runner script
COPY deploy/run.sh /opt/mudbase/run.sh
# Start
WORKDIR /opt/mudbase
EXPOSE 8080
ENV PORT 8080
ENV API "/api"
ENV FRONTEND_PREFIX "/"
ENV FRONTEND "/opt/mudbase/frontend"
ENV NODE_ENV "production"
ENV ENVIRONMENT "production"
ENTRYPOINT './run.sh'

@ -0,0 +1,10 @@
#!/bin/bash
SCRIPT=`realpath $0`
SCRIPTPATH=`dirname $SCRIPT`
pushd "$SCRIPTPATH/.."
docker build . -f deploy/Dockerfile -t mudbase:latest
popd

@ -0,0 +1,12 @@
#!/bin/bash
MUDBASE_DB_CONFIG=$(printenv MUDBASE_DB_CONFIG)
if [ -z "$MUDBASE_DB_CONFIG" ]; then
echo "Cannot start MuDBase without a database configuration." >&2
echo "Please set the MUDBASE_DB_CONFIG env variable to a valid" >&2
echo "Knex.js config JSON object." >&2
exit 1
fi
echo "Starting MuDBase..."
cd server && npm start

@ -2,15 +2,12 @@
"name": "mudbase",
"version": "1.0.0",
"scripts": {
"client": "cd client && yarn start",
"server": "cd server && yarn start",
"dev": "concurrently --kill-others-on-fail \"yarn server\" \"yarn client\""
},
"devDependencies": {
"concurrently": "^4.0.1"
"client": "cd client && npm run-script dev",
"server": "cd server && npm run-script dev",
"dev": "concurrently --kill-others-on-fail \"npm run-script server\" \"npm run-script client\"",
"start": "npm run-script dev"
},
"dependencies": {
"react-dnd": "^11.1.3",
"react-dnd-html5-backend": "^11.1.3"
"concurrently": "^4.0.1"
}
}

@ -31,7 +31,7 @@ const invokeHandler = (handler:endpointTypes.EndpointHandler, knex: Knex) => {
};
}
const SetupApp = (app: any, knex: Knex) => {
const SetupApp = (app: any, knex: Knex, apiBaseUrl: string) => {
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
@ -40,19 +40,19 @@ const SetupApp = (app: any, knex: Knex) => {
}
// Set up REST API endpoints
app.post(api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler));
app.post(api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler));
app.post(api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler));
app.put(api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler));
app.put(api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler));
app.get(api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler));
app.get(api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler));
app.post(api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler));
app.put(api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler));
app.get(api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler));
app.post(api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler));
app.put(api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler));
app.get(api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateSongEndpoint, invokeWithKnex(CreateSongEndpointHandler));
app.post(apiBaseUrl + api.QueryEndpoint, invokeWithKnex(QueryEndpointHandler));
app.post(apiBaseUrl + api.CreateArtistEndpoint, invokeWithKnex(CreateArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifyArtistEndpoint, invokeWithKnex(ModifyArtistEndpointHandler));
app.put(apiBaseUrl + api.ModifySongEndpoint, invokeWithKnex(ModifySongEndpointHandler));
app.get(apiBaseUrl + api.SongDetailsEndpoint, invokeWithKnex(SongDetailsEndpointHandler));
app.get(apiBaseUrl + api.ArtistDetailsEndpoint, invokeWithKnex(ArtistDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateTagEndpoint, invokeWithKnex(CreateTagEndpointHandler));
app.put(apiBaseUrl + api.ModifyTagEndpoint, invokeWithKnex(ModifyTagEndpointHandler));
app.get(apiBaseUrl + api.TagDetailsEndpoint, invokeWithKnex(TagDetailsEndpointHandler));
app.post(apiBaseUrl + api.CreateAlbumEndpoint, invokeWithKnex(CreateAlbumEndpointHandler));
app.put(apiBaseUrl + api.ModifyAlbumEndpoint, invokeWithKnex(ModifyAlbumEndpointHandler));
app.get(apiBaseUrl + api.AlbumDetailsEndpoint, invokeWithKnex(AlbumDetailsEndpointHandler));
}
export { SetupApp }

@ -0,0 +1,23 @@
const environment = process.env.ENVIRONMENT || 'development'
import config from '../knexfile';
export default function get_knex() {
if (!Object.keys(config).includes(environment)) {
throw "No Knex database configuration was found for environment '" +
environment + "'. Please check your configuration.";
}
var _knex = undefined;
console.log("Using Knex config: ", config[environment])
try {
_knex = require('knex')(config[environment]);
} catch (e) {
throw [
"Failed to initialize Knex database connection with config: "
+ JSON.stringify(config[environment]),
e
];
}
return _knex;
}

@ -1,3 +0,0 @@
const environment = process.env.ENVIRONMENT || 'development'
import config from '../knexfile';
export default require('knex')(config[environment]);

@ -2,6 +2,7 @@
export default <Record<string,any>> {
// Development settings use a simple SQLite file.
development: {
client: "sqlite3",
connection: {
@ -9,36 +10,8 @@ export default <Record<string,any>> {
}
},
// staging: {
// client: "postgresql",
// connection: {
// database: "my_db",
// user: "username",
// password: "password"
// },
// pool: {
// min: 2,
// max: 10
// },
// migrations: {
// tableName: "knex_migrations"
// }
// },
// production: {
// client: "postgresql",
// connection: {
// database: "my_db",
// user: "username",
// password: "password"
// },
// pool: {
// min: 2,
// max: 10
// },
// migrations: {
// tableName: "knex_migrations"
// }
// }
// In production, we base the config on an environment
// variable setting.
production: JSON.parse(process.env.MUDBASE_DB_CONFIG || "")
};

File diff suppressed because it is too large Load Diff

@ -2,7 +2,8 @@
"name": "mudbase-server",
"version": "1.0.0",
"scripts": {
"start": "nodemon server.ts",
"start": "ts-node server.ts",
"dev": "nodemon server.ts",
"build": "tsc",
"test": "ts-node node_modules/jasmine/bin/jasmine --config=test/jasmine.json"
},
@ -13,6 +14,12 @@
"express": "^4.16.4",
"jasmine": "^3.5.0",
"knex": "^0.21.5",
"mssql": "^6.2.1",
"mysql": "^2.18.1",
"mysql2": "^2.1.0",
"nodemon": "^2.0.4",
"oracledb": "^5.0.0",
"pg": "^8.3.3",
"sqlite3": "^5.0.0",
"ts-node": "^8.10.2",
"typescript": "~3.7.2"

@ -1,14 +1,24 @@
const express = require('express');
import knex from './knex/knex';
import get_knex from './knex/get_knex';
import { SetupApp } from './app';
const app = express();
const knex = get_knex();
knex.migrate.latest().then(() => {
SetupApp(app, knex);
const port = process.env.PORT || 5000;
const apiBase = process.env.API || "";
const frontEndPrefix = process.env.FRONTEND_PREFIX || undefined;
const frontEnd = process.env.FRONTEND || undefined;
SetupApp(app, knex, apiBase);
if(frontEnd && frontEndPrefix) {
console.log(`Hosting front-end ${frontEnd} at ${frontEndPrefix}.`)
app.use(frontEndPrefix, express.static(frontEnd))
}
app.listen(port, () => console.log(`Listening on port ${port}`));
})

@ -8,7 +8,7 @@ import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB());
SetupApp(app, await helpers.initTestDB(), '');
return app;
}

@ -8,7 +8,7 @@ import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB());
SetupApp(app, await helpers.initTestDB(), '');
return app;
}

@ -8,7 +8,7 @@ import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB());;
SetupApp(app, await helpers.initTestDB(), '');;
return app;
}

@ -8,7 +8,7 @@ import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB());
SetupApp(app, await helpers.initTestDB(), '');
return app;
}

@ -8,7 +8,7 @@ import * as helpers from './helpers';
async function init() {
chai.use(chaiHttp);
const app = express();
SetupApp(app, await helpers.initTestDB());
SetupApp(app, await helpers.initTestDB(), '');
return app;
}

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save