diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index 269f90501..3b8bc4e72 100644 --- a/extensions/ql-vscode/package-lock.json +++ b/extensions/ql-vscode/package-lock.json @@ -15,6 +15,7 @@ "@primer/react": "^35.0.0", "@vscode/codicons": "^0.0.31", "@vscode/webview-ui-toolkit": "^1.0.1", + "ajv": "^8.11.0", "child-process-promise": "^2.2.1", "chokidar": "^3.5.3", "classnames": "~2.2.6", @@ -105,7 +106,6 @@ "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", "@vscode/test-electron": "^2.2.0", - "ajv": "^8.11.0", "ansi-colors": "^4.1.1", "applicationinsights": "^2.3.5", "babel-loader": "^8.2.5", @@ -14650,7 +14650,6 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -21602,8 +21601,7 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "node_modules/fast-glob": { "version": "3.2.11", @@ -29613,8 +29611,7 @@ "node_modules/json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -33897,7 +33894,6 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true, "engines": { "node": ">=6" } @@ -34880,7 +34876,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -38512,7 +38507,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "dependencies": { "punycode": "^2.1.0" } @@ -51543,7 +51537,6 @@ "version": "8.11.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.11.0.tgz", "integrity": "sha512-wGgprdCvMalC0BztXvitD2hC04YffAvtsUn93JbGXYLAtCUO4xd17mCCZQxUOItiBwZvJScWo8NIvQMQ71rdpg==", - "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "json-schema-traverse": "^1.0.0", @@ -57032,8 +57025,7 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, "fast-glob": { "version": "3.2.11", @@ -63145,8 +63137,7 @@ "json-schema-traverse": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==" }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -66535,8 +66526,7 @@ "punycode": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==", - "dev": true + "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" }, "qs": { "version": "6.10.3", @@ -67289,8 +67279,7 @@ "require-from-string": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==" }, "require-main-filename": { "version": "1.0.1", @@ -70084,7 +70073,6 @@ "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", - "dev": true, "requires": { "punycode": "^2.1.0" } diff --git a/extensions/ql-vscode/package.json b/extensions/ql-vscode/package.json index 45b6d79d2..50bef2126 100644 --- a/extensions/ql-vscode/package.json +++ b/extensions/ql-vscode/package.json @@ -1299,6 +1299,7 @@ "@primer/react": "^35.0.0", "@vscode/codicons": "^0.0.31", "@vscode/webview-ui-toolkit": "^1.0.1", + "ajv": "^8.11.0", "child-process-promise": "^2.2.1", "chokidar": "^3.5.3", "classnames": "~2.2.6", @@ -1389,7 +1390,6 @@ "@typescript-eslint/eslint-plugin": "^4.26.0", "@typescript-eslint/parser": "^4.26.0", "@vscode/test-electron": "^2.2.0", - "ajv": "^8.11.0", "ansi-colors": "^4.1.1", "applicationinsights": "^2.3.5", "babel-loader": "^8.2.5", diff --git a/extensions/ql-vscode/src/databases/db-config-store.ts b/extensions/ql-vscode/src/databases/db-config-store.ts index f0ec3cbdb..e922943c6 100644 --- a/extensions/ql-vscode/src/databases/db-config-store.ts +++ b/extensions/ql-vscode/src/databases/db-config-store.ts @@ -3,20 +3,25 @@ import * as path from 'path'; import { cloneDbConfig, DbConfig } from './db-config'; import * as chokidar from 'chokidar'; import { DisposableObject } from '../pure/disposable-object'; +import { DbConfigValidator } from './db-config-validator'; export class DbConfigStore extends DisposableObject { private readonly configPath: string; + private readonly configValidator: DbConfigValidator; private config: DbConfig; private configWatcher: chokidar.FSWatcher | undefined; - public constructor(workspaceStoragePath: string) { + public constructor( + workspaceStoragePath: string, + extensionPath: string) { super(); this.configPath = path.join(workspaceStoragePath, 'workspace-databases.json'); this.config = this.createEmptyConfig(); this.configWatcher = undefined; + this.configValidator = new DbConfigValidator(extensionPath); } public async initialize(): Promise { @@ -37,6 +42,10 @@ export class DbConfigStore extends DisposableObject { return this.configPath; } + public validateConfig(): string[] { + return this.configValidator.validate(this.config); + } + private async loadConfig(): Promise { if (!await fs.pathExists(this.configPath)) { await fs.writeJSON(this.configPath, this.createEmptyConfig(), { spaces: 2 }); diff --git a/extensions/ql-vscode/src/databases/db-config-validator.ts b/extensions/ql-vscode/src/databases/db-config-validator.ts new file mode 100644 index 000000000..f9dc90c56 --- /dev/null +++ b/extensions/ql-vscode/src/databases/db-config-validator.ts @@ -0,0 +1,24 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import Ajv from 'ajv'; +import { DbConfig } from './db-config'; + +export class DbConfigValidator { + private readonly schema: any; + + constructor(extensionPath: string) { + const schemaPath = path.resolve(extensionPath, 'workspace-databases-schema.json'); + this.schema = fs.readJsonSync(schemaPath); + } + + public validate(dbConfig: DbConfig): string[] { + const ajv = new Ajv({ allErrors: true }); + ajv.validate(this.schema, dbConfig); + + if (ajv.errors) { + return ajv.errors.map((error) => `${error.instancePath} ${error.message}`); + } + + return []; + } +} diff --git a/extensions/ql-vscode/src/databases/db-module.ts b/extensions/ql-vscode/src/databases/db-module.ts index b212ddd41..4619aec50 100644 --- a/extensions/ql-vscode/src/databases/db-module.ts +++ b/extensions/ql-vscode/src/databases/db-module.ts @@ -22,7 +22,8 @@ export class DbModule extends DisposableObject { void logger.log('Initializing database module'); const storagePath = extensionContext.storageUri?.fsPath || extensionContext.globalStorageUri.fsPath; - const dbConfigStore = new DbConfigStore(storagePath); + const extensionPath = extensionContext.extensionPath; + const dbConfigStore = new DbConfigStore(storagePath, extensionPath); await dbConfigStore.initialize(); const dbManager = new DbManager(dbConfigStore); diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts index 6a1a38025..d921f62eb 100644 --- a/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-store.test.ts @@ -4,6 +4,7 @@ import { DbConfigStore } from '../../../src/databases/db-config-store'; import { expect } from 'chai'; describe('db config store', async () => { + const extensionPath = path.join(__dirname, '../../..'); const tempWorkspaceStoragePath = path.join(__dirname, 'test-workspace'); const testDataStoragePath = path.join(__dirname, 'data'); @@ -18,7 +19,7 @@ describe('db config store', async () => { it('should create a new config if one does not exist', async () => { const configPath = path.join(tempWorkspaceStoragePath, 'workspace-databases.json'); - const configStore = new DbConfigStore(tempWorkspaceStoragePath); + const configStore = new DbConfigStore(tempWorkspaceStoragePath, extensionPath); await configStore.initialize(); expect(await fs.pathExists(configPath)).to.be.true; @@ -29,7 +30,7 @@ describe('db config store', async () => { }); it('should load an existing config', async () => { - const configStore = new DbConfigStore(testDataStoragePath); + const configStore = new DbConfigStore(testDataStoragePath, extensionPath); await configStore.initialize(); const config = configStore.getConfig(); @@ -44,7 +45,7 @@ describe('db config store', async () => { }); it('should not allow modification of the config', async () => { - const configStore = new DbConfigStore(testDataStoragePath); + const configStore = new DbConfigStore(testDataStoragePath, extensionPath); await configStore.initialize(); const config = configStore.getConfig(); diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts new file mode 100644 index 000000000..a449cb7bc --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts @@ -0,0 +1,33 @@ +import { expect } from 'chai'; +import * as path from 'path'; +import { DbConfig } from '../../../src/databases/db-config'; +import { DbConfigValidator } from '../../../src/databases/db-config-validator'; + +describe('db config validation', async () => { + const extensionPath = path.join(__dirname, '../../..'); + const configValidator = new DbConfigValidator(extensionPath); + + it('should return error when file is not valid', async () => { + // We're intentionally bypassing the type check because we'd + // like to make sure validation errors are highlighted. + const dbConfig = { + 'remote': { + 'repositoryLists': [ + { + 'name': 'repoList1', + 'repositories': ['foo/bar', 'foo/baz'] + } + ], + 'repositories': ['owner/repo1', 'owner/repo2', 'owner/repo3'], + 'somethingElse': 'bar' + } + } as any as DbConfig; + + const validationOutput = configValidator.validate(dbConfig); + + expect(validationOutput).to.have.length(2); + + expect(validationOutput[0]).to.deep.equal('/remote must have required property \'owners\''); + expect(validationOutput[1]).to.deep.equal('/remote must NOT have additional properties'); + }); +});