diff --git a/package.json b/package.json index bafaa88..b59ba8a 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,10 @@ "dns-zonefile": "0.2.2", "extend": "^3.0.0", "is": "^3.0.1", + "lodash.flatten": "^4.4.0", + "lodash.groupby": "^4.6.0", "methmeth": "^1.0.0", + "propprop": "^0.3.1", "string-format-obj": "^1.0.0" }, "devDependencies": { diff --git a/src/zone.js b/src/zone.js index d656474..5352582 100644 --- a/src/zone.js +++ b/src/zone.js @@ -20,8 +20,11 @@ var arrify = require('arrify'); var common = require('@google-cloud/common'); var exec = require('methmeth'); var extend = require('extend'); +var flatten = require('lodash.flatten'); var fs = require('fs'); +var groupBy = require('lodash.groupby'); var is = require('is'); +var prop = require('propprop'); var util = require('util'); var zonefile = require('dns-zonefile'); @@ -342,14 +345,40 @@ Zone.prototype.createChange = function(config, callback) { throw new Error('Cannot create a change with no additions or deletions.'); } - var body = extend({}, config, { - additions: arrify(config.add).map(exec('toJSON')), - deletions: arrify(config.delete).map(exec('toJSON')), - }); - + var body = extend( + { + additions: groupByType(arrify(config.add).map(exec('toJSON'))), + deletions: groupByType(arrify(config.delete).map(exec('toJSON'))), + }, + config + ); delete body.add; delete body.delete; + function groupByType(changes) { + changes = groupBy(changes, 'type'); + + var changesArray = []; + + for (var recordType in changes) { + var recordsByName = groupBy(changes[recordType], 'name'); + + for (var recordName in recordsByName) { + var records = recordsByName[recordName]; + var templateRecord = extend({}, records[0]); + + if (records.length > 1) { + // Combine the `rrdatas` values from all records of the same type. + templateRecord.rrdatas = flatten(records.map(prop('rrdatas'))); + } + + changesArray.push(templateRecord); + } + } + + return changesArray; + } + this.request( { method: 'POST', diff --git a/system-test/data/zonefile.zone b/system-test/data/zonefile.zone index 468686b..cd68c74 100644 --- a/system-test/data/zonefile.zone +++ b/system-test/data/zonefile.zone @@ -1,3 +1,9 @@ $TTL 3600 {DNS_DOMAIN} 21600 IN SPF "v=spf1" "mx:{DNS_DOMAIN}" "-all" {DNS_DOMAIN} IN TXT "google-site-verification=xxxxxxxxxxxxYYYYYYXXX" +{DNS_DOMAIN} 21600 MX 10 mail.example.com. +{DNS_DOMAIN} 21600 MX 30 mail.example.com. +{DNS_DOMAIN} A 128.9.0.32 + A 10.1.0.52 +{DNS_DOMAIN} A 10.2.0.27 + A 128.9.0.33 diff --git a/test/zone.js b/test/zone.js index ef51738..3892f9b 100644 --- a/test/zone.js +++ b/test/zone.js @@ -18,11 +18,15 @@ var arrify = require('arrify'); var assert = require('assert'); +var exec = require('methmeth'); var extend = require('extend'); +var flatten = require('lodash.flatten'); var nodeutil = require('util'); +var prop = require('propprop'); var proxyquire = require('proxyquire'); var ServiceObject = require('@google-cloud/common').ServiceObject; var util = require('@google-cloud/common').util; +var uuid = require('uuid'); var promisified = false; var fakeUtil = extend({}, util, { @@ -192,6 +196,23 @@ describe('Zone', function() { }); describe('createChange', function() { + function generateRecord(recordJson) { + recordJson = extend( + { + name: uuid.v1(), + type: uuid.v1(), + rrdatas: [uuid.v1(), uuid.v1()], + }, + recordJson + ); + + return { + toJSON: function() { + return recordJson; + }, + }; + } + it('should throw error if add or delete is not provided', function() { assert.throws(function() { zone.createChange({}, util.noop); @@ -199,19 +220,9 @@ describe('Zone', function() { }); it('should parse and rename add to additions', function(done) { - var recordsToAdd = [ - { - toJSON: function() { - return 'a'; - }, - }, - { - toJSON: function() { - return 'a'; - }, - }, - ]; - var expectedAdditions = ['a', 'a']; + var recordsToAdd = [generateRecord(), generateRecord()]; + + var expectedAdditions = recordsToAdd.map(exec('toJSON')); zone.request = function(reqOpts) { assert.strictEqual(reqOpts.json.add, undefined); @@ -223,19 +234,9 @@ describe('Zone', function() { }); it('should parse and rename delete to deletions', function(done) { - var recordsToDelete = [ - { - toJSON: function() { - return 'a'; - }, - }, - { - toJSON: function() { - return 'a'; - }, - }, - ]; - var expectedDeletions = ['a', 'a']; + var recordsToDelete = [generateRecord(), generateRecord()]; + + var expectedDeletions = recordsToDelete.map(exec('toJSON')); zone.request = function(reqOpts) { assert.strictEqual(reqOpts.json.delete, undefined); @@ -246,6 +247,31 @@ describe('Zone', function() { zone.createChange({delete: recordsToDelete}, assert.ifError); }); + it('should group changes by name and type', function(done) { + var recordsToAdd = [ + generateRecord({name: 'name.com.', type: 'mx'}), + generateRecord({name: 'name.com.', type: 'mx'}), + ]; + + zone.request = function(reqOpts) { + var expectedRRDatas = flatten( + recordsToAdd.map(exec('toJSON')).map(prop('rrdatas')) + ); + + assert.deepStrictEqual(reqOpts.json.additions, [ + { + name: 'name.com.', + type: 'mx', + rrdatas: expectedRRDatas, + }, + ]); + + done(); + }; + + zone.createChange({add: recordsToAdd}, assert.ifError); + }); + it('should make correct API request', function(done) { zone.request = function(reqOpts) { assert.strictEqual(reqOpts.method, 'POST');