restructured backend, added test, docker support and info route

This commit is contained in:
Sascha Kühl 2025-04-21 16:03:48 +02:00
parent 9071886991
commit c4725a5d73
25 changed files with 776 additions and 165 deletions

4
.gitignore vendored
View File

@ -1,3 +1,5 @@
.idea/
node_modules/
backend/.env
backend/.env.test
backend/.env.development
backend/.env.production

4
backend/.dockerignore Normal file
View File

@ -0,0 +1,4 @@
.idea
node_modules
npm-debug.log
.env.*

View File

@ -1,20 +1,9 @@
DB_TEST_USER=dev_user
DB_TEST_PASSWORD=dev_password
DB_TEST_NAME=dev_db
DB_TEST_HOST=127.0.0.1
DB_TEST_PORT=5432
DB_TEST_DIALECT=postgres
DB_USER=dev_user
DB_PASSWORD=dev_password
DB_NAME=dev_db
DB_HOST=127.0.0.1
DB_PORT=5432
DB_DIALECT=postgres
DB_DEV_USER=dev_user
DB_DEV_PASSWORD=dev_password
DB_DEV_NAME=dev_db
DB_DEV_HOST=127.0.0.1
DB_DEV_PORT=5432
DB_DEV_DIALECT=postgres
DB_PROD_USER=prod_user
DB_PROD_PASSWORD=prod_password
DB_PROD_NAME=prod_db
DB_PROD_HOST=prod-db-server.com
DB_PROD_PORT=5432
DB_PROD_DIALECT=postgres
NODE_LOCAL_PORT=8124
NODE_DOCKER_PORT=8080

9
backend/.sequelizerc Normal file
View File

@ -0,0 +1,9 @@
// .sequelizerc
const path = require('path');
module.exports = {
'config': path.resolve('config/config.js'),
'models-path': path.resolve('models'),
'seeders-path': path.resolve('seeders'),
'migrations-path': path.resolve('migrations')
};

42
backend/Dockerfile Normal file
View File

@ -0,0 +1,42 @@
ARG NODE_VERSION=18.0.0
FROM node:${NODE_VERSION}-alpine AS base
WORKDIR /user/src/app
EXPOSE 8080
FROM base as production
ENV NODE_ENV=production
COPY .env.production .env
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --omit=dev
USER node
COPY . .
EXPOSE 8080
CMD ["node", "server.js"]
FROM base as development
ENV NODE_ENV=development
COPY .env.development .env
RUN --mount=type=bind,source=package.json,target=package.json \
--mount=type=bind,source=package-lock.json,target=package-lock.json \
--mount=type=cache,target=/root/.npm \
npm ci --include=dev
USER node
COPY . .
CMD ["npm", "run", "dev"]
#FROM node:20-alpine as build
#WORKDIR /app
#COPY package*.json ./
#RUN npm install --omit dev
#COPY . .
#
#FROM node:20-alpine
#WORKDIR /app
#COPY --from=build /app /app
#ENV NODE_ENV=production
#COPY .env.production .env
#EXPOSE 8080
#CMD ["node", "server.js"]

View File

@ -20,44 +20,11 @@ app.use(express.urlencoded({ extended: true }));
// initial();
// });
// simple route
app.get("/", (req, res) => {
res.json({ message: "Welcome to bullpen application." });
});
// routes
require('./routes/info.routes')(app);
require('./routes/auth.routes')(app);
require('./routes/user.routes')(app);
require('./routes/pitchType.routes')(app);
require('./routes/bullpenSession.routes')(app);
// function initial() {
// Role.bulkCreate([
// { name: 'user' },
// { name: 'administrator' },
// ]);
// User.bulkCreate([
// { firstName: 'Nolan', lastName: 'Ryan', dateOfBirth: new Date(1947, 1, 31), email: 'ryan.nolan@bullpen.com', password: bcrypt.hashSync('nolan', 8) },
// { firstName: 'Sandy', lastName: 'Koufax', dateOfBirth: new Date(1935, 12, 30), email: 'sandy.koufax@bullpen.com', password: bcrypt.hashSync('sandy', 8) },
// { firstName: 'Pedro', lastName: 'Martinez', dateOfBirth: new Date(1971, 10, 25), email: 'pedro.martinez@bullpen.com', password: bcrypt.hashSync('pedro', 8) },
// { firstName: 'randy', lastName: 'johnson', dateOfBirth: new Date(1963, 9, 10), email: 'randy.johnson@bullpen.com', password: bcrypt.hashSync('randy', 8) }
// ]);
//
// User.findAll().then(users => {
// users.forEach(user => {
// user.setRoles([1]);
// });
// });
//
// 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' },
// ]);
// }
module.exports = app;

View File

@ -1,31 +1,23 @@
require('dotenv').config();
const dotenv = require('dotenv');
const path = require('path');
const envFile = {
production: '.env.production',
test: '.env.test'
}[process.env.NODE_ENV || ''] || '.env';
dotenv.config({
path: path.resolve(process.cwd(), envFile)
});
module.exports = {
test: {
username: process.env.DB_TEST_USER,
password: process.env.DB_TEST_PASSWORD,
database: process.env.DB_TEST_NAME,
host: process.env.DB_TEST_HOST,
port: process.env.DB_TEST_PORT,
dialect: process.env.DB_TEST_DIALECT,
[process.env.NODE_ENV || 'development']: {
username: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
host: process.env.DB_HOST,
port: process.env.DB_PORT,
dialect: process.env.DB_DIALECT,
logging: false,
},
development: {
username: process.env.DB_DEV_USER,
password: process.env.DB_DEV_PASSWORD,
database: process.env.DB_DEV_NAME,
host: process.env.DB_DEV_HOST,
port: process.env.DB_DEV_PORT,
dialect: process.env.DB_DEV_DIALECT,
logging: false,
},
production: {
username: process.env.DB_PROD_USER,
password: process.env.DB_PROD_PASSWORD,
database: process.env.DB_PROD_NAME,
host: process.env.DB_PROD_HOST,
port: process.env.DB_PROD_PORT,
dialect: process.env.DB_PROD_DIALECT,
logging: false,
},
}
};

View File

@ -1,9 +1,24 @@
const db = require("../models/index");
const BullpenSession = db.BullpenSession;
const Pitch = db.Pitch;
const Op = db.Sequelize.Op;
const getPagination = (page, size) => {
const limit = size ? +size : 3;
const offset = page ? page * limit : 0;
return { limit, offset };
};
const getPageData = (data, totalCount, page, limit) => {
const currentPage = page ? +page : 0;
const totalPages = Math.ceil(totalCount / limit);
return { totalCount, data, totalPages, currentPage };
};
exports.insert = (req, res) => {
BullpenSession.create(req.body.bullpen, { include: [{
BullpenSession.create(req.body, { include: [{
model: Pitch,
as: 'pitches'
}]}
@ -17,20 +32,31 @@ exports.insert = (req, res) => {
};
exports.findAll = (req, res) => {
const { user } = req.query;
const filter = {};
if (user) {
filter.pitcherId = parseInt(user, 10);
}
BullpenSession.findAll({
where: filter,
const { page, size, user } = req.query;
const condition = user ? { pitcherId: { [Op.eq]: parseInt(user, 10) } } : null;
const { limit, offset } = getPagination(page, size);
let totalCount = 0;
BullpenSession.count({
where: condition,
limit,
offset
}).then(count => {
totalCount = count;
return BullpenSession.findAll({
where: condition,
limit,
offset,
include: {
model: Pitch,
as: 'pitches'
}
});
})
.then(bullpenSessions => {
res.status(200).send(bullpenSessions);
const response = getPageData(bullpenSessions, totalCount, page, limit);
res.status(200).send(response);
});
}

View File

@ -0,0 +1,9 @@
const process = require('process');
exports.info = (req, res) => {
res.json({
message: "Welcome to bullpen",
version: process.env.npm_package_version,
environment: process.env.NODE_ENV,
});
}

View File

@ -0,0 +1,11 @@
services:
test-db:
image: postgres:15
ports:
- "5433:5432"
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: bullpen-test
volumes:
- /tmp/test-pgdata:/var/lib/postgresql/data

View File

@ -0,0 +1,44 @@
services:
db:
image: postgres:15
container_name: postgres
restart: always
secrets:
- db-password
environment:
POSTGRES_DB: bullpen-prod
POSTGRES_USER: test
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
expose:
- 5432
volumes:
- db_data:/var/lib/postgresql/data
healthcheck:
test: [ "CMD", "pg_isready", "-U", "test", "-d", "bullpen-prod"]
interval: 10s
timeout: 5s
retries: 5
app:
build:
context: .
target: development
depends_on:
db:
condition: service_healthy
ports:
- "8124:8080"
container_name: bullpen
environment:
NODE_ENV: production
POSTGRES_HOST: db
POSTGRES_USER: test
POSTGRES_PASSWORD_FILE: /run/secrets/db-password
POSTGRES_DB: bullpen-prod
POSTGRES_PORT: 5432
secrets:
- db-password
volumes:
db_data:
secrets:
db-password:
file: db/password.txt

5
backend/jest.config.js Normal file
View File

@ -0,0 +1,5 @@
module.exports = {
// preset: 'ts-jest',
testEnvironment: 'node',
setupFilesAfterEnv: ['./jest.setup.js']
};

55
backend/jest.setup.js Normal file
View File

@ -0,0 +1,55 @@
const db = require("./models/index");
const bcrypt = require("bcryptjs");
const { beforeAll, afterAll } = require('@jest/globals');
const { Auth: Auth, User: User, Role: Role, PitchType: PitchType } = db;
const Op = db.Sequelize.Op;
beforeAll(async () => {
await db.sequelize.sync({ force: true }); // Reset DB once for the entire test suite
await Role.destroy({ where: {} });
await Role.bulkCreate([
{ name: 'player' },
{ name: 'coach' },
{ name: 'admin' }
]);
const playerRole = await Role.findAll({
where: {
name: {
[Op.eq]: 'player'
}
}
});
await Auth.destroy({ where: {} });
await User.destroy({ where: {} });
await Auth.create({
email: 'player@example.com', password: 'hash1234'
}).then(auth => {
return User.create({
firstName: 'Alice',
lastName: 'Player',
dateOfBirth: '1990-01-01',
gender: 'female',
handedness: 'right',
authId: auth.id
});
}).then(user => {
return user.setRoles(playerRole);
});
await PitchType.destroy({ where: {} });
await 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' }
]);
});
afterAll(async () => {
await db.sequelize.close(); // Close connection after all tests
});

View File

@ -1,18 +1,23 @@
const fs = require('fs');
const path = require('path');
const Sequelize = require('sequelize');
const process = require('process');
const basename = path.basename(__filename);
const dbConfigs = require('../config/config');
const env = process.env.NODE_ENV || 'development';
const config = require(__dirname + '/../config/config.js')[env];
const config = dbConfigs[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);
}
const sequelize = new Sequelize(
config.database,
config.username,
config.password, {
host: config.host,
port: config.port,
dialect: config.dialect,
logging: config.logging
}
);
fs
.readdirSync(__dirname)

View File

@ -25,8 +25,10 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"cross-env": "^7.0.3",
"dotenv": "^16.4.7",
"dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0",
"jest": "^29.7.0",
"nodemon": "^3.1.9",
"sequelize-cli": "^6.6.2",
"supertest": "^7.0.0"
},
@ -1627,6 +1629,19 @@
"bcrypt": "bin/bcrypt"
}
},
"node_modules/binary-extensions": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
"integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
@ -2020,6 +2035,31 @@
"node": ">=10"
}
},
"node_modules/chokidar": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
"integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"anymatch": "~3.1.2",
"braces": "~3.0.2",
"glob-parent": "~5.1.2",
"is-binary-path": "~2.1.0",
"is-glob": "~4.0.1",
"normalize-path": "~3.0.0",
"readdirp": "~3.6.0"
},
"engines": {
"node": ">= 8.10.0"
},
"funding": {
"url": "https://paulmillr.com/funding/"
},
"optionalDependencies": {
"fsevents": "~2.3.2"
}
},
"node_modules/chownr": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz",
@ -2552,9 +2592,9 @@
}
},
"node_modules/dotenv": {
"version": "16.4.7",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz",
"integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==",
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz",
"integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
@ -2564,6 +2604,32 @@
"url": "https://dotenvx.com"
}
},
"node_modules/dotenv-cli": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/dotenv-cli/-/dotenv-cli-8.0.0.tgz",
"integrity": "sha512-aLqYbK7xKOiTMIRf1lDPbI+Y+Ip/wo5k3eyp6ePysVaSqbyxjyK3dK35BTxG+rmd7djf5q2UPs4noPNH+cj0Qw==",
"dev": true,
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.6",
"dotenv": "^16.3.0",
"dotenv-expand": "^10.0.0",
"minimist": "^1.2.6"
},
"bin": {
"dotenv": "cli.js"
}
},
"node_modules/dotenv-expand": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz",
"integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/dottie": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz",
@ -3461,6 +3527,19 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/glob-parent": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.1"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/glob/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
@ -3684,6 +3763,13 @@
],
"license": "BSD-3-Clause"
},
"node_modules/ignore-by-default": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz",
"integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==",
"dev": true,
"license": "ISC"
},
"node_modules/import-local": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz",
@ -3794,6 +3880,19 @@
"dev": true,
"license": "MIT"
},
"node_modules/is-binary-path": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
"dev": true,
"license": "MIT",
"dependencies": {
"binary-extensions": "^2.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/is-core-module": {
"version": "2.16.1",
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz",
@ -3810,6 +3909,16 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-fullwidth-code-point": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
@ -3830,6 +3939,19 @@
"node": ">=6"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-lambda": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz",
@ -5633,6 +5755,82 @@
"dev": true,
"license": "MIT"
},
"node_modules/nodemon": {
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz",
"integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==",
"dev": true,
"license": "MIT",
"dependencies": {
"chokidar": "^3.5.2",
"debug": "^4",
"ignore-by-default": "^1.0.1",
"minimatch": "^3.1.2",
"pstree.remy": "^1.1.8",
"semver": "^7.5.3",
"simple-update-notifier": "^2.0.0",
"supports-color": "^5.5.0",
"touch": "^3.1.0",
"undefsafe": "^2.0.5"
},
"bin": {
"nodemon": "bin/nodemon.js"
},
"engines": {
"node": ">=10"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/nodemon"
}
},
"node_modules/nodemon/node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/nodemon/node_modules/has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/nodemon/node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/nodemon/node_modules/supports-color": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^3.0.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/nopt": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz",
@ -6214,6 +6412,13 @@
"node": ">= 0.10"
}
},
"node_modules/pstree.remy": {
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz",
"integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==",
"dev": true,
"license": "MIT"
},
"node_modules/pump": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
@ -6316,6 +6521,19 @@
"node": ">= 6"
}
},
"node_modules/readdirp": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"picomatch": "^2.2.1"
},
"engines": {
"node": ">=8.10.0"
}
},
"node_modules/require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@ -6829,6 +7047,19 @@
"simple-concat": "^1.0.0"
}
},
"node_modules/simple-update-notifier": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz",
"integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"semver": "^7.5.3"
},
"engines": {
"node": ">=10"
}
},
"node_modules/sisteransi": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz",
@ -7419,6 +7650,16 @@
"integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
"license": "MIT"
},
"node_modules/touch": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz",
"integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==",
"dev": true,
"license": "ISC",
"bin": {
"nodetouch": "bin/nodetouch.js"
}
},
"node_modules/tunnel-agent": {
"version": "0.6.0",
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
@ -7487,6 +7728,13 @@
"node": ">=6.0.0"
}
},
"node_modules/undefsafe": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz",
"integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==",
"dev": true,
"license": "MIT"
},
"node_modules/underscore": {
"version": "1.13.7",
"resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.7.tgz",

View File

@ -7,8 +7,16 @@
"pretest": "cross-env NODE_ENV=test npm run db:reset",
"db:create:test": "cross-env NODE_ENV=test npx sequelize-cli db:create",
"db:reset": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all --debug",
"db:reset:dev": "cross-env NODE_ENV=development npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npm run seed:dev",
"db:reset:prod": "cross-env NODE_ENV=production npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npm run seed:prod",
"setup-db": "npx sequelize-cli db:drop && npx sequelize-cli db:create && npx sequelize-cli db:migrate && npx sequelize-cli db:seed:all --debug",
"start": "cross-env NODE_ENV=test node server.js"
"seed:dev": "NODE_ENV=development npx sequelize-cli db:seed:all --debug",
"start:prod": "npm run cross-env NODE_ENV=production node server.js",
"start:dev": "cross-env NODE_ENV=development nodemon server.js",
"test:db:start": "docker-compose -f docker-compose.test.yml up -d",
"test:db:stop": "docker-compose -f docker-compose.test.yml down -v",
"test:run": "dotenv -e .env.test -- jest --runInBand --detectOpenHandles",
"test:full": "npm run test:db:start && npm run test:run && npm run test:db:stop"
},
"engines": {
"node": ">=10"
@ -33,8 +41,10 @@
"devDependencies": {
"@jest/globals": "^29.7.0",
"cross-env": "^7.0.3",
"dotenv": "^16.4.7",
"dotenv": "^16.5.0",
"dotenv-cli": "^8.0.0",
"jest": "^29.7.0",
"nodemon": "^3.1.9",
"sequelize-cli": "^6.6.2",
"supertest": "^7.0.0"
}

22
backend/publish.sh Normal file
View File

@ -0,0 +1,22 @@
#!/bin/bash
# Replace with your actual Gitea Docker registry
REGISTRY_URL=git.palaeomatiker.home64.de
IMAGE_NAME=express-sequelize-app
TAG=latest
# Authenticate with Docker registry
echo "Logging into Docker registry..."
docker login $REGISTRY_URL
# Build the image
echo "Building Docker image..."
docker build -t $IMAGE_NAME .
# Tag the image
echo "Tagging image..."
docker tag $IMAGE_NAME $REGISTRY_URL/$IMAGE_NAME:$TAG
# Push the image
echo "Pushing to Gitea registry..."
docker push $REGISTRY_URL/$IMAGE_NAME:$TAG

View File

@ -0,0 +1,13 @@
const controller = require("../controllers/info.controller");
module.exports = function(app) {
app.use(function(req, res, next) {
res.header(
"Access-Control-Allow-Headers",
"x-access-token, Origin, Content-Type, Accept"
);
next();
});
app.get("/", controller.info);
};

View File

@ -0,0 +1,99 @@
const bcrypt = require("bcryptjs");
const process = require('process');
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'development') return;
await queryInterface.bulkInsert('Authentications', [
{ email: 'ryan.nolan@bullpen.com', password: bcrypt.hashSync('ryan$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'sandy.koufax@bullpen.com', password: bcrypt.hashSync('sandy$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'pedro.martinez@bullpen.com', password: bcrypt.hashSync('pedro$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'randy.johnson@bullpen.com', password: bcrypt.hashSync('randy$123', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'sparky.anderson@bullpen.com', password: bcrypt.hashSync('sparky$123', 8), createdAt: new Date(), updatedAt: new Date() },
]);
const auths = await queryInterface.select(null, 'Authentications');
const ryanAuthId = auths.filter((auth) => auth.email === 'ryan.nolan@bullpen.com').map((auth) => auth.id).shift();
const sandyAuthId = auths.filter((auth) => auth.email === 'sandy.koufax@bullpen.com').map((auth) => auth.id).shift();
const pedroAuthId = auths.filter((auth) => auth.email === 'pedro.martinez@bullpen.com').map((auth) => auth.id).shift();
const randyAuthId = auths.filter((auth) => auth.email === 'randy.johnson@bullpen.com').map((auth) => auth.id).shift();
const sparkyAuthId = auths.filter((auth) => auth.email === 'sparky.anderson@bullpen.com').map((auth) => auth.id).shift();
await queryInterface.bulkInsert('Users', [{
firstName: 'Nolan',
lastName: 'Ryan',
dateOfBirth: new Date(1947, 1, 31),
gender: 'male',
handedness: 'right',
authId: ryanAuthId,
createdAt: new Date(),
updatedAt: new Date()
}, {
firstName: 'Sandy',
lastName: 'Koufax',
dateOfBirth: new Date(1935, 12, 30),
gender: 'male',
handedness: 'right',
authId: sandyAuthId,
createdAt: new Date(),
updatedAt: new Date()
}, {
firstName: 'Pedro',
lastName: 'Martinez',
dateOfBirth: new Date(1971, 10, 25),
gender: 'male',
handedness: 'right',
authId: pedroAuthId,
createdAt: new Date(),
updatedAt: new Date()
}, {
firstName: 'randy',
lastName: 'johnson',
dateOfBirth: new Date(1963, 9, 10),
gender: 'male',
handedness: 'right',
authId: randyAuthId,
createdAt: new Date(),
updatedAt: new Date()
}, {
firstName: 'Sparky',
lastName: 'Anderson',
dateOfBirth: new Date(1934, 22, 2),
gender: 'male',
authId: sparkyAuthId,
createdAt: new Date(),
updatedAt: new Date(),
}]);
const users = await queryInterface.select(null, 'Users');
const ryanId = users.filter((user) => user.firstName === 'Ryan').map((user) => user.id).shift();
const sandyId = users.filter((user) => user.firstName === 'Sandy').map((user) => user.id).shift();
const pedroId = users.filter((user) => user.firstName === 'Pedro').map((user) => user.id).shift();
const randyId = users.filter((user) => user.firstName === 'Randy').map((user) => user.id).shift();
const sparkyId = users.filter((user) => user.firstName === 'Sparky').map((user) => user.id).shift();
const roles = await queryInterface.select(null, 'Roles');
const playerId = roles.filter((role) => role.name === 'player').map((role) => role.id).shift();
const coachId = roles.filter((role) => role.name === 'coach').map((role) => role.id).shift();
const adminId = roles.filter((role) => role.name === 'admin').map((role) => role.id).shift();
await queryInterface.bulkInsert('UserRoles', [
{ userId: ryanId, roleId: playerId, createdAt: new Date(), updatedAt: new Date() },
{ userId: sandyId, roleId: playerId, createdAt: new Date(), updatedAt: new Date() },
{ userId: pedroId, roleId: playerId, createdAt: new Date(), updatedAt: new Date() },
{ userId: randyId, roleId: playerId, createdAt: new Date(), updatedAt: new Date() },
{ userId: sparkyId, roleId: coachId, createdAt: new Date(), updatedAt: new Date() },
{ userId: sparkyId, roleId: adminId, createdAt: new Date(), updatedAt: new Date() },
]);
},
async down (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'development') return;
await queryInterface.dropTable('Authentications');
await queryInterface.dropTable('Users');
await queryInterface.dropTable('UserRoles');
}
};

View File

@ -0,0 +1,46 @@
const bcrypt = require("bcryptjs");
const process = require('process');
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'production') return;
await queryInterface.bulkInsert('Authentications', [
{ email: 'admin@example.com', password: bcrypt.hashSync('admin$123', 8), createdAt: new Date(), updatedAt: new Date() }
]);
const auths = await queryInterface.select(null, 'Authentications');
const adminAuthId = auths.filter((auth) => auth.email === 'admin@example.com').map((auth) => auth.id).shift();
await queryInterface.bulkInsert('Users', [{
firstName: 'Admin',
lastName: 'Bullpen',
dateOfBirth: '1970-01-01',
gender: 'other',
authId: adminAuthId,
createdAt: new Date(),
updatedAt: new Date(),
}]
);
const users = await queryInterface.select(null, 'Users');
const adminUserId = users.filter((user) => user.firstName === 'Admin').map((user) => user.id).shift();
const roles = await queryInterface.select(null, 'Roles');
const adminRoleId = roles.filter((role) => role.name === 'admin').map((role) => role.id).shift();
await queryInterface.bulkInsert('UserRoles', [
{ userId: adminUserId, roleId: adminRoleId, createdAt: new Date(), updatedAt: new Date() }
]);
},
async down (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'production') return;
await queryInterface.dropTable('Authentications');
await queryInterface.dropTable('Users');
await queryInterface.dropTable('UserRoles');
}
};

View File

@ -1,8 +1,11 @@
const bcrypt = require("bcryptjs");
const process = require('process');
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'test') return;
await queryInterface.bulkInsert('Authentications', [
{ email: 'player@example.com', password: bcrypt.hashSync('hash1234', 8), createdAt: new Date(), updatedAt: new Date() },
{ email: 'coach@example.com', password: bcrypt.hashSync('hash2345', 8), createdAt: new Date(), updatedAt: new Date() },
@ -59,10 +62,11 @@ module.exports = {
{ userId: bobId, roleId: coachId, createdAt: new Date(), updatedAt: new Date() },
{ userId: charlieId, roleId: adminId, createdAt: new Date(), updatedAt: new Date() },
]);
},
async down (queryInterface, /*Sequelize*/) {
if (process.env.NODE_ENV !== 'test') return;
await queryInterface.dropTable('Authentications');
await queryInterface.dropTable('Users');
await queryInterface.dropTable('UserRoles');

View File

@ -1,7 +1,14 @@
const app = require('./app');
require('dotenv').config({
path: process.env.NODE_ENV === 'production' ? '.env.production' : '.env'
});
const dbConfigs = require('./config/config');
const env = process.env.NODE_ENV || 'development';
const config = dbConfigs[env];
// set port, listen for requests
const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
console.log(`Server is running on port ${PORT}.`);
console.log(`Server is running on port ${PORT} in ${process.env.NODE_ENV} mode.`);
});

View File

@ -9,6 +9,14 @@ const app = require("../app")
const { bullpenSession } = require("./data/bullpenSession.test.data")
const validateBullpenSession = (bullpenSession) => {
expect(bullpenSession.id).toBeDefined();
expect(bullpenSession.id).toBeGreaterThan(0);
expect(bullpenSession.pitches).toBeDefined();
expect(Array.isArray(bullpenSession.pitches)).toBe(true);
expect(bullpenSession.pitches.length).toBe(2);
}
describe("Test bullpen session", () => {
test("should create bullpen session with pitches", async () => {
let response = await request(app)
@ -27,14 +35,10 @@ describe("Test bullpen session", () => {
expect(response.statusCode).toBe(201);
const bullpenSessionData = await response.body;
expect(bullpenSessionData).toBeDefined();
expect(bullpenSessionData.id).toBeDefined();
expect(bullpenSessionData.id).toBeGreaterThan(0);
expect(bullpenSessionData.pitches).toBeDefined();
expect(Array.isArray(bullpenSessionData.pitches)).toBe(true);
expect(bullpenSessionData.pitches.length).toBe(2);
validateBullpenSession(bullpenSessionData);
});
test.skip("should get all bullpen sessions", async () => {
test("should get all bullpen sessions", async () => {
let response = await request(app)
.post("/api/auth/login")
.send({
@ -48,16 +52,15 @@ describe("Test bullpen session", () => {
.set('x-access-token', user.accessToken);
expect(response.statusCode).toBe(200);
const bullpenSessionDataArray = await response.body;
expect(bullpenSessionDataArray).toBeDefined();
expect(Array.isArray(bullpenSessionDataArray)).toBe(true);
expect(bullpenSessionDataArray.length).toBe(1);
const bullpenSessionData = bullpenSessionDataArray[0];
expect(bullpenSessionData.id).toBeDefined();
expect(bullpenSessionData.id).toBeGreaterThan(0);
expect(bullpenSessionData.pitches).toBeDefined();
expect(Array.isArray(bullpenSessionData.pitches)).toBe(true);
expect(bullpenSessionData.pitches.length).toBe(2);
const { totalCount, data, totalPages, currentPage } = await response.body;
expect(data).toBeDefined();
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(1);
expect(totalCount).toBe(1);
expect(totalPages).toBe(1);
expect(currentPage).toBe(0);
const bullpenSessionData = data[0];
validateBullpenSession(bullpenSessionData);
})
test("should get all bullpen sessions for user", async () => {
@ -75,15 +78,14 @@ describe("Test bullpen session", () => {
.query({ user: user.id });
expect(response.statusCode).toBe(200);
const bullpenSessionDataArray = await response.body;
expect(bullpenSessionDataArray).toBeDefined();
expect(Array.isArray(bullpenSessionDataArray)).toBe(true);
expect(bullpenSessionDataArray.length).toBe(1);
const bullpenSessionData = bullpenSessionDataArray[0];
expect(bullpenSessionData.id).toBeDefined();
expect(bullpenSessionData.id).toBeGreaterThan(0);
expect(bullpenSessionData.pitches).toBeDefined();
expect(Array.isArray(bullpenSessionData.pitches)).toBe(true);
expect(bullpenSessionData.pitches.length).toBe(2);
const { totalCount, data, totalPages, currentPage } = await response.body;
expect(data).toBeDefined();
expect(Array.isArray(data)).toBe(true);
expect(data.length).toBe(1);
expect(totalCount).toBe(1);
expect(totalPages).toBe(1);
expect(currentPage).toBe(0);
const bullpenSessionData = data[0];
validateBullpenSession(bullpenSessionData);
})
})

View File

@ -2,7 +2,7 @@ const request = require("supertest")
const {
expect,
describe,
test,
test, beforeAll, afterAll,
} = require('@jest/globals');
const app = require("../app")

View File

@ -18,7 +18,7 @@ describe("Test user authentication", () => {
expect(response.statusCode).toBe(200);
});
test("Test user login", async() => {
test("should login user", async() => {
let response = await request(app)
.post("/api/auth/login")
.send({