From 9b1f0b98271367492ceead70a6dbbc80efa175ce Mon Sep 17 00:00:00 2001 From: Philipp Fromme Date: Tue, 10 Sep 2019 16:50:00 +0200 Subject: [PATCH 1/6] feat(config): get and set config for files and plugins * unify ClientConfig and Config into Config * use /config.json as default * add provider for custom functionality * add Client#getForFile, Client#setForFile, Client#getForPlugin and Client#setForPlugin * rename App#loadConfig to #getConfig Related to #1066 --- .../__tests__/client-config-spec.js | 128 --------- app/lib/client-config/index.js | 47 ---- .../providers/element-templates-provider.js | 71 ----- .../client-config/providers/find-templates.js | 86 ------ .../client-config/providers/none-provider.js | 27 -- app/lib/config.js | 89 ------ app/lib/config/__tests__/config-spec.js | 199 ++++++++++++++ .../broken/element-templates/broken.json | 0 app/lib/config/__tests__/fixtures/config.json | 3 + .../fixtures/ok/element-templates/list.json | 0 .../ok/element-templates/not-a-template.txt | 0 .../fixtures/ok/element-templates/single.json | 0 .../.camunda/element-templates/list.json | 0 app/lib/config/index.js | 57 ++++ app/lib/config/providers/DefaultProvider.js | 94 +++++++ .../providers/ElementTemplatesProvider.js | 149 ++++++++++ app/lib/index.js | 49 ++-- client/src/app/App.js | 12 +- client/src/app/__tests__/AppSpec.js | 9 +- client/src/app/tabs/MultiSheetTab.js | 2 +- client/src/app/tabs/bpmn/BpmnEditor.js | 5 +- .../app/tabs/bpmn/__tests__/BpmnEditorSpec.js | 38 +-- client/src/remote/Config.js | 147 +++++++++- client/src/remote/__tests__/ConfigSpec.js | 254 ++++++++++++++++++ client/src/remote/__tests__/mocks/index.js | 34 ++- 25 files changed, 983 insertions(+), 517 deletions(-) delete mode 100644 app/lib/client-config/__tests__/client-config-spec.js delete mode 100644 app/lib/client-config/index.js delete mode 100644 app/lib/client-config/providers/element-templates-provider.js delete mode 100644 app/lib/client-config/providers/find-templates.js delete mode 100644 app/lib/client-config/providers/none-provider.js delete mode 100644 app/lib/config.js create mode 100644 app/lib/config/__tests__/config-spec.js rename app/lib/{client-config => config}/__tests__/fixtures/broken/element-templates/broken.json (100%) create mode 100644 app/lib/config/__tests__/fixtures/config.json rename app/lib/{client-config => config}/__tests__/fixtures/ok/element-templates/list.json (100%) rename app/lib/{client-config => config}/__tests__/fixtures/ok/element-templates/not-a-template.txt (100%) rename app/lib/{client-config => config}/__tests__/fixtures/ok/element-templates/single.json (100%) rename app/lib/{client-config => config}/__tests__/fixtures/project/.camunda/element-templates/list.json (100%) create mode 100644 app/lib/config/index.js create mode 100644 app/lib/config/providers/DefaultProvider.js create mode 100644 app/lib/config/providers/ElementTemplatesProvider.js create mode 100644 client/src/remote/__tests__/ConfigSpec.js diff --git a/app/lib/client-config/__tests__/client-config-spec.js b/app/lib/client-config/__tests__/client-config-spec.js deleted file mode 100644 index 1180e2e032..0000000000 --- a/app/lib/client-config/__tests__/client-config-spec.js +++ /dev/null @@ -1,128 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -const ClientConfig = require('..'); - -const path = require('path'); - - -describe('ClientConfig', function() { - - describe('', function() { - - it('should provide', function(done) { - - // given - var fakeDiagram = { - file: { - path: absPath('fixtures/project/bar.bpmn') - } - }; - - const clientConfig = new ClientConfig({ - paths: [ - absPath('fixtures/ok') - ] - }); - - // when - clientConfig.get('bpmn.elementTemplates', fakeDiagram, function(err, templates) { - - if (err) { - return done(err); - } - - // then - expect(err).not.to.exist; - - // local templates loaded first - expect(templates).to.eql([ - { id: 'com.foo.Bar' }, // local (!!!) - { id: 'com.foo.Bar', FOO: 'BAR' }, - { id: 'single', FOO: 'BAR' } - ]); - - done(); - }); - - }); - - - it('should not throw for a new file', function(done) { - - // given - var fakeDiagram = { - file: { - path: null - } - }; - - const clientConfig = new ClientConfig({ - paths: [ - absPath('fixtures/ok') - ] - }); - - // when - clientConfig.get('bpmn.elementTemplates', fakeDiagram, function(err, templates) { - - if (err) { - return done(err); - } - - // then - expect(err).not.to.exist; - - // local templates loaded first - expect(templates).to.eql([ - { id: 'com.foo.Bar', FOO: 'BAR' }, - { id: 'single', FOO: 'BAR' } - ]); - - done(); - }); - - }); - - - it('should propagate JSON parse error', function(done) { - - // given - const fakeDiagram = null; - - const clientConfig = new ClientConfig({ - paths: [ - absPath('fixtures/broken') - ] - }); - - // when - clientConfig.get('bpmn.elementTemplates', fakeDiagram, function(err, templates) { - - // then - expect(err).to.exist; - - expect(err.message).to.match(/template .* parse error: Unexpected token I.*/); - - done(); - }); - - }); - - }); - -}); - - -// helpers /////////////////// - -function absPath(file) { - return path.resolve(__dirname, file); -} diff --git a/app/lib/client-config/index.js b/app/lib/client-config/index.js deleted file mode 100644 index b7f24a78a6..0000000000 --- a/app/lib/client-config/index.js +++ /dev/null @@ -1,47 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -'use strict'; - -var NoneProvider = require('./providers/none-provider'); - -var ElementTemplatesProvider = require('./providers/element-templates-provider'); - - -/** - * A way to allow clients to retrieve configuration at run-time. - * - * @param {Object} options - */ -function ClientConfig(options) { - - this._providers = { - '_': new NoneProvider(), - 'bpmn.elementTemplates': new ElementTemplatesProvider(options) - }; - - /** - * Retrieve a configuration entry. - * - * @param {String} key - * @param {Object...} args - * @param {Function} callback - * - * @return {Object} - */ - this.get = function(key) { - - var provider = this._providers[key] || this._providers['_']; - - return provider.get.apply(provider, arguments); - }; -} - -module.exports = ClientConfig; \ No newline at end of file diff --git a/app/lib/client-config/providers/element-templates-provider.js b/app/lib/client-config/providers/element-templates-provider.js deleted file mode 100644 index 3ddac84bca..0000000000 --- a/app/lib/client-config/providers/element-templates-provider.js +++ /dev/null @@ -1,71 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -'use strict'; - -var parents = require('parents'); - -var path = require('path'); - -var findTemplates = require('./find-templates'); - - -/** - * Nop, you aint gonna load this configuration. - */ -function ElementTemplatesProvider(options) { - - const defaultPaths = options.paths; - - /** - * Return element templates for the given diagram. - * - * @param {String} key - * @param {DiagramDescriptor} diagram - * @param {Function} done - */ - this.get = function(key, diagram, done) { - - if (typeof done !== 'function') { - throw new Error('expected callback'); - } - - var localPaths = diagram && diagram.file && diagram.file.path ? parents(diagram.file.path) : []; - - var searchPaths = [ - ...suffixAll(localPaths, '.camunda'), - ...defaultPaths - ]; - - var templates = []; - - try { - templates = findTemplates(searchPaths); - - done(null, templates); - } catch (err) { - done(err); - } - }; - -} - -module.exports = ElementTemplatesProvider; - - - -// helpers ////////////////// - -function suffixAll(paths, suffix) { - - return paths.map(function(p) { - return path.join(p, suffix); - }); -} diff --git a/app/lib/client-config/providers/find-templates.js b/app/lib/client-config/providers/find-templates.js deleted file mode 100644 index f86320abe0..0000000000 --- a/app/lib/client-config/providers/find-templates.js +++ /dev/null @@ -1,86 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -const fs = require('fs'); - -const glob = require('glob'); - -const { - isArray -} = require('min-dash'); - -const log = require('../../log')('app:client-config:element-templates'); - - -/** - * Finds templates under given search paths. - * - * @param {Array} searchPaths - * - * @return {Array} - */ -module.exports = function findTemplates(searchPaths) { - - var allTemplates = searchPaths.reduce(function(templates, path) { - - var files; - - // treat globbing errors as warnings to - // gracefully handle permission / file not found errors - try { - files = globTemplates(path); - } catch (err) { - log.error('glob failed', err); - - return templates; - } - - return files.reduce(function(templates, filePath) { - try { - var parsedTemplates = JSON.parse(fs.readFileSync(filePath, 'utf8')); - - if (!isArray(parsedTemplates)) { - parsedTemplates = [ parsedTemplates ]; - } - - return [].concat(templates, parsedTemplates); - } catch (err) { - log.error('failed to parse template %s', filePath, err); - - throw new Error('template ' + filePath + ' parse error: ' + err.message); - } - }, templates); - }, []); - - return allTemplates; -}; - - - -// helpers ////////////////// - -/** - * Locate element templates in 'element-templates' - * sub directories local to given path. - * - * @param {String} path - * - * @return {Array} found templates. - */ -function globTemplates(path) { - - var globOptions = { - cwd: path, - nodir: true, - realpath: true - }; - - return glob.sync('element-templates/**/*.json', globOptions); -} \ No newline at end of file diff --git a/app/lib/client-config/providers/none-provider.js b/app/lib/client-config/providers/none-provider.js deleted file mode 100644 index 8ed72f7a2e..0000000000 --- a/app/lib/client-config/providers/none-provider.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -'use strict'; - -/** - * Nop, you aint gonna load this configuration. - */ -function NoneProvider() { - - this.get = function() { - - var key = arguments[0]; - var done = arguments[arguments.length - 1]; - - return done(new Error('no provider for <' + key + '>')); - }; -} - -module.exports = NoneProvider; \ No newline at end of file diff --git a/app/lib/config.js b/app/lib/config.js deleted file mode 100644 index bb49959812..0000000000 --- a/app/lib/config.js +++ /dev/null @@ -1,89 +0,0 @@ -/** - * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH - * under one or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information regarding copyright - * ownership. - * - * Camunda licenses this file to you under the MIT; you may not use this file - * except in compliance with the MIT License. - */ - -const fs = require('fs'); -const path = require('path'); - -const log = require('./log')('app:config'); - -function Config(options) { - this._configPath = path.join(options.path, 'config.json'); - - this.loadSync(); -} - -module.exports = Config; - - -/** - * Loads the current configuration, defaulting to reseting - * if loading fails. - * - * @throws {Error} if reading the file or deserializing fails. - */ -Config.prototype.loadSync = function() { - - const path = this._configPath; - - log.info('loading from %s', path); - - var stringifiedData; - - try { - stringifiedData = fs.readFileSync(path, { encoding: 'utf8' }); - - this._data = JSON.parse(stringifiedData); - } catch (err) { - this._data = {}; - - // ignore non-existing file error - if (err.code === 'ENOENT') { - return; - } - - log.error('failed to load', err); - } -}; - -/** - * Saves the current configuration. - * - * @throws {Error} if serializing to JSON or writing the file failed - */ -Config.prototype.saveSync = function() { - fs.writeFileSync(this._configPath, JSON.stringify(this._data), { encoding: 'utf8' }); - - log.info('saved'); -}; - - -Config.prototype.get = function(key, defaultValue) { - var result = this._data[key]; - - if (result !== undefined) { - return result; - } else { - return defaultValue; - } -}; - - -Config.prototype.set = function(key, value) { - this._data[key] = value; - - log.info('set %s', key); - - try { - this.saveSync(); - } catch (err) { - log.error('failed to write', err); - } -}; - diff --git a/app/lib/config/__tests__/config-spec.js b/app/lib/config/__tests__/config-spec.js new file mode 100644 index 0000000000..c45bd09cae --- /dev/null +++ b/app/lib/config/__tests__/config-spec.js @@ -0,0 +1,199 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +const Config = require('..'); + +const fs = require('fs'); +const path = require('path'); + + +describe('Config', function() { + + describe('default', function() { + + let file; + + beforeEach(function() { + file = fs.readFileSync(getAbsolutePath('fixtures/config.json'), { encoding: 'utf8' }); + }); + + afterEach(function() { + fs.writeFileSync(getAbsolutePath('fixtures/config.json'), file, { encoding: 'utf8' }); + }); + + + describe('#get', function() { + + it('should get', function() { + + // given + const config = new Config({ + userPath: getAbsolutePath('fixtures') + }); + + // when + const value = config.get('foo'); + + // then + expect(value).to.equal(42); + }); + + + it('should get all', function() { + + // given + const config = new Config({ + userPath: getAbsolutePath('fixtures') + }); + + // when + const value = config.get(); + + // then + expect(value).to.eql({ + foo: 42 + }); + }); + + + it('should return null', function() { + + // given + const config = new Config({ + userPath: getAbsolutePath('fixtures') + }); + + // when + const value = config.get('bar'); + + // then + expect(value).to.eql(null); + }); + + + it('should return default value', function() { + + // given + const config = new Config({ + userPath: getAbsolutePath('fixtures') + }); + + // when + const value = config.get('bar', 42); + + // then + expect(value).to.eql(42); + }); + + }); + + + describe('#set', function() { + + it('should set', function() { + + // given + const config = new Config({ + userPath: getAbsolutePath('fixtures') + }); + + // when + config.set('foo', false); + + // then + const value = config.get('foo'); + + expect(value).to.equal(false); + }); + + }); + + }); + + describe('', function() { + + it('should get', function() { + + // given + const file = { + path: getAbsolutePath('fixtures/project/bar.bpmn') + }; + + const config = new Config({ + resourcesPaths: [ + getAbsolutePath('fixtures/ok') + ], + userPath: 'foo' + }); + + // when + const templates = config.get('bpmn.elementTemplates', file); + + // then + expect(templates).to.eql([ + { id: 'com.foo.Bar' }, // local + { id: 'com.foo.Bar', FOO: 'BAR' }, // global + { id: 'single', FOO: 'BAR' } // global + ]); + }); + + + it('should NOT throw if new file', function() { + + // given + const file = { + path: null + }; + + const config = new Config({ + resourcesPaths: [ + getAbsolutePath('fixtures/ok') + ], + userPath: 'foo' + }); + + // when + const templates = config.get('bpmn.elementTemplates', file); + + // then + expect(templates).to.eql([ + { id: 'com.foo.Bar', FOO: 'BAR' }, + { id: 'single', FOO: 'BAR' } + ]); + }); + + + it('should throw if JSON#parse errors', function() { + + // given + const file = null; + + const config = new Config({ + resourcesPaths: [ + getAbsolutePath('fixtures/broken') + ], + userPath: 'foo' + }); + + // when + expect(() => config.get('bpmn.elementTemplates', file)) + .to.throw(/template .* parse error: Unexpected token I.*/); + }); + + }); + +}); + + +// helpers /////////////////// + +function getAbsolutePath(relativePath) { + return path.resolve(__dirname, relativePath); +} diff --git a/app/lib/client-config/__tests__/fixtures/broken/element-templates/broken.json b/app/lib/config/__tests__/fixtures/broken/element-templates/broken.json similarity index 100% rename from app/lib/client-config/__tests__/fixtures/broken/element-templates/broken.json rename to app/lib/config/__tests__/fixtures/broken/element-templates/broken.json diff --git a/app/lib/config/__tests__/fixtures/config.json b/app/lib/config/__tests__/fixtures/config.json new file mode 100644 index 0000000000..74ca687c22 --- /dev/null +++ b/app/lib/config/__tests__/fixtures/config.json @@ -0,0 +1,3 @@ +{ + "foo": 42 +} \ No newline at end of file diff --git a/app/lib/client-config/__tests__/fixtures/ok/element-templates/list.json b/app/lib/config/__tests__/fixtures/ok/element-templates/list.json similarity index 100% rename from app/lib/client-config/__tests__/fixtures/ok/element-templates/list.json rename to app/lib/config/__tests__/fixtures/ok/element-templates/list.json diff --git a/app/lib/client-config/__tests__/fixtures/ok/element-templates/not-a-template.txt b/app/lib/config/__tests__/fixtures/ok/element-templates/not-a-template.txt similarity index 100% rename from app/lib/client-config/__tests__/fixtures/ok/element-templates/not-a-template.txt rename to app/lib/config/__tests__/fixtures/ok/element-templates/not-a-template.txt diff --git a/app/lib/client-config/__tests__/fixtures/ok/element-templates/single.json b/app/lib/config/__tests__/fixtures/ok/element-templates/single.json similarity index 100% rename from app/lib/client-config/__tests__/fixtures/ok/element-templates/single.json rename to app/lib/config/__tests__/fixtures/ok/element-templates/single.json diff --git a/app/lib/client-config/__tests__/fixtures/project/.camunda/element-templates/list.json b/app/lib/config/__tests__/fixtures/project/.camunda/element-templates/list.json similarity index 100% rename from app/lib/client-config/__tests__/fixtures/project/.camunda/element-templates/list.json rename to app/lib/config/__tests__/fixtures/project/.camunda/element-templates/list.json diff --git a/app/lib/config/index.js b/app/lib/config/index.js new file mode 100644 index 0000000000..0cc5cf3688 --- /dev/null +++ b/app/lib/config/index.js @@ -0,0 +1,57 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +const path = require('path'); + +const DefaultProvider = require('./providers/DefaultProvider'); +const ElementTemplatesProvider = require('./providers/ElementTemplatesProvider'); + +const { isFunction } = require('min-dash'); + +/** + * Configuration functionality. Gets/sets config from/to /config.json by default. + * Add provider for key for custom get/set functionality. + */ +class Config { + constructor(options) { + const { + resourcesPaths, + userPath + } = options; + + this._defaultProvider = new DefaultProvider(path.join(userPath, 'config.json')); + + this._providers = { + 'bpmn.elementTemplates': new ElementTemplatesProvider(resourcesPaths) + }; + } + + get(key, ...args) { + const provider = this._providers[ key ] || this._defaultProvider; + + if (!isFunction(provider.get)) { + throw new Error(`provider for <${ key }> cannot get`); + } + + return provider.get(key, ...args); + } + + set(key, value, ...args) { + const provider = this._providers[ key ] || this._defaultProvider; + + if (!isFunction(provider.set)) { + throw new Error(`provider for <${ key }> cannot set`); + } + + return provider.set(key, value, ...args); + } +} + +module.exports = Config; \ No newline at end of file diff --git a/app/lib/config/providers/DefaultProvider.js b/app/lib/config/providers/DefaultProvider.js new file mode 100644 index 0000000000..442a3dd1df --- /dev/null +++ b/app/lib/config/providers/DefaultProvider.js @@ -0,0 +1,94 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +const fs = require('fs'); + +const log = require('../../log')('app:config:default'); + +const { isNil } = require('min-dash'); + +/** + * Default config provider. Reads and writes config to `config.json` under `userPath`. + */ +class DefaultProvider { + constructor(path) { + this._path = path; + + this._json = null; + } + + /** + * Get configuration value by key. + * + * @param {string} [key] + * @param {*} [defaultValue] + * + * @returns {*} + */ + get(key, defaultValue = null) { + const json = this._json || this._readFile(); + + if (!key) { + return json; + } + + const value = json[ key ]; + + if (isNil(value)) { + return defaultValue; + } + + return value; + } + + _readFile() { + try { + return JSON.parse(fs.readFileSync(this._path, 'utf8')); + } catch (error) { + + // do not throw if no such file + if (error.code === 'ENOENT') { + return {}; + } + + this._json = null; + + log.error(`cannot read file ${ this._path }`, error); + + throw new Error(`cannot read file ${ this._path }`); + } + } + + /** + * Set a configuration value by key. + * + * @param {string} keys + * @param {*} value + */ + set(key, value) { + const json = this._json = this._json || this._readFile(); + + json[ key ] = value; + + this._writeFile(); + } + + _writeFile() { + try { + fs.writeFileSync(this._path, JSON.stringify(this._json, null, 2), 'utf8'); + } catch (error) { + log.error(`cannot write file ${ this.path }`, error); + + throw new Error(`cannot write file ${ this.path }`); + } + } +} + +module.exports = DefaultProvider; \ No newline at end of file diff --git a/app/lib/config/providers/ElementTemplatesProvider.js b/app/lib/config/providers/ElementTemplatesProvider.js new file mode 100644 index 0000000000..3886771af8 --- /dev/null +++ b/app/lib/config/providers/ElementTemplatesProvider.js @@ -0,0 +1,149 @@ +/** + * Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH + * under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright + * ownership. + * + * Camunda licenses this file to you under the MIT; you may not use this file + * except in compliance with the MIT License. + */ + +const fs = require('fs'); +const glob = require('glob'); +const parents = require('parents'); +const path = require('path'); + +const { isArray } = require('min-dash'); + +const log = require('../../log')('app:config:element-templates'); + + +/** + * Get element templates. + */ +class ElementTemplatesProvider { + constructor(paths) { + this._paths = paths; + } + + /** + * Get element templates for file. + * + * @param {string} _ + * @param {File} file + * + * @returns {Array