diff --git a/lib/cartodb/controllers/layergroup.js b/lib/cartodb/controllers/layergroup.js index 4119655f..79268d6e 100644 --- a/lib/cartodb/controllers/layergroup.js +++ b/lib/cartodb/controllers/layergroup.js @@ -391,13 +391,15 @@ LayergroupController.prototype.getAffectedTables = function(user, dbName, layerg function getSQL(err, mapConfig) { assert.ifError(err); - var queries = mapConfig.getLayers() - .map(function(lyr) { - return lyr.options.sql; - }) - .filter(function(sql) { - return !!sql; - }); + var queries = []; + mapConfig.getLayers().forEach(function(layer) { + queries.push(layer.options.sql); + if (layer.options.affected_tables) { + layer.options.affected_tables.map(function(table) { + queries.push('SELECT * FROM ' + table + ' LIMIT 0'); + }); + } + }); return queries.length ? queries.join(';') : null; }, diff --git a/lib/cartodb/controllers/map.js b/lib/cartodb/controllers/map.js index cfd9e0d6..b3ca2b4a 100644 --- a/lib/cartodb/controllers/map.js +++ b/lib/cartodb/controllers/map.js @@ -306,9 +306,15 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la done(); }); - var sql = mapconfig.getLayers().map(function(layer) { - return layer.options.sql; - }).join(';'); + var sql = []; + mapconfig.getLayers().forEach(function(layer) { + sql.push(layer.options.sql); + if (layer.options.affected_tables) { + layer.options.affected_tables.map(function(table) { + sql.push('SELECT * FROM ' + table + ' LIMIT 0'); + }); + } + }); var dbName = req.params.dbname; var layergroupId = layergroup.layergroupid; @@ -319,7 +325,7 @@ MapController.prototype.afterLayergroupCreate = function(req, res, mapconfig, la }, function getAffectedTablesAndLastUpdatedTime(err, connection) { assert.ifError(err); - QueryTables.getAffectedTablesFromQuery(connection, sql, this); + QueryTables.getAffectedTablesFromQuery(connection, sql.join(';'), this); }, function handleAffectedTablesAndLastUpdatedTime(err, result) { req.profiler.done('queryTablesAndLastUpdated'); diff --git a/lib/cartodb/models/mapconfig/adapter/analysis-mapconfig-adapter.js b/lib/cartodb/models/mapconfig/adapter/analysis-mapconfig-adapter.js index b59684c7..8cb63f48 100644 --- a/lib/cartodb/models/mapconfig/adapter/analysis-mapconfig-adapter.js +++ b/lib/cartodb/models/mapconfig/adapter/analysis-mapconfig-adapter.js @@ -115,6 +115,7 @@ AnalysisMapConfigAdapter.prototype.getMapConfig = function(user, requestMapConfi } layer.options.sql = analysisSql; layer.options.columns = getDataviewsColumns(getLayerDataviews(layer, dataviews)); + layer.options.affected_tables = getAllAffectedTablesFromSourceNodes(layerNode); } else { missingNodesErrors.push( new Error('Missing analysis node.id="' + layerSourceId +'" for layer='+layerIndex) @@ -330,4 +331,13 @@ function AnalysisError(message) { this.message = message; } +function getAllAffectedTablesFromSourceNodes(node) { + var affectedTables = node.getAllInputNodes(function (node) { + return node.getType() === 'source'; + }).reduce(function(list, node) { + return list.concat(node.getAffectedTables()); + },[]); + return affectedTables; +} + require('util').inherits(AnalysisError, Error); diff --git a/package.json b/package.json index 6c665702..be83898e 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ ], "dependencies": { "body-parser": "~1.14.0", - "camshaft": "0.52.0", + "camshaft": "0.53.0", "cartodb-psql": "~0.7.1", "cartodb-query-tables": "0.2.0", "cartodb-redis": "0.13.2", diff --git a/test/acceptance/cache/cache_headers.js b/test/acceptance/cache/cache_headers.js new file mode 100644 index 00000000..2cd916af --- /dev/null +++ b/test/acceptance/cache/cache_headers.js @@ -0,0 +1,393 @@ +var testHelper = require('../../support/test_helper'); + +var assert = require('../../support/assert'); +var qs = require('querystring'); + +var CartodbWindshaft = require('../../../lib/cartodb/server'); +var serverOptions = require('../../../lib/cartodb/server_options'); +var server = new CartodbWindshaft(serverOptions); +server.setMaxListeners(0); + +var LayergroupToken = require('../../support/layergroup-token'); + +describe('get requests with cache headers', function() { + + var keysToDelete; + beforeEach(function() { + keysToDelete = {}; + }); + + afterEach(function(done) { + testHelper.deleteRedisKeys(keysToDelete, done); + }); + + var statusOkResponse = { + status: 200 + }; + + var mapConfigs = [ + { + "description": "cache headers should be present", + "cache_headers": { + "x_cache_channel": { + "db_name": "test_windshaft_cartodb_user_1_db", + "tables": ["public.test_table"] + }, + "surrogate_keys": "t:77pJnX" + }, + "data": + { + version: '1.5.0', + layers: [ + { + options: { + source: { + id: "2570e105-7b37-40d2-bdf4-1af889598745" + }, + sql: 'select * from test_table limit 2', + cartocss: '#layer { marker-fill:red; }', + cartocss_version: '2.3.0', + attributes: { + id:'cartodb_id', + columns: [ + 'name', + 'address' + ] + } + } + } + ], + analyses: [ + { + "id": "2570e105-7b37-40d2-bdf4-1af889598745", + "type": "source", + "params": { + "query": "select * from test_table limit 2" + } + } + ] + }, + }, + { + "description": "cache headers should be present and be composed with source table name", + "cache_headers": { + "x_cache_channel": { + "db_name": "test_windshaft_cartodb_user_1_db", + "tables": ["public.analysis_2f13a3dbd7_9eb239903a1afd8a69130d1ece0fc8b38de8592d", + "public.test_table"] + }, + "surrogate_keys": "t:77pJnX t:iL4eth" + }, + "data": + { + version: '1.5.0', + layers: [ + { + options: { + source: { + id: "2570e105-7b37-40d2-bdf4-1af889598745" + }, + sql: 'select * from test_table limit 2', + cartocss: '#layer { marker-fill:red; }', + cartocss_version: '2.3.0', + attributes: { + id:'cartodb_id', + columns: [ + 'name', + 'address' + ] + } + } + } + ], + analyses: [ + { + "id": "2570e105-7b37-40d2-bdf4-1af889598745", + "type": "buffer", + "params": { + "source": { + "type": "source", + "params": { + "query": "select * from test_table limit 2" + } + }, + "radius": 50000 + } + } + ] + } + }]; + + var layergroupRequest = function(mapConfig) { + return { + url: '/api/v1/map?api_key=1234&config=' + encodeURIComponent(JSON.stringify(mapConfig)), + method: 'GET', + headers: { + host: 'localhost' + } + }; + }; + + function getRequest(url, addApiKey, callbackName) { + var params = {}; + if (!!addApiKey) { + params.api_key = '1234'; + } + if (!!callbackName) { + params.callback = callbackName; + } + + return { + url: url + '?' + qs.stringify(params), + method: 'GET', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + } + }; + } + + function validateCacheHeaders(done, expectedCacheHeaders) { + return function(res, err) { + if (err) { + return done(err); + } + + assert.ok(res.headers['x-cache-channel']); + assert.ok(res.headers['surrogate-key']); + if (expectedCacheHeaders) { + validateXChannelHeaders(res.headers, expectedCacheHeaders); + assert.equal(res.headers['surrogate-key'], expectedCacheHeaders.surrogate_keys); + } + + done(); + }; + } + + function validateXChannelHeaders(headers, expectedCacheHeaders) { + var dbName = headers['x-cache-channel'].split(':')[0]; + var tables = headers['x-cache-channel'].split(':')[1].split(',').sort(); + assert.equal(dbName, expectedCacheHeaders.x_cache_channel.db_name); + assert.deepEqual(tables, expectedCacheHeaders.x_cache_channel.tables.sort()); + } + + function noCacheHeaders(done) { + return function(res, err) { + if (err) { + return done(err); + } + + assert.ok( + !res.headers['x-cache-channel'], + 'did not expect x-cache-channel header, got: `' + res.headers['x-cache-channel'] + '`' + ); + assert.ok( + !res.headers['surrogate-key'], + 'did not expect surrogate-key header, got: `' + res.headers['surrogate-key'] + '`' + ); + done(); + }; + } + + function withLayergroupId(mapConfig, callback) { + assert.response( + server, + layergroupRequest(mapConfig), + statusOkResponse, + function(res, err) { + if (err) { + return callback(err); + } + var layergroupId = JSON.parse(res.body).layergroupid; + keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0; + keysToDelete['user:localhost:mapviews:global'] = 5; + callback(null, layergroupId, res); + } + ); + } + + mapConfigs.forEach(function(mapConfigData) { + describe(mapConfigData.description, function() { + var mapConfig = mapConfigData.data; + var expectedCacheHeaders = mapConfigData.cache_headers; + it('/api/v1/map Map instantiation', function(done) { + var testFn = validateCacheHeaders(done, expectedCacheHeaders); + withLayergroupId(mapConfig, function(err, layergroupId, res) { + testFn(res); + }); + }); + + it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/' + layergroupId + '/0/0/0@2x.png', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + + it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik tiles', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/' + layergroupId + '/0/0/0.png', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + + it ('/api/v1/map/:token/:layer/:z/:x/:y.(:format) Per :layer rendering', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/' + layergroupId + '/0/0/0/0.png', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + + it ('/api/v1/map/:token/:layer/attributes/:fid endpoint for info windows', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/' + layergroupId + '/0/attributes/1', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + + it ('/api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format static maps', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/static/center/' + layergroupId + '/0/0/0/400/300.png', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + + it ('/api/v1/map/static/bbox/:token/:bbox/:width/:height.:format static maps', function(done) { + withLayergroupId(mapConfig, function(err, layergroupId) { + assert.response( + server, + getRequest('/api/v1/map/static/bbox/' + layergroupId + '/-45,-45,45,45/400/300.png', true), + validateCacheHeaders(done, expectedCacheHeaders) + ); + }); + }); + }); + }); + + describe('cache headers should NOT be present', function() { + + it('/', function(done) { + assert.response( + server, + getRequest('/'), + statusOkResponse, + noCacheHeaders(done) + ); + }); + + it('/version', function(done) { + assert.response( + server, + getRequest('/version'), + statusOkResponse, + noCacheHeaders(done) + ); + }); + + it('/health', function(done) { + assert.response( + server, + getRequest('/health'), + statusOkResponse, + noCacheHeaders(done) + ); + }); + + it('/api/v1/map/named list named maps', function(done) { + assert.response( + server, + getRequest('/api/v1/map/named', true), + statusOkResponse, + noCacheHeaders(done) + ); + }); + + describe('with named maps', function() { + + var templateName = 'x_cache'; + + beforeEach(function(done) { + var template = { + version: '0.0.1', + name: templateName, + auth: { + method: 'open' + }, + layergroup: mapConfigs[0].data + }; + + var namedMapRequest = { + url: '/api/v1/map/named?api_key=1234', + method: 'POST', + headers: { + host: 'localhost', + 'Content-Type': 'application/json' + }, + data: JSON.stringify(template) + }; + + assert.response( + server, + namedMapRequest, + statusOkResponse, + function(res, err) { + done(err); + } + ); + }); + + afterEach(function(done) { + assert.response( + server, + { + url: '/api/v1/map/named/' + templateName + '?api_key=1234', + method: 'DELETE', + headers: { + host: 'localhost' + } + }, + { + status: 204 + }, + function(res, err) { + done(err); + } + ); + }); + + + it('/api/v1/map/named/:template_id Named map retrieval', function(done) { + assert.response( + server, + getRequest('/api/v1/map/named/' + templateName, true), + statusOkResponse, + noCacheHeaders(done) + ); + }); + + it('/api/v1/map/named/:template_id/jsonp Named map retrieval', function(done) { + assert.response( + server, + getRequest('/api/v1/map/named/' + templateName, true, 'cb'), + statusOkResponse, + noCacheHeaders(done) + ); + }); + }); + }); +}); diff --git a/test/acceptance/templates.js b/test/acceptance/templates.js index 7201b7fb..74d34d82 100644 --- a/test/acceptance/templates.js +++ b/test/acceptance/templates.js @@ -1052,8 +1052,9 @@ describe('template_api', function() { 'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body); assert.equal(res.headers['content-type'], "application/json; charset=utf-8"); var cc = res.headers['x-cache-channel']; + var expectedCC = 'test_windshaft_cartodb_user_1_db:public.test_table_private_1'; assert.ok(cc); - assert.ok(cc.match, /ciao/, cc); + assert.equal(cc, expectedCC); // hack simulating restart... server.layergroupAffectedTablesCache.cache.reset(); // need to clean channel cache var get_request = { @@ -1072,8 +1073,9 @@ describe('template_api', function() { 'Unexpected error for authorized instance: ' + res.statusCode + ' -- ' + res.body); assert.equal(res.headers['content-type'], "application/json; charset=utf-8"); var cc = res.headers['x-cache-channel']; + var expectedCC = 'test_windshaft_cartodb_user_1_db:public.test_table_private_1'; assert.ok(cc, "Missing X-Cache-Channel on fetch-after-restart"); - assert.ok(cc.match, /ciao/, cc); + assert.equal(cc, expectedCC); return null; }, function deleteTemplate(err) diff --git a/test/acceptance/x_cache_channel.js b/test/acceptance/x_cache_channel.js deleted file mode 100644 index 4ebb45e8..00000000 --- a/test/acceptance/x_cache_channel.js +++ /dev/null @@ -1,307 +0,0 @@ -var testHelper = require('../support/test_helper'); - -var assert = require('../support/assert'); -var qs = require('querystring'); - -var CartodbWindshaft = require('../../lib/cartodb/server'); -var serverOptions = require('../../lib/cartodb/server_options'); -var server = new CartodbWindshaft(serverOptions); -server.setMaxListeners(0); - -var LayergroupToken = require('../support/layergroup-token'); - -describe('get requests x-cache-channel', function() { - - var keysToDelete; - beforeEach(function() { - keysToDelete = {}; - }); - - afterEach(function(done) { - testHelper.deleteRedisKeys(keysToDelete, done); - }); - - var statusOkResponse = { - status: 200 - }; - - var mapConfig = { - version: '1.3.0', - layers: [ - { - options: { - sql: 'select * from test_table limit 2', - cartocss: '#layer { marker-fill:red; }', - cartocss_version: '2.3.0', - attributes: { - id:'cartodb_id', - columns: [ - 'name', - 'address' - ] - } - } - } - ] - }; - - var layergroupRequest = { - url: '/api/v1/map?config=' + encodeURIComponent(JSON.stringify(mapConfig)), - method: 'GET', - headers: { - host: 'localhost' - } - }; - - function getRequest(url, addApiKey, callbackName) { - var params = {}; - if (!!addApiKey) { - params.api_key = '1234'; - } - if (!!callbackName) { - params.callback = callbackName; - } - - return { - url: url + '?' + qs.stringify(params), - method: 'GET', - headers: { - host: 'localhost', - 'Content-Type': 'application/json' - } - }; - } - - function validateXCacheChannel(done, expectedCacheChannel) { - return function(res, err) { - if (err) { - return done(err); - } - - assert.ok(res.headers['x-cache-channel']); - if (expectedCacheChannel) { - assert.equal(res.headers['x-cache-channel'], expectedCacheChannel); - } - - done(); - }; - } - - function noXCacheChannelHeader(done) { - return function(res, err) { - if (err) { - return done(err); - } - - assert.ok( - !res.headers['x-cache-channel'], - 'did not expect x-cache-channel header, got: `' + res.headers['x-cache-channel'] + '`' - ); - done(); - }; - } - - function withLayergroupId(callback) { - assert.response( - server, - layergroupRequest, - statusOkResponse, - function(res, err) { - if (err) { - return callback(err); - } - var layergroupId = JSON.parse(res.body).layergroupid; - keysToDelete['map_cfg|' + LayergroupToken.parse(layergroupId).token] = 0; - keysToDelete['user:localhost:mapviews:global'] = 5; - callback(null, layergroupId, res); - } - ); - } - - describe('header should be present', function() { - - it('/api/v1/map Map instantiation', function(done) { - var testFn = validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table'); - withLayergroupId(function(err, layergroupId, res) { - testFn(res); - }); - }); - - it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik retina tiles', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/' + layergroupId + '/0/0/0@2x.png'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - it ('/api/v1/map/:token/:z/:x/:y@:scale_factor?x.:format Mapnik tiles', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/' + layergroupId + '/0/0/0.png'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - it ('/api/v1/map/:token/:layer/:z/:x/:y.(:format) Per :layer rendering', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/' + layergroupId + '/0/0/0/0.png'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - it ('/api/v1/map/:token/:layer/attributes/:fid endpoint for info windows', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/' + layergroupId + '/0/attributes/1'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - it ('/api/v1/map/static/center/:token/:z/:lat/:lng/:width/:height.:format static maps', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/static/center/' + layergroupId + '/0/0/0/400/300.png'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - it ('/api/v1/map/static/bbox/:token/:bbox/:width/:height.:format static maps', function(done) { - withLayergroupId(function(err, layergroupId) { - assert.response( - server, - getRequest('/api/v1/map/static/bbox/' + layergroupId + '/-45,-45,45,45/400/300.png'), - validateXCacheChannel(done, 'test_windshaft_cartodb_user_1_db:public.test_table') - ); - }); - }); - - }); - - describe('header should NOT be present', function() { - - it('/', function(done) { - assert.response( - server, - getRequest('/'), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - it('/version', function(done) { - assert.response( - server, - getRequest('/version'), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - it('/health', function(done) { - assert.response( - server, - getRequest('/health'), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - it('/api/v1/map/named list named maps', function(done) { - assert.response( - server, - getRequest('/api/v1/map/named', true), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - describe('with named maps', function() { - - var templateName = 'x_cache'; - - beforeEach(function(done) { - var template = { - version: '0.0.1', - name: templateName, - auth: { - method: 'open' - }, - layergroup: mapConfig - }; - - var namedMapRequest = { - url: '/api/v1/map/named?api_key=1234', - method: 'POST', - headers: { - host: 'localhost', - 'Content-Type': 'application/json' - }, - data: JSON.stringify(template) - }; - - assert.response( - server, - namedMapRequest, - statusOkResponse, - function(res, err) { - done(err); - } - ); - }); - - afterEach(function(done) { - assert.response( - server, - { - url: '/api/v1/map/named/' + templateName + '?api_key=1234', - method: 'DELETE', - headers: { - host: 'localhost' - } - }, - { - status: 204 - }, - function(res, err) { - done(err); - } - ); - }); - - - it('/api/v1/map/named/:template_id Named map retrieval', function(done) { - assert.response( - server, - getRequest('/api/v1/map/named/' + templateName, true), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - it('/api/v1/map/named/:template_id/jsonp Named map retrieval', function(done) { - assert.response( - server, - getRequest('/api/v1/map/named/' + templateName, true, 'cb'), - statusOkResponse, - noXCacheChannelHeader(done) - ); - }); - - }); - - }); - - -}); diff --git a/test/support/prepare_db.sh b/test/support/prepare_db.sh index 1a6d929c..4fbf4b7a 100755 --- a/test/support/prepare_db.sh +++ b/test/support/prepare_db.sh @@ -75,7 +75,7 @@ if test x"$PREPARE_PGSQL" = xyes; then dropdb "${TEST_DB}" createdb -Ttemplate_postgis -EUTF8 "${TEST_DB}" || die "Could not create test database" - LOCAL_SQL_SCRIPTS='analysis_catalog windshaft.test gadm4 ported/populated_places_simple_reduced cdb_analysis_check' + LOCAL_SQL_SCRIPTS='analysis_catalog windshaft.test gadm4 ported/populated_places_simple_reduced cdb_analysis_check cdb_invalidate_varnish' REMOTE_SQL_SCRIPTS='CDB_QueryStatements CDB_QueryTables CDB_CartodbfyTable CDB_TableMetadata CDB_ForeignTable CDB_UserTables CDB_ColumnNames CDB_ZoomFromScale CDB_OverviewsSupport CDB_Overviews CDB_QuantileBins CDB_JenksBins CDB_HeadsTailsBins CDB_EqualIntervalBins CDB_Hexagon CDB_XYZ' CURL_ARGS="" diff --git a/test/support/sql/cdb_invalidate_varnish.sql b/test/support/sql/cdb_invalidate_varnish.sql new file mode 100644 index 00000000..7cd2d8f1 --- /dev/null +++ b/test/support/sql/cdb_invalidate_varnish.sql @@ -0,0 +1,6 @@ +CREATE OR REPLACE FUNCTION CDB_Invalidate_Varnish(table_name TEXT) +RETURNS void AS +$$ +BEGIN +END; +$$ LANGUAGE PLPGSQL; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8c739b00..92c3c673 100644 --- a/yarn.lock +++ b/yarn.lock @@ -194,9 +194,9 @@ camelcase@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" -camshaft@0.52.0: - version "0.52.0" - resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.52.0.tgz#d7cac00884783c059fcaaa791879429b80c0e7b8" +camshaft@0.53.0: + version "0.53.0" + resolved "https://registry.yarnpkg.com/camshaft/-/camshaft-0.53.0.tgz#89708ca89bea93c4d94b0587b692935e23f1e9d6" dependencies: async "^1.5.2" bunyan "1.8.1"