Merge pull request #3527 from HyperBrain/filter-self-reference

Persist self references with "${self:}" and restore it correctly
This commit is contained in:
Eslam λ Hefnawy 2017-04-27 18:35:13 +07:00 committed by GitHub
commit 4e752fbc5e
5 changed files with 179 additions and 1 deletions

View File

@ -3,6 +3,7 @@
const path = require('path');
const BbPromise = require('bluebird');
const _ = require('lodash');
const findReferences = require('../../utils/findReferences');
module.exports = {
extendedValidate() {
@ -17,7 +18,11 @@ module.exports = {
}
const state = this.serverless
.utils.readFileSync(serviceStateFilePath);
const selfReferences = findReferences(state.service, '${self:}');
_.forEach(selfReferences, ref => _.set(state.service, ref, this.serverless.service));
_.assign(this.serverless.service, state.service);
this.serverless.service.package.artifactDirectoryName = state.package.artifactDirectoryName;
// only restore the default artifact path if the user is not using a custom path
if (!_.isEmpty(state.package.artifact) && this.serverless.service.artifact) {

View File

@ -3,6 +3,7 @@
const BbPromise = require('bluebird');
const path = require('path');
const _ = require('lodash');
const findReferences = require('../../utils/findReferences');
module.exports = {
saveServiceState() {
@ -20,8 +21,14 @@ module.exports = {
)
);
const strippedService = _.assign(
{}, _.omit(this.serverless.service, ['serverless', 'package'])
);
const selfReferences = findReferences(strippedService, this.serverless.service);
_.forEach(selfReferences, refPath => _.set(strippedService, refPath, '${self:}'));
const state = {
service: _.assign({}, _.omit(this.serverless.service, ['serverless', 'package'])),
service: strippedService,
package: {
individually: this.serverless.service.package.individually,
artifactDirectoryName: this.serverless.service.package.artifactDirectoryName,

View File

@ -66,4 +66,38 @@ describe('#saveServiceState()', () => {
.to.equal(true);
});
});
it('should remove self references correctly', () => {
const filePath = path.join(
awsPackage.serverless.config.servicePath,
'.serverless',
'service-state.json'
);
serverless.service.custom = {
mySelfRef: serverless.service,
};
return awsPackage.saveServiceState().then(() => {
const expectedStateFileContent = {
service: {
provider: {
compiledCloudFormationTemplate: 'compiled content',
},
custom: {
mySelfRef: '${self:}',
},
},
package: {
individually: false,
artifactDirectoryName: 'artifact-directory',
artifact: 'service.zip',
},
};
expect(getServiceStateFileNameStub.calledOnce).to.equal(true);
expect(writeFileSyncStub.calledWithExactly(filePath, expectedStateFileContent))
.to.equal(true);
});
});
});

View File

@ -0,0 +1,44 @@
'use strict';
const _ = require('lodash');
/**
* Find all objects with a given value within a given root object.
* The search is implemented non-recursive to prevent stackoverflows and will
* do a complete deep search including arrays.
* @param root {Object} Root object for search
* @param value {Object} Value to search
* @returns {Array<String>} Paths to all self references found within the object
*/
function findReferences(root, value) {
const visitedObjects = [];
const resourcePaths = [];
const stack = [{ propValue: root, path: '' }];
while (!_.isEmpty(stack)) {
const property = stack.pop();
_.forOwn(property.propValue, (propValue, key) => {
let propKey;
if (_.isArray(property.propValue)) {
propKey = `[${key}]`;
} else {
propKey = _.isEmpty(property.path) ? `${key}` : `.${key}`;
}
if (propValue === value) {
resourcePaths.push(`${property.path}${propKey}`);
} else if (_.isObject(propValue)) {
// Prevent circular references
if (_.includes(visitedObjects, propValue)) {
return;
}
visitedObjects.push(propValue);
stack.push({ propValue, path: `${property.path}${propKey}` });
}
});
}
return resourcePaths;
}
module.exports = findReferences;

View File

@ -0,0 +1,88 @@
'use strict';
const expect = require('chai').expect;
const _ = require('lodash');
const findReferences = require('./findReferences');
describe('#findReferences()', () => {
it('should succeed on invalid input', () => {
const withoutArgs = findReferences();
const nullArgs = findReferences(null);
expect(withoutArgs).to.be.a('Array').to.have.lengthOf(0);
expect(nullArgs).to.be.a('Array').have.lengthOf(0);
});
it('should return paths', () => {
const testObject = {
prop1: 'test',
array1: [
{
prop1: 'hit',
prop2: 4,
},
'hit',
[
{
prop1: null,
prop2: 'hit',
},
],
],
prop2: {
prop1: 'foo',
prop2: {
prop1: 'hit',
},
},
};
const expectedResult = [
'array1[0].prop1',
'array1[1]',
'array1[2][0].prop2',
'prop2.prop2.prop1',
];
const paths = findReferences(testObject, 'hit');
expect(paths).to.be.a('Array').to.have.lengthOf(4);
expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true);
});
it('should not fail with circular references', () => {
const testObject = {
prop1: 'test',
array1: [
{
prop1: 'hit',
prop2: 4,
},
'hit',
[
{
prop1: null,
prop2: 'hit',
},
],
],
prop2: {
prop1: 'foo',
prop2: {
prop1: 'hit',
},
},
};
testObject.array1.push(testObject.prop2);
const expectedResult = [
'array1[0].prop1',
'array1[1]',
'array1[2][0].prop2',
'prop2.prop2.prop1',
];
const paths = findReferences(testObject, 'hit');
expect(paths).to.be.a('Array').to.have.lengthOf(4);
expect(_.every(paths, path => _.includes(expectedResult, path))).to.equal(true);
});
});