Skip to content
Snippets Groups Projects
Commit f7b067a1 authored by leandet98's avatar leandet98
Browse files

Merge branch 'main' into '29-add-missing-attributes-to-database'

# Conflicts:
#   server/src/server.ts
parents d602601b 5a666431
No related branches found
No related tags found
No related merge requests found
Showing
with 374 additions and 194 deletions
......@@ -4,17 +4,17 @@ services:
- postgres:latest
variables:
DB_NAME: $POSTGRES_DB
DB_USER: $POSTGRES_USER
DB_HOST: postgres
DB_DRIVER: postgres
DB_PASSWORD: $POSTGRES_PASSWORD
TEST_DB_NAME: test
POSTGRES_DB: $POSTGRES_DB
POSTGRES_USER: $POSTGRES_USER
POSTGRES_PASSWORD: $POSTGRES_PASSWORD
POSTGRES_DB: fahrtenbuch
POSTGRES_HOST: postgres
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_HOST_AUTH_METHOD: trust
POSTGRES_DRIVER: postgres
SKIP_PREFLIGHT_CHECK: "true"
INITIAL_COORDINATOR_EMAIL: "initial@fahrtenbuch.example"
INITIAL_COORDINATOR_PASSWORD: "password"
JWT_SECRET: "RANDOM_SECRET_HARD_TO_GUESS"
# These folders and files are cached between builds
......@@ -22,7 +22,6 @@ cache:
paths:
- client/node_modules/
- server/node_modules/
- server/.env
- server/dist/
# Stages for better overview
......
# Fahrtenbuch
<br />
<div align="center">
<h3 align="center">Fahrtenbuch - Team Einhorn 🦄</h3>
<p align="center">
<a href="/doc/localization">Explore the docs</a>
·
<a href="/doc/localization">View Live</a>
</p>
</div>
The application is now dockerized.
That means: Every part of the app (client, server, postgres, pgAdmin) is now an own Docker container. Client and server both have a Dockerfile which defines the routine to be run when building the container from the source.
All parts work together by composing them with docker-compose. The file `docker-compose.yaml` defines, which services there are and how Docker should initialize them. By composing the multiple services, they all run inside their own container but can communicate inside their own network with each other.
By using **volumes**, we can directly map the source code on our machine to the source code inside the container, which enables us to sync our changes with the container -> Hot reload :)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#how-to-develop">How to develop</a>
</li>
<!--<li><a href="#roadmap">Roadmap</a></li>-->
<!--<li><a href="#team-members">Team Members</a></li>-->
</ol>
</details>
## About The Project
Here maybe a screenshot
Here some nice text later
<p align="right">(<a href="#top">back to top</a>)</p>
### Built With
* [Node.js](https://nodejs.org/en/)
* [Express.js](https://expressjs.com/de/)
* [Sequelize](https://sequelize.org)
* [React.js](https://reactjs.org)
<p align="right">(<a href="#top">back to top</a>)</p>
## How to develop
- Install Docker: https://www.docker.com/get-started
- Install [Docker](https://www.docker.com/get-started):
- In the root of the project, run `docker compose build` to build all containers.
- Then run `docker compose up` to start the build.
- Done
<p align="right">(<a href="#top">back to top</a>)</p>
# Getting Started with Create React App
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you can’t go back!**
If you aren’t satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
......@@ -13,6 +13,8 @@
"labelDestination": "Fahrtziel",
"labelEmail": "Email",
"labelName": "Name (Verantwortliche*r)",
"labelAdditionalName": "Name (Mitfahrende*r)",
"labelAdditionalNames": "Namen der Mitfahrenden",
"labelAnnotations": "Anmerkungen",
"buttonBookBoat": "Boot jetzt buchen!",
"messages": {
......
......@@ -12,7 +12,9 @@
"labelEstimatedEndTime":"Estimated end time",
"labelDestination": "Destination",
"labelEmail": "Email",
"labelName": "Name (responsible person)",
"labelName": "Name (resposible person)",
"labelAdditionalName": "Additional Name",
"labelAdditionalNames": "Additional Names",
"labelAnnotations": "Annotations",
"buttonBookBoat": "Book this boat!",
"messages": {
......
import { Button, Col, Container, Form, Row } from "react-bootstrap";
import { Controller, useForm } from "react-hook-form";
import { Controller, useForm, useFieldArray } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router";
import validator from "validator";
import Divider from "../../components/layout/Divider";
import Modal from "../../components/Modal";
import { faTrashAlt } from "@fortawesome/free-regular-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
type FormData = {
sport: string;
......@@ -14,27 +16,41 @@ type FormData = {
destination: string;
name: string;
email: string;
annotations: string;
persons: { name: string }[];
};
function Book() {
let date = new Date();
const zeroPad = (num: number, places: number) => String(num).padStart(places, '0')
const zeroPad = (num: number, places: number) =>
String(num).padStart(places, "0");
const navigate = useNavigate();
const seatCount = 4;
const {
control,
handleSubmit,
formState: { errors },
watch,
} = useForm<FormData>({
mode: "onBlur", defaultValues: {
mode: "onBlur",
defaultValues: {
sport: "Sport2",
boatName: "Boot1",
startTime: zeroPad(date.getHours(), 2) + ":" + zeroPad(date.getMinutes(), 2),
estimatedEndTime: zeroPad(date.getHours() + 2, 2) + ":" + zeroPad(date.getMinutes(), 2),
startTime:
zeroPad(date.getHours(), 2) + ":" + zeroPad(date.getMinutes(), 2),
estimatedEndTime:
zeroPad(date.getHours() + 2, 2) + ":" + zeroPad(date.getMinutes(), 2),
destination: "",
name: "",
// annotations: ""
}
persons: [],
},
});
const {
fields: persons,
append: appendPerson,
remove: removePerson,
} = useFieldArray({
control,
name: "persons",
});
const { t } = useTranslation(); // i18n
const onSubmit = (data: FormData) => {
......@@ -158,7 +174,11 @@ function Book() {
render={({ field }) => (
<div className="mb-2 required">
<Form.Label>{t("bookingForm.labelDestination")}</Form.Label>
<Form.Control type="text" {...field} isInvalid={!!errors.destination}/>
<Form.Control
type="text"
{...field}
isInvalid={!!errors.destination}
/>
<Form.Control.Feedback type="invalid">
{errors.destination?.message}
</Form.Control.Feedback>
......@@ -180,7 +200,11 @@ function Book() {
render={({ field }) => (
<div className="mb-2 required">
<Form.Label>{t("bookingForm.labelEmail")}</Form.Label>
<Form.Control type="email" {...field} isInvalid={!!errors.email}/>
<Form.Control
type="email"
{...field}
isInvalid={!!errors.email}
/>
<Form.Control.Feedback type="invalid">
{errors.email?.message}
</Form.Control.Feedback>
......@@ -205,7 +229,11 @@ function Book() {
render={({ field }) => (
<div className="mb-2 required">
<Form.Label>{t("bookingForm.labelName")}</Form.Label>
<Form.Control type="text" {...field} isInvalid={!!errors.name}/>
<Form.Control
type="text"
{...field}
isInvalid={!!errors.name}
/>
<Form.Control.Feedback type="invalid">
{errors.name?.message}
</Form.Control.Feedback>
......@@ -220,25 +248,47 @@ function Book() {
},
}}
/>
{/* <Controller
name="annotations"
control={control}
defaultValue=""
render={({ field }) => (
<div className="mb-2">
<Form.Label>{t("bookingForm.labelAnnotations")}</Form.Label>
<Form.Control as="textarea" type="text" {...field} />
</div>
)}
/> */}
<div className="d-flex justify-content-between">
<h4>{t("bookingForm.labelAdditionalNames")}</h4>
<Button
variant="secondary"
disabled={watch("persons").length === seatCount - 1}
type="button"
onClick={() => appendPerson({ name: "" })}
>
+
</Button>
</div>
<ul>
{persons.map((item: any, index: number) => (
<li key={item.id}>
<Form.Label>
{t("bookingForm.labelAdditionalName") + (index + 1)}
</Form.Label>
<div className="d-flex">
<Form.Control
type="text"
name={`persons.${index}.name` as const}
/>
<Button
className="mx-3"
variant="danger"
type="button"
onClick={() => removePerson(index)}
>
<FontAwesomeIcon
icon={faTrashAlt}
className="text-white"
/>
</Button>
</div>
</li>
))}
</ul>
<Button type="submit" variant="secondary" className="mt-2 w-100">
{t("bookingForm.buttonBookBoat")}
</Button>
{/* <div className="mt-2">
{Object.values(errors).map((error) => (
<p key={error.message} className="m-0 text-center text-danger">{error.message}</p>
))}
</div> */}
</Form>
</Modal>
</Col>
......
import fs from 'fs';
const languages = ["de", "en"];
const defaultLocale = "de";
function testKeys(defaultTranslation: any, translation: any,messages: string[]){
if (defaultTranslation === undefined || translation == undefined){
return;
}
// check if keys in default locale file are all translated in other locale
Object.keys(defaultTranslation).forEach((key) => {
if (Object.keys(translation).indexOf(key) === -1) {
messages.push(`key missing: '${key}'`);
}
});
// check if there is unused key in other locale
Object.keys(translation).forEach((key) => {
if (Object.keys(defaultTranslation).indexOf(key) === -1) {
messages.push(`unexpected key: '${key}'`);
}
});
// recursive
Object.entries(translation).forEach(([key,value]) => {
if (typeof value == "object") {
testKeys(defaultTranslation[key], translation[key], messages)
}
});
}
describe('Translations should', () => {
let translations: {lang:string, translation: any}[] = [];
beforeAll(async ()=>{
for (let l of languages){
let f;
try {
f = fs.readFileSync('./public/locales/'+l+'/translation.json','utf-8');
} catch (e){
console.log(e)
throw new Error(`Could not find file for locale '${l}'.`)
}
try {
let t = JSON.parse(<string>f)
translations.push({lang:l, translation: t})
} catch (e){
throw new Error(`locale '${l}' has no valid json content.`)
}
}
})
test('All languages Should exist', async () => {
expect(translations.length).toBe(languages.length)
});
test.each(languages)("Test all Locales have the same keys",async (a:string) => {
if (a === defaultLocale){
return;
}
let messages: string[] = [];
let defaultTranslation = translations.find(x=>x.lang === defaultLocale)?.translation;
let translation = translations.find(x=>x.lang === a)?.translation;
testKeys(defaultTranslation, translation, messages)
if (messages.length >0){
throw new Error(`Problems in Locale ${a}:\n ${messages.join('\n - ')}`)
}
});
});
NODE_ENV=development
DB_NAME=fahrtenbuch
DB_USER=postgres
DB_HOST=localhost
DB_DRIVER=postgres
DB_PASSWORD=postgres
POSTGRES_DB=fahrtenbuch
POSTGRES_USER=postgres
POSTGRES_HOST=localhost
POSTGRES_DRIVER=postgres
POSTGRES_PASSWORD=postgres
TEST_DB_NAME=test
EMAIL_SERVER=smtp.example.com
EMAIL_IS_TLS=false
......
/** @type {import('ts-jest/dist/types').InitialOptionsTsJest} */
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
};
\ No newline at end of file
preset: "ts-jest",
testEnvironment: "node",
modulePathIgnorePatterns: ["<rootDir>/dist/"],
};
......@@ -7,7 +7,7 @@
"dev": "nodemon",
"build": "tsc",
"server": "nodemon server --ignore client",
"start": "node ./dist/server.js",
"start": "node ./dist/src/server.js",
"test": "jest"
},
"author": "",
......
import { Dialect } from "sequelize";
const envVars = {
DB_NAME: process.env.DB_NAME,
DB_USER: process.env.DB_USER,
DB_HOST: process.env.DB_HOST,
DB_DRIVER: process.env.DB_DRIVER as Dialect,
DB_PASSWORD: process.env.DB_PASSWORD,
DB_NAME: process.env.POSTGRES_DB,
DB_USER: process.env.POSTGRES_USER,
DB_HOST: process.env.POSTGRES_HOST,
DB_DRIVER: process.env.POSTGRES_DRIVER as Dialect,
DB_PASSWORD: process.env.POSTGRES_PASSWORD,
JWT_SECRET: process.env.JWT_SECRET,
INITIAL_COORDINATOR_PASSWORD: process.env.INITIAL_COORDINATOR_PASSWORD,
INITIAL_COORDINATOR_EMAIL: process.env.INITIAL_COORDINATOR_EMAIL,
......
......@@ -44,5 +44,4 @@ const authControllers = {
authLoginController,
};
export default authControllers;
import { QueryTypes, Sequelize } from "sequelize";
const showAllDBs = async (sequelizeConnection: Sequelize) => {
const DBs = await sequelizeConnection.query(
"SELECT datname FROM pg_database",
{ type: QueryTypes.SELECT }
);
console.log(
"----------Found Databases----------\n",
DBs,
"\n-----------------------------------\n"
);
};
export default showAllDBs;
import { QueryTypes, Sequelize } from "sequelize";
import envVars from "../../config";
const showTables = async (sequelizeConnection: Sequelize) => {
//list all tables of the actual database
const DB_Tables = await sequelizeConnection.query(
`SELECT TABLE_NAME FROM information_schema.tables WHERE TABLE_TYPE = 'BASE TABLE' AND TABLE_CATALOG='${envVars.DB_NAME}' AND TABLE_SCHEMA='public';`,
{ type: QueryTypes.SELECT }
);
//------------------------------------------------then
console.log(
`-----TABELS IN ${envVars.DB_NAME}------\n`,
DB_Tables,
"\n-----------------------------------\n"
);
};
export default showTables;
//to show content of specific table
import { QueryTypes, Sequelize } from "sequelize";
const showTableContent = async (table: string, sequelize: Sequelize) => {
const tableContent = await sequelize.query(`SELECT * FROM ${table}`, {
type: QueryTypes.SELECT,
});
console.log(
`-------------Content of ${table}-table---------------\n`,
tableContent,
"\n---------------------------------------------------\n"
);
};
export default showTableContent;
......@@ -3,9 +3,6 @@ import "dotenv/config";
import express from "express";
import initializeDatabase from "./db";
import createInitialEmployeeIfNotExists from "./db/createInitialEmployee";
import showAllDBs from "./db/DB_Information/showAllDBs";
import showTables from "./db/DB_Information/showDBTables";
import showTableContent from "./db/DB_Information/showTableContent";
import accountsRouter from "./routes/accounts.routes";
import authRouter from "./routes/auth.routes";
import boatsRouter from "./routes/boat.routes";
......@@ -23,8 +20,7 @@ let init = async () => {
app.use(express.json());
app.use(cookieParser());
const port = process.env.PORT || 4000;
app.listen(port, () => console.log(`Server listening on port: ${port}\n`));
app.get("/", (req, res) => res.send("hello in server"));
app.listen(port, () => {});
app.use("/", authRouter);
app.use("/", accountsRouter);
app.use("/", boatsRouter);
......@@ -32,11 +28,6 @@ let init = async () => {
app.use("/", boatTypeRouter);
app.use("/", entryRouter);
app.use("/", sportRouter);
//DB-information section
await showAllDBs(sequelize);
await showTables(sequelize);
await showTableContent("employee", sequelize);
};
init().then(() => {});
require('dotenv').config();
import request from 'supertest';
require("dotenv").config();
import request from "supertest";
describe('GET Endpoints', () => {
/*
* NOTICE:
* Those are structural test examples.
* I've made it, that they all expect 404 errors,
* since the database models have changes while the API hasn't
*/
it('if response is 404, server is alive (since GET / isn\'t defined)', (done) => {
request("http://localhost:4000")
.get('/')
.set('Accept', 'text/html')
.expect('Content-Type', /html/)
.expect(404, done);
});
it('Get Vehicle Type with ID (returns 404 since no data)', (done) => {
request("http://localhost:4000")
.get('/vehicleType/1')
.set('Accept', 'text/html')
.expect('Content-Type', /html/)
.expect(404, done);
});
it('Get all Vehicle Types', (done) => {
request("http://localhost:4000")
.get('/vehicleType')
.set('Accept', 'text/html')
.expect('Content-Type', /html/)
.expect(404, done);
});
});
\ No newline at end of file
describe("GET Endpoints", () => {
/*
* NOTICE:
* Those are structural test examples.
* I've made it, that they all expect 404 errors,
* since the database models have changes while the API hasn't
*/
it("if response is 200, server is alive ", (done) => {
request("http://localhost:4000")
.get("/")
.set("Accept", "text/html")
.expect("Content-Type", /html/)
.expect(200, done);
});
it("Get Vehicle Type with ID (returns 404 since no data)", (done) => {
request("http://localhost:4000")
.get("/vehicleType/1")
.set("Accept", "text/html")
.expect("Content-Type", /html/)
.expect(404, done);
});
it("Get all Vehicle Types", (done) => {
request("http://localhost:4000")
.get("/vehicleType")
.set("Accept", "text/html")
.expect("Content-Type", /html/)
.expect(404, done);
});
});
import { Client } from "pg";
it("has the database", async () => {
const client = new Client({
user: process.env.POSTGRES_USER,
host: process.env.POSTGRES_HOST,
password: process.env.POSTGRES_PASSWORD,
});
await client.connect();
const query = await client.query(
`SELECT * FROM pg_database WHERE datname='${process.env.POSTGRES_DB}'`
);
expect(query.rows.length).toBeGreaterThan(0);
await client.end();
});
describe("Database Tests", () => {
let client: Client;
const databaseTables = [
{
table_name: "checkin",
columns: [
{ column_name: "id", data_type: "uuid" },
{ column_name: "startTime", data_type: "timestamp with time zone" },
{
column_name: "estimatedEndTime",
data_type: "timestamp with time zone",
},
{ column_name: "boatId", data_type: "uuid" },
{ column_name: "createdAt", data_type: "timestamp with time zone" },
{ column_name: "updatedAt", data_type: "timestamp with time zone" },
{ column_name: "email", data_type: "character varying" },
{
column_name: "fullNameOfResponsableClient",
data_type: "character varying",
},
{ column_name: "additionalClients", data_type: "ARRAY" },
],
},
{
table_name: "boat",
columns: [
{ column_name: "status", data_type: "boolean" },
{ column_name: "createdAt", data_type: "timestamp with time zone" },
{ column_name: "updatedAt", data_type: "timestamp with time zone" },
{ column_name: "boattype", data_type: "uuid" },
{ column_name: "id", data_type: "uuid" },
{ column_name: "name", data_type: "character varying" },
{ column_name: "tags", data_type: "ARRAY" },
],
},
{
table_name: "boattype",
columns: [
{ column_name: "id", data_type: "uuid" },
{ column_name: "seats", data_type: "integer" },
{ column_name: "createdAt", data_type: "timestamp with time zone" },
{ column_name: "updatedAt", data_type: "timestamp with time zone" },
{ column_name: "name", data_type: "character varying" },
],
},
{
table_name: "employee",
columns: [
{ column_name: "id", data_type: "uuid" },
{ column_name: "createdAt", data_type: "timestamp with time zone" },
{ column_name: "updatedAt", data_type: "timestamp with time zone" },
{ column_name: "last_name", data_type: "character varying" },
{ column_name: "password", data_type: "character varying" },
{ column_name: "role", data_type: "character varying" },
{ column_name: "email", data_type: "character varying" },
{ column_name: "first_name", data_type: "character varying" },
],
},
];
beforeAll(async () => {
client = new Client({
user: process.env.POSTGRES_USER,
host: process.env.POSTGRES_HOST,
database: process.env.POSTGRES_DB,
password: process.env.POSTGRES_PASSWORD,
});
await client.connect();
});
afterAll(async () => await client.end());
it("has the correct tables", async () => {
const query = await client.query(
`SELECT table_name FROM information_schema.tables WHERE table_schema='public'`
);
const tableNames = query.rows.map((row) => row.table_name).sort();
const shouldHaveTableNames = databaseTables
.map((table) => table.table_name)
.sort();
expect(tableNames).toEqual(shouldHaveTableNames);
});
it.each(databaseTables)(
"has correct columns for each table",
async ({ table_name, columns }) => {
const query = await client.query(
`SELECT column_name, data_type FROM information_schema.columns WHERE table_name='${table_name}';`
);
const queryColumns = query.rows;
for (let queryColumn of queryColumns) {
expect(columns).toContainEqual(queryColumn);
}
}
);
it("has the initial employee", async () => {
const query = await client.query(
`SELECT * FROM employee WHERE email='${process.env.INITIAL_COORDINATOR_EMAIL}';`
);
expect(query.rows.length).toEqual(1);
});
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment