1- import { pathExists , writeJSON , readJSON , readJSONSync } from "fs-extra" ;
1+ import { pathExists , outputJSON , readJSON , readJSONSync } from "fs-extra" ;
22import { join } from "path" ;
33import {
44 cloneDbConfig ,
@@ -9,9 +9,13 @@ import {
99import * as chokidar from "chokidar" ;
1010import { DisposableObject , DisposeHandler } from "../../pure/disposable-object" ;
1111import { DbConfigValidator } from "./db-config-validator" ;
12- import { ValueResult } from "../../common/value-result" ;
1312import { App } from "../../common/app" ;
1413import { AppEvent , AppEventEmitter } from "../../common/events" ;
14+ import {
15+ DbConfigValidationError ,
16+ DbConfigValidationErrorKind ,
17+ } from "../db-validation-errors" ;
18+ import { ValueResult } from "../../common/value-result" ;
1519
1620export class DbConfigStore extends DisposableObject {
1721 public readonly onDidChangeConfig : AppEvent < void > ;
@@ -21,10 +25,10 @@ export class DbConfigStore extends DisposableObject {
2125 private readonly configValidator : DbConfigValidator ;
2226
2327 private config : DbConfig | undefined ;
24- private configErrors : string [ ] ;
28+ private configErrors : DbConfigValidationError [ ] ;
2529 private configWatcher : chokidar . FSWatcher | undefined ;
2630
27- public constructor ( app : App ) {
31+ public constructor ( private readonly app : App ) {
2832 super ( ) ;
2933
3034 const storagePath = app . workspaceStoragePath || app . globalStoragePath ;
@@ -48,7 +52,7 @@ export class DbConfigStore extends DisposableObject {
4852 this . configWatcher ?. unwatch ( this . configPath ) ;
4953 }
5054
51- public getConfig ( ) : ValueResult < DbConfig , string > {
55+ public getConfig ( ) : ValueResult < DbConfig , DbConfigValidationError > {
5256 if ( this . config ) {
5357 // Clone the config so that it's not modified outside of this class.
5458 return ValueResult . ok ( cloneDbConfig ( this . config ) ) ;
@@ -95,66 +99,131 @@ export class DbConfigStore extends DisposableObject {
9599 throw Error ( "Cannot add remote list if config is not loaded" ) ;
96100 }
97101
102+ if ( this . doesRemoteListExist ( listName ) ) {
103+ throw Error ( `A remote list with the name '${ listName } ' already exists` ) ;
104+ }
105+
98106 const config : DbConfig = cloneDbConfig ( this . config ) ;
99107 config . databases . remote . repositoryLists . push ( {
100108 name : listName ,
101109 repositories : [ ] ,
102110 } ) ;
103111
104- // TODO: validate that the name doesn't already exist
105112 await this . writeConfig ( config ) ;
106113 }
107114
115+ public doesRemoteListExist ( listName : string ) : boolean {
116+ if ( ! this . config ) {
117+ throw Error ( "Cannot check remote list existence if config is not loaded" ) ;
118+ }
119+
120+ return this . config . databases . remote . repositoryLists . some (
121+ ( l ) => l . name === listName ,
122+ ) ;
123+ }
124+
108125 private async writeConfig ( config : DbConfig ) : Promise < void > {
109- await writeJSON ( this . configPath , config , {
126+ await outputJSON ( this . configPath , config , {
110127 spaces : 2 ,
111128 } ) ;
112129 }
113130
114131 private async loadConfig ( ) : Promise < void > {
115132 if ( ! ( await pathExists ( this . configPath ) ) ) {
133+ void this . app . logger . log (
134+ `Creating new database config file at ${ this . configPath } ` ,
135+ ) ;
116136 await this . writeConfig ( this . createEmptyConfig ( ) ) ;
117137 }
118138
119139 await this . readConfig ( ) ;
140+ void this . app . logger . log ( `Database config loaded from ${ this . configPath } ` ) ;
120141 }
121142
122143 private async readConfig ( ) : Promise < void > {
123144 let newConfig : DbConfig | undefined = undefined ;
124145 try {
125146 newConfig = await readJSON ( this . configPath ) ;
126147 } catch ( e ) {
127- this . configErrors = [ `Failed to read config file: ${ this . configPath } ` ] ;
148+ this . configErrors = [
149+ {
150+ kind : DbConfigValidationErrorKind . InvalidJson ,
151+ message : `Failed to read config file: ${ this . configPath } ` ,
152+ } ,
153+ ] ;
128154 }
129155
130156 if ( newConfig ) {
131157 this . configErrors = this . configValidator . validate ( newConfig ) ;
132158 }
133159
134- this . config = this . configErrors . length === 0 ? newConfig : undefined ;
160+ if ( this . configErrors . length === 0 ) {
161+ this . config = newConfig ;
162+ await this . app . executeCommand (
163+ "setContext" ,
164+ "codeQLDatabasesExperimental.configError" ,
165+ false ,
166+ ) ;
167+ } else {
168+ this . config = undefined ;
169+ await this . app . executeCommand (
170+ "setContext" ,
171+ "codeQLDatabasesExperimental.configError" ,
172+ true ,
173+ ) ;
174+ }
135175 }
136176
137177 private readConfigSync ( ) : void {
138178 let newConfig : DbConfig | undefined = undefined ;
139179 try {
140180 newConfig = readJSONSync ( this . configPath ) ;
141181 } catch ( e ) {
142- this . configErrors = [ `Failed to read config file: ${ this . configPath } ` ] ;
182+ this . configErrors = [
183+ {
184+ kind : DbConfigValidationErrorKind . InvalidJson ,
185+ message : `Failed to read config file: ${ this . configPath } ` ,
186+ } ,
187+ ] ;
143188 }
144189
145190 if ( newConfig ) {
146191 this . configErrors = this . configValidator . validate ( newConfig ) ;
147192 }
148193
149- this . config = this . configErrors . length === 0 ? newConfig : undefined ;
150-
194+ if ( this . configErrors . length === 0 ) {
195+ this . config = newConfig ;
196+ void this . app . executeCommand (
197+ "setContext" ,
198+ "codeQLDatabasesExperimental.configError" ,
199+ false ,
200+ ) ;
201+ } else {
202+ this . config = undefined ;
203+ void this . app . executeCommand (
204+ "setContext" ,
205+ "codeQLDatabasesExperimental.configError" ,
206+ true ,
207+ ) ;
208+ }
151209 this . onDidChangeConfigEventEmitter . fire ( ) ;
152210 }
153211
154212 private watchConfig ( ) : void {
155- this . configWatcher = chokidar . watch ( this . configPath ) . on ( "change" , ( ) => {
156- this . readConfigSync ( ) ;
157- } ) ;
213+ this . configWatcher = chokidar
214+ . watch ( this . configPath , {
215+ // In some cases, change events are emitted while the file is still
216+ // being written. The awaitWriteFinish option tells the watcher to
217+ // poll the file size, holding its add and change events until the size
218+ // does not change for a configurable amount of time. We set that time
219+ // to 1 second, but it may need to be adjusted if there are issues.
220+ awaitWriteFinish : {
221+ stabilityThreshold : 1000 ,
222+ } ,
223+ } )
224+ . on ( "change" , ( ) => {
225+ this . readConfigSync ( ) ;
226+ } ) ;
158227 }
159228
160229 private createEmptyConfig ( ) : DbConfig {
0 commit comments