Database changes. Added Snippet model and controller

This commit is contained in:
unknown
2021-09-20 17:57:32 +02:00
parent dd34f2ae02
commit 7cfe484a1d
15 changed files with 232 additions and 28 deletions

100
package-lock.json generated
View File

@@ -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",

View File

@@ -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"
}
}

View File

@@ -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<void> => {
const snippet = await SnippetModel.create(req.body);
res.status(201).json({
data: snippet
});
}
);

View File

@@ -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);
}

View File

@@ -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<void> => {
export const up = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.createTable<Model<Snippet, SnippetCreationAttributes>>(
'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<void> => {
export const down = async (queryInterface: QueryInterface): Promise<void> => {
await queryInterface.dropTable('snippets');
};

View File

@@ -0,0 +1,8 @@
import { Request, Response, NextFunction } from 'express';
type Foo = (req: Request, res: Response, next: NextFunction) => Promise<void>;
export const asyncWrapper =
(foo: Foo) => (req: Request, res: Response, next: NextFunction) => {
return Promise.resolve(foo(req, res, next)).catch(next);
};

2
src/middleware/index.ts Normal file
View File

@@ -0,0 +1,2 @@
export * from './asyncWrapper';
export * from './requireBody';

View File

@@ -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();
};

31
src/models/Snippet.ts Normal file
View File

@@ -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, SnippetCreationAttributes>,
Snippet {}
export const SnippetModel = sequelize.define<SnippetInstance>('Snippet', {
id: {
type: INTEGER,
primaryKey: true,
autoIncrement: true
},
title: {
type: STRING,
allowNull: false
},
language: {
type: STRING,
allowNull: false
},
createdAt: {
type: DATE
},
updatedAt: {
type: DATE
}
});

1
src/models/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './Snippet';

7
src/routes/snippets.ts Normal file
View File

@@ -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);

View File

@@ -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();

View File

@@ -1 +1 @@
export type ErrorLevel = 'INFO' | 'ERROR' | 'WARN';
export type ErrorLevel = 'INFO' | 'ERROR' | 'WARN' | 'DEV';

View File

@@ -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 {

View File

@@ -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"]
}