From 7cfe484a1dc2a24d4f8a20e8e35d42128548e438 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 20 Sep 2021 17:57:32 +0200 Subject: [PATCH] Database changes. Added Snippet model and controller --- package-lock.json | 100 +++++++++++++++++++++++++++++ package.json | 11 ++-- src/controllers/snippets.ts | 13 ++++ src/db/index.ts | 33 +++++++--- src/db/migrations/00_initial.ts | 14 ++-- src/middleware/asyncWrapper.ts | 8 +++ src/middleware/index.ts | 2 + src/middleware/requireBody.ts | 16 +++++ src/models/Snippet.ts | 31 +++++++++ src/models/index.ts | 1 + src/routes/snippets.ts | 7 ++ src/server.ts | 8 ++- src/typescript/types/ErrorLevel.ts | 2 +- src/utils/Logger.ts | 10 ++- tsconfig.json | 4 +- 15 files changed, 232 insertions(+), 28 deletions(-) create mode 100644 src/controllers/snippets.ts create mode 100644 src/middleware/asyncWrapper.ts create mode 100644 src/middleware/index.ts create mode 100644 src/middleware/requireBody.ts create mode 100644 src/models/Snippet.ts create mode 100644 src/models/index.ts create mode 100644 src/routes/snippets.ts diff --git a/package-lock.json b/package-lock.json index 1e1b43b..4b918eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,11 @@ "integrity": "sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==", "dev": true }, + "@types/bluebird": { + "version": "3.5.36", + "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.36.tgz", + "integrity": "sha512-HBNx4lhkxN7bx6P0++W8E289foSu8kO8GCk2unhuVggO+cE7rh9DhZUyPhUxNRG9m+5B5BTKxZQ5ZP92x/mx9Q==" + }, "@types/body-parser": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", @@ -68,6 +73,14 @@ "@types/node": "*" } }, + "@types/bson": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.2.0.tgz", + "integrity": "sha512-ELCPqAdroMdcuxqwMgUpifQyRoTpyYCNr1V9xKyF40VsBobsj+BbWNRvwGchMgBPGqkw655ypkjj2MEF5ywVwg==", + "requires": { + "bson": "*" + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -77,6 +90,14 @@ "@types/node": "*" } }, + "@types/continuation-local-storage": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.3.tgz", + "integrity": "sha512-4LYeWblV+6puK9tFGM7Zr4OLZkVXmaL7hUK6/wHwbfwM+q7v+HZyBWTXkNOiC9GqOxv7ehhi5TMCbebZWeVYtw==", + "requires": { + "@types/node": "*" + } + }, "@types/express": { "version": "4.17.13", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", @@ -100,12 +121,26 @@ "@types/range-parser": "*" } }, + "@types/lodash": { + "version": "4.14.173", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.173.tgz", + "integrity": "sha512-vv0CAYoaEjCw/mLy96GBTnRoZrSxkGE0BKzKimdR8P3OzrNYNvBgtW7p055A+E8C31vXNUhWKoFCbhq7gbyhFg==" + }, "@types/mime": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", "dev": true }, + "@types/mongodb": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.6.20.tgz", + "integrity": "sha512-WcdpPJCakFzcWWD9juKoZbRtQxKIMYF/JIAM4JrNHrMcnJL6/a2NWjXxW7fo9hxboxxkg+icff8d7+WIEvKgYQ==", + "requires": { + "@types/bson": "*", + "@types/node": "*" + } + }, "@types/node": { "version": "16.9.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-16.9.2.tgz", @@ -123,6 +158,17 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "@types/sequelize": { + "version": "4.28.10", + "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.10.tgz", + "integrity": "sha512-GKbEbl6uyEYTPvU2JZvmqZHfpwTTjaZvNSd2gFJrhcxUL1bcyG7i+S8Od2L0/+skrk2bBINl7J1Sugo0mgIY3g==", + "requires": { + "@types/bluebird": "*", + "@types/continuation-local-storage": "*", + "@types/lodash": "*", + "@types/validator": "*" + } + }, "@types/serve-static": { "version": "1.13.10", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", @@ -133,6 +179,20 @@ "@types/node": "*" } }, + "@types/umzug": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@types/umzug/-/umzug-2.3.2.tgz", + "integrity": "sha512-ozfpVpUwzozBVVII2T1iUTByJ1n+xgi3xpjzUnZ50tnAhBCzQ+RiewGgjyzuK2RAb3cug0mqKTvMbvd336lkDw==", + "requires": { + "@types/mongodb": "^3.6.20", + "@types/sequelize": "*" + } + }, + "@types/validator": { + "version": "13.6.3", + "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.6.3.tgz", + "integrity": "sha512-fWG42pMJOL4jKsDDZZREnXLjc3UE0R8LOJfARWYg6U966rxDT7TYejYzLnUF5cvSObGg34nd0+H2wHHU5Omdfw==" + }, "abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -306,6 +366,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" + }, "bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -330,6 +395,11 @@ "inherits": "~2.0.0" } }, + "bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" + }, "body-parser": { "version": "1.19.0", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", @@ -470,6 +540,23 @@ "fill-range": "^7.0.1" } }, + "bson": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/bson/-/bson-4.5.2.tgz", + "integrity": "sha512-8CEMJpwc7qlQtrn2rney38jQSEeMar847lz0LyitwRmVknAW8iHXrzW4fTjHfyWm0E3sukyD/zppdH+QU1QefA==", + "requires": { + "buffer": "^5.6.0" + } + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1266,6 +1353,11 @@ "safer-buffer": ">= 2.1.2 < 3" } }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + }, "ignore-by-default": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", @@ -2831,6 +2923,14 @@ "integrity": "sha512-4xfscpisVgqqDfPaJo5vkd+Qd/ItkoagnHpufr+i2QCHBsNYp+G7UAoyFl8aPtx879u38wPV65rZ8qbGZijalA==", "dev": true }, + "umzug": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/umzug/-/umzug-2.3.0.tgz", + "integrity": "sha512-Z274K+e8goZK8QJxmbRPhl89HPO1K+ORFtm6rySPhFKfKc5GHhqdzD0SGhSWHkzoXasqJuItdhorSvY7/Cgflw==", + "requires": { + "bluebird": "^3.7.2" + } + }, "unbox-primitive": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", diff --git a/package.json b/package.json index 5f91ab7..3d7f32b 100644 --- a/package.json +++ b/package.json @@ -2,14 +2,14 @@ "name": "snippet-hub", "version": "1.0.0", "description": "", - "main": "src/server.ts", + "main": "build/server.js", "scripts": { "init:client": "npm install --prefix=client", "init:server": "npm install", "init": "npm-run-all -n init:**", "dev:client": "npm start --prefix=client", - "dev:server": "nodemon src/server.ts", - "dev": "npm-run-all -n dev:**", + "dev:server": "nodemon", + "dev": "npm-run-all -n --parallel dev:**", "build:clear": "rm -rf build", "build:tsc": "tsc", "build": "npm-run-all -n build:**" @@ -20,15 +20,18 @@ "devDependencies": { "@types/express": "^4.17.13", "@types/node": "^16.9.2", + "@types/validator": "^13.6.3", "nodemon": "^2.0.12", "npm-run-all": "^4.1.5", "ts-node": "^10.2.1", "typescript": "^4.4.3" }, "dependencies": { + "@types/umzug": "^2.3.2", "dotenv": "^10.0.0", "express": "^4.17.1", "sequelize": "^6.6.5", - "sqlite3": "^5.0.2" + "sqlite3": "^5.0.2", + "umzug": "^2.3.0" } } diff --git a/src/controllers/snippets.ts b/src/controllers/snippets.ts new file mode 100644 index 0000000..5c045e1 --- /dev/null +++ b/src/controllers/snippets.ts @@ -0,0 +1,13 @@ +import { Request, Response, NextFunction } from 'express'; +import { asyncWrapper } from '../middleware'; +import { SnippetModel } from '../models'; + +export const createSnippet = asyncWrapper( + async (req: Request, res: Response, next: NextFunction): Promise => { + const snippet = await SnippetModel.create(req.body); + + res.status(201).json({ + data: snippet + }); + } +); diff --git a/src/db/index.ts b/src/db/index.ts index e67077c..43fe6d2 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,9 +1,9 @@ -require('ts-node/register'); +import path from 'path'; import { Sequelize } from 'sequelize'; -import { Umzug, SequelizeStorage } from 'umzug'; +import Umzug from 'umzug'; import { Logger } from '../utils'; -const logger = new Logger(); +const logger = new Logger('db'); // DB config export const sequelize = new Sequelize({ @@ -14,15 +14,21 @@ export const sequelize = new Sequelize({ // Migrations config const umzug = new Umzug({ - migrations: { glob: '**/migrations/*.ts' }, - context: sequelize.getQueryInterface(), - storage: new SequelizeStorage({ sequelize }), - logger: undefined + migrations: { + path: path.join(__dirname, './migrations'), + params: [sequelize.getQueryInterface()], + pattern: /^\d+[\w-]+\.(js|ts)$/ + }, + storage: 'sequelize', + storageOptions: { + sequelize + }, + logging: false }); -export type Migration = typeof umzug._types.migration; - export const connectDB = async () => { + const isDev = process.env.NODE_ENV == 'development'; + try { // Create & connect db await sequelize.authenticate(); @@ -30,15 +36,22 @@ export const connectDB = async () => { // Check migrations const pendingMigrations = await umzug.pending(); + if (pendingMigrations.length > 0) { logger.log(`Found pending migrations. Executing...`); + + if (isDev) { + pendingMigrations.forEach(({ file }) => + logger.log(`Executing ${file} migration`, 'DEV') + ); + } } await umzug.up(); } catch (err) { logger.log(`Database connection error`, 'ERROR'); - if (process.env.NODE_ENV == 'development') { + if (isDev) { console.log(err); } diff --git a/src/db/migrations/00_initial.ts b/src/db/migrations/00_initial.ts index 275a7a9..d5c0c14 100644 --- a/src/db/migrations/00_initial.ts +++ b/src/db/migrations/00_initial.ts @@ -1,5 +1,4 @@ -import { DataTypes, Model } from 'sequelize'; -import type { Migration } from '../'; +import { DataTypes, Model, QueryInterface } from 'sequelize'; import { Snippet, SnippetCreationAttributes @@ -7,16 +6,15 @@ import { const { INTEGER, STRING, DATE } = DataTypes; -export const up: Migration = async ({ - context: queryInterface -}): Promise => { +export const up = async (queryInterface: QueryInterface): Promise => { await queryInterface.createTable>( 'snippets', { id: { type: INTEGER, allowNull: false, - primaryKey: true + primaryKey: true, + autoIncrement: true }, title: { type: STRING, @@ -38,8 +36,6 @@ export const up: Migration = async ({ ); }; -export const down: Migration = async ({ - context: queryInterface -}): Promise => { +export const down = async (queryInterface: QueryInterface): Promise => { await queryInterface.dropTable('snippets'); }; diff --git a/src/middleware/asyncWrapper.ts b/src/middleware/asyncWrapper.ts new file mode 100644 index 0000000..ae23ab6 --- /dev/null +++ b/src/middleware/asyncWrapper.ts @@ -0,0 +1,8 @@ +import { Request, Response, NextFunction } from 'express'; + +type Foo = (req: Request, res: Response, next: NextFunction) => Promise; + +export const asyncWrapper = + (foo: Foo) => (req: Request, res: Response, next: NextFunction) => { + return Promise.resolve(foo(req, res, next)).catch(next); + }; diff --git a/src/middleware/index.ts b/src/middleware/index.ts new file mode 100644 index 0000000..6a139d4 --- /dev/null +++ b/src/middleware/index.ts @@ -0,0 +1,2 @@ +export * from './asyncWrapper'; +export * from './requireBody'; diff --git a/src/middleware/requireBody.ts b/src/middleware/requireBody.ts new file mode 100644 index 0000000..5995890 --- /dev/null +++ b/src/middleware/requireBody.ts @@ -0,0 +1,16 @@ +import { Request, Response, NextFunction } from 'express'; + +export const requireBody = + (...fields: string[]) => + (req: Request, res: Response, next: NextFunction): void => { + const bodyKeys = Object.keys(req.body); + const missingKeys: string[] = []; + + fields.forEach(field => { + if (!bodyKeys.includes(field)) { + missingKeys.push(field); + } + }); + + next(); + }; diff --git a/src/models/Snippet.ts b/src/models/Snippet.ts new file mode 100644 index 0000000..407550b --- /dev/null +++ b/src/models/Snippet.ts @@ -0,0 +1,31 @@ +import { Model, DataTypes } from 'sequelize'; +import { sequelize } from '../db'; +import { Snippet, SnippetCreationAttributes } from '../typescript/interfaces'; + +const { INTEGER, STRING, DATE } = DataTypes; + +interface SnippetInstance + extends Model, + Snippet {} + +export const SnippetModel = sequelize.define('Snippet', { + id: { + type: INTEGER, + primaryKey: true, + autoIncrement: true + }, + title: { + type: STRING, + allowNull: false + }, + language: { + type: STRING, + allowNull: false + }, + createdAt: { + type: DATE + }, + updatedAt: { + type: DATE + } +}); diff --git a/src/models/index.ts b/src/models/index.ts new file mode 100644 index 0000000..02fc3fa --- /dev/null +++ b/src/models/index.ts @@ -0,0 +1 @@ +export * from './Snippet'; diff --git a/src/routes/snippets.ts b/src/routes/snippets.ts new file mode 100644 index 0000000..9fa2e09 --- /dev/null +++ b/src/routes/snippets.ts @@ -0,0 +1,7 @@ +import { Router } from 'express'; +import { createSnippet } from '../controllers/snippets'; +import { requireBody } from '../middleware'; + +export const snippetRouter = Router(); + +snippetRouter.route('/').post(requireBody('title', 'language'), createSnippet); diff --git a/src/server.ts b/src/server.ts index aeca8fa..996470b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,16 +3,22 @@ import express from 'express'; import { Logger } from './utils'; import { connectDB } from './db'; +// Routers +import { snippetRouter } from './routes/snippets'; + // Env config dotenv.config({ path: './src/config/.env' }); const app = express(); -const logger = new Logger(); +const logger = new Logger('server'); const PORT = process.env.PORT; // App config app.use(express.json()); +// Routes +app.use('/api/snippets', snippetRouter); + (async () => { await connectDB(); diff --git a/src/typescript/types/ErrorLevel.ts b/src/typescript/types/ErrorLevel.ts index a72f69d..136c461 100644 --- a/src/typescript/types/ErrorLevel.ts +++ b/src/typescript/types/ErrorLevel.ts @@ -1 +1 @@ -export type ErrorLevel = 'INFO' | 'ERROR' | 'WARN'; +export type ErrorLevel = 'INFO' | 'ERROR' | 'WARN' | 'DEV'; diff --git a/src/utils/Logger.ts b/src/utils/Logger.ts index f01c230..5696f99 100644 --- a/src/utils/Logger.ts +++ b/src/utils/Logger.ts @@ -1,8 +1,16 @@ import { ErrorLevel } from '../typescript/types/ErrorLevel'; export class Logger { + private namespace: string; + + constructor(namespace: string) { + this.namespace = namespace; + } + public log(message: string, level: ErrorLevel = 'INFO'): void { - console.log(`[${this.generateTimestamp()}] [${level}] ${message}`); + console.log( + `[${this.generateTimestamp()}] [${level}] ${this.namespace}: ${message}` + ); } private generateTimestamp(): string { diff --git a/tsconfig.json b/tsconfig.json index 442ef72..0da8820 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -26,7 +26,7 @@ /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, - // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "noImplicitAny": true /* Raise error on expressions and declarations with an implied 'any' type. */, // "strictNullChecks": true, /* Enable strict null checks. */ // "strictFunctionTypes": true, /* Enable strict checking of function types. */ // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ @@ -68,5 +68,5 @@ "skipLibCheck": true /* Skip type checking of declaration files. */, "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ }, - "exclude": ["client"] + "include": ["src"] }