Skip to content

Commit

Permalink
Load yaml settings files synchronously
Browse files Browse the repository at this point in the history
refs https://github.com/TryGhost/Team/issues/65

- it's easier for the architecture if we read the setting files synchronously,
  because the dynamic routing component is part of the express bootstrap and
  the whole routing bootstrap is synchronously
- for now: we only read one file anyway
- it's for now easier to read the file synchronously, then i don't have to change
  any existing express bootstrap architecture
  • Loading branch information
kirrg001 committed Apr 20, 2018
1 parent e43bdad commit 0ac19dc
Show file tree
Hide file tree
Showing 4 changed files with 110 additions and 109 deletions.
33 changes: 12 additions & 21 deletions core/server/services/settings/index.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
'use strict';

/**
* Settings Lib
* A collection of utilities for handling settings including a cache
*/

var SettingsModel = require('../../models/settings').Settings,
const _ = require('lodash'),
SettingsModel = require('../../models/settings').Settings,
SettingsCache = require('./cache'),
SettingsLoader = require('./loader'),
// EnsureSettingsFiles = require('./ensure-settings'),
_ = require('lodash'),
common = require('../../lib/common'),
debug = require('ghost-ignition').debug('services:settings:index');

Expand Down Expand Up @@ -49,7 +50,7 @@ module.exports = {
* will return an Object like this:
* {routes: {}, collections: {}, resources: {}}
* @param {String} setting type of supported setting.
* @returns {Promise<Object>} settingsFile
* @returns {Object} settingsFile
* @description Returns settings object as defined per YAML files in
* `/content/settings` directory.
*/
Expand All @@ -59,18 +60,13 @@ module.exports = {
// CASE: this should be an edge case and only if internal usage of the
// getter is incorrect.
if (!setting || _.indexOf(knownSettings, setting) < 0) {
return Promise.reject(new common.errors.IncorrectUsageError({
throw new common.errors.IncorrectUsageError({
message: `Requested setting is not supported: '${setting}'.`,
help: `Please use only the supported settings: ${knownSettings}.`
}));
});
}

return SettingsLoader(setting)
.then((settingsFile) => {
debug('setting loaded and parsed:', settingsFile);

return settingsFile;
});
return SettingsLoader(setting);
},

/**
Expand All @@ -88,23 +84,18 @@ module.exports = {
* config: { url: 'testblog.com' }
* }
* }
* @returns {Promise<Object>} settingsObject
* @returns {Object} settingsObject
* @description Returns all settings object as defined per YAML files in
* `/content/settings` directory.
*/
getAll: function getAll() {
const knownSettings = this.knownSettings(),
props = {};
settingsToReturn = {};

_.each(knownSettings, function (setting) {
props[setting] = SettingsLoader(setting);
settingsToReturn[setting] = SettingsLoader(setting);
});

return Promise.props(props)
.then((settingsFile) => {
debug('all settings loaded and parsed:', settingsFile);

return settingsFile;
});
return settingsToReturn;
}
};
32 changes: 15 additions & 17 deletions core/server/services/settings/loader.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,31 +11,29 @@ const fs = require('fs-extra'),
* Reads the desired settings YAML file and passes the
* file to the YAML parser which then returns a JSON object.
* @param {String} setting the requested settings as defined in setting knownSettings
* @returns {Promise<Object>} settingsFile
* @returns {Object} settingsFile
*/
module.exports = function loadSettings(setting) {
// we only support the `yaml` file extension. `yml` will be ignored.
const fileName = `${setting}.yaml`;
const contentPath = config.getContentPath('settings');
const filePath = path.join(contentPath, fileName);

return fs.readFile(filePath, 'utf8')
.then((file) => {
debug('settings file found for', setting);
try {
const file = fs.readFileSync(filePath, 'utf8');
debug('settings file found for', setting);

// yamlParser returns a JSON object
const parsed = yamlParser(file, fileName);
// yamlParser returns a JSON object
return yamlParser(file, fileName);
} catch (err) {
if (common.errors.utils.isIgnitionError(err)) {
throw err;
}

return parsed;
}).catch((error) => {
if (common.errors.utils.isIgnitionError(error)) {
throw error;
}

throw new common.errors.GhostError({
message: common.i18n.t('errors.services.settings.loader', {setting: setting, path: contentPath}),
context: filePath,
err: error
});
throw new common.errors.GhostError({
message: common.i18n.t('errors.services.settings.loader', {setting: setting, path: contentPath}),
context: filePath,
err: err
});
}
};
68 changes: 39 additions & 29 deletions core/test/unit/services/settings/loader_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@ const sinon = require('sinon'),
should = require('should'),
rewire = require('rewire'),
fs = require('fs-extra'),
yaml = require('js-yaml'),
path = require('path'),
configUtils = require('../../../utils/configUtils'),
common = require('../../../../server/lib/common'),

loadSettings = rewire('../../../../server/services/settings/loader'),

sandbox = sinon.sandbox.create();

describe('UNIT > Settings Service:', function () {
Expand Down Expand Up @@ -41,54 +38,67 @@ describe('UNIT > Settings Service:', function () {
});

it('can find yaml settings file and returns a settings object', function () {
const fsReadFileSpy = sandbox.spy(fs, 'readFile');
const fsReadFileSpy = sandbox.spy(fs, 'readFileSync');
const expectedSettingsFile = path.join(__dirname, '../../../utils/fixtures/settings/goodroutes.yaml');

yamlParserStub.returns(yamlStubFile);
loadSettings.__set__('yamlParser', yamlParserStub);

return loadSettings('goodroutes').then((setting) => {
should.exist(setting);
setting.should.be.an.Object().with.properties('routes', 'collections', 'resources');
// There are 4 files in the fixtures folder, but only 1 supported and valid yaml files
fsReadFileSpy.calledOnce.should.be.true();
fsReadFileSpy.calledWith(expectedSettingsFile).should.be.true();
yamlParserStub.callCount.should.be.eql(1);
});
const setting = loadSettings('goodroutes');
should.exist(setting);
setting.should.be.an.Object().with.properties('routes', 'collections', 'resources');
// There are 4 files in the fixtures folder, but only 1 supported and valid yaml files
fsReadFileSpy.calledOnce.should.be.true();
fsReadFileSpy.calledWith(expectedSettingsFile).should.be.true();
yamlParserStub.callCount.should.be.eql(1);
});

it('can handle errors from YAML parser', function () {
yamlParserStub.rejects(new common.errors.GhostError({
it('can handle errors from YAML parser', function (done) {
yamlParserStub.throws(new common.errors.GhostError({
message: 'could not parse yaml file',
context: 'bad indentation of a mapping entry at line 5, column 10'
}));

loadSettings.__set__('yamlParser', yamlParserStub);

return loadSettings('goodroutes').then((setting) => {
should.not.exist(setting);
}).catch((error) => {
should.exist(error);
error.message.should.be.eql('could not parse yaml file');
error.context.should.be.eql('bad indentation of a mapping entry at line 5, column 10');
try {
loadSettings('goodroutes');
done(new Error('Loader should fail'));
} catch (err) {
should.exist(err);
err.message.should.be.eql('could not parse yaml file');
err.context.should.be.eql('bad indentation of a mapping entry at line 5, column 10');
yamlParserStub.calledOnce.should.be.true();
});
done();
}
});

it('throws error if file can\'t be accessed', function () {
it('throws error if file can\'t be accessed', function (done) {
const expectedSettingsFile = path.join(__dirname, '../../../utils/fixtures/settings/routes.yaml');
const fsError = new Error('no permission');
fsError.code = 'EPERM';
const fsReadFileStub = sandbox.stub(fs, 'readFile').rejects(fsError);

const originalFn = fs.readFileSync;
const fsReadFileStub = sandbox.stub(fs, 'readFileSync').callsFake(function (filePath, options) {
if (filePath.match(/routes\.yaml/)) {
throw fsError;
}

return originalFn(filePath, options);
});

yamlParserStub = sinon.spy();
loadSettings.__set__('yamlParser', yamlParserStub);

return loadSettings('routes').then((settings) => {
should.not.exist(settings);
}).catch((error) => {
should.exist(error);
error.message.should.match(/Error trying to load YAML setting for routes from/);
try {
loadSettings('routes');
done(new Error('Loader should fail'));
} catch (err) {
err.message.should.match(/Error trying to load YAML setting for routes from/);
fsReadFileStub.calledWith(expectedSettingsFile).should.be.true();
yamlParserStub.calledOnce.should.be.false();
});
done();
}
});
});
});
86 changes: 44 additions & 42 deletions core/test/unit/services/settings/settings_spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ const sinon = require('sinon'),
should = require('should'),
rewire = require('rewire'),
common = require('../../../../server/lib/common'),

settings = rewire('../../../../server/services/settings/index'),

sandbox = sinon.sandbox.create();

describe('UNIT > Settings Service:', function () {
Expand Down Expand Up @@ -42,40 +40,43 @@ describe('UNIT > Settings Service:', function () {
});

it('returns settings object for `routes`', function () {
settingsLoaderStub.resolves(settingsStubFile);
settingsLoaderStub.returns(settingsStubFile);
settings.__set__('SettingsLoader', settingsLoaderStub);

settings.get('routes').then((result) => {
should.exist(result);
result.should.be.an.Object().with.properties('routes', 'collections', 'resources');
settingsLoaderStub.calledOnce.should.be.true();
});
const result = settings.get('routes');
should.exist(result);
result.should.be.an.Object().with.properties('routes', 'collections', 'resources');
settingsLoaderStub.calledOnce.should.be.true();
});

it('rejects when requested settings type is not supported', function () {
settingsLoaderStub.resolves(settingsStubFile);
it('rejects when requested settings type is not supported', function (done) {
settingsLoaderStub.returns(settingsStubFile);
settings.__set__('SettingsLoader', settingsLoaderStub);

return settings.get('something').then((result) => {
should.not.exist(result);
}).catch((error) => {
should.exist(error);
error.message.should.be.eql('Requested setting is not supported: \'something\'.');
try {
settings.get('something');
done(new Error('SettingsLoader should fail'));
} catch (err) {
should.exist(err);
err.message.should.be.eql('Requested setting is not supported: \'something\'.');
settingsLoaderStub.callCount.should.be.eql(0);
});
done();
}
});

it('passes SettingsLoader error through', function () {
settingsLoaderStub.rejects(new common.errors.GhostError({message: 'oops'}));
it('passes SettingsLoader error through', function (done) {
settingsLoaderStub.throws(new common.errors.GhostError({message: 'oops'}));
settings.__set__('SettingsLoader', settingsLoaderStub);

return settings.get('routes').then((result) => {
should.not.exist(result);
}).catch((error) => {
should.exist(error);
error.message.should.be.eql('oops');
try {
settings.get('routes');
done(new Error('SettingsLoader should fail'));
} catch (err) {
should.exist(err);
err.message.should.be.eql('oops');
settingsLoaderStub.calledOnce.should.be.true();
});
done();
}
});
});

Expand Down Expand Up @@ -106,32 +107,33 @@ describe('UNIT > Settings Service:', function () {
});

it('returns settings object for all known settings', function () {
settingsLoaderStub.onFirstCall().resolves(settingsStubFile1);
settingsLoaderStub.onSecondCall().resolves(settingsStubFile2);
settingsLoaderStub.onFirstCall().returns(settingsStubFile1);
settingsLoaderStub.onSecondCall().returns(settingsStubFile2);
settings.__set__('SettingsLoader', settingsLoaderStub);

return settings.getAll().then((result) => {
should.exist(result);
result.should.be.an.Object().with.properties('routes', 'globals');
result.routes.should.be.an.Object().with.properties('routes', 'collections', 'resources');
result.globals.should.be.an.Object().with.properties('config');
const result = settings.getAll();
should.exist(result);
result.should.be.an.Object().with.properties('routes', 'globals');
result.routes.should.be.an.Object().with.properties('routes', 'collections', 'resources');
result.globals.should.be.an.Object().with.properties('config');

settingsLoaderStub.calledTwice.should.be.true();
});
settingsLoaderStub.calledTwice.should.be.true();
});

it('passes SettinsLoader error through', function () {
settingsLoaderStub.onFirstCall().resolves(settingsStubFile1);
settingsLoaderStub.onSecondCall().rejects(new common.errors.GhostError({message: 'oops'}));
it('passes SettinsLoader error through', function (done) {
settingsLoaderStub.onFirstCall().returns(settingsStubFile1);
settingsLoaderStub.onSecondCall().throws(new common.errors.GhostError({message: 'oops'}));
settings.__set__('SettingsLoader', settingsLoaderStub);

return settings.getAll().then((result) => {
should.not.exist(result);
}).catch((error) => {
should.exist(error);
error.message.should.be.eql('oops');
try {
settings.getAll();
done(new Error('SettingsLoader should fail'));
} catch (err) {
should.exist(err);
err.message.should.be.eql('oops');
settingsLoaderStub.calledTwice.should.be.true();
});
done();
}
});
});
});

0 comments on commit 0ac19dc

Please sign in to comment.