Skip to content

Commit 8183c31

Browse files
authored
Surface db config errors (#1730)
1 parent 5390a11 commit 8183c31

6 files changed

Lines changed: 111 additions & 29 deletions

File tree

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Represents a result that can be either a value or some errors.
3+
*/
4+
export class ValueResult<TValue> {
5+
private constructor(
6+
private readonly errorMsgs: string[],
7+
private readonly val?: TValue,
8+
) {
9+
}
10+
11+
public static ok<TValue>(value: TValue): ValueResult<TValue> {
12+
if (value === undefined) {
13+
throw new Error('Value must be set for successful result');
14+
}
15+
16+
return new ValueResult([], value);
17+
}
18+
19+
public static fail<TValue>(errorMsgs: string[]): ValueResult<TValue> {
20+
if (errorMsgs.length === 0) {
21+
throw new Error('At least one error message must be set for a failed result');
22+
}
23+
24+
return new ValueResult<TValue>(errorMsgs, undefined);
25+
}
26+
27+
public get isOk(): boolean {
28+
return this.errorMsgs.length === 0;
29+
}
30+
31+
public get isFailure(): boolean {
32+
return this.errorMsgs.length > 0;
33+
}
34+
35+
public get errors(): string[] {
36+
if (!this.errorMsgs) {
37+
throw new Error('Cannot get error for successful result');
38+
}
39+
40+
return this.errorMsgs;
41+
}
42+
43+
public get value(): TValue {
44+
if (this.val === undefined) {
45+
throw new Error('Cannot get value for unsuccessful result');
46+
}
47+
48+
return this.val;
49+
}
50+
}

extensions/ql-vscode/src/databases/db-config-store.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { cloneDbConfig, DbConfig } from './db-config';
44
import * as chokidar from 'chokidar';
55
import { DisposableObject } from '../pure/disposable-object';
66
import { DbConfigValidator } from './db-config-validator';
7+
import { ValueResult } from '../common/value-result';
78

89
export class DbConfigStore extends DisposableObject {
910
private readonly configPath: string;
1011
private readonly configValidator: DbConfigValidator;
1112

12-
private config: DbConfig;
13+
private config: DbConfig | undefined;
14+
private configErrors: string[];
1315
private configWatcher: chokidar.FSWatcher | undefined;
1416

1517
public constructor(
@@ -20,6 +22,7 @@ export class DbConfigStore extends DisposableObject {
2022
this.configPath = path.join(workspaceStoragePath, 'workspace-databases.json');
2123

2224
this.config = this.createEmptyConfig();
25+
this.configErrors = [];
2326
this.configWatcher = undefined;
2427
this.configValidator = new DbConfigValidator(extensionPath);
2528
}
@@ -33,19 +36,19 @@ export class DbConfigStore extends DisposableObject {
3336
this.configWatcher?.unwatch(this.configPath);
3437
}
3538

36-
public getConfig(): DbConfig {
37-
// Clone the config so that it's not modified outside of this class.
38-
return cloneDbConfig(this.config);
39+
public getConfig(): ValueResult<DbConfig> {
40+
if (this.config) {
41+
// Clone the config so that it's not modified outside of this class.
42+
return ValueResult.ok(cloneDbConfig(this.config));
43+
} else {
44+
return ValueResult.fail(this.configErrors);
45+
}
3946
}
4047

4148
public getConfigPath(): string {
4249
return this.configPath;
4350
}
4451

45-
public validateConfig(): string[] {
46-
return this.configValidator.validate(this.config);
47-
}
48-
4952
private async loadConfig(): Promise<void> {
5053
if (!await fs.pathExists(this.configPath)) {
5154
await fs.writeJSON(this.configPath, this.createEmptyConfig(), { spaces: 2 });
@@ -55,11 +58,33 @@ export class DbConfigStore extends DisposableObject {
5558
}
5659

5760
private async readConfig(): Promise<void> {
58-
this.config = await fs.readJSON(this.configPath);
61+
let newConfig: DbConfig | undefined = undefined;
62+
try {
63+
newConfig = await fs.readJSON(this.configPath);
64+
} catch (e) {
65+
this.configErrors = [`Failed to read config file: ${this.configPath}`];
66+
}
67+
68+
if (newConfig) {
69+
this.configErrors = this.configValidator.validate(newConfig);
70+
}
71+
72+
this.config = this.configErrors.length === 0 ? newConfig : undefined;
5973
}
6074

6175
private readConfigSync(): void {
62-
this.config = fs.readJSONSync(this.configPath);
76+
let newConfig: DbConfig | undefined = undefined;
77+
try {
78+
newConfig = fs.readJSONSync(this.configPath);
79+
} catch (e) {
80+
this.configErrors = [`Failed to read config file: ${this.configPath}`];
81+
}
82+
83+
if (newConfig) {
84+
this.configErrors = this.configValidator.validate(newConfig);
85+
}
86+
87+
this.config = this.configErrors.length === 0 ? newConfig : undefined;
6388
}
6489

6590
private watchConfig(): void {

extensions/ql-vscode/src/databases/db-manager.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { ValueResult } from '../common/value-result';
12
import { DbConfigStore } from './db-config-store';
23
import { DbItem } from './db-item';
34
import { createLocalTree, createRemoteTree } from './db-tree-creator';
@@ -8,13 +9,16 @@ export class DbManager {
89
) {
910
}
1011

11-
public getDbItems(): DbItem[] {
12-
const config = this.dbConfigStore.getConfig();
12+
public getDbItems(): ValueResult<DbItem[]> {
13+
const configResult = this.dbConfigStore.getConfig();
14+
if (configResult.isFailure) {
15+
return ValueResult.fail(configResult.errors);
16+
}
1317

14-
return [
15-
createRemoteTree(config),
18+
return ValueResult.ok([
19+
createRemoteTree(configResult.value),
1620
createLocalTree()
17-
];
21+
]);
1822
}
1923

2024
public getConfigPath(): string {

extensions/ql-vscode/src/databases/ui/db-tree-data-provider.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ProviderResult, TreeDataProvider, TreeItem } from 'vscode';
2-
import { createDbTreeViewItemWarning, DbTreeViewItem } from './db-tree-view-item';
2+
import { createDbTreeViewItemError, DbTreeViewItem } from './db-tree-view-item';
33
import { DbManager } from '../db-manager';
44
import { mapDbItemToTreeViewItem } from './db-item-mapper';
55

@@ -36,14 +36,17 @@ export class DbTreeDataProvider implements TreeDataProvider<DbTreeViewItem> {
3636
}
3737

3838
private createTree(): DbTreeViewItem[] {
39-
const dbItems = this.dbManager.getDbItems();
39+
const dbItemsResult = this.dbManager.getDbItems();
4040

41-
// Add a sample warning as a proof of concept.
42-
const warningTreeViewItem = createDbTreeViewItemWarning(
43-
'There was an error',
44-
'Fix it'
45-
);
41+
if (dbItemsResult.isFailure) {
42+
const errorTreeViewItem = createDbTreeViewItemError(
43+
'Error when reading databases config',
44+
'Please open your databases config and address errors'
45+
);
4646

47-
return [...dbItems.map(mapDbItemToTreeViewItem), warningTreeViewItem];
47+
return [errorTreeViewItem];
48+
}
49+
50+
return dbItemsResult.value.map(mapDbItemToTreeViewItem);
4851
}
4952
}

extensions/ql-vscode/src/databases/ui/db-tree-view-item.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,10 @@ export class DbTreeViewItem extends vscode.TreeItem {
2626
}
2727
}
2828

29-
export function createDbTreeViewItemWarning(label: string, tooltip: string): DbTreeViewItem {
29+
export function createDbTreeViewItemError(label: string, tooltip: string): DbTreeViewItem {
3030
return new DbTreeViewItem(
3131
undefined,
32-
new vscode.ThemeIcon('warning', new vscode.ThemeColor('problemsWarningIcon.foreground')),
32+
new vscode.ThemeIcon('error', new vscode.ThemeColor('problemsErrorIcon.foreground')),
3333
label,
3434
tooltip,
3535
vscode.TreeItemCollapsibleState.None,

extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ describe('db config store', async () => {
2323
await configStore.initialize();
2424

2525
expect(await fs.pathExists(configPath)).to.be.true;
26-
const config = configStore.getConfig();
26+
const config = configStore.getConfig().value;
2727
expect(config.remote.repositoryLists).to.be.empty;
2828
expect(config.remote.owners).to.be.empty;
2929
expect(config.remote.repositories).to.be.empty;
@@ -33,7 +33,7 @@ describe('db config store', async () => {
3333
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
3434
await configStore.initialize();
3535

36-
const config = configStore.getConfig();
36+
const config = configStore.getConfig().value;
3737
expect(config.remote.repositoryLists).to.have.length(1);
3838
expect(config.remote.repositoryLists[0]).to.deep.equal({
3939
'name': 'repoList1',
@@ -48,10 +48,10 @@ describe('db config store', async () => {
4848
const configStore = new DbConfigStore(testDataStoragePath, extensionPath);
4949
await configStore.initialize();
5050

51-
const config = configStore.getConfig();
51+
const config = configStore.getConfig().value;
5252
config.remote.repositoryLists = [];
5353

54-
const reRetrievedConfig = configStore.getConfig();
54+
const reRetrievedConfig = configStore.getConfig().value;
5555
expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1);
5656
});
5757
});

0 commit comments

Comments
 (0)