From 3df94b92cdf54b2f76dab61ac6c9a211a98cbdbb Mon Sep 17 00:00:00 2001 From: Nora Date: Fri, 4 Nov 2022 17:14:29 +0100 Subject: [PATCH 1/5] Add basic public config validation --- .../corruptedData/workspace-databases.json | 12 +++++++++ .../databases/db-config-store.test.ts | 25 +++++++++++++++++++ 2 files changed, 37 insertions(+) create mode 100644 extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json diff --git a/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json b/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json new file mode 100644 index 000000000..4ed90539a --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json @@ -0,0 +1,12 @@ +{ + "remote": { + "repositoryLists": [ + { + "name": "repoList1", + "repositories": ["foo/bar", "foo/baz"] + } + ], + "repositories": ["owner/repo1", "owner/repo2", "owner/repo3"], + "somethingElse": "bar" + } +} 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..a25cafae1 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 @@ -6,6 +6,7 @@ import { expect } from 'chai'; describe('db config store', async () => { const tempWorkspaceStoragePath = path.join(__dirname, 'test-workspace'); const testDataStoragePath = path.join(__dirname, 'data'); + const corruptedTestDataStoragePath = path.join(__dirname, 'corruptedData'); beforeEach(async () => { await fs.ensureDir(tempWorkspaceStoragePath); @@ -53,4 +54,28 @@ describe('db config store', async () => { const reRetrievedConfig = configStore.getConfig(); expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1); }); + + it('should return error when file is not valid', async () => { + const configStore = new DbConfigStore(corruptedTestDataStoragePath); + await configStore.initialize(); + + const validationOutput = configStore.validateConfig(); + expect(validationOutput).to.have.length(2); + if (validationOutput) { + expect(validationOutput[0]).to.deep.equal({ + 'instancePath': '/remote', + 'keyword': 'required', + 'message': 'must have required property \'owners\'', + 'params': { 'missingProperty': 'owners' }, + 'schemaPath': '#/properties/remote/required' + }); + expect(validationOutput[1]).to.deep.equal({ + 'instancePath': '/remote', + 'keyword': 'additionalProperties', + 'message': 'must NOT have additional properties', + 'params': { 'additionalProperty': 'somethingElse' }, + 'schemaPath': '#/properties/remote/additionalProperties' + }); + } + }); }); From daec8b691da8c07a7752389b7e8523e3fb8b969d Mon Sep 17 00:00:00 2001 From: Nora Date: Mon, 7 Nov 2022 15:50:11 +0100 Subject: [PATCH 2/5] Extract validation to its own file --- extensions/ql-vscode/package-lock.json | 26 +++----- extensions/ql-vscode/package.json | 2 +- .../src/databases/db-config-validation.ts | 63 +++++++++++++++++++ .../corruptedData/workspace-databases.json | 12 ---- .../databases/db-config-store.test.ts | 25 -------- .../databases/db-config-validation.test.ts | 28 +++++++++ 6 files changed, 99 insertions(+), 57 deletions(-) create mode 100644 extensions/ql-vscode/src/databases/db-config-validation.ts delete mode 100644 extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json create mode 100644 extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts diff --git a/extensions/ql-vscode/package-lock.json b/extensions/ql-vscode/package-lock.json index e279cff05..667466f9a 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 40bb49f68..f575ad275 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-validation.ts b/extensions/ql-vscode/src/databases/db-config-validation.ts new file mode 100644 index 000000000..90b506324 --- /dev/null +++ b/extensions/ql-vscode/src/databases/db-config-validation.ts @@ -0,0 +1,63 @@ +import { DbConfig } from './db-config'; +import Ajv from 'ajv'; + +export function validateDbConfig(dbConfig: DbConfig): string[] { + const ajv = new Ajv({ allErrors: true }); + + const schema = { + type: 'object', + properties: { + remote: { + type: 'object', + properties: { + repositoryLists: { + type: 'array', + items: { + type: 'object', + properties: { + name: { + type: 'string' + }, + repositories: { + type: 'array', + items: { + type: 'string', + pattern: '^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$' + } + } + }, + required: ['name', 'repositories'], + additionalProperties: false + } + }, + owners: { + type: 'array', + items: { + type: 'string', + pattern: '^[a-zA-Z0-9-_\\.]+$' + } + }, + repositories: { + type: 'array', + items: { + type: 'string', + pattern: '^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$' + } + } + }, + required: ['repositoryLists', 'owners', 'repositories'], + additionalProperties: false + } + }, + required: ['remote'], + additionalProperties: false + }; + + ajv.validate(schema, dbConfig); + + if (ajv.errors) { + return ajv.errors.map((error) => `${error.instancePath} ${error.message}`); + } + + return []; +} diff --git a/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json b/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json deleted file mode 100644 index 4ed90539a..000000000 --- a/extensions/ql-vscode/test/pure-tests/databases/corruptedData/workspace-databases.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "remote": { - "repositoryLists": [ - { - "name": "repoList1", - "repositories": ["foo/bar", "foo/baz"] - } - ], - "repositories": ["owner/repo1", "owner/repo2", "owner/repo3"], - "somethingElse": "bar" - } -} 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 a25cafae1..6a1a38025 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 @@ -6,7 +6,6 @@ import { expect } from 'chai'; describe('db config store', async () => { const tempWorkspaceStoragePath = path.join(__dirname, 'test-workspace'); const testDataStoragePath = path.join(__dirname, 'data'); - const corruptedTestDataStoragePath = path.join(__dirname, 'corruptedData'); beforeEach(async () => { await fs.ensureDir(tempWorkspaceStoragePath); @@ -54,28 +53,4 @@ describe('db config store', async () => { const reRetrievedConfig = configStore.getConfig(); expect(reRetrievedConfig.remote.repositoryLists).to.have.length(1); }); - - it('should return error when file is not valid', async () => { - const configStore = new DbConfigStore(corruptedTestDataStoragePath); - await configStore.initialize(); - - const validationOutput = configStore.validateConfig(); - expect(validationOutput).to.have.length(2); - if (validationOutput) { - expect(validationOutput[0]).to.deep.equal({ - 'instancePath': '/remote', - 'keyword': 'required', - 'message': 'must have required property \'owners\'', - 'params': { 'missingProperty': 'owners' }, - 'schemaPath': '#/properties/remote/required' - }); - expect(validationOutput[1]).to.deep.equal({ - 'instancePath': '/remote', - 'keyword': 'additionalProperties', - 'message': 'must NOT have additional properties', - 'params': { 'additionalProperty': 'somethingElse' }, - 'schemaPath': '#/properties/remote/additionalProperties' - }); - } - }); }); diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts new file mode 100644 index 000000000..cc053f033 --- /dev/null +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts @@ -0,0 +1,28 @@ +import { validateDbConfig } from '../../../src/databases/db-config-validation'; +import { DbConfig } from '../../../src/databases/db-config'; + +describe.only('db config validation', async () => { + 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 = validateDbConfig(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'); + }); +}); From 4858be6ac8bbdb67bceb029aba99d939f8255de9 Mon Sep 17 00:00:00 2001 From: Nora Date: Mon, 7 Nov 2022 15:51:42 +0100 Subject: [PATCH 3/5] Extract validation to its own file --- .../test/pure-tests/databases/db-config-validation.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts index cc053f033..cce86be63 100644 --- a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts @@ -1,7 +1,7 @@ import { validateDbConfig } from '../../../src/databases/db-config-validation'; import { DbConfig } from '../../../src/databases/db-config'; -describe.only('db config validation', async () => { +describe('db config validation', async () => { 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. From 916b8404075c879f4b1b6ba666f4bd58da0eda5f Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Mon, 7 Nov 2022 14:56:03 +0000 Subject: [PATCH 4/5] Use new function in db config store --- extensions/ql-vscode/src/databases/db-config-store.ts | 5 +++-- .../test/pure-tests/databases/db-config-validation.test.ts | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/extensions/ql-vscode/src/databases/db-config-store.ts b/extensions/ql-vscode/src/databases/db-config-store.ts index f0ec3cbdb..4788083c5 100644 --- a/extensions/ql-vscode/src/databases/db-config-store.ts +++ b/extensions/ql-vscode/src/databases/db-config-store.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import { cloneDbConfig, DbConfig } from './db-config'; import * as chokidar from 'chokidar'; import { DisposableObject } from '../pure/disposable-object'; +import { validateDbConfig } from './db-config-validation'; export class DbConfigStore extends DisposableObject { private readonly configPath: string; @@ -33,8 +34,8 @@ export class DbConfigStore extends DisposableObject { return cloneDbConfig(this.config); } - public getConfigPath(): string { - return this.configPath; + public validateConfig(): string[] { + return validateDbConfig(this.config); } private async loadConfig(): Promise { diff --git a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts index cce86be63..52497cbc3 100644 --- a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts @@ -1,3 +1,4 @@ +import { expect } from 'chai'; import { validateDbConfig } from '../../../src/databases/db-config-validation'; import { DbConfig } from '../../../src/databases/db-config'; From 03bc63c6898b2c6255d73063fe73dd1a64ca2b2d Mon Sep 17 00:00:00 2001 From: Charis Kyriakou Date: Mon, 7 Nov 2022 16:07:10 +0000 Subject: [PATCH 5/5] Read schema from file --- .../src/databases/db-config-store.ts | 14 ++++- .../src/databases/db-config-validation.ts | 63 ------------------- .../src/databases/db-config-validator.ts | 24 +++++++ .../ql-vscode/src/databases/db-module.ts | 3 +- .../databases/db-config-store.test.ts | 7 ++- ...on.test.ts => db-config-validator.test.ts} | 8 ++- 6 files changed, 47 insertions(+), 72 deletions(-) delete mode 100644 extensions/ql-vscode/src/databases/db-config-validation.ts create mode 100644 extensions/ql-vscode/src/databases/db-config-validator.ts rename extensions/ql-vscode/test/pure-tests/databases/{db-config-validation.test.ts => db-config-validator.test.ts} (75%) diff --git a/extensions/ql-vscode/src/databases/db-config-store.ts b/extensions/ql-vscode/src/databases/db-config-store.ts index 4788083c5..e922943c6 100644 --- a/extensions/ql-vscode/src/databases/db-config-store.ts +++ b/extensions/ql-vscode/src/databases/db-config-store.ts @@ -3,21 +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 { validateDbConfig } from './db-config-validation'; +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 { @@ -34,8 +38,12 @@ export class DbConfigStore extends DisposableObject { return cloneDbConfig(this.config); } + public getConfigPath(): string { + return this.configPath; + } + public validateConfig(): string[] { - return validateDbConfig(this.config); + return this.configValidator.validate(this.config); } private async loadConfig(): Promise { diff --git a/extensions/ql-vscode/src/databases/db-config-validation.ts b/extensions/ql-vscode/src/databases/db-config-validation.ts deleted file mode 100644 index 90b506324..000000000 --- a/extensions/ql-vscode/src/databases/db-config-validation.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { DbConfig } from './db-config'; -import Ajv from 'ajv'; - -export function validateDbConfig(dbConfig: DbConfig): string[] { - const ajv = new Ajv({ allErrors: true }); - - const schema = { - type: 'object', - properties: { - remote: { - type: 'object', - properties: { - repositoryLists: { - type: 'array', - items: { - type: 'object', - properties: { - name: { - type: 'string' - }, - repositories: { - type: 'array', - items: { - type: 'string', - pattern: '^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$' - } - } - }, - required: ['name', 'repositories'], - additionalProperties: false - } - }, - owners: { - type: 'array', - items: { - type: 'string', - pattern: '^[a-zA-Z0-9-_\\.]+$' - } - }, - repositories: { - type: 'array', - items: { - type: 'string', - pattern: '^[a-zA-Z0-9-_\\.]+/[a-zA-Z0-9-_\\.]+$' - } - } - }, - required: ['repositoryLists', 'owners', 'repositories'], - additionalProperties: false - } - }, - required: ['remote'], - additionalProperties: false - }; - - ajv.validate(schema, dbConfig); - - if (ajv.errors) { - return ajv.errors.map((error) => `${error.instancePath} ${error.message}`); - } - - return []; -} 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-validation.test.ts b/extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts similarity index 75% rename from extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts rename to extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts index 52497cbc3..a449cb7bc 100644 --- a/extensions/ql-vscode/test/pure-tests/databases/db-config-validation.test.ts +++ b/extensions/ql-vscode/test/pure-tests/databases/db-config-validator.test.ts @@ -1,8 +1,12 @@ import { expect } from 'chai'; -import { validateDbConfig } from '../../../src/databases/db-config-validation'; +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. @@ -19,7 +23,7 @@ describe('db config validation', async () => { } } as any as DbConfig; - const validationOutput = validateDbConfig(dbConfig); + const validationOutput = configValidator.validate(dbConfig); expect(validationOutput).to.have.length(2);