diff --git a/documentation/md/_toc.md b/documentation/md/_toc.md index b6545ee..fd6080c 100644 --- a/documentation/md/_toc.md +++ b/documentation/md/_toc.md @@ -13,6 +13,7 @@ > [Deleting Relationships](/docs/Models/Deleting-Relationships) > [Finding Nodes and Relationships](/docs/Models/Finding-Nodes-And-Relationships) > [Hooks](/docs/Models/Hooks) +> [Temporary Databases](/docs/Temporary-Databases) [Sessions and Transactions](/docs/Sessions-and-Transactions) diff --git a/documentation/md/docs/Temporary-Databases.md b/documentation/md/docs/Temporary-Databases.md new file mode 100644 index 0000000..e24d450 --- /dev/null +++ b/documentation/md/docs/Temporary-Databases.md @@ -0,0 +1,40 @@ +# Temp DBs + +You can work with temporary dbs (which can be cleared all at once) by using the static `fromTempDatabase` to initialize your Neogma instance. You pass in all the same props as in the constructor except the name of the database, as it will be created for you and managed internally by Neogma. + +```js +/* --> create a neogma instance that is latched onto an internally managed temp database */ +const neogma = Neogma.fromTempDatabase( + { + /* --> use your connection details */ + url: 'bolt://localhost', + username: 'neo4j', + password: 'password', + }, + { + logger: console.log, + } +); +``` + +To dispose of a temp databse when you're done using it, you can use one of the following three methods: + +```js +const database = neogma.database; +await neogma.clearTempDatabase(database); +``` + +As shown above this method requires you to pass the exact name of the database to dispose of. Sometimes that may not be what's needed if working with a large number of temp dbs so you could get them all at once. + +```js +await neogma.clearAllTempDatabases(); +``` + +You could also specify the time frame (of creation) in seconds to delete the dbs that are older than it. + +```js +const seconds = 1000; +await neogma.clearTempDatabasesOlderThan(seconds); +``` + +> :ToCPrevNext \ No newline at end of file diff --git a/src/Neogma.spec.ts b/src/Neogma.spec.ts new file mode 100644 index 0000000..cc12555 --- /dev/null +++ b/src/Neogma.spec.ts @@ -0,0 +1,33 @@ +import { Neogma } from './Neogma'; +import * as dotenv from 'dotenv'; +import { TEMPORARY_DB_PREFIX } from './utils/temp'; + +describe('Neogma', () => { + let neogma: Neogma; + + beforeAll(async () => { + dotenv.config(); + neogma = await Neogma.fromTempDatabase({ + url: process.env.NEO4J_URL ?? '', + username: process.env.NEO4J_USERNAME ?? '', + password: process.env.NEO4J_PASSWORD ?? '', + }); + + await neogma.verifyConnectivity(); + }); + + afterAll(async () => { + await neogma.clearAllTempDatabases(); + await neogma.driver.close(); + }); + + it('should be defined', () => { + expect(neogma).toBeDefined(); + expect(neogma.database).toBeDefined(); + expect(neogma.database).not.toBeNull(); + }); + + it('should have created a temp db', async () => { + expect(neogma.database?.indexOf(TEMPORARY_DB_PREFIX)).toBe(0); + }); +}); diff --git a/src/Neogma.ts b/src/Neogma.ts index 36c70e9..e4d867e 100644 --- a/src/Neogma.ts +++ b/src/Neogma.ts @@ -5,6 +5,12 @@ import { QueryRunner, Runnable } from './Queries/QueryRunner'; import { getRunnable, getSession, getTransaction } from './Sessions/Sessions'; import { NeogmaConnectivityError } from './Errors/NeogmaConnectivityError'; import { QueryBuilder } from './Queries'; +import { + clearAllTempDatabases, + clearTempDatabase, + clearTempDatabasesOlderThan, + createTempDatabase, +} from './utils/temp'; const neo4j = neo4j_driver; interface ConnectParamsI { @@ -57,6 +63,38 @@ export class Neogma { QueryBuilder.queryRunner = this.queryRunner; } + /** + * + * @param {ConnectParamsI} params - the connection params + * @param {ConnectOptionsI} options - additional options for the QueryRunner + */ + public static fromTempDatabase = async ( + params: ConnectParamsI, + options?: ConnectOptionsI, + ): Promise => { + const { url, username, password } = params; + + const driver = neo4j.driver( + url, + neo4j.auth.basic(username, password), + options, + ); + + const database = await createTempDatabase(driver); + + await driver.close(); + + return new Neogma({ ...params, database }); + }; + + public clearTempDatabase = async (database: string) => + clearTempDatabase(this.driver, database); + + public clearAllTempDatabases = async () => clearAllTempDatabases(this.driver); + + public clearTempDatabasesOlderThan = async (seconds: number) => + clearTempDatabasesOlderThan(this.driver, seconds); + public verifyConnectivity = async (): Promise => { try { await this.driver.verifyConnectivity(); diff --git a/src/utils/temp.ts b/src/utils/temp.ts new file mode 100644 index 0000000..d94647b --- /dev/null +++ b/src/utils/temp.ts @@ -0,0 +1,124 @@ +import * as uuid from 'uuid'; +import neo4j, { Driver, QueryResult, Session } from 'neo4j-driver'; + +export const TEMPORARY_DB_PREFIX = 'tempneogmadb'; + +const getCurrentTimestamp = () => { + return Math.floor(new Date().getTime() / 1000); +}; + +const filterConsoleDatabasesFromResult = (result: QueryResult) => { + return result.records.filter( + (record) => record.get('name').indexOf(TEMPORARY_DB_PREFIX) === 0, + ); +}; + +const deleteDatabaseUserAndRole = async ( + session: Session, + database: string, +) => { + try { + await session.run(`STOP DATABASE ${database};`); + } catch (error) { + console.error(error); + } + try { + await session.run(`DROP DATABASE ${database};`); + } catch (error) { + console.error(error); + } + try { + await session.run(`DROP USER ${database};`); + } catch (error) { + console.error(error); + } + try { + await session.run(`DROP ROLE ${database};`); + } catch (error) { + console.error(error); + } +}; + +export const createTempDatabase = async (driver: Driver) => { + const sessionId = uuid.v4().replace(/-/g, ''); + const currentTimestamp = getCurrentTimestamp(); + const database = `${TEMPORARY_DB_PREFIX}${sessionId}${currentTimestamp}`; + + const session = driver.session({ + database: 'system', + defaultAccessMode: neo4j.session.WRITE, + }); + + try { + await session.run(`CREATE DATABASE ${database} WAIT;`); + await session.run( + `CREATE USER ${database} SET PASSWORD '${database}' SET PASSWORD CHANGE NOT REQUIRED;`, + ); + await session.run(`CREATE ROLE ${database};`); + await session.run(`GRANT ROLE ${database} TO ${database};`); + await session.run(`GRANT ALL ON DATABASE ${database} TO ${database};`); + await session.run(`GRANT ACCESS ON DATABASE ${database} TO ${database};`); + await session.run(`GRANT READ {*} ON GRAPH ${database} TO ${database};`); + await session.run(`GRANT TRAVERSE ON GRAPH ${database} TO ${database};`); + await session.run(`GRANT WRITE ON GRAPH ${database} TO ${database};`); + } catch (error) { + console.error(error); + } finally { + await session.close(); + } + + return database; +}; + +export const clearTempDatabase = async (driver: Driver, database: string) => { + const session = driver.session({ database: 'system' }); + try { + await deleteDatabaseUserAndRole(session, database); + } catch (error) { + console.error(error); + } finally { + await session.close(); + } +}; + +export const clearTempDatabasesOlderThan = async ( + driver: Driver, + seconds: number, +) => { + const session = driver.session({ database: 'system' }); + const result = await session.run('SHOW DATABASES'); + const shouldExpireAt = getCurrentTimestamp() - seconds; + try { + const records = filterConsoleDatabasesFromResult(result); + console.log('Databases found: ' + records.length); + for (const record of records) { + const database = record.get('name'); //tempneogmadb56e7794ad165454282ee0a7c32a5e3eb1705341040 + const dbTimestamp = parseInt(database.slice(44), 10); + const isExpired = dbTimestamp <= shouldExpireAt; + if (isExpired) { + await deleteDatabaseUserAndRole(session, database); + } else { + } + } + } catch (error) { + console.error(error); + } finally { + await session.close(); + } +}; + +export const clearAllTempDatabases = async (driver: Driver) => { + const session = driver.session({ database: 'system' }); + const result = await session.run('SHOW DATABASES'); + try { + const records = filterConsoleDatabasesFromResult(result); + for (const record of records) { + const database = record.get('name'); + await deleteDatabaseUserAndRole(session, database); + } + } catch (error) { + console.error(error); + } finally { + await session.close(); + } +};