new todo-backend

This commit is contained in:
Guy Bianco IV 2021-05-19 12:50:48 -04:00
parent 20d773370e
commit 0bc1c87dda
17 changed files with 7782 additions and 342 deletions

2
todo-backend/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
node_modules
dist

View file

@ -1,36 +0,0 @@
var restify = require('restify');
var controller = require('./controllers/items');
var serverinfo = require('./controllers/serverinfo');
var db = require('./models/db');
var model = require('./models/items');
model.connect(db.params, function(err) {
if (err) throw err;
});
var server = restify.createServer()
.use(restify.fullResponse())
.use(restify.queryParser())
.use(restify.bodyParser())
.use(restify.CORS());;
controller.context(server, '/todo/api', model);
serverinfo.context(server, '/todo/api');
var port = process.env.PORT || 8080;
server.listen(port, function (err) {
if (err)
console.error(err);
else
console.log('App is ready at : ' + port);
});
/*
process.on('uncaughtException', function (err) {
console.error(JSON.parse(JSON.stringify(err, ['stack', 'message', 'inner'], 2)))
});
*/

35
todo-backend/build.ts Normal file
View file

@ -0,0 +1,35 @@
import fs from "fs-extra";
import childProcess from "child_process";
(async () => {
try {
// Remove current build
await remove("./dist/");
// Copy back-end files
await exec("tsc --build tsconfig.prod.json", "./");
} catch (err) {
console.error(err);
}
})();
function remove(loc: string): Promise<void> {
return new Promise((res, rej) => {
return fs.remove(loc, (err) => {
return !!err ? rej(err) : res();
});
});
}
function exec(cmd: string, loc: string): Promise<void> {
return new Promise((res, rej) => {
return childProcess.exec(cmd, { cwd: loc }, (err, stdout, stderr) => {
if (!!stdout) {
console.log(stdout);
}
if (!!stderr) {
console.warn(stderr);
}
return !!err ? rej(err) : res();
});
});
}

View file

@ -1,131 +0,0 @@
var model = undefined;
exports.context = function(server, path, itemsModel) {
if (!server)
done('has to provide a restify server object');
var context = "/items";
if (path)
context = path + context;
server.get(context + '/', this.list);
server.get(context + '/:id', this.read);
server.get(context + '-count', this.count);
server.post(context + '/', this.save);
server.del(context + '/:id', this.destroy);
model = itemsModel;
};
exports.list = function(req, res, next) {
var page_no = req.query.page || 1;
var sortField = req.query.sortFields || "id";
var sortDirection = req.query.sortDirections || "asc";
model.listAll(page_no, sortField, sortDirection, function(err, items) {
if (err) {
res.send(err);
}
else {
if (items) {
model.countAll(function(err, n) {
if (err) {
res.send(err);
}
else {
if (n) {
var page = {
"currentPage" : page_no,
"list" : items,
"pageSize" : 10,
"sortDirections" : sortDirection,
"sortFields" : sortField,
"totalResults" : n
};
res.json(page);
next();
}
}
});
}
else {
res.send(err);
}
}
})
};
exports.read = function(req, res, next) {
var key = req.params.id;
model.read(key, function(err, item) {
if (err) {
res.send(err);
}
else {
if (item) {
res.json(item);
next();
}
else {
res.send(err);
}
}
})
};
exports.count = function(req, res, next) {
model.countAll(function(err, n) {
if (err) {
res.send(err);
}
else {
var page = {
count: n
};
res.json(page)
next();
}
})
};
exports.save = function(req, res, next) {
if (req.params.id) {
model.update(req.params.id, req.params.description, req.params.done, function(err, item) {
if (err) {
res.send(err);
}
else {
res.json(item);
next();
}
});
}
else {
model.create(req.params.description, req.params.done, function(err, item) {
if (err) {
res.send(err);
}
else {
res.json(item);
next();
}
});
}
};
exports.destroy = function(req, res, next) {
if (req.params.id) {
model.destroy(req.params.id, function(err, item) {
if (err) {
res.send(err);
}
else {
res.json(item);
}
});
}
}

View file

@ -1,29 +0,0 @@
var os = require('os');
exports.context = function(server, path) {
if (!server)
done('has to provide a restify server object');
server.get(path + '/host', this.serverInfo);
};
exports.serverInfo = function(req, res, next) {
var address;
var ifaces = os.networkInterfaces();
for (var dev in ifaces) {
var iface = ifaces[dev].filter(function(details) {
return details.family === 'IPv4' && details.internal === false;
});
if (iface.length > 0)
address = iface[0].address;
}
var reply = {
ip: address,
hostname: os.hostname()
};
res.json(reply);
next();
};

View file

@ -1,10 +0,0 @@
module.exports.params = {
dbname: process.env.DATABASE_NAME,
username: process.env.DATABASE_USER,
password: process.env.DATABASE_PASSWORD,
params: {
host: process.env.DATABASE_SVC,
dialect: 'mysql',
}
};

View file

@ -1,127 +0,0 @@
var Sequelize = require("sequelize");
var Item = undefined;
module.exports.connect = function(params, callback) {
var sequlz = new Sequelize(
params.dbname, params.username, params.password,
params.params);
Item = sequlz.define('Item', {
id: { type: Sequelize.BIGINT,
primaryKey: true, unique: true, allowNull: false,
autoIncrement: true },
description: { type: Sequelize.STRING,
allowNull: true },
done: { type: Sequelize.BOOLEAN,
allowNull: true }
}, {
timestamps: false,
freezeTableName: true
});
if (process.env.DATABASE_INIT == 'true') {
Item.sync({ force: true }).then(function() {
callback();
}).catch(function(err) {
callback(err);
});
}
}
exports.disconnect = function(callback) {
//XXX shouln'd to something to close or release the db connection?
callback();
}
exports.create = function(description, done, callback) {
Item.create({
//id: id,
description: description,
done: (done) ? true : false
}).then(function(item) {
callback(null, item);
}).catch(function(err) {
callback(err);
});
}
exports.update = function(key, description, done, callback) {
Item.find({ where:{ id: key } }).then(function(item) {
if (!item) {
callback(new Error("Nothing found for key " + key));
}
else {
item.updateAttributes({
description: description,
done: (done) ? true : false
}).then(function() {
callback(null, item);
}).error(function(err) {
callback(err);
});
}
}).catch(function(err) {
callback(err);
});
}
exports.read = function(key, callback) {
Item.find({ where:{ id: key } }).then(function(item) {
if (!item) {
callback(new Error("Nothing found for key " + key));
}
else {
//XXX why recreating the item object?
callback(null, {
id: item.id,
description: item.description,
done: item.done
});
}
}).catch(function(err) {
callback(err);
});
}
exports.destroy = function(key, callback) {
Item.find({ where:{ id: key } }).then(function(item) {
if (!item) {
callback(new Error("Nothing found for " + key));
}
else {
item.destroy().then(function() {
callback(null, item);
}).error(function(err) {
callback(err);
});
}
}).catch(function(err) {
callback(err);
});
}
exports.countAll = function(callback) {
Item.findAll({
attributes: [[Sequelize.fn('COUNT', Sequelize.col('id')), 'no_items']]
}).then(function(n) {
callback(null, n[0].get('no_items'));
}).catch(function(err) {
callback(err);
});
}
exports.listAll = function(page, sortField, sortDirection, callback) {
Item.findAll({ offset: 10 * (page - 1), limit: 10, order: [[sortField, sortDirection]] }).then(function(items) {
var theitems = [];
items.forEach(function(item) {
//XXX why recreating the item objects for theitems?
theitems.push({
id: item.id, description: item.description, done: item.done });
});
callback(null, theitems);
}).catch(function(err) {
callback(err);
});
}

7461
todo-backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,16 +1,92 @@
{
"name": "todo-backend",
"description": "multi-container version of the todoapp",
"version": "0.0.2",
"private": true,
"author": "Red Hat Training",
"license": "ASL",
"version": "2.0.0",
"scripts": {
"start": "node app.js"
"build": "./node_modules/.bin/ts-node build.ts",
"lint": "eslint . --ext .ts",
"start": "node -r module-alias/register ./dist",
"start:dev": "nodemon"
},
"nodemonConfig": {
"watch": [
"src"
],
"ext": "ts, html",
"ignore": [
"src/public"
],
"exec": "./node_modules/.bin/ts-node -r tsconfig-paths/register ./src"
},
"_moduleAliases": {
"@daos": "dist/daos",
"@entities": "dist/entities",
"@shared": "dist/shared",
"@server": "dist/Server"
},
"eslintConfig": {
"parser": "@typescript-eslint/parser",
"plugins": [
"@typescript-eslint"
],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking"
],
"parserOptions": {
"project": "./tsconfig.json"
},
"rules": {
"max-len": [
"error",
{
"code": 100
}
],
"no-extra-boolean-cast": 0,
"@typescript-eslint/restrict-plus-operands": 0,
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-floating-promises": 0,
"@typescript-eslint/no-unsafe-member-access": 0,
"@typescript-eslint/no-unsafe-assignment": 0
}
},
"eslintIgnore": [
"src/public/",
"build.ts"
],
"dependencies": {
"restify": "4.3.0",
"sequelize": "3.14.2",
"mysql": "2.9.0"
"command-line-args": "^5.1.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-async-errors": "^3.1.1",
"jsonfile": "^6.1.0",
"module-alias": "^2.2.2",
"mysql2": "^2.2.5",
"sequelize": "^6.6.2"
},
"devDependencies": {
"@types/command-line-args": "^5.0.0",
"@types/cookie-parser": "^1.4.2",
"@types/cors": "^2.8.10",
"@types/express": "^4.17.11",
"@types/find": "^0.2.1",
"@types/fs-extra": "^9.0.11",
"@types/jasmine": "^3.6.10",
"@types/jsonfile": "^6.0.0",
"@types/node": "^15.0.1",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^4.22.0",
"@typescript-eslint/parser": "^4.22.0",
"eslint": "^7.25.0",
"find": "^0.3.0",
"fs-extra": "^9.1.0",
"nodemon": "^2.0.7",
"supertest": "^6.1.3",
"ts-node": "^9.1.1",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.2.4"
}
}

View file

@ -0,0 +1,43 @@
import cookieParser from "cookie-parser";
import express, { Request, Response } from "express";
import "express-async-errors";
import cors from "cors";
import { Sequelize } from "sequelize";
import BaseRouter from "./routes";
import { dbConnectionOptions } from "./entities/db";
const app = express();
// DO NOT USE in production as this allows any site to use our backend
// you will need to configure cors separately for your application
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(cookieParser());
const sequelize = new Sequelize(dbConnectionOptions);
sequelize
.authenticate()
.then(() => {
console.log("Connection has been established successfully.");
})
.catch((err) => {
console.error("Unable to connect to the database:", err);
});
// Add APIs
app.use("/api", BaseRouter);
// Print API errors
// eslint-disable-next-line @typescript-eslint/no-unused-vars
app.use((err: Error, req: Request, res: Response) => {
console.error(err, true);
return res.status(500).json({
error: err.message,
});
});
// Export express instance
export default app;

View file

@ -0,0 +1,54 @@
import { Sequelize, DataTypes, Model, Optional } from "sequelize";
import { dbConnectionOptions } from "./db";
export interface TodoItemAttributes {
id: number;
description: string;
done: boolean;
}
type TodoItemCreationAttributes = Optional<TodoItemAttributes, "id">;
interface TodoItemInstance
extends Model<TodoItemAttributes, TodoItemCreationAttributes> {
createdAt: Date;
updatedAt: Date;
}
const sequelize = new Sequelize(dbConnectionOptions);
export const TodoItem = sequelize.define<TodoItemInstance>(
"TodoItem",
{
id: {
type: DataTypes.BIGINT,
primaryKey: true,
unique: true,
allowNull: false,
autoIncrement: true,
},
description: { type: DataTypes.STRING, allowNull: true },
done: { type: DataTypes.BOOLEAN, allowNull: true },
},
{
timestamps: false,
freezeTableName: true,
}
);
// (re-)creates table (NOT database)
if (process.env.DATABASE_INIT === "true") {
TodoItem.sync({ force: true });
}
export async function createTodoItem(todoItem: TodoItemCreationAttributes) {
return TodoItem.create(todoItem).then((item) => item.get());
}
export async function deleteTodoItem(id: number) {
return TodoItem.destroy({ where: { id } });
}
export async function listAllTodoItems(): Promise<TodoItemAttributes[]> {
return TodoItem.findAll({}).then((items) => items.map((i) => i.get()));
}

View file

@ -0,0 +1,18 @@
import { Options } from "sequelize";
const {
DATABASE_NAME,
DATABASE_USER,
DATABASE_PASSWORD,
DATABASE_SVC,
DATABASE_PORT,
} = process.env;
export const dbConnectionOptions: Options = {
database: DATABASE_NAME ?? "todo",
username: DATABASE_USER ?? "root",
password: DATABASE_PASSWORD ?? "",
host: DATABASE_SVC ?? "localhost",
port: Number(DATABASE_PORT) ?? 3306,
dialect: "mysql",
};

View file

@ -0,0 +1,7 @@
import app from "@server";
// Start the server
const port = Number(process.env.PORT || 8080);
app.listen(port, () => {
console.log("Express server started on port: " + port);
});

View file

@ -0,0 +1,27 @@
import {
createTodoItem,
deleteTodoItem,
listAllTodoItems,
} from "@entities/Item";
import { Request, Response } from "express";
export function handleCreate(req: Request, res: Response) {
const { description, done } = req.body;
// NOTE production applications should use a validation framework
if (typeof description !== "string" || typeof done !== "boolean") {
res
.status(400)
.send("required parameters were either missing or the wrong type");
} else {
createTodoItem({ description, done }).then((item) => res.json(item));
}
}
export function handleReadAll(req: Request, res: Response) {
listAllTodoItems().then((items) => res.json(items));
}
export function handleDelete(req: Request, res: Response) {
deleteTodoItem(Number(req.params.id)).then(() => res.json({}));
}

View file

@ -0,0 +1,13 @@
import { Router } from "express";
import { handleCreate, handleReadAll, handleDelete } from "./Items";
// Item routes
const itemRouter = Router();
itemRouter.get("/", handleReadAll);
itemRouter.post("/", handleCreate);
itemRouter.delete("/:id", handleDelete);
// Export the base-router
const baseRouter = Router();
baseRouter.use("/items", itemRouter);
export default baseRouter;

View file

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es6",
"module": "commonjs",
"checkJs": true,
"outDir": "dist",
"removeComments": true,
"strict": true,
"noImplicitAny": true,
"alwaysStrict": true,
"moduleResolution": "node",
"baseUrl": "./",
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"paths": {
"@daos/*": ["src/daos/*"],
"@entities/*": ["src/entities/*"],
"@shared/*": ["src/shared/*"],
"@server": ["src/Server"]
}
},
"include": ["src/**/*.ts", "spec/**/*.ts"],
"exclude": ["src/public/"]
}

View file

@ -0,0 +1,11 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false
},
"exclude": [
"spec",
"src/**/*.mock.ts",
"src/public/"
]
}