Skip to content

Commit

Permalink
Merge pull request #259 from ljay79/develop
Browse files Browse the repository at this point in the history
#255 merge develop into master
  • Loading branch information
ljay79 authored Jun 16, 2020
2 parents ca16c34 + fda028e commit 7b81062
Show file tree
Hide file tree
Showing 13 changed files with 257 additions and 207 deletions.
67 changes: 50 additions & 17 deletions src/Storage.gs
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
* related information.
* @param {string} prefix The prefix to use for keys in the properties and
* cache.
* @param {PropertiesService.Properties} properties The properties instance to
* use.
* @param {PropertiesService.Properties} optProperties The optional properties
* instance to use.
* @param {CacheService.Cache} [optCache] The optional cache instance to use.
* @constructor
*/
function Storage_(prefix, properties, optCache) {
function Storage_(prefix, optProperties, optCache) {
this.prefix_ = prefix;
this.properties_ = properties;
this.properties_ = optProperties;
this.cache_ = optCache;
this.memory_ = {};
}
Expand Down Expand Up @@ -62,7 +62,7 @@ Storage_.prototype.getValue = function(key, optSkipMemoryCheck) {

if (!optSkipMemoryCheck) {
// Check in-memory cache.
if (value = this.memory_[key]) {
if (value = this.memory_[prefixedKey]) {
if (value === Storage_.CACHE_NULL_VALUE) {
return null;
}
Expand All @@ -71,31 +71,32 @@ Storage_.prototype.getValue = function(key, optSkipMemoryCheck) {
}

// Check cache.
StorageCounter.increase('cache', 'get');
if (this.cache_ && (jsonValue = this.cache_.get(prefixedKey))) {
value = JSON.parse(jsonValue);
this.memory_[key] = value;
this.memory_[prefixedKey] = value;
if (value === Storage_.CACHE_NULL_VALUE) {
return null;
}
StorageCounter.increase('cache', 'get');
return value;
}

// Check properties.
StorageCounter.increase('properties', 'get');
if (jsonValue = this.properties_.getProperty(prefixedKey)) {
if (this.properties_ &&
(jsonValue = this.properties_.getProperty(prefixedKey))) {
if (this.cache_) {
this.cache_.put(prefixedKey,
jsonValue, Storage_.CACHE_EXPIRATION_TIME_SECONDS);
}
value = JSON.parse(jsonValue);
this.memory_[key] = value;
this.memory_[prefixedKey] = value;
StorageCounter.increase('properties', 'get');
return value;
}

// Not found. Store a special null value in the memory and cache to reduce
// hits on the PropertiesService.
this.memory_[key] = Storage_.CACHE_NULL_VALUE;
this.memory_[prefixedKey] = Storage_.CACHE_NULL_VALUE;
if (this.cache_) {
this.cache_.put(prefixedKey, JSON.stringify(Storage_.CACHE_NULL_VALUE),
Storage_.CACHE_EXPIRATION_TIME_SECONDS);
Expand All @@ -111,14 +112,17 @@ Storage_.prototype.getValue = function(key, optSkipMemoryCheck) {
Storage_.prototype.setValue = function(key, value) {
var prefixedKey = this.getPrefixedKey_(key);
var jsonValue = JSON.stringify(value);
StorageCounter.increase('cache', 'set');
StorageCounter.increase('properties', 'set');
this.properties_.setProperty(prefixedKey, jsonValue);

if (this.properties_) {
StorageCounter.increase('properties', 'set');
this.properties_.setProperty(prefixedKey, jsonValue);
}
if (this.cache_) {
StorageCounter.increase('cache', 'set');
this.cache_.put(prefixedKey, jsonValue,
Storage_.CACHE_EXPIRATION_TIME_SECONDS);
}
this.memory_[key] = value;
this.memory_[prefixedKey] = value;
};

/**
Expand All @@ -127,11 +131,40 @@ Storage_.prototype.setValue = function(key, value) {
*/
Storage_.prototype.removeValue = function(key) {
var prefixedKey = this.getPrefixedKey_(key);
this.properties_.deleteProperty(prefixedKey);
this.removeValueWithPrefixedKey_(prefixedKey);
};

/**
* Resets the storage, removing all stored data.
* @param {string} key The key.
*/
Storage_.prototype.reset = function() {
var prefix = this.getPrefixedKey_();
var prefixedKeys = Object.keys(this.memory_);

if (this.properties_) {
var props = this.properties_.getProperties();
prefixedKeys = Object.keys(props).filter(function(prefixedKey) {
return prefixedKey === prefix || prefixedKey.indexOf(prefix + '.') === 0;
});
}
for (var i = 0; i < prefixedKeys.length; i++) {
this.removeValueWithPrefixedKey_(prefixedKeys[i]);
};
};

/**
* Removes a stored value.
* @param {string} prefixedKey The key.
*/
Storage_.prototype.removeValueWithPrefixedKey_ = function(prefixedKey) {
if (this.properties_) {
this.properties_.deleteProperty(prefixedKey);
}
if (this.cache_) {
this.cache_.remove(prefixedKey);
}
delete this.memory_[key];
delete this.memory_[prefixedKey];
};

/**
Expand Down
3 changes: 2 additions & 1 deletion src/models/gas/UserStorage.gs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ var UserStorage = {
_getAppStorage: function() {
if (!this._appStorage) {
// https://developers.google.com/apps-script/guides/services/quotas
this._appStorage = new Storage_('jst', PropertiesService.getUserProperties()||{});
this._appStorage = new Storage_('jst', PropertiesService.getUserProperties()||{}, CacheService.getUserCache()||{});
}
return this._appStorage;
},
Expand All @@ -58,6 +58,7 @@ var UserStorage = {
_resetLocalStorage: function() {
this._appStorage = false;
}

};


Expand Down
29 changes: 16 additions & 13 deletions src/models/jira/CustomFields.gs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,19 @@ var CustomFields = {
if (_length > _limit) {
for(_i=0; _i<_length; _i++) {
_chunk = data.splice(0, _limit);
UserStorage.setValue('favoriteCustomFields_' + _i, JSON.stringify(_chunk));
UserStorage.setValue('favoriteFields_' + _i, JSON.stringify(_chunk));
_length -= _limit - 1;
_chunkLen++;
debug.log("Saved CustomFields chunks[%d]: %s", _chunk.length, _chunk);
debug.log("Saved favorite custom fields chunks[%d]: %s", _chunk.length, _chunk);
}

} else {
UserStorage.setValue('favoriteCustomFields', JSON.stringify(data));
debug.log("Saved CustomFields[%d]: %s", _length, JSON.stringify(data));
UserStorage.setValue('favoriteFields', JSON.stringify(data));
debug.log("Saved favorite custom fields[%d]: %s", _length, JSON.stringify(data));
}

UserStorage.setValue('favoriteCustomFields_length', _chunkLen);
UserStorage.setValue('favoriteFields_length', _chunkLen);
this.fields = data;

StorageCounter.log();
},
Expand All @@ -44,21 +45,23 @@ var CustomFields = {

_loadFromStorage: function() {
var customFields = [];
var _chunkLen = UserStorage.getValue('favoriteCustomFields_length') || 0;
var _data = [], data = UserStorage.getValue('favoriteCustomFields') || '[]';
var _chunkLen = UserStorage.getValue('favoriteFields_length') || 0;
var _data = [], data = UserStorage.getValue('favoriteFields') || '[]';

if (_chunkLen == 0) {
customFields = JSON.parse(data);
debug.log("Loaded CustomFields[%d]: %s", customFields.length, JSON.stringify(customFields));
debug.log("Loaded favorite custom fields[%d]: %s", customFields.length, JSON.stringify(customFields));
} else {
var _i = 0;
for(_i=0; _i<_chunkLen; _i++) {
_data = JSON.parse(UserStorage.getValue('favoriteCustomFields_' + _i) || '[]');
customFields.push.apply(customFields, _data);
debug.log("Loaded CustomFields chunks[%d]: %s", _data.length, JSON.stringify(_data));
_data = JSON.parse(UserStorage.getValue('favoriteFields_' + _i) || '[]');
if (_data.length > 0) {
customFields.push.apply(customFields, _data);
debug.log("Loaded favorite custom fields chunks[%d]: %s", _data.length, JSON.stringify(_data));
}
}
}

StorageCounter.log();

return customFields;
Expand All @@ -72,5 +75,5 @@ var CustomFields = {


// Node required code block
module.exports = CustomFields
module.exports = CustomFields;
// End of Node required code block
1 change: 1 addition & 0 deletions src/models/jira/IssueFields.gs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ IssueFields = (function () {
function validateCustomFields_(customFields) {
var schemaUpdated = false;
var customTypeUpdateNeeded = false;

customFields.forEach(function (field) {
// using attribute schemaType conistently across the code base
// however a user may have an object stored with attribute "type" in their preferences
Expand Down
7 changes: 7 additions & 0 deletions src/settings.gs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
const BUILD = require("./Code.gs").BUILD;
const Storage_ = require("./Storage.gs").Storage_;
const UserStorage = require("src/models/gas/UserStorage.gs");
const CustomFields = require("src/models/jira/CustomFields.gs");
// End of Node required code block


Expand Down Expand Up @@ -91,6 +92,12 @@ function initDefaults() {
if (server_type == null) server_type = 'onDemand';
setCfg_('server_type', server_type);

// migrate from 1.4.4 to <
var _cfields = UserStorage.getValue('favoriteCustomFields') || [];
debug.info('Migrated custom fields from: %o', _cfields);
CustomFields.save(_cfields);
debug.info('Migrated custom fields to : %o', CustomFields.load());

// set done
UserStorage.setValue('defaults_initialized', 'true');
}
Expand Down
5 changes: 2 additions & 3 deletions test/Code.test.js
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
/**
* Tests based on documentation here
* https://developers.google.com/gsuite/add-ons/concepts/addon-authorization#editor_add-on_authorization
*/


beforeEach(() => {
SpreadsheetApp.resetMocks();
});

test('onOpen builds menu', () => {
var onOpen = require('../src/Code.gs').onOpen;
var e = {
Expand All @@ -34,4 +33,4 @@ test('Update Jira menu option appears based on feature switch', () => {
expect(addItemMock.calls[menuItemCount-7][0]).toBe('Update Jira Issues');
expect(addItemMock.calls[menuItemCount-7][1]).toBe('menuUpdateJiraIssues');
SpreadsheetApp.resetMocks();
});
});
1 change: 1 addition & 0 deletions test/controllers/customFields.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ debug = require("src/debug.gs").debug;
PropertiesService = require('test/mocks/PropertiesService');
global.environmentConfiguration = require('src/environmentConfiguration.gs');
const UserStorage = require("src/models/gas/UserStorage.gs");
const CustomFields = require("src/models/jira/CustomFields.gs");
global.EpicField = require("src/models/jira/EpicField.gs");

beforeEach(() => {
Expand Down
1 change: 1 addition & 0 deletions test/controllers/jiraFieldMap.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ debug = require("src/debug.gs").debug;
PropertiesService = require('test/mocks/PropertiesService');
global.environmentConfiguration = require('src/environmentConfiguration.gs');
const UserStorage = require("src/models/gas/UserStorage.gs");
const CustomFields = require("src/models/jira/CustomFields.gs");
global.EpicField = require("src/models/jira/EpicField.gs");

beforeEach(() => {
Expand Down
1 change: 1 addition & 0 deletions test/jiraUpdateIssue.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
var jiraApiMock = require('./mocks/mockJiraApi.js');
const UserStorage = require('src/models/gas/UserStorage.gs');
const CustomFields = require("src/models/jira/CustomFields.gs");
const IssueFields = require('src/models/jira/IssueFields.gs');

beforeAll(() => {
Expand Down
1 change: 1 addition & 0 deletions test/jsLib.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
jsLib = require('src/jsLib.gs');
const UserStorage = require('src/models/gas/UserStorage.gs');
const CustomFields = require("src/models/jira/CustomFields.gs");

test('jsLib - buildUrl() accepts multiple ways of passing parameters', () => {
var result = jsLib.buildUrl('https://www.example.org', {});
Expand Down
2 changes: 2 additions & 0 deletions test/mocks/PropertiesService.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
/*
* @see https://developers.google.com/apps-script/reference/properties/properties
*/

var _userPropData = {};
var _scriptPropData = {};
var _documentPropData = {};
Expand All @@ -12,6 +13,7 @@ var _documentPropData = {};
var UserProps = {
getProperty: jest.fn().mockImplementation((key)=> _userPropData[key]),
setProperty: jest.fn().mockImplementation(function(key) { _userPropData[key] = data; }),
getProperties: jest.fn().mockImplementation(() => _userPropData ),
deleteProperty: jest.fn().mockImplementation(function(key) {
_userPropData[key] = null;
delete _userPropData[key];
Expand Down
21 changes: 10 additions & 11 deletions test/models/gas/UserStorage.test.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@

const UserStorage = require('src/models/gas/UserStorage.gs');
const CustomFields = require("src/models/jira/CustomFields.gs");

beforeEach(()=> {

PropertiesService.resetMocks();
CacheService.resetMocks();
// this is equivalent to google deleting data stored in the Properties service
// this is where the data is stored centrally and persistently
PropertiesService.resetMockUserData();
CacheService.resetMockUserData();
// this clears where the data is stored locally to cache and prevent multiple calls to the Properties service
UserStorage._resetLocalStorage();
})
Expand All @@ -22,19 +23,18 @@ test("It should save items in PropertiesService.", () => {
expect(PropertiesService.mockUserProps.setProperty.mock.calls[1][1]).toBe(JSON.stringify({"key":"value"}));
});

test("It should get values from PropertiesService", () => {
PropertiesService.mockUserProps.getProperty.mockImplementationOnce(() => {
test("It should get values from CacheService", () => {
CacheService.mockUserCache.get.mockImplementationOnce(() => {
return JSON.stringify("simple string value");
});
expect(UserStorage.getValue("test3")).toBe("simple string value");
expect(PropertiesService.mockUserProps.getProperty.mock.calls[0][0]).toBe("jst.test3");

expect(CacheService.mockUserCache.get.mock.calls[0][0]).toBe("jst.test3");

PropertiesService.mockUserProps.getProperty.mockImplementationOnce(() => {
CacheService.mockUserCache.get.mockImplementationOnce(() => {
return JSON.stringify({more:"complex",object:"to store"});
});
expect(UserStorage.getValue("test5")).toEqual({more:"complex",object:"to store"});
expect(PropertiesService.mockUserProps.getProperty.mock.calls[1][0]).toBe("jst.test5");
expect(CacheService.mockUserCache.get.mock.calls[1][0]).toBe("jst.test5");
});

test("The in memory cache of the Storage class should prevent multiple calls to PropertiesService", () => {
Expand All @@ -61,7 +61,6 @@ test("It should delete a value",() => {
})

test("Data should be retained in Properties service", ()=> {

UserStorage.setValue("test4","something");
expect(UserStorage.getValue("test4")).toBe("something");
UserStorage._resetLocalStorage();
Expand All @@ -72,5 +71,5 @@ test("Errors thrown in property service are caught", ()=> {
PropertiesService.mockUserProps.getProperty.mockImplementationOnce(() => {
throw "this is a technical error"
});
expect(() => UserStorage.getValue("test4")).toThrowError("There was a problem fetching your settings from the Google Service. Please try again later.");
});
expect(() => UserStorage.getValue("test5")).toThrowError("There was a problem fetching your settings from the Google Service. Please try again later.");
});
Loading

0 comments on commit 7b81062

Please sign in to comment.