added sqlite backend and express routes

This commit is contained in:
Sascha Kühl 2025-02-27 22:19:16 +01:00
parent 20cba85fd3
commit 27f636e3fc
17 changed files with 2920 additions and 98 deletions

View File

@ -0,0 +1,9 @@
function pickRandom(args) {
return args[Math.floor(Math.random() * args.length)];
}
function randomDate() {
return new Date(new Date() - 200000000000 * Math.random());
}
module.exports = { pickRandom, randomDate };

57
backend/database/setup.js Normal file
View File

@ -0,0 +1,57 @@
const sequelize = require('../sequelize');
const { pickRandom, randomDate } = require('./helpers/random');
async function reset() {
console.log('Will rewrite the SQLite example database, adding some dummy data.');
await sequelize.sync({ force: true });
await sequelize.models.user.bulkCreate([
{ firstName: 'Nolan', lastName: 'Ryan', email: 'ryan.nolan@bullpen.com', password: 'nolan' },
{ firstName: 'Sandy', lastName: 'Koufax', email: 'sandy.koufax@bullpen.com', password: 'sandy' },
{ firstName: 'Pedro', lastName: 'Martinez', email: 'pedro.martinez@bullpen.com', password: 'pedro' },
{ firstName: 'randy', lastName: 'johnson', email: 'randy.johnson@bullpen.com', password: 'randy' },
]);
await sequelize.models.pitchType.bulkCreate([
{ name: 'Fastball', abbreviation: 'FB' },
{ name: 'Curveball', abbreviation: 'CB' },
{ name: 'Slider', abbreviation: 'SL' },
{ name: 'Changeup', abbreviation: 'CH' },
{ name: 'Cutter', abbreviation: 'CUT' },
{ name: 'Sweeper', abbreviation: 'SW' },
{ name: 'Slurve', abbreviation: 'SLV' },
]);
// Let's create random instruments for each orchestra
// for (const orchestra of await sequelize.models.orchestra.findAll()) {
// for (let i = 0; i < 10; i++) {
// const type = pickRandom([
// 'violin',
// 'trombone',
// 'flute',
// 'harp',
// 'trumpet',
// 'piano',
// 'guitar',
// 'pipe organ',
// ]);
//
// await orchestra.createInstrument({
// type: type,
// purchaseDate: randomDate()
// });
//
// // The following would be equivalent in this case:
// // await sequelize.models.instrument.create({
// // type: type,
// // purchaseDate: randomDate(),
// // orchestraId: orchestra.id
// // });
// }
// }
console.log('Done!');
}
reset();

27
backend/index.js Normal file
View File

@ -0,0 +1,27 @@
const app = require('./services/app');
const sequelize = require('./sequelize');
const PORT = 8080;
async function assertDatabaseConnectionOk() {
console.log(`Checking database connection...`);
try {
await sequelize.authenticate();
console.log('Database connection OK!');
} catch (error) {
console.log('Unable to connect to the database:');
console.log(error.message);
process.exit(1);
}
}
async function init() {
await assertDatabaseConnectionOk();
console.log(`Starting Sequelize + Express example on port ${PORT}...`);
app.listen(PORT, () => {
console.log(`Express server started on port ${PORT}. Try some routes, such as '/api/users'.`);
});
}
init().then(r => console.log('finished'));

View File

@ -1,43 +0,0 @@
'use strict';
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.json')[env];
const db = {};
let sequelize;
if (config.use_env_variable) {
sequelize = new Sequelize(process.env[config.use_env_variable], config);
} else {
sequelize = new Sequelize(config.database, config.username, config.password, config);
}
fs
.readdirSync(__dirname)
.filter(file => {
return (
file.indexOf('.') !== 0 &&
file !== basename &&
file.slice(-3) === '.js' &&
file.indexOf('.test.js') === -1
);
})
.forEach(file => {
const model = require(path.join(__dirname, file))(sequelize, Sequelize.DataTypes);
db[model.name] = model;
});
Object.keys(db).forEach(modelName => {
if (db[modelName].associate) {
db[modelName].associate(db);
}
});
db.sequelize = sequelize;
db.Sequelize = Sequelize;
module.exports = db;

View File

@ -1,44 +0,0 @@
'use strict';
const Sequelize = require("sequelize");
const { Model } = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class User extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate(models) {
// define association here
}
}
User.init({
id: {
type: Sequelize.UUIDV4,
allowNull: false,
primaryKey: true,
},
firstName: {
type: Sequelize.STRING,
allowNull: false
},
lastName: {
type: Sequelize.STRING,
allowNull: false
},
email: {
type: Sequelize.STRING,
allowNull: false
},
password: {
type: Sequelize.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'User',
tableName: "Users",
});
return User;
};

2445
backend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,15 +3,23 @@
"version": "1.0.0",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
"test": "echo \"Error: no test specified\" && exit 1",
"start": "node index.js",
"setup-example-db": "node database/setup.js"
},
"engines": {
"node": ">=10"
},
"author": "skuehl1972@gmail.com",
"license": "ISC",
"description": "",
"dependencies": {
"body-parser": "^1.20.3",
"express": "^4.21.2",
"pg": "^8.13.3",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.5"
"sequelize": "^6.37.5",
"sqlite3": "^5.1.7"
},
"devDependencies": {
"sequelize-cli": "^6.6.2"

View File

@ -0,0 +1,10 @@
function applyExtraSetup(sequelize) {
const { user, pitch, pitchType, bullpenSession } = sequelize.models;
bullpenSession.hasMany(pitch);
bullpenSession.belongsTo(user);
pitch.belongsTo(bullpenSession);
pitchType.belongsTo(pitchType);
}
module.exports = { applyExtraSetup };

View File

@ -0,0 +1,30 @@
const { Sequelize } = require('sequelize');
const { applyExtraSetup } = require('./extra-setup');
// In a real app, you should keep the database connection URL as an environment variable.
// But for this example, we will just use a local SQLite database.
// const sequelize = new Sequelize(process.env.DB_CONNECTION_URL);
const sequelize = new Sequelize({
dialect: 'sqlite',
storage: 'database/example-db.sqlite',
logQueryParameters: true,
benchmark: true
});
const modelDefiners = [
require('./models/user.model'),
require('./models/pitchType.model'),
require('./models/pitch.model'),
require('./models/bullpenSession.model'),
];
// We define all models according to their files.
for (const modelDefiner of modelDefiners) {
modelDefiner(sequelize);
}
// We execute any extra setup after the models are defined, such as adding associations.
applyExtraSetup(sequelize);
// We export the sequelize connection instance to be used around our app.
module.exports = sequelize;

View File

@ -0,0 +1,32 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
sequelize.define('bullpenSession', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
pitcher: {
type: DataTypes.INTEGER,
allowNull: false,
references: { // User belongsTo User 1:1
model: 'User',
key: 'id'
}
},
aimedArea: {
type: DataTypes.INTEGER,
allowNull: false
},
hitArea: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
sequelize,
modelName: 'BullpenSession',
tableName: 'BullpenSessions',
})
};

View File

@ -0,0 +1,40 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
sequelize.define('pitch', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
type: {
type: DataTypes.INTEGER,
allowNull: false,
references: { // User belongsTo PitchType 1:1
model: 'PitchType',
key: 'id'
}
},
bullpenSessionId: {
type: DataTypes.INTEGER,
allowNull: false,
references: { // User belongsTo PitchType 1:1
model: 'BullpenSession',
key: 'id'
}
},
aimedArea: {
type: DataTypes.INTEGER,
allowNull: false
},
hitArea: {
type: DataTypes.INTEGER,
allowNull: false
}
}, {
sequelize,
modelName: 'Pitch',
tableName: 'Pitches',
})
};

View File

@ -0,0 +1,25 @@
const { DataTypes } = require('sequelize');
module.exports = (sequelize) => {
sequelize.define('pitchType', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
name: {
type: DataTypes.STRING,
allowNull: false,
unique: true
},
abbreviation: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'PitchType',
tableName: 'PitchTypes'
})
};

View File

@ -0,0 +1,42 @@
const { DataTypes } = require('sequelize');
// We export a function that defines the model.
// This function will automatically receive as parameter the Sequelize connection object.
module.exports = (sequelize) => {
sequelize.define('user', {
// The following specification of the 'id' attribute could be omitted
// since it is the default.
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: DataTypes.INTEGER
},
firstName: {
type: DataTypes.STRING,
allowNull: false
},
lastName: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
allowNull: false,
unique: true,
validate: {
// We require usernames to have length of at least 3, and
// only use letters, numbers and underscores.
is: /^\w{3,}$/
}
},
password: {
type: DataTypes.STRING,
allowNull: false
}
}, {
sequelize,
modelName: 'User',
tableName: "Users",
});
};

71
backend/services/app.js Normal file
View File

@ -0,0 +1,71 @@
const express = require('express');
const bodyParser = require('body-parser');
const routes = {
users: require('./routes/users'),
pitch_types: require('./routes/pitchTypes'),
// Add more routes here...
// items: require('./routes/items'),
};
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
// We create a wrapper to workaround async errors not being transmitted correctly.
function makeHandlerAwareOfAsyncErrors(handler) {
return async function(req, res, next) {
try {
await handler(req, res);
} catch (error) {
next(error);
}
};
}
// We provide a root route just as an example
app.get('/', (req, res) => {
res.send(`
<h2>Hello, Sequelize + Express!</h2>
<p>Make sure you have executed <b>npm run setup-example-db</b> once to have a populated example database. Otherwise, you will get <i>'no such table'</i> errors.</p>
<p>Try some routes, such as <a href='/api/users'>/api/users</a> or <a href='/api/orchestras?includeInstruments'>/api/orchestras?includeInstruments</a>!</p>
<p>To experiment with POST/PUT/DELETE requests, use a tool for creating HTTP requests such as <a href='https://github.com/jakubroztocil/httpie#readme'>HTTPie</a>, <a href='https://www.postman.com/downloads/'>Postman</a>, or even <a href='https://en.wikipedia.org/wiki/CURL'>the curl command</a>, or write some JS code for it with <a href='https://github.com/sindresorhus/got#readme'>got</a>, <a href='https://github.com/sindresorhus/ky#readme'>ky</a> or <a href='https://github.com/axios/axios#readme'>axios</a>.</p>
`);
});
// We define the standard REST APIs for each route (if they exist).
for (const [routeName, routeController] of Object.entries(routes)) {
if (routeController.getAll) {
app.get(
`/api/${routeName}`,
makeHandlerAwareOfAsyncErrors(routeController.getAll)
);
}
if (routeController.getById) {
app.get(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.getById)
);
}
if (routeController.create) {
app.post(
`/api/${routeName}`,
makeHandlerAwareOfAsyncErrors(routeController.create)
);
}
if (routeController.update) {
app.put(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.update)
);
}
if (routeController.remove) {
app.delete(
`/api/${routeName}/:id`,
makeHandlerAwareOfAsyncErrors(routeController.remove)
);
}
}
module.exports = app;

View File

@ -0,0 +1,11 @@
// A helper function to assert the request ID param is valid
// and convert it to a number (since it comes as a string by default)
function getIdParam(req) {
const id = req.params.id;
if (/^\d+$/.test(id)) {
return Number.parseInt(id, 10);
}
throw new TypeError(`Invalid ':id' param: "${id}"`);
}
module.exports = { getIdParam };

View File

@ -0,0 +1,60 @@
const { models } = require('../../sequelize');
const { getIdParam } = require('../helpers');
async function getAll(req, res) {
const pitchType = await models.pitchType.findAll();
res.status(200).json(pitchType);
}
async function getById(req, res) {
const id = getIdParam(req);
const pitchType = await models.pitchType.findByPk(id);
if (pitchType) {
res.status(200).json(pitchType);
} else {
res.status(404).send('404 - Not found');
}
}
async function create(req, res) {
if (req.body.id) {
res.status(400).send(`Bad request: ID should not be provided, since it is determined automatically by the database.`)
} else {
await models.pitchType.create(req.body);
res.status(201).end();
}
}
async function update(req, res) {
const id = getIdParam(req);
// We only accept an UPDATE request if the `:id` param matches the body `id`
if (req.body.id === id) {
await models.pitchType.update(req.body, {
where: {
id: id
}
});
res.status(200).end();
} else {
res.status(400).send(`Bad request: param ID (${id}) does not match body ID (${req.body.id}).`);
}
}
async function remove(req, res) {
const id = getIdParam(req);
await models.pitchType.destroy({
where: {
id: id
}
});
res.status(200).end();
}
module.exports = {
getAll,
getById,
create,
update,
remove,
};

View File

@ -0,0 +1,60 @@
const { models } = require('../../sequelize');
const { getIdParam } = require('../helpers');
async function getAll(req, res) {
const users = await models.user.findAll();
res.status(200).json(users);
}
async function getById(req, res) {
const id = getIdParam(req);
const user = await models.user.findByPk(id);
if (user) {
res.status(200).json(user);
} else {
res.status(404).send('404 - Not found');
}
}
async function create(req, res) {
if (req.body.id) {
res.status(400).send(`Bad request: ID should not be provided, since it is determined automatically by the database.`)
} else {
await models.user.create(req.body);
res.status(201).end();
}
}
async function update(req, res) {
const id = getIdParam(req);
// We only accept an UPDATE request if the `:id` param matches the body `id`
if (req.body.id === id) {
await models.user.update(req.body, {
where: {
id: id
}
});
res.status(200).end();
} else {
res.status(400).send(`Bad request: param ID (${id}) does not match body ID (${req.body.id}).`);
}
}
async function remove(req, res) {
const id = getIdParam(req);
await models.user.destroy({
where: {
id: id
}
});
res.status(200).end();
}
module.exports = {
getAll,
getById,
create,
update,
remove,
};