Skip to content

Commit

Permalink
Added support for temp dbs (#88) (#89)
Browse files Browse the repository at this point in the history
Added util functions to manage temp databases (create and delete) and
integrated them as methods on the `Neogma` class.

- Copied over the functions from the
[@neo4j-labs/temp-dbs](https://github.com/neo4j-contrib/neo4j-temp-db)
repo and ported them to typescript.
- Integrated the functions into the `Neogma` class and removed the
`driver.close()` calls in the functions in favor of the global driver
manager.
- Added tests to make sure the intended behaviour was successfully
implemented.

---------

Co-authored-by: Jason Athanasoglou <jathanasoglou@outlook.com>
  • Loading branch information
AndySakov and themetalfleece authored Jan 15, 2024
1 parent 4537e8b commit c05c4b5
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 0 deletions.
1 change: 1 addition & 0 deletions documentation/md/_toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
40 changes: 40 additions & 0 deletions documentation/md/docs/Temporary-Databases.md
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions src/Neogma.spec.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
38 changes: 38 additions & 0 deletions src/Neogma.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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<Neogma> => {
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<void> => {
try {
await this.driver.verifyConnectivity();
Expand Down
124 changes: 124 additions & 0 deletions src/utils/temp.ts
Original file line number Diff line number Diff line change
@@ -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<any>) => {
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();
}
};

0 comments on commit c05c4b5

Please sign in to comment.