diff --git a/.gitignore b/.gitignore index 331e819..f043384 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ npm-debug.log test/*.log dist -.idea \ No newline at end of file +.idea +config.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c2e86b8..ca6704a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + +- Connection to Postgres Database +- Graphql Schema +- default-config file and generation of config file on startup diff --git a/gulpfile.js b/gulpfile.js index e474b8c..c1abd43 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -34,9 +34,14 @@ function compileSass() { .pipe(dest('dist/public/stylesheets')); } -task('default', series(clearDist, compileTypescript, minifyJs, compileSass)); +function moveRemaining() { + return src(['src/**/*', '!src/**/*.ts', '!src/**/*.sass', '!src/**/*.js']) + .pipe(dest('dist')); +} + +task('default', series(clearDist, compileTypescript, minifyJs, compileSass, moveRemaining)); task('watch', () => { watch('src/public/stylesheets/sass/**/*.sass', compileSass); watch('**/*.js', minifyJs); watch('**/*.ts', compileTypescript); -}); \ No newline at end of file +}); diff --git a/package-lock.json b/package-lock.json index ae10b13..212f9d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -137,6 +137,11 @@ "integrity": "sha512-UoCovaxbJIxagCvVfalfK7YaNhmxj3BQFRQ2RHQKLiu+9wNXhJnlbspsLHt/YQM99IaLUUFJNzCwzc6W0ypMeQ==", "dev": true }, + "@types/js-yaml": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-3.12.1.tgz", + "integrity": "sha512-SGGAhXLHDx+PK4YLNcNGa6goPf9XRWQNAUUbffkwVGGXIxmDKWyGGL4inzq2sPmExu431Ekb9aEMn9BkPqEYFA==" + }, "@types/mime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-2.0.1.tgz", @@ -370,7 +375,6 @@ "version": "1.0.10", "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "dev": true, "requires": { "sprintf-js": "~1.0.2" } @@ -1776,8 +1780,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esutils": { "version": "2.0.3", @@ -3815,7 +3818,6 @@ "version": "3.13.1", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, "requires": { "argparse": "^1.0.7", "esprima": "^4.0.0" @@ -5913,8 +5915,7 @@ "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true + "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, "sshpk": { "version": "1.16.1", diff --git a/package.json b/package.json index 2041d5a..ae3c9f0 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "typescript": "^3.5.3" }, "dependencies": { + "@types/js-yaml": "^3.12.1", "@types/winston": "^2.4.4", "connect-pg-simple": "^6.0.0", "cookie-parser": "^1.4.4", @@ -50,6 +51,7 @@ "fs-extra": "^8.1.0", "graphql": "^14.4.2", "graphql-import": "^0.7.1", + "js-yaml": "^3.13.1", "pg": "^7.12.1", "socket.io": "^2.2.0", "winston": "^3.2.1" diff --git a/src/config.yaml b/src/config.yaml deleted file mode 100644 index 536f82f..0000000 --- a/src/config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -database: - url: localhost - port: 54332 - user: greenvironment - password: greendev - database: greenvironment diff --git a/src/default-config.yaml b/src/default-config.yaml new file mode 100644 index 0000000..6b7f17d --- /dev/null +++ b/src/default-config.yaml @@ -0,0 +1,11 @@ +# database connection info +database: + host: + port: + user: + password: + database: + +# http server configuration +server: + port: diff --git a/src/index.ts b/src/index.ts index b4265bb..7e34c95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,16 @@ +import * as fsx from "fs-extra"; import App from "./app"; -const app = new App(); +const configPath = "config.yaml"; +const defaultConfig = __dirname + "/default-config.yaml"; + +/** + * async main function wrapper. + */ +(async () => { + if (!(await fsx.pathExists(configPath))) { + await fsx.copy(defaultConfig, configPath); + } + const app = new App(); +})(); + diff --git a/src/lib/DAO.ts b/src/lib/DAO.ts new file mode 100644 index 0000000..7d60b87 --- /dev/null +++ b/src/lib/DAO.ts @@ -0,0 +1,19 @@ +import {Pool} from "pg"; +import globals from "./globals"; +import {QueryHelper} from "./QueryHelper"; + +const config = globals.config; + +export class DAO { + private queryHelper: QueryHelper; + constructor() { + const dbClient: Pool = new Pool({ + database: config.database.database, + host: config.database.host, + password: config.database.password, + port: config.database.port, + user: config.database.user, + }); + this.queryHelper = new QueryHelper(dbClient); + } +} diff --git a/src/lib/QueryHelper.ts b/src/lib/QueryHelper.ts new file mode 100644 index 0000000..7f22fb5 --- /dev/null +++ b/src/lib/QueryHelper.ts @@ -0,0 +1,116 @@ +import {Pool, PoolClient, QueryConfig, QueryResult} from "pg"; +import globals from "./globals"; + +const logger = globals.logger; + +export class SqlTransaction { + constructor(private client: PoolClient) { + } + + /** + * Begins the transaction. + */ + public async begin() { + return await this.client.query("BEGIN"); + } + + /** + * Commits the transaction + */ + public async commit() { + return await this.client.query("COMMIT"); + } + + /** + * Rolls back the transaction + */ + public async rollback() { + return await this.client.query("ROLLBACK"); + } + + /** + * Executes a query inside the transaction. + * @param query + */ + public async query(query: QueryConfig) { + return await this.client.query(query); + } + + /** + * Releases the client back to the pool. + */ + public async release() { + this.client.release(); + } +} + +export class QueryHelper { + private pool: Pool; + + constructor(pgPool: Pool) { + this.pool = pgPool; + } + + /** + * executes the sql query with values and returns all results. + * @param query + */ + public async all(query: QueryConfig): Promise { + const result = await this.query(query); + return result.rows; + } + + /** + * executes the sql query with values and returns the first result. + * @param query + */ + public async first(query: QueryConfig): Promise { + const result = await this.query(query); + if (result.rows && result.rows.length > 0) { + return result.rows[0]; + } + } + + /** + * Creates a new Transaction to be uses with error handling. + */ + public async createTransaction() { + const client: PoolClient = await this.pool.connect(); + return new SqlTransaction(client); + } + + /** + * Queries the database with error handling. + * @param query - the sql and values to execute + */ + private async query(query: QueryConfig): Promise { + try { + return await this.pool.query(query); + } catch (err) { + logger.debug(`Error on query "${query}".`); + logger.error(`Sql query failed: ${err}`); + logger.verbose(err.stack); + return { + rows: null, + }; + } + } +} + +/** + * Returns the parameterized value sql for inserting + * @param columnCount + * @param rowCount + * @param [offset] + */ +export function buildSqlParameters(columnCount: number, rowCount: number, offset?: number): string { + let sql = ""; + for (let i = 0; i < rowCount; i++) { + sql += "("; + for (let j = 0; j < columnCount; j++) { + sql += `$${(i * columnCount) + j + 1 + offset},`; + } + sql = sql.replace(/,$/, "") + "),"; + } + return sql.replace(/,$/, ""); +} diff --git a/src/lib/globals.ts b/src/lib/globals.ts new file mode 100644 index 0000000..ddd044a --- /dev/null +++ b/src/lib/globals.ts @@ -0,0 +1,25 @@ +import * as fsx from "fs-extra"; +import * as yaml from "js-yaml"; +import * as winston from "winston"; + +/** + * Defines global variables to be used. + */ +namespace globals { + export const config = yaml.safeLoad(fsx.readFileSync("config.yaml", "utf-8")); + export const logger = winston.createLogger({ + transports: [ + new winston.transports.Console({ + format: winston.format.combine( + winston.format.timestamp(), + winston.format.colorize(), + winston.format.printf(({ level, message, label, timestamp }) => { + return `${timestamp} [${label}] ${level}: ${message}`; + }), + ), + }), + ], + }); +} + +export default globals; diff --git a/tslint.json b/tslint.json index 21b8db5..78800e0 100644 --- a/tslint.json +++ b/tslint.json @@ -18,11 +18,14 @@ "no-console": { "severity": "warning", "options": ["debug", "info", "log", "time", "timeEnd", "trace"] - } + }, + "no-namespace": false, + "no-internal-module": false, + "max-classes-per-file": false }, "jsRules": { "max-line-length": { "options": [120] } } -} \ No newline at end of file +}