diff --git a/database/ignite/ignite.js b/database/ignite/ignite.js new file mode 100644 index 00000000..549919e2 --- /dev/null +++ b/database/ignite/ignite.js @@ -0,0 +1,76 @@ +const IgniteClient = require("apache-ignite-client"); +const logger = require("./../../lib/log")(__filename); +const { SqlFieldsQuery } = require("apache-ignite-client/lib/Query"); +const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration; +const CacheConfiguration = IgniteClient.CacheConfiguration; +require("dotenv").config(); +const igniteModule = {}; +let igniteClient; +igniteModule.startIgnite = async () => { + try { + igniteClient = new IgniteClient(); + const configuration = new IgniteClientConfiguration(process.env.IGNITE_HOST) + .setUserName(process.env.IGNITE_USER) + .setPassword(process.env.IGNITE_PASSWORD); + await igniteClient.connect(configuration); + logger.info("Connected to Ignite database"); + } catch (err) { + logger.error(err); + throw new Error(err); + } +}; +igniteModule.checkAccount = async (username) => { + if (!username) return; + try { + const caches = await igniteClient.cacheNames(); + return caches.includes(username); + } catch (err) { + logger.error(`Error checking ignite database for ${username}`); + throw new Error(err); + } +}; +igniteModule.closeIgnite = async () => { + try { + await igniteClient.disconnect(); + logger.info("Closed connection to Ignite database"); + } catch (err) { + logger.error(`Error closing connection to Ignite database`); + throw new Error(err); + } +}; + +igniteModule.createAccount = async (user) => { + const { username, dbPassword } = user; + if (!username || !dbPassword) return; + try { + const cache = await igniteClient.getOrCreateCache( + username, + new CacheConfiguration().setSqlSchema("PUBLIC") + ); + await cache.query( + new SqlFieldsQuery( + `CREATE USER "${username}" WITH PASSWORD '${dbPassword}'` + ) + ); + logger.info(`Successfully created ${username} cache`); + } catch (err) { + logger.error(`Error creating ignite ${username}`); + throw new Error(err); + } +}; +igniteModule.deleteAccount = async (username) => { + if (!username) return; + try { + const cache = await igniteClient.getOrCreateCache( + username, + new CacheConfiguration().setSqlSchema("PUBLIC") + ); + await cache.query(new SqlFieldsQuery(`DROP USER "${username}"`)); + await igniteClient.destroyCache(username); + logger.info(`Successfully deleted ${username} cache`); + } catch (err) { + logger.error(`Error deleting ignite ${username}`); + throw new Error(err); + } +}; +module.exports = igniteModule; diff --git a/database/ignite/ignite.test.js b/database/ignite/ignite.test.js new file mode 100644 index 00000000..b5a4e5c7 --- /dev/null +++ b/database/ignite/ignite.test.js @@ -0,0 +1,110 @@ +jest.mock("apache-ignite-client"); +jest.mock("../../lib/log"); +const logGen = require("../../lib/log"); +const logger = { + info: jest.fn(), + error: jest.fn(), +}; +logGen.mockReturnValue(logger); +require("dotenv").config(); +const { + startIgnite, + createAccount, + deleteAccount, + checkAccount, + closeIgnite, +} = require("./ignite"); + +const IgniteClient = require("apache-ignite-client"); +const mockClient = { + cacheNames: jest.fn(), + connect: jest.fn(), + disconnect: jest.fn(), + getOrCreateCache: jest.fn().mockReturnThis(), + query: jest.fn(), + destroyCache: jest.fn(), +}; +IgniteClient.mockImplementation(() => mockClient); +const mockConfig = { + setUserName: jest.fn().mockReturnThis(), + setPassword: jest.fn().mockReturnThis(), +}; +IgniteClient.IgniteClientConfiguration.mockImplementation(() => mockConfig); +describe("IgniteDb functions", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + const user = { username: "username", dbPassword: "password" }; + it("should start Ignite client", () => { + startIgnite(); + expect(IgniteClient).toHaveBeenCalledTimes(1); + expect(mockClient.connect).toHaveBeenCalledTimes(1); + expect(IgniteClient.IgniteClientConfiguration).toHaveBeenCalledTimes(1); + expect(mockConfig.setUserName.mock.calls[0][0]).toEqual("ignite"); + expect(mockConfig.setPassword.mock.calls[0][0]).toEqual("ignite"); + }); + it("should throw an error if starting client goes wrong", async () => { + try { + mockClient.connect.mockReturnValue(Promise.reject()); + expect(await startIgnite()).rejects.toThrow(); + } catch (err) { + expect(logger.error).toHaveBeenCalledTimes(1); + } + }); + it("should close ignite client", () => { + closeIgnite(); + expect(mockClient.disconnect).toHaveBeenCalledTimes(1); + }); + it("should call logger error if closing client goes wrong", async () => { + try { + mockClient.disconnect.mockReturnValue(Promise.reject()); + await closeIgnite(); + } catch (err) { + expect(logger.error).toHaveBeenCalledTimes(1); + } + }); + it("should check for existsting accounts", async () => { + mockClient.cacheNames.mockReturnValue(["testName", "", "username"]); + const result = await checkAccount(user.username); + expect(mockClient.cacheNames).toHaveBeenCalledTimes(1); + expect(result).toEqual(true); + }); + it("should call logger error if checking for account goes wrong", async () => { + try { + mockClient.cacheNames.mockReturnValue(Promise.reject()); + await checkAccount(user.username); + } catch (err) { + expect(logger.error).toHaveBeenCalledTimes(1); + } + }); + it("should create new account", async () => { + await createAccount(user); + expect(mockClient.getOrCreateCache.mock.calls[0][0]).toEqual(user.username); + }); + it("should delete user", async () => { + await deleteAccount(user.username); + expect(mockClient.getOrCreateCache.mock.calls[0][0]).toEqual(user.username); + expect(mockClient.destroyCache.mock.calls[0][0]).toEqual(user.username); + }); + it("should call logger error if creating a user goes wrong", async () => { + try { + mockClient.getOrCreateCache.mockReturnValue(Promise.reject()); + expect(await createAccount(user)).rejects.toThrow(); + } catch (err) { + expect(logger.error).toHaveBeenCalledTimes(1); + } + }); + it("should return undefined if no arguments were provided", async () => { + expect(await createAccount({})).toEqual(undefined); + expect(await checkAccount("")).toEqual(undefined); + expect(await deleteAccount("")).toEqual(undefined); + }); + it("should calll logger error if deleting user goes wrong", async () => { + try { + mockClient.getOrCreateCache.mockReturnValue(Promise.reject()); + await deleteAccount(user.username); + } catch (err) { + expect(logger.error).toHaveBeenCalledTimes(1); + } + }); +}); diff --git a/database/ignite/igniteConfig.xml b/database/ignite/igniteConfig.xml new file mode 100644 index 00000000..076e17a6 --- /dev/null +++ b/database/ignite/igniteConfig.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/index.js b/index.js index 94e2ad62..d94f7874 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,3 @@ -const {startServer} = require('./src/server') +const { startServer } = require("./src/server"); -startServer(process.env.PORT || 3052) +startServer(process.env.PORT || 3052); diff --git a/lib/log.js b/lib/log.js index cb379c57..f8f0d47c 100644 --- a/lib/log.js +++ b/lib/log.js @@ -1,42 +1,40 @@ -const {createLogger, format, transports} = require('winston') -const util = require('util') +const { createLogger, format, transports } = require("winston"); +const util = require("util"); module.exports = (file) => { - const logger = createLogger({ - format: format.combine( - format.timestamp({format:'YYYY-MM-DD HH:mm:ss'}), - format.label({ - label: file.split(/(\\|\/)/g).pop() - }), - format.colorize(), - format.printf((info) => { - return `${info.timestamp} ${info.level} [${info.label}]: ${info.message}` - }) - ), - transports : [ - new transports.Console() - ] - }) + const logger = createLogger({ + format: format.combine( + format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }), + format.label({ + label: file.split(/(\\|\/)/g).pop(), + }), + format.colorize(), + format.printf((info) => { + return `${info.timestamp} ${info.level} [${info.label}]: ${info.message}`; + }) + ), + transports: [new transports.Console()], + }); - const processArgs = (args) => { - const mappedArgs = args.map((e) => { - if (e instanceof Error) return e.toString() - return e - }) - return util.format('%j', mappedArgs) - } + const processArgs = (args) => { + const mappedArgs = args.map((e) => { + if (e instanceof Error) return e.toString(); + return e; + }); + return util.format("%j", mappedArgs); + }; - const obj = { - info: (...args) => { - logger.info(processArgs(args)) - }, - warn: (...args) => { - logger.warn(processArgs(args)) - }, - error: (...args) => { - logger.error(processArgs(args)) - } - } + const obj = { + info: (...args) => { + logger.info(processArgs(args)); + }, + warn: (...args) => { + logger.warn(processArgs(args)); + }, + error: (...args) => { + logger.error(processArgs(args)); + }, + }; - return obj -} \ No newline at end of file + return obj; +}; diff --git a/lib/log.test.js b/lib/log.test.js index 1be6c690..e6dbdc6b 100644 --- a/lib/log.test.js +++ b/lib/log.test.js @@ -1,46 +1,48 @@ -const winston = require('winston') -const util = require('util') +const winston = require("winston"); +const util = require("util"); winston.createLogger = jest.fn().mockReturnValue({ - info: () => {}, - warn: () => {}, - error: () => {} -}) + info: () => {}, + warn: () => {}, + error: () => {}, +}); winston.format = { - combine: () => {}, - timestamp: () => {}, - label: () => {}, - colorize: () => {}, - printf: jest.fn() -} + combine: () => {}, + timestamp: () => {}, + label: () => {}, + colorize: () => {}, + printf: jest.fn(), +}; -util.format = jest.fn() +util.format = jest.fn(); -describe('Testing log.js', () => { - beforeEach(() => { - jest.clearAllMocks() - }) - it('should call util.format everytime when log.xxx is called', () => { - const logger = require('./log')(__filename) - logger.info('message', [0,1,2]) - logger.warn('message', {obj: () => {}}) - logger.error('message', new Error('error message')) - expect(util.format).toHaveBeenCalled() - }) - it('should call winston.createLogger when log is required', () => { - const logger = require('./log')(__filename) - expect(winston.createLogger).toHaveBeenCalled() - }) - it('should return log string correctly', () => { - const logger = require('./log')(__filename) - const argInPrintf = winston.format.printf.mock.calls[0][0] - const testData = { - timestamp: "2020-04-30 22:43:00", - level: "info", - label: "log.test.js", - message: "message" - } - expect(argInPrintf(testData)).toBe("2020-04-30 22:43:00 info [log.test.js]: message") - }) -}) +describe("Testing log.js", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it("should call util.format everytime when log.xxx is called", () => { + const logger = require("./log")(__filename); + logger.info("message", [0, 1, 2]); + logger.warn("message", { obj: () => {} }); + logger.error("message", new Error("error message")); + expect(util.format).toHaveBeenCalled(); + }); + it("should call winston.createLogger when log is required", () => { + const logger = require("./log")(__filename); + expect(winston.createLogger).toHaveBeenCalled(); + }); + it("should return log string correctly", () => { + const logger = require("./log")(__filename); + const argInPrintf = winston.format.printf.mock.calls[0][0]; + const testData = { + timestamp: "2020-04-30 22:43:00", + level: "info", + label: "log.test.js", + message: "message", + }; + expect(argInPrintf(testData)).toBe( + "2020-04-30 22:43:00 info [log.test.js]: message" + ); + }); +}); diff --git a/lib/util.js b/lib/util.js index 42f678f7..df79207c 100644 --- a/lib/util.js +++ b/lib/util.js @@ -3,6 +3,7 @@ const db = require("../sequelize/db"); const pg = require("../database/postgres/pg"); const arango = require("../database/arango/arango"); const es = require("../database/elasticsearch/elastic"); +const ignite = require("../database/ignite/ignite"); const logger = require("./log")(__filename); const util = {}; @@ -37,6 +38,9 @@ util.cleanAnonymous = async () => { const arangoDbExists = await arango.checkIfDatabaseExists(username); if (arangoDbExists) await arango.deleteAccount(username); + const igniteDbExists = await ignite.checkAccount(username); + if (igniteDbExists) await ignite.deleteAccount(username); + return await user.destroy(); }) ).then(() => { diff --git a/lib/util.test.js b/lib/util.test.js index 1246099d..cbdc4006 100644 --- a/lib/util.test.js +++ b/lib/util.test.js @@ -3,6 +3,7 @@ jest.mock("../sequelize/db"); jest.mock("../database/postgres/pg"); jest.mock("../database/arango/arango"); jest.mock("../database/elasticsearch/elastic"); +jest.mock("../database/ignite/ignite"); jest.mock("./log"); const sequelize = require("sequelize"); @@ -10,6 +11,7 @@ const db = require("../sequelize/db"); const pg = require("../database/postgres/pg"); const es = require("../database/elasticsearch/elastic"); const arango = require("../database/arango/arango"); +const ignite = require("../database/ignite/ignite"); sequelize.Op = { and: "and", lt: "lt" }; const Accounts = { @@ -21,6 +23,7 @@ db.getModels = () => { pg.deletePgAccount = jest.fn(); es.deleteAccount = jest.fn(); arango.deleteAccount = jest.fn(); +ignite.deleteAccount = jest.fn(); const logGen = require("./log"); const logger = { info: jest.fn(), @@ -58,16 +61,19 @@ describe("Testing cleanAnonymous function", () => { expect(pg.deletePgAccount).not.toHaveBeenCalled(); expect(es.deleteAccount).not.toHaveBeenCalled(); expect(arango.deleteAccount).not.toHaveBeenCalled(); + expect(ignite.deleteAccount).not.toHaveBeenCalled(); }); it("should call database functions if expired accounts are found", async () => { Accounts.findAll.mockReturnValue([{ destroy: () => {} }]); pg.userHasPgAccount = () => true; es.checkAccount = () => true; arango.checkIfDatabaseExists = () => true; + ignite.checkAccount = () => true; await util.cleanAnonymous(); expect(pg.deletePgAccount).toHaveBeenCalled(); expect(es.deleteAccount).toHaveBeenCalled(); expect(arango.deleteAccount).toHaveBeenCalled(); + expect(ignite.deleteAccount).toHaveBeenCalled(); }); it("should call logger.error if cleaning fails", async () => { Accounts.findAll.mockImplementation(() => { diff --git a/package-lock.json b/package-lock.json index de6ce7c2..8b11a351 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1590,6 +1590,15 @@ "picomatch": "^2.0.4" } }, + "apache-ignite-client": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/apache-ignite-client/-/apache-ignite-client-1.0.0.tgz", + "integrity": "sha512-rWSDd/PArxkmhQr0e0e89RfPt/AU3j1B+g+vVNTE4qKAIi5zssm6CTot7tRLRTcuKGvU+yVxqyzmwtVzwFiklQ==", + "requires": { + "decimal.js": "^10.2.1", + "long": "^4.0.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -3074,6 +3083,11 @@ "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true }, + "decimal.js": { + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.2.1.tgz", + "integrity": "sha512-KaL7+6Fw6i5A2XSnsbhm/6B+NuEA7TZ4vqxnd5tXz9sbKtrN9Srj8ab4vKVdK8YAqZO9P1kg45Y6YLoduPf+kw==" + }, "decode-uri-component": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", @@ -5982,6 +5996,11 @@ "@sinonjs/commons": "^1.7.0" } }, + "long": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", + "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" + }, "lowercase-keys": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", diff --git a/package.json b/package.json index 7ecf1be8..3bf6dfd8 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ }, "dependencies": { "@types/node-fetch": "^2.5.7", + "apache-ignite-client": "^1.0.0", "arangojs": "^7.0.2", "base-x": "^3.0.8", "bcrypt": "^5.0.0", diff --git a/public/lessons/Ignite.md b/public/lessons/Ignite.md new file mode 100644 index 00000000..c4962c00 --- /dev/null +++ b/public/lessons/Ignite.md @@ -0,0 +1,59 @@ + +# Intro + +In-memory database is a database which relies primarily on memory for data storage. + +Pros: +* Performance. RAM is way faster than any disks IOs + +Cons: +* Data persistence can be tricky + +Some datasets could be stored entirely in memory but most often in-memory databases are used as caches. + +# Apache Ignite + +Ignite is an open source distributed in-memory database. It can be used as cache, data grid or key-value store. In our case we will use the latter. + +Key-value store is the simplest data model, one unique key which is mapped to some value. With no schema limitations writes are very fast, but since database doesn't know anything about the value you can't use indexes to speed up searches. + +# Client + +Ignite provides their own node.js client [apache-ignite-client](https://www.npmjs.com/package/apache-ignite-client). + +To connect to database: +``` +const IgniteClient = require("apache-ignite-client"); +const IgniteClientConfiguration = IgniteClient.IgniteClientConfiguration; + +const igniteClientConfiguration = new IgniteClientConfiguration("host") + .setUserName("username") + .setPassword("password"); +const connect = async () => { + const igniteClient = new IgniteClient(); + await igniteClient.connect(igniteClientConfiguration); +}; +connect(); +``` + +To add values into cache you need to connect to cache, specify data type and its key: + +``` +const ObjectType = IgniteClient.ObjectType; +const basicOperations = async () => { + const igniteClient = new IgniteClient(); + await igniteClient.connect(igniteClientConfiguration); + console.log("successfully connected"); + const cache = (await igniteClient.getOrCreateCache("username")).setKeyType( + ObjectType.PRIMITIVE_TYPE.INTEGER + ); + await cache.put(1, "hello world"); + console.log(await cache.get(1)); + igniteClient.disconnect(); +}; +basicOperations(); +``` + +You can find more examples [here](https://github.com/apache/ignite/tree/master/modules/platforms/nodejs/examples). + +*Under construction* \ No newline at end of file diff --git a/sequelize/db.test.js b/sequelize/db.test.js index d9228c19..9bbcc0eb 100644 --- a/sequelize/db.test.js +++ b/sequelize/db.test.js @@ -1,65 +1,65 @@ -jest.mock('sequelize') -jest.mock('../lib/log') -const { Sequelize } = require('sequelize') +jest.mock("sequelize"); +jest.mock("../lib/log"); +const { Sequelize } = require("sequelize"); -const logGen = require('../lib/log') +const logGen = require("../lib/log"); const logger = { error: jest.fn(), - info: jest.fn() -} -logGen.mockReturnValue(logger) + info: jest.fn(), +}; +logGen.mockReturnValue(logger); const mockSequelize = { authenticate: jest.fn().mockReturnValue(Promise.resolve()), define: jest.fn(), sync: jest.fn().mockReturnValue(Promise.resolve()), - close: jest.fn().mockReturnValue(Promise.resolve()) -} + close: jest.fn().mockReturnValue(Promise.resolve()), +}; Sequelize.mockImplementation(function () { - return mockSequelize -}) + return mockSequelize; +}); -const { start, update, close, getModels, Account } = require('./db') +const { start, update, close, getModels, Account } = require("./db"); -describe('Test sequelize', () => { +describe("Test sequelize", () => { beforeEach(() => { - jest.clearAllMocks() - start() - }) - it('should test how many times authenticate is called', () => { - expect(logger.info).toHaveBeenCalledTimes(3) - expect(mockSequelize.define).toHaveBeenCalledTimes(1) - expect(mockSequelize.authenticate).toHaveBeenCalledTimes(1) - }) - it('should test how many times close is called', () => { - close() - expect(logger.info).toHaveBeenCalledTimes(4) - expect(mockSequelize.close).toHaveBeenCalledTimes(1) - }) - it('should test throw on sequelize authentication', async () => { + jest.clearAllMocks(); + start(); + }); + it("should test how many times authenticate is called", () => { + expect(logger.info).toHaveBeenCalledTimes(3); + expect(mockSequelize.define).toHaveBeenCalledTimes(1); + expect(mockSequelize.authenticate).toHaveBeenCalledTimes(1); + }); + it("should test how many times close is called", () => { + close(); + expect(logger.info).toHaveBeenCalledTimes(4); + expect(mockSequelize.close).toHaveBeenCalledTimes(1); + }); + it("should test throw on sequelize authentication", async () => { try { - await mockSequelize.authenticate.mockReturnValue(Promise.reject()) - const resStart = await start() - expect(resStart).rejects.toThrow() + await mockSequelize.authenticate.mockReturnValue(Promise.reject()); + const resStart = await start(); + expect(resStart).rejects.toThrow(); } catch (err) { - expect(logger.error).toHaveBeenCalledTimes(1) + expect(logger.error).toHaveBeenCalledTimes(1); } - }) -}) + }); +}); -describe('Test models', () => { +describe("Test models", () => { beforeEach(() => { - jest.clearAllMocks() - }) - it('should get models when get models function is called', async () => { - mockSequelize.authenticate.mockReturnValue(Promise.resolve()) - mockSequelize.define.mockReturnValue({ name: 'ACCTest' }) - await start() - const models = getModels() + jest.clearAllMocks(); + }); + it("should get models when get models function is called", async () => { + mockSequelize.authenticate.mockReturnValue(Promise.resolve()); + mockSequelize.define.mockReturnValue({ name: "ACCTest" }); + await start(); + const models = getModels(); return expect(models).toEqual({ Accounts: { - name: 'ACCTest' - } - }) - }) -}) + name: "ACCTest", + }, + }); + }); +}); diff --git a/services/mailer.js b/services/mailer.js index 6e89e730..c8f17615 100644 --- a/services/mailer.js +++ b/services/mailer.js @@ -10,7 +10,9 @@ const mg = mailgun({ const mgModule = {}; mgModule.sendPasswordResetEmail = (receiver, token) => { - const link = `${process.env.HOSTNAME||"https://learndatabases.dev"}/setPassword/${token}` + const link = `${ + process.env.HOSTNAME || "https://learndatabases.dev" + }/setPassword/${token}`; const data = { from: "admin@learndatabases.dev", to: receiver, @@ -22,7 +24,11 @@ mgModule.sendPasswordResetEmail = (receiver, token) => {

You have requested a (re)set password token. The button below will redirect you to our website with an autheticated token. Please click the button and set your password.

- Set my Password ${link!=`https://learndatabases.dev/setPassword/${token}`?"

DEVELOPMENT MODE IS ON. This link will redirect you to your development server

" : ""} + Set my Password ${ + link != `https://learndatabases.dev/setPassword/${token}` + ? "

DEVELOPMENT MODE IS ON. This link will redirect you to your development server

" + : "" + }

Warning: Anyone with access to this email has access to your account. Don't share this email with other people.

diff --git a/services/mailer.test.js b/services/mailer.test.js index c63e3f4b..14af3497 100644 --- a/services/mailer.test.js +++ b/services/mailer.test.js @@ -1,57 +1,65 @@ -jest.mock('../lib/log') +jest.mock("../lib/log"); require("dotenv").config(); -const logGen = require('../lib/log') +const logGen = require("../lib/log"); const logger = { - info: jest.fn(), - error: jest.fn() -} -logGen.mockReturnValue(logger) + info: jest.fn(), + error: jest.fn(), +}; +logGen.mockReturnValue(logger); -jest.mock('mailgun-js') -const mailgun = require('mailgun-js'); -const messages = {} +jest.mock("mailgun-js"); +const mailgun = require("mailgun-js"); +const messages = {}; mailgun.mockImplementation(() => { - return { - messages: () => { - return messages - } - } -}) + return { + messages: () => { + return messages; + }, + }; +}); -const email = require('./mailer'); +const email = require("./mailer"); -describe('Test mailgun', () => { - beforeEach(() => { - jest.clearAllMocks() - }) +describe("Test mailgun", () => { + beforeEach(() => { + jest.clearAllMocks(); + }); - it('should test if mocksend and mailgun is called', async () => { - process.env.HOSTNAME = "" - messages.send = jest.fn().mockReturnValue(Promise.resolve('hello')) - await email.sendPasswordResetEmail('paul@github.com', 'token123') - expect(messages.send).toHaveBeenCalledTimes(1) - expect(messages.send.mock.calls[0][0]).toMatchSnapshot() - expect(logger.info).toHaveBeenCalledTimes(1) - expect(logger.info.mock.calls[0][0]).toEqual('Confirmation Email successfully sent') - }) + it("should test if mocksend and mailgun is called", async () => { + process.env.HOSTNAME = ""; + messages.send = jest.fn().mockReturnValue(Promise.resolve("hello")); + await email.sendPasswordResetEmail("paul@github.com", "token123"); + expect(messages.send).toHaveBeenCalledTimes(1); + expect(messages.send.mock.calls[0][0]).toMatchSnapshot(); + expect(logger.info).toHaveBeenCalledTimes(1); + expect(logger.info.mock.calls[0][0]).toEqual( + "Confirmation Email successfully sent" + ); + }); - it('should call logger.error when function is called with invalid argument', async () => { - process.env.HOSTNAME = "" - messages.send = jest.fn().mockReturnValue(Promise.reject('rejected')) - await email.sendPasswordResetEmail(null, null) - expect(logger.error).toHaveBeenCalledTimes(1) - expect(logger.error.mock.calls[0][0]).toEqual('Confirmation Email Error:') - }) - it('should notify that development mode is on in confirmation email', async () => { - process.env.HOSTNAME= "http://localhost:4000" - messages.send = jest.fn().mockReturnValue(Promise.resolve('hello')) - await email.sendPasswordResetEmail('paul@github.com', 'token123') - expect(messages.send.mock.calls[0][0].html.includes('DEVELOPMENT MODE IS ON')).toEqual(true) - }) - it('should redirect to learndatabases.dev in confirmation email if no HOSTNAME was provided in .env file', async () => { - process.env.HOSTNAME = "" - messages.send = jest.fn().mockReturnValue(Promise.resolve('hello')) - await email.sendPasswordResetEmail('paul@github.com', 'token123') - expect(messages.send.mock.calls[0][0].html.includes('https://learndatabases.dev/setPassword')).toEqual(true) - }) -}) + it("should call logger.error when function is called with invalid argument", async () => { + process.env.HOSTNAME = ""; + messages.send = jest.fn().mockReturnValue(Promise.reject("rejected")); + await email.sendPasswordResetEmail(null, null); + expect(logger.error).toHaveBeenCalledTimes(1); + expect(logger.error.mock.calls[0][0]).toEqual("Confirmation Email Error:"); + }); + it("should notify that development mode is on in confirmation email", async () => { + process.env.HOSTNAME = "http://localhost:4000"; + messages.send = jest.fn().mockReturnValue(Promise.resolve("hello")); + await email.sendPasswordResetEmail("paul@github.com", "token123"); + expect( + messages.send.mock.calls[0][0].html.includes("DEVELOPMENT MODE IS ON") + ).toEqual(true); + }); + it("should redirect to learndatabases.dev in confirmation email if no HOSTNAME was provided in .env file", async () => { + process.env.HOSTNAME = ""; + messages.send = jest.fn().mockReturnValue(Promise.resolve("hello")); + await email.sendPasswordResetEmail("paul@github.com", "token123"); + expect( + messages.send.mock.calls[0][0].html.includes( + "https://learndatabases.dev/setPassword" + ) + ).toEqual(true); + }); +}); diff --git a/src/routes/renderRoutes.js b/src/routes/renderRoutes.js index b4a02438..457e2f4f 100644 --- a/src/routes/renderRoutes.js +++ b/src/routes/renderRoutes.js @@ -2,6 +2,7 @@ const db = require("../../sequelize/db"); const es = require("../../database/elasticsearch/elastic"); const pg = require("../../database/postgres/pg"); const arangoModule = require("../../database/arango/arango"); +const igniteModule = require("../../database/ignite/ignite"); require("dotenv").config(); const routes = {}; @@ -10,6 +11,7 @@ const dev_dbHost = { Postgres: process.env.HOST, Elasticsearch: process.env.ES_HOST, Arango: process.env.ARANGO_URL, + Ignite: process.env.IGNITE_HOST, }; // This is the 'host' url for a person's database credentials in prod. @@ -17,12 +19,14 @@ const dbHost = { Postgres: "learndatabases.dev", Elasticsearch: "elastic.learndatabases.dev", Arango: "arangodb.learndatabases.dev", + Ignite: "ADD_IGNITE_HOST", }; const checkAccount = { Postgres: pg.userHasPgAccount, Elasticsearch: es.checkAccount, Arango: arangoModule.checkIfDatabaseExists, + Ignite: igniteModule.checkAccount, }; const prod = () => { diff --git a/src/routes/userRoutes.js b/src/routes/userRoutes.js index a00191e6..627481c5 100644 --- a/src/routes/userRoutes.js +++ b/src/routes/userRoutes.js @@ -10,6 +10,7 @@ const db = require("../../sequelize/db"); const pgModule = require("../../database/postgres/pg"); const es = require("../../database/elasticsearch/elastic"); const arangoModule = require("../../database/arango/arango"); +const igniteModule = require("../../database/ignite/ignite"); const routes = {}; routes.resetPasswordEmail = async (req, res) => { @@ -143,6 +144,7 @@ const createDatabaseAccount = { Postgres: pgModule.userHasPgAccount, Elasticsearch: es.createAccount, Arango: arangoModule.createAccount, + Ignite: igniteModule.createAccount, }; routes.createDatabase = async (req, res) => { diff --git a/src/server.js b/src/server.js index d53d710d..cd6033ef 100644 --- a/src/server.js +++ b/src/server.js @@ -4,6 +4,7 @@ const util = require("../lib/util"); const dbModule = require("../sequelize/db"); const session = require("express-session"); const pgModule = require("../database/postgres/pg"); +const igniteModule = require("../database/ignite/ignite"); const { resetPasswordEmail, createUser, @@ -30,6 +31,7 @@ const startServer = async (portNumber) => { await dbModule.start(); await pgModule.startPGDB(); await arangoModule.startArangoDB(); + await igniteModule.startIgnite(); cleaner = await util.cleanAnonymous(); @@ -89,6 +91,7 @@ const stopServer = () => { dbModule.close(); pgModule.closePGDB(); arangoModule.closeArangoDB(); + igniteModule.closeIgnite(); logger.info("DB has been closed"); server.close(() => { logger.info("The server has been closed"); diff --git a/tests/integration/__snapshots__/welcome.test.js.snap b/tests/integration/__snapshots__/welcome.test.js.snap index a2604270..ac6ad3e9 100644 --- a/tests/integration/__snapshots__/welcome.test.js.snap +++ b/tests/integration/__snapshots__/welcome.test.js.snap @@ -91,6 +91,7 @@ exports[`test welcome page should render arango page correctly 1`] = ` Postgres: \`database: \${username}\`, Elasticsearch: \`index: \${username}-*\`, Arango: \`databaseName: \${username}\`, + Ignite: \`cache: \${username}\` }; credentials.innerHTML = \`\${creds}\${endingChoice[database]}\`; introduction.append(credentials); @@ -281,6 +282,198 @@ exports[`test welcome page should render elasticsearch page correctly 1`] = ` Postgres: \`database: \${username}\`, Elasticsearch: \`index: \${username}-*\`, Arango: \`databaseName: \${username}\`, + Ignite: \`cache: \${username}\` + }; + credentials.innerHTML = \`\${creds}\${endingChoice[database]}\`; + introduction.append(credentials); + }; + + const content = document.getElementById(\\"content\\"); + const renderContent = async (username, dbPassword) => { + const r = await fetch(\`/lessons/\${database}.md\`).then((r) => r.text()); + marked.setOptions({ + highlight: function (code) { + return hljs.highlightAuto(code).value; + }, + }); + const markedHTML = marked(r); + const markedHTMLwithUserData = markedHTML + .replace(/@username/gi, username) + .replace(/@dbPassword/gi, dbPassword); + content.innerHTML = markedHTMLwithUserData; + }; + + let disabled; + const createDb = async () => { + if (disabled) return; + disabled = true; + createDb_button.classList.add(\\"disabled\\"); + createDb_button.innerText = \\"Creating...\\"; + + const res = await fetch(\`/api/createDatabase/\${database}\`, { + method: \\"POST\\", + }).then((r) => r.json()); + const { error, username, dbPassword, email } = res; + if (error) { + alert(error.message); + disabled = false; + createDb_button.classList.remove(\\"disabled\\"); + createDb_button.innerText = \\"Create my database\\"; + return; + } + alert( + email + ? \\"Your database has been successfully created!\\" + : \\"Your temporary database has been successfully created!\\\\r\\\\nSince credentials are provided only once, please keep the information somewhere safe.\\" + ); + createDb_button.style.opacity = 0; + setTimeout(() => { + createDb_button.innerText = \\"Complete!\\"; + createDb_button.style.background = \\"rgb(10, 170, 75)\\"; + createDb_button.style.opacity = 1; + setTimeout(() => { + renderCredentials(username, dbPassword); + renderContent(username, dbPassword); + }, 200); + }, 200); + }; + + const createDb_button = document.createElement(\\"button\\"); + createDb_button.id = \\"createDb\\"; + createDb_button.innerText = \\"Create my database\\"; + createDb_button.addEventListener(\\"click\\", createDb); + + (function setIntroduction() { + const vowelsTable = [\\"A\\", \\"E\\", \\"I\\", \\"O\\", \\"U\\"].reduce((acc, vowel) => { + acc[vowel] = true; + return acc; + }, {}); + const a = \`a\${vowelsTable[database[0]] ? \\"n\\" : \\"\\"}\`; + const introduction = document.getElementById(\\"introduction\\"); + const messageOpening = \`

In this module, you can learn how to use \${database}. \`; + if (!username || !dbExists) { + const tense = !username ? \\"temporary \\" : \\"\\"; + const messageMiddle = \`Don't have \${database} installed on your local machine yet? Fear not! We can make \${a} \${database} database for you. Simply click on the button below to create your \${tense}database. Credentials for your database will appear after it is done being created.

\`; + const messageEnding = !username + ? \`

Temporary database credentials will expire in 5 days. If you want, you can sign up with us and get a non-expiring \${database} database!

\` + : \\"\\"; + introduction.innerHTML = \`\${messageOpening}\${messageMiddle}\${messageEnding}\`; + introduction.append(createDb_button); + return renderContent(\\"username\\", \\"dbPassword\\"); + } + introduction.innerHTML = \`\${messageOpening}You have already created \${a} \${database} database on our server. Here are the credentials for your database.

\`; + renderCredentials(username, dbPassword); + renderContent(username, dbPassword); + })(); + + +" +`; + +exports[`test welcome page should render ignite page correctly 1`] = ` +" + + + + + + + + + learndatabases.dev + +
+

Learn Databases

+ + +

/

+ +
+ + + +

Learn Ignite

+
+
+ + + + diff --git a/tests/integration/welcome.test.js b/tests/integration/welcome.test.js index 32a522dc..18838691 100644 --- a/tests/integration/welcome.test.js +++ b/tests/integration/welcome.test.js @@ -78,4 +78,11 @@ describe("test welcome page", () => { ); expect(result).toMatchSnapshot(); }); + test("should render ignite page correctly", async () => { + process.env = { ...process.env, NODE_ENV: "CI" }; + const result = await fetch(baseUrl + "tutorial/Ignite").then((r) => + r.text() + ); + expect(result).toMatchSnapshot(); + }); }); diff --git a/views/tutorial.ejs b/views/tutorial.ejs index 8fba84d3..07377543 100644 --- a/views/tutorial.ejs +++ b/views/tutorial.ejs @@ -33,6 +33,7 @@ Postgres: `database: ${username}`, Elasticsearch: `index: ${username}-*`, Arango: `databaseName: ${username}`, + Ignite: `cache: ${username}` }; credentials.innerHTML = `${creds}${endingChoice[database]}`; introduction.append(credentials); diff --git a/views/welcome.ejs b/views/welcome.ejs index 9095bbc5..d0a41039 100644 --- a/views/welcome.ejs +++ b/views/welcome.ejs @@ -18,6 +18,10 @@

Arango

A multi-model database. Whether you want to store data in tables or graphs is up to you!

+
+

Apache Ignite

+

In-memory database

+

MongoDB (coming next sprint!)

An open source database management system using a document-oriented database model that supports various forms of data.

@@ -77,6 +81,9 @@ // }) document.getElementById('arango').addEventListener('click', () => { location.href = '/tutorial/Arango' + }) + document.getElementById('ignite').addEventListener('click', () => { + location.href = '/tutorial/Ignite' })