Skip to content

Commit e9eab4f

Browse files
authored
Merge pull request #119 from dolthub/taylor/db-meta
graphql: Option to persist metadata to database
2 parents e408365 + f857caf commit e9eab4f

File tree

8 files changed

+213
-10
lines changed

8 files changed

+213
-10
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
**/*.env

README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,13 +107,51 @@ Docker installed databases.
107107

108108
### Saving connection information between runs
109109

110-
If you want to save connection information between Docker runs, you can mount a local
110+
#### Using the file store
111+
112+
If you want to save connection metadata between Docker runs, you can mount a local
111113
directory to the `store` directory in `/app/graphql-server` in the container.
112114

113-
```
115+
```bash
114116
% docker run -p 9002:9002 -p 3000:3000 -v ~/path/to/store:/app/graphql-server/store dolthub/dolt-workbench:latest
115117
```
116118

119+
#### Using a MySQL database
120+
121+
You can also persist connection metadata in a MySQL-compatible database by providing
122+
database connection information through environment variables.
123+
124+
Using a `.env` file:
125+
126+
```bash
127+
# Specify individual fields:
128+
DW_DB_DBNAME=dolt_workbench
129+
DW_DB_PORT=3306
130+
DW_DB_USER=<username>
131+
DW_DB_PASS=<password>
132+
DW_DB_HOST=host.docker.internal
133+
134+
# Or use a connection URI:
135+
DW_DB_CONNECTION_URI=mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench
136+
```
137+
138+
```bash
139+
% docker run -p 9002:9002 -p 3000:3000 --env-file <env_file_name> dolthub/dolt-workbench:latest
140+
```
141+
142+
Or use the `-e` flag:
143+
144+
```bash
145+
% 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
146+
```
147+
148+
Note that we do not create the database `dolt_workbench` for you. You must create it
149+
yourself:
150+
151+
```sql
152+
CREATE DATABASE dolt_workbench;
153+
```
154+
117155
## Getting started from source
118156

119157
First, clone this repository.

docker/README.md

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,51 @@ You can also access the GraphQL Playground at http://localhost:9002/graphql.
3434

3535
### Saving connection information between runs
3636

37-
If you want to save connection information between Docker runs, you can mount a local
37+
#### Using the file store
38+
39+
If you want to save connection metadata between Docker runs, you can mount a local
3840
directory to the `store` directory in `/app/graphql-server` in the container.
3941

40-
```
42+
```bash
4143
% docker run -p 9002:9002 -p 3000:3000 -v ~/path/to/store:/app/graphql-server/store dolthub/dolt-workbench:latest
4244
```
4345

46+
#### Using a MySQL database
47+
48+
You can also persist connection metadata in a MySQL-compatible database by providing
49+
database connection information through environment variables.
50+
51+
Using a `.env` file:
52+
53+
```bash
54+
# Specify individual fields:
55+
DW_DB_DBNAME=dolt_workbench
56+
DW_DB_PORT=3306
57+
DW_DB_USER=<username>
58+
DW_DB_PASS=<password>
59+
DW_DB_HOST=host.docker.internal
60+
61+
# Or use a connection URI:
62+
DW_DB_CONNECTION_URI=mysql://<username>:<password>@host.docker.internal:3306/dolt_workbench
63+
```
64+
65+
```bash
66+
% docker run -p 9002:9002 -p 3000:3000 --env-file <env_file_name> dolthub/dolt-workbench:latest
67+
```
68+
69+
Or use the `-e` flag:
70+
71+
```bash
72+
% 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
73+
```
74+
75+
Note that we do not create the database `dolt_workbench` for you. You must create it
76+
yourself:
77+
78+
```sql
79+
CREATE DATABASE dolt_workbench;
80+
```
81+
4482
## Connecting to an internet accessible database
4583

4684
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.

graphql-server/src/app.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { ApolloDriver, ApolloDriverConfig } from "@nestjs/apollo";
22
import { Module } from "@nestjs/common";
3+
import { ConfigModule } from "@nestjs/config";
34
import { GraphQLModule } from "@nestjs/graphql";
45
import { TerminusModule } from "@nestjs/terminus";
6+
import { DataStoreModule } from "./dataStore/dataStore.module";
57
import { FileStoreModule } from "./fileStore/fileStore.module";
68
import resolvers from "./resolvers";
79

@@ -14,6 +16,8 @@ import resolvers from "./resolvers";
1416
}),
1517
FileStoreModule,
1618
TerminusModule,
19+
ConfigModule.forRoot({ isGlobal: true }),
20+
DataStoreModule,
1721
],
1822
providers: resolvers,
1923
})
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { Module } from "@nestjs/common";
2+
import { DataStoreService } from "./dataStore.service";
3+
4+
@Module({
5+
providers: [DataStoreService],
6+
exports: [DataStoreService],
7+
})
8+
export class DataStoreModule {}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import { Injectable } from "@nestjs/common";
2+
import { ConfigService } from "@nestjs/config";
3+
import { DataSource, DataSourceOptions, Repository } from "typeorm";
4+
import { DatabaseConnection } from "../databases/database.model";
5+
import { DatabaseConnectionsEntity } from "./databaseConnection.entity";
6+
7+
@Injectable()
8+
export class DataStoreService {
9+
private ds: DataSource | undefined;
10+
11+
constructor(private configService: ConfigService) {}
12+
13+
hasDataStoreConfig(): boolean {
14+
const host = this.configService.get<string | undefined>("DW_DB_HOST");
15+
const uri = this.configService.get<string | undefined>(
16+
"DW_DB_CONNECTION_URI",
17+
);
18+
return !!(host ?? uri);
19+
}
20+
21+
getEnvConfig(): DataSourceOptions {
22+
if (!this.hasDataStoreConfig()) {
23+
throw new Error("Data store config not found");
24+
}
25+
return {
26+
type: "mysql",
27+
connectorPackage: "mysql2",
28+
url: this.configService.get<string | undefined>("DW_DB_CONNECTION_URI"),
29+
host: this.configService.get<string | undefined>("DW_DB_HOST"),
30+
port: this.configService.get<number | undefined>("DW_DB_PORT"),
31+
username: this.configService.get<string | undefined>("DW_DB_USER"),
32+
password: this.configService.get<string | undefined>("DW_DB_PASS"),
33+
database: this.configService.get<string | undefined>("DW_DB_DBNAME"),
34+
entities: [DatabaseConnectionsEntity],
35+
synchronize: true,
36+
logging: "all",
37+
};
38+
}
39+
40+
async connection(): Promise<DataSource> {
41+
if (!this.ds) {
42+
const config = this.getEnvConfig();
43+
this.ds = new DataSource(config);
44+
await this.ds.initialize();
45+
}
46+
return this.ds;
47+
}
48+
49+
async getConnRepo(): Promise<Repository<DatabaseConnectionsEntity>> {
50+
const conn = await this.connection();
51+
return conn.getRepository(DatabaseConnectionsEntity);
52+
}
53+
54+
async getStoredConnections(): Promise<DatabaseConnection[]> {
55+
const repo = await this.getConnRepo();
56+
const conns = await repo.find();
57+
return conns;
58+
}
59+
60+
async addStoredConnection(item: DatabaseConnection): Promise<void> {
61+
const repo = await this.getConnRepo();
62+
const existing = await repo.findOneBy({ name: item.name });
63+
64+
if (existing) {
65+
if (existing.connectionUrl === item.connectionUrl) return undefined;
66+
throw new Error("name already exists");
67+
}
68+
69+
await repo.save(item);
70+
return undefined;
71+
}
72+
73+
async removeStoredConnection(name: string): Promise<void> {
74+
const repo = await this.getConnRepo();
75+
await repo.delete({ name });
76+
}
77+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Column, Entity, PrimaryColumn } from "typeorm";
2+
import { DatabaseType } from "../databases/database.enum";
3+
4+
@Entity({ name: "database_connections" })
5+
export class DatabaseConnectionsEntity {
6+
@PrimaryColumn()
7+
name: string;
8+
9+
@Column()
10+
connectionUrl: string;
11+
12+
@Column({ nullable: true })
13+
hideDoltFeatures?: boolean;
14+
15+
@Column()
16+
useSSL?: boolean;
17+
18+
@Column()
19+
type?: DatabaseType;
20+
21+
@Column({ nullable: true })
22+
schema?: string;
23+
}

graphql-server/src/databases/database.resolver.ts

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
Resolver,
99
} from "@nestjs/graphql";
1010
import { ConnectionResolver } from "../connections/connection.resolver";
11+
import { DataStoreService } from "../dataStore/dataStore.service";
1112
import { FileStoreService } from "../fileStore/fileStore.service";
1213
import { DBArgs, SchemaArgs } from "../utils/commonTypes";
1314
import { DatabaseType } from "./database.enum";
@@ -67,6 +68,7 @@ export class DatabaseResolver {
6768
constructor(
6869
private readonly conn: ConnectionResolver,
6970
private readonly fileStoreService: FileStoreService,
71+
private readonly dataStoreService: DataStoreService,
7072
) {}
7173

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

7880
@Query(_returns => [DatabaseConnection])
7981
async storedConnections(): Promise<DatabaseConnection[]> {
82+
if (this.dataStoreService.hasDataStoreConfig()) {
83+
return this.dataStoreService.getStoredConnections();
84+
}
8085
return this.fileStoreService.getStore();
8186
}
8287

@@ -124,19 +129,24 @@ export class DatabaseResolver {
124129
@Args() args: AddDatabaseConnectionArgs,
125130
): Promise<CurrentDatabaseState> {
126131
const type = args.type ?? DatabaseType.Mysql;
132+
127133
const workbenchConfig = {
128134
connectionUrl: args.connectionUrl,
129135
hideDoltFeatures: !!args.hideDoltFeatures,
130136
useSSL: !!args.useSSL,
131137
type,
132138
schema: args.schema,
133139
};
134-
await this.conn.addConnection(workbenchConfig);
135140

136-
this.fileStoreService.addItemToStore({
137-
...workbenchConfig,
138-
name: args.name,
139-
});
141+
const storeArgs = { ...workbenchConfig, name: args.name };
142+
143+
if (this.dataStoreService.hasDataStoreConfig()) {
144+
await this.dataStoreService.addStoredConnection(storeArgs);
145+
} else {
146+
this.fileStoreService.addItemToStore(storeArgs);
147+
}
148+
149+
await this.conn.addConnection(workbenchConfig);
140150

141151
const db = await this.currentDatabase();
142152
if (type === DatabaseType.Mysql) {
@@ -152,7 +162,11 @@ export class DatabaseResolver {
152162
async removeDatabaseConnection(
153163
@Args() args: RemoveDatabaseConnectionArgs,
154164
): Promise<boolean> {
155-
this.fileStoreService.removeItemFromStore(args.name);
165+
if (this.dataStoreService.hasDataStoreConfig()) {
166+
await this.dataStoreService.removeStoredConnection(args.name);
167+
} else {
168+
this.fileStoreService.removeItemFromStore(args.name);
169+
}
156170
return true;
157171
}
158172

0 commit comments

Comments
 (0)