diff --git a/.gitignore b/.gitignore index 22f55ad..896ed30 100644 --- a/.gitignore +++ b/.gitignore @@ -32,4 +32,7 @@ lerna-debug.log* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json -!.vscode/extensions.json \ No newline at end of file +!.vscode/extensions.json + +# Environement +.env \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..d1f6f42 --- /dev/null +++ b/example.env @@ -0,0 +1,10 @@ + +# DO NOT COMMIT THIS FILE! +# 1. Validate that the .gitignore file includes .env +# 2. Update the below template with the correct values +# 3. Rename this file to .env + +DB_HOST=database_host_name +DB_PORT=database_port +DB_USER=database_user +DB_PASS=database_password \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 72cde42..577b1e7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,7 +16,9 @@ "@nestjs/graphql": "^12.0.1", "@nestjs/platform-express": "^10.0.0", "apollo-server-core": "^3.12.0", + "dotenv": "^16.3.1", "graphql": "^16.6.0", + "neo4j-driver": "^5.11.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "ts-morph": "^19.0.0" @@ -3582,7 +3584,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -4346,6 +4347,17 @@ "node": ">=6.0.0" } }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -5465,7 +5477,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -6844,6 +6855,62 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/neo4j-driver": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-5.11.0.tgz", + "integrity": "sha512-2IPKXH9najfKJyczIZ8R15p/oYsb4P+nwp76XRjO46Zl+ssc22+gMe/8FLRYw3tRJYc3b87ikx2s8ZNuseOAxQ==", + "dependencies": { + "neo4j-driver-bolt-connection": "5.11.0", + "neo4j-driver-core": "5.11.0", + "rxjs": "^7.8.1" + } + }, + "node_modules/neo4j-driver-bolt-connection": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-bolt-connection/-/neo4j-driver-bolt-connection-5.11.0.tgz", + "integrity": "sha512-jfptm6W/a4CIoip4S/KubxrPIIV3hdOJ8B5t2RtMJwVfup8uJFzRsQLW/ljg7PJdMiE1hHQ94/qcVKd3gCC3og==", + "dependencies": { + "buffer": "^6.0.3", + "neo4j-driver-core": "5.11.0", + "string_decoder": "^1.3.0" + } + }, + "node_modules/neo4j-driver-bolt-connection/node_modules/buffer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz", + "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.2.1" + } + }, + "node_modules/neo4j-driver-bolt-connection/node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/neo4j-driver-core": { + "version": "5.11.0", + "resolved": "https://registry.npmjs.org/neo4j-driver-core/-/neo4j-driver-core-5.11.0.tgz", + "integrity": "sha512-HIZrX1wIkwb1BlXtDk0thbyzYrlDKQK9PuzcgeKF9/fTORxr5K39kdIiwVi3gkoGOcFCSoBu+fTnlnav1BcgRg==" + }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", diff --git a/package.json b/package.json index e382eaf..df1960e 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,9 @@ "@nestjs/graphql": "^12.0.1", "@nestjs/platform-express": "^10.0.0", "apollo-server-core": "^3.12.0", + "dotenv": "^16.3.1", "graphql": "^16.6.0", + "neo4j-driver": "^5.11.0", "reflect-metadata": "^0.1.13", "rxjs": "^7.8.1", "ts-morph": "^19.0.0" diff --git a/src/app.module.ts b/src/app.module.ts index 5fdd51f..7ba8ba5 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -1,4 +1,5 @@ -import { Module, Post } from '@nestjs/common'; +import 'dotenv/config'; +import { Injectable, Logger, Module, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { AppController } from './app.controller'; import { AppService } from './app.service'; import { GraphQLModule } from '@nestjs/graphql'; @@ -8,6 +9,41 @@ import { MemberResolver } from './services/reputation/components/member/member.r import { PostResolver } from './services/reputation/components/post/post.resolver'; import { CitationsResolver } from './services/reputation/components/citation/citation.resolver'; +import { DatabaseConnector } from './util/database-connector'; +import { MemberRepository } from './services/reputation/components/member/member.repository'; +const dbConnector = new DatabaseConnector(new Logger('DatabaseConnector')); + +@Injectable() +export class StartupService implements OnModuleInit { + async onModuleInit() { + + const testSuite = process.env.TEST_SUITE; + if (testSuite?.toLowerCase().includes("integration")) return; + + const host = process.env.DB_HOST; + const port = process.env.DB_PORT; + const user = process.env.DB_USER; + const pass = process.env.DB_PASS; + + if (!host) throw new Error('Missing environment variable, DB_HOST'); + if (!port) throw new Error('Missing environment variable, DB_PORT'); + if (!user) throw new Error('Missing environment variable, DB_USER'); + if (!pass) throw new Error('Missing environment variable, DB_PASSWORD'); + + const options = {host, port, user, pass}; + await dbConnector.connect(options, [ + MemberRepository + ]); + } +} + +@Injectable() +export class ShutdownService implements OnModuleDestroy { + async onModuleDestroy() { + await dbConnector.disconnect(); + } +} + @Module({ imports: [ GraphQLModule.forRoot({ @@ -19,6 +55,14 @@ import { CitationsResolver } from './services/reputation/components/citation/cit }), ], controllers: [AppController], - providers: [AppService, MemberResolver, PostResolver, CitationsResolver], + providers: [ + AppService, + StartupService, + ShutdownService, + MemberResolver, + PostResolver, + CitationsResolver + ], }) + export class AppModule {} diff --git a/src/services/reputation/components/citation/citation.spec.ts b/src/services/reputation/components/citation/citation.spec.ts index 57791a6..79b02bd 100644 --- a/src/services/reputation/components/citation/citation.spec.ts +++ b/src/services/reputation/components/citation/citation.spec.ts @@ -110,6 +110,7 @@ describe('Rep Service Citation - Unit Tests', () => { describe ('Rep Service Citation - Integration Tests', () => { + process.env.TEST_SUITE = "Rep Service Citation - Integration Tests"; let testServer: ApolloTestServer; let createUser1 = `createMember(id: "testingUser1") { id }`; diff --git a/src/services/reputation/components/member/member.repository.ts b/src/services/reputation/components/member/member.repository.ts new file mode 100644 index 0000000..a275964 --- /dev/null +++ b/src/services/reputation/components/member/member.repository.ts @@ -0,0 +1,23 @@ +import { DatabaseConnector, Neo4jRepo } from "src/util/database-connector" + + +export class MemberRepository implements Neo4jRepo { + + private static instance: MemberRepository; + private static connector: DatabaseConnector; + + private constructor() { } + + public getInstance() { return MemberRepository.getInstance(); } + public static getInstance() { + if (!MemberRepository.instance) MemberRepository.instance = new MemberRepository(); + return MemberRepository.instance; + } + + public setConnector(connector: DatabaseConnector) { MemberRepository.setConnector(connector); } + public static setConnector(connector: DatabaseConnector) { + MemberRepository.connector = connector; + } + + +} \ No newline at end of file diff --git a/src/services/reputation/components/member/member.spec.ts b/src/services/reputation/components/member/member.spec.ts index a777624..feea78f 100644 --- a/src/services/reputation/components/member/member.spec.ts +++ b/src/services/reputation/components/member/member.spec.ts @@ -76,6 +76,7 @@ describe('Rep Service Member - Unit Tests', () => { describe('Rep Service Member - Integration Tests', () => { + process.env.TEST_SUITE = "Rep Service Member - Integration Tests"; let testServer: ApolloTestServer; let createUser1 = `createMember(id: "testingUser1") { id }`; diff --git a/src/services/reputation/components/post/post.spec.ts b/src/services/reputation/components/post/post.spec.ts index ce4514d..fde6264 100644 --- a/src/services/reputation/components/post/post.spec.ts +++ b/src/services/reputation/components/post/post.spec.ts @@ -170,6 +170,7 @@ describe('Rep Service Post - Unit Tests', () => { describe('Rep Service Post - Integration Tests', () => { + process.env.TEST_SUITE = "Rep Service Post - Integration Tests"; let testServer: ApolloTestServer; let createUser1 = `createMember(id: "testingUser1") { id }`; diff --git a/src/util/database-connector.ts b/src/util/database-connector.ts new file mode 100644 index 0000000..314d6c8 --- /dev/null +++ b/src/util/database-connector.ts @@ -0,0 +1,59 @@ + +import neo4j, { Driver } from 'neo4j-driver'; +import { Logger } from '@nestjs/common'; + +export interface DBOptions { + host: string; + port: string; + user: string; + pass: string; +} + +export interface Neo4jRepo { + getInstance(): Neo4jRepo; + setConnector(connector: DatabaseConnector): void +} + +export class DatabaseConnector { + + private driver: Driver; + private logger: Logger + + constructor(logger: Logger) { + this.logger = logger; + } + + async connect(options: DBOptions, repos: Neo4jRepo[]) { + const connectionString = `neo4j+s://${options.host}:${options.port}`; + this.logger.log(`Connection: ${connectionString}`); + const authentication = neo4j.auth.basic(options.user, options.pass); + try { + this.driver = neo4j.driver(connectionString, authentication); + const info = await this.driver.getServerInfo(); + if (!info) throw new Error('Unable to retrieve database info'); + this.logger.log(`Successfully connected to ${info.agent}`); + for (const repo of repos) { + this.logger.log(`Attaching to ${(repo).name}`); + repo.getInstance().setConnector(this); + } + } catch(error) { + this.logger.error(`Failed to connect to Database - ${error.message}`); + throw error; + } + } + + async disconnect() { + this.logger.log('Closing database connection...'); + if (this.driver) await this.driver.close(); + } + + async runQuery(query: string, parameters: any = {}) { + const session = this.driver.session(); + try { + const result = await session.writeTransaction(tx => tx.run(query, parameters)); + return result.records; + } finally { + session.close(); + } + } +} \ No newline at end of file