Skip to content

Commit

Permalink
Merge pull request #119 from dolthub/taylor/db-meta
Browse files Browse the repository at this point in the history
graphql: Option to persist metadata to database
  • Loading branch information
tbantle22 authored Feb 7, 2024
2 parents e408365 + f857caf commit e9eab4f
Show file tree
Hide file tree
Showing 8 changed files with 213 additions and 10 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
**/*.env
42 changes: 40 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,51 @@ Docker installed databases.

### Saving connection information between runs

If you want to save connection information between Docker runs, you can mount a local
#### Using the file store

If you want to save connection metadata between Docker runs, you can mount a local
directory to the `store` directory in `/app/graphql-server` in the container.

```
```bash
% docker run -p 9002:9002 -p 3000:3000 -v ~/path/to/store:/app/graphql-server/store dolthub/dolt-workbench:latest
```

#### Using a MySQL database

You can also persist connection metadata in a MySQL-compatible database by providing
database connection information through environment variables.

Using a `.env` file:

```bash
# Specify individual fields:
DW_DB_DBNAME=dolt_workbench
DW_DB_PORT=3306
DW_DB_USER=<username>
DW_DB_PASS=<password>
DW_DB_HOST=host.docker.internal

# Or use a connection URI:
DW_DB_CONNECTION_URI=mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench
```

```bash
% docker run -p 9002:9002 -p 3000:3000 --env-file <env_file_name> dolthub/dolt-workbench:latest
```

Or use the `-e` flag:

```bash
% docker run -p 9002:9002 -p 3000:3000 -e DW_DB_CONNECTION_URI="mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench" dolthub/dolt-workbench:latest
```

Note that we do not create the database `dolt_workbench` for you. You must create it
yourself:

```sql
CREATE DATABASE dolt_workbench;
```

## Getting started from source

First, clone this repository.
Expand Down
42 changes: 40 additions & 2 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,51 @@ You can also access the GraphQL Playground at http://localhost:9002/graphql.

### Saving connection information between runs

If you want to save connection information between Docker runs, you can mount a local
#### Using the file store

If you want to save connection metadata between Docker runs, you can mount a local
directory to the `store` directory in `/app/graphql-server` in the container.

```
```bash
% docker run -p 9002:9002 -p 3000:3000 -v ~/path/to/store:/app/graphql-server/store dolthub/dolt-workbench:latest
```

#### Using a MySQL database

You can also persist connection metadata in a MySQL-compatible database by providing
database connection information through environment variables.

Using a `.env` file:

```bash
# Specify individual fields:
DW_DB_DBNAME=dolt_workbench
DW_DB_PORT=3306
DW_DB_USER=<username>
DW_DB_PASS=<password>
DW_DB_HOST=host.docker.internal

# Or use a connection URI:
DW_DB_CONNECTION_URI=mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench
```

```bash
% docker run -p 9002:9002 -p 3000:3000 --env-file <env_file_name> dolthub/dolt-workbench:latest
```

Or use the `-e` flag:

```bash
% docker run -p 9002:9002 -p 3000:3000 -e DW_DB_CONNECTION_URI="mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench" dolthub/dolt-workbench:latest
```

Note that we do not create the database `dolt_workbench` for you. You must create it
yourself:

```sql
CREATE DATABASE dolt_workbench;
```

## Connecting to an internet accessible database

If your database is already internet accessible (i.e. your database is hosted on a service like [AWS RDS](https://aws.amazon.com/rds/) or [Hosted Dolt](https://hosted.doltdb.com)), simply enter your database connection information through the UI.
Expand Down
4 changes: 4 additions & 0 deletions graphql-server/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
import { Module } from "@nestjs/common";
import { ConfigModule } from "@nestjs/config";
import { GraphQLModule } from "@nestjs/graphql";
import { TerminusModule } from "@nestjs/terminus";
import { DataStoreModule } from "./dataStore/dataStore.module";
import { FileStoreModule } from "./fileStore/fileStore.module";
import resolvers from "./resolvers";

Expand All @@ -14,6 +16,8 @@ import resolvers from "./resolvers";
}),
FileStoreModule,
TerminusModule,
ConfigModule.forRoot({ isGlobal: true }),
DataStoreModule,
],
providers: resolvers,
})
Expand Down
8 changes: 8 additions & 0 deletions graphql-server/src/dataStore/dataStore.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { DataStoreService } from "./dataStore.service";

@Module({
providers: [DataStoreService],
exports: [DataStoreService],
})
export class DataStoreModule {}
77 changes: 77 additions & 0 deletions graphql-server/src/dataStore/dataStore.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { DataSource, DataSourceOptions, Repository } from "typeorm";
import { DatabaseConnection } from "../databases/database.model";
import { DatabaseConnectionsEntity } from "./databaseConnection.entity";

@Injectable()
export class DataStoreService {
private ds: DataSource | undefined;

constructor(private configService: ConfigService) {}

hasDataStoreConfig(): boolean {
const host = this.configService.get<string | undefined>("DW_DB_HOST");
const uri = this.configService.get<string | undefined>(
"DW_DB_CONNECTION_URI",
);
return !!(host ?? uri);
}

getEnvConfig(): DataSourceOptions {
if (!this.hasDataStoreConfig()) {
throw new Error("Data store config not found");
}
return {
type: "mysql",
connectorPackage: "mysql2",
url: this.configService.get<string | undefined>("DW_DB_CONNECTION_URI"),
host: this.configService.get<string | undefined>("DW_DB_HOST"),
port: this.configService.get<number | undefined>("DW_DB_PORT"),
username: this.configService.get<string | undefined>("DW_DB_USER"),
password: this.configService.get<string | undefined>("DW_DB_PASS"),
database: this.configService.get<string | undefined>("DW_DB_DBNAME"),
entities: [DatabaseConnectionsEntity],
synchronize: true,
logging: "all",
};
}

async connection(): Promise<DataSource> {
if (!this.ds) {
const config = this.getEnvConfig();
this.ds = new DataSource(config);
await this.ds.initialize();
}
return this.ds;
}

async getConnRepo(): Promise<Repository<DatabaseConnectionsEntity>> {
const conn = await this.connection();
return conn.getRepository(DatabaseConnectionsEntity);
}

async getStoredConnections(): Promise<DatabaseConnection[]> {
const repo = await this.getConnRepo();
const conns = await repo.find();
return conns;
}

async addStoredConnection(item: DatabaseConnection): Promise<void> {
const repo = await this.getConnRepo();
const existing = await repo.findOneBy({ name: item.name });

if (existing) {
if (existing.connectionUrl === item.connectionUrl) return undefined;
throw new Error("name already exists");
}

await repo.save(item);
return undefined;
}

async removeStoredConnection(name: string): Promise<void> {
const repo = await this.getConnRepo();
await repo.delete({ name });
}
}
23 changes: 23 additions & 0 deletions graphql-server/src/dataStore/databaseConnection.entity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { Column, Entity, PrimaryColumn } from "typeorm";
import { DatabaseType } from "../databases/database.enum";

@Entity({ name: "database_connections" })
export class DatabaseConnectionsEntity {
@PrimaryColumn()
name: string;

@Column()
connectionUrl: string;

@Column({ nullable: true })
hideDoltFeatures?: boolean;

@Column()
useSSL?: boolean;

@Column()
type?: DatabaseType;

@Column({ nullable: true })
schema?: string;
}
26 changes: 20 additions & 6 deletions graphql-server/src/databases/database.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
Resolver,
} from "@nestjs/graphql";
import { ConnectionResolver } from "../connections/connection.resolver";
import { DataStoreService } from "../dataStore/dataStore.service";
import { FileStoreService } from "../fileStore/fileStore.service";
import { DBArgs, SchemaArgs } from "../utils/commonTypes";
import { DatabaseType } from "./database.enum";
Expand Down Expand Up @@ -67,6 +68,7 @@ export class DatabaseResolver {
constructor(
private readonly conn: ConnectionResolver,
private readonly fileStoreService: FileStoreService,
private readonly dataStoreService: DataStoreService,
) {}

@Query(_returns => String, { nullable: true })
Expand All @@ -77,6 +79,9 @@ export class DatabaseResolver {

@Query(_returns => [DatabaseConnection])
async storedConnections(): Promise<DatabaseConnection[]> {
if (this.dataStoreService.hasDataStoreConfig()) {
return this.dataStoreService.getStoredConnections();
}
return this.fileStoreService.getStore();
}

Expand Down Expand Up @@ -124,19 +129,24 @@ export class DatabaseResolver {
@Args() args: AddDatabaseConnectionArgs,
): Promise<CurrentDatabaseState> {
const type = args.type ?? DatabaseType.Mysql;

const workbenchConfig = {
connectionUrl: args.connectionUrl,
hideDoltFeatures: !!args.hideDoltFeatures,
useSSL: !!args.useSSL,
type,
schema: args.schema,
};
await this.conn.addConnection(workbenchConfig);

this.fileStoreService.addItemToStore({
...workbenchConfig,
name: args.name,
});
const storeArgs = { ...workbenchConfig, name: args.name };

if (this.dataStoreService.hasDataStoreConfig()) {
await this.dataStoreService.addStoredConnection(storeArgs);
} else {
this.fileStoreService.addItemToStore(storeArgs);
}

await this.conn.addConnection(workbenchConfig);

const db = await this.currentDatabase();
if (type === DatabaseType.Mysql) {
Expand All @@ -152,7 +162,11 @@ export class DatabaseResolver {
async removeDatabaseConnection(
@Args() args: RemoveDatabaseConnectionArgs,
): Promise<boolean> {
this.fileStoreService.removeItemFromStore(args.name);
if (this.dataStoreService.hasDataStoreConfig()) {
await this.dataStoreService.removeStoredConnection(args.name);
} else {
this.fileStoreService.removeItemFromStore(args.name);
}
return true;
}

Expand Down

0 comments on commit e9eab4f

Please sign in to comment.