@autofleet/settings 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintrc.json +66 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +112 -0
- package/dist/index.test.d.ts +4 -0
- package/dist/index.test.js +147 -0
- package/jest.config.js +14 -0
- package/nodemon.json +13 -0
- package/package.json +36 -0
- package/src/alltypes.d.ts +2 -0
- package/src/index.test.ts +171 -0
- package/src/index.ts +148 -0
- package/tsconfig.json +10 -0
package/.eslintrc.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"root": true,
|
|
3
|
+
"env": {
|
|
4
|
+
"node": true,
|
|
5
|
+
"jest": true
|
|
6
|
+
},
|
|
7
|
+
"extends": [
|
|
8
|
+
"airbnb-base",
|
|
9
|
+
"plugin:@typescript-eslint/recommended"
|
|
10
|
+
],
|
|
11
|
+
"rules": {
|
|
12
|
+
"import/extensions": [
|
|
13
|
+
"error",
|
|
14
|
+
"ignorePackages",
|
|
15
|
+
{
|
|
16
|
+
"js": "never",
|
|
17
|
+
"jsx": "never",
|
|
18
|
+
"ts": "never",
|
|
19
|
+
"tsx": "never"
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
"@typescript-eslint/no-unused-vars": ["error"],
|
|
23
|
+
"@typescript-eslint/no-var-requires": ["off"],
|
|
24
|
+
"@typescript-eslint/camelcase": ["off"],
|
|
25
|
+
"max-len": ["off"],
|
|
26
|
+
"@typescript-eslint/explicit-function-return-type": ["off"]
|
|
27
|
+
},
|
|
28
|
+
"overrides": [
|
|
29
|
+
{
|
|
30
|
+
// enable the rule specifically for TypeScript files
|
|
31
|
+
"files": ["src/**.js", "src/**.ts"],
|
|
32
|
+
"rules": {
|
|
33
|
+
"@typescript-eslint/explicit-function-return-type": ["off"],
|
|
34
|
+
"@typescript-eslint/no-var-requires": ["off"],
|
|
35
|
+
"@typescript-eslint/camelcase": ["off"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
],
|
|
39
|
+
"parser": "@typescript-eslint/parser",
|
|
40
|
+
"parserOptions": {
|
|
41
|
+
"project": "./tsconfig.json",
|
|
42
|
+
"sourceType": "module"
|
|
43
|
+
},
|
|
44
|
+
"ignorePatterns": ["dist/*", "node_modules/", "jest.config.js", "migrations/*", "newrelic.js"],
|
|
45
|
+
"plugins": ["@typescript-eslint"],
|
|
46
|
+
"settings": {
|
|
47
|
+
"import/extensions": [
|
|
48
|
+
"error",
|
|
49
|
+
"ignorePackages",
|
|
50
|
+
{
|
|
51
|
+
"js": "never",
|
|
52
|
+
"jsx": "never",
|
|
53
|
+
"ts": "never",
|
|
54
|
+
"tsx": "never"
|
|
55
|
+
}
|
|
56
|
+
],
|
|
57
|
+
"import/parsers": {
|
|
58
|
+
"@typescript-eslint/parser": [".ts",".tsx"]
|
|
59
|
+
},
|
|
60
|
+
"import/resolver": {
|
|
61
|
+
"node": {
|
|
62
|
+
"extensions": [".js",".jsx",".ts",".tsx", ".json"]
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
// eslint-disable-next-line max-classes-per-file
|
|
16
|
+
const dotenv_1 = __importDefault(require("dotenv"));
|
|
17
|
+
const network_1 = __importDefault(require("@autofleet/network"));
|
|
18
|
+
const logger_1 = __importDefault(require("@autofleet/logger"));
|
|
19
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
20
|
+
const events_1 = __importDefault(require("events"));
|
|
21
|
+
const util_1 = __importDefault(require("util"));
|
|
22
|
+
dotenv_1.default.config();
|
|
23
|
+
const nextTick = util_1.default.promisify(process.nextTick);
|
|
24
|
+
const logger = logger_1.default();
|
|
25
|
+
const fiveMinutes = 60 * 5;
|
|
26
|
+
const waitingToNetwork = 'waitingToNetwork';
|
|
27
|
+
const findUrl = (serviceUrl) => serviceUrl
|
|
28
|
+
|| (process.env.SETTING_MS_SERVICE_HOST && process.env.SETTING_MS_SERVICE_HOST.length > 0 ? `http://${process.env.SETTING_MS_SERVICE_HOST}/` : undefined)
|
|
29
|
+
|| (process.env.NODE_ENV !== 'test' ? 'http://setting-ms.autofleet.io/' : 'http://localhost:9999/');
|
|
30
|
+
class ConnotGetNetworkValueError extends Error {
|
|
31
|
+
}
|
|
32
|
+
class SettingsManager {
|
|
33
|
+
constructor({ serviceUrl, ttl = fiveMinutes } = {}) {
|
|
34
|
+
const localServiceUrl = findUrl(serviceUrl);
|
|
35
|
+
this.NEVER_DEFAULT_VALUE = SettingsManager.NEVER_DEFAULT_VALUE;
|
|
36
|
+
this.ttl = ttl;
|
|
37
|
+
this.settingsCache = new node_cache_1.default();
|
|
38
|
+
this.networkEvents = new events_1.default();
|
|
39
|
+
this.network = new network_1.default({
|
|
40
|
+
serviceUrl: localServiceUrl,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
static get NEVER_DEFAULT_VALUE() {
|
|
44
|
+
return 'NEVER_DEFAULT_VALUE';
|
|
45
|
+
}
|
|
46
|
+
static readNetworkValue(networkValue, defaultValue) {
|
|
47
|
+
const returnValue = typeof networkValue !== 'undefined' ? networkValue : defaultValue;
|
|
48
|
+
if (returnValue === SettingsManager.NEVER_DEFAULT_VALUE) {
|
|
49
|
+
throw new Error('Cannot find value from network or cache, default value is set to never');
|
|
50
|
+
}
|
|
51
|
+
return returnValue;
|
|
52
|
+
}
|
|
53
|
+
get(key, defaultValue, labels = [], { timeout = 5000, rejectOnFail = false } = {}) {
|
|
54
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
55
|
+
if (typeof defaultValue === 'undefined') {
|
|
56
|
+
throw new Error('Can\'t get a key without defaultValue');
|
|
57
|
+
}
|
|
58
|
+
const cacheKey = `${key}_${JSON.stringify(labels)}`;
|
|
59
|
+
const cacheValue = this.settingsCache.get(cacheKey);
|
|
60
|
+
if (cacheValue !== undefined) {
|
|
61
|
+
if (waitingToNetwork === cacheValue) {
|
|
62
|
+
yield nextTick();
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
this.networkEvents.once(cacheKey, (networkValue) => {
|
|
65
|
+
resolve(SettingsManager.readNetworkValue(networkValue, defaultValue));
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return cacheValue;
|
|
70
|
+
}
|
|
71
|
+
if (process.env.NODE_ENV === 'test') {
|
|
72
|
+
return defaultValue;
|
|
73
|
+
}
|
|
74
|
+
let networkValue;
|
|
75
|
+
try {
|
|
76
|
+
this.settingsCache.set(cacheKey, waitingToNetwork, this.ttl);
|
|
77
|
+
const networkResponse = yield this.network.get(`/api/v1/settings/get-setting/${key}`, {
|
|
78
|
+
params: {
|
|
79
|
+
labels,
|
|
80
|
+
},
|
|
81
|
+
timeout,
|
|
82
|
+
});
|
|
83
|
+
networkValue = networkResponse.data.value;
|
|
84
|
+
}
|
|
85
|
+
catch (error) {
|
|
86
|
+
logger.error('Cant get setting from network');
|
|
87
|
+
if (rejectOnFail) {
|
|
88
|
+
throw new ConnotGetNetworkValueError();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
this.networkEvents.emit(cacheKey, networkValue);
|
|
92
|
+
let returnValue;
|
|
93
|
+
try {
|
|
94
|
+
returnValue = SettingsManager.readNetworkValue(networkValue, defaultValue);
|
|
95
|
+
}
|
|
96
|
+
catch (e) {
|
|
97
|
+
this.settingsCache.del(cacheKey);
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
this.settingsCache.set(cacheKey, returnValue, this.ttl);
|
|
101
|
+
return returnValue;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
setLocal(key, labels, value) {
|
|
105
|
+
const cacheKey = `${key}_${JSON.stringify(labels)}`;
|
|
106
|
+
this.settingsCache.set(cacheKey, value, this.ttl);
|
|
107
|
+
}
|
|
108
|
+
flush() {
|
|
109
|
+
return this.settingsCache.flushAll();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
module.exports = SettingsManager;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
const nock = require('nock');
|
|
12
|
+
const Settings = require('./index');
|
|
13
|
+
process.env.NODE_ENV = 'node-common-test';
|
|
14
|
+
const serviceUrl = 'localhost:8085';
|
|
15
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
16
|
+
const mockSetting = (key, value, labels = undefined, response = 200) => {
|
|
17
|
+
const n = nock(`http://${serviceUrl}`)
|
|
18
|
+
.get(`/api/v1/settings/get-setting/${key}`)
|
|
19
|
+
.query(labels ? { labels } : undefined);
|
|
20
|
+
return n.reply(response, { value });
|
|
21
|
+
};
|
|
22
|
+
describe('Settings', () => {
|
|
23
|
+
it('Throws exeption if there is no defualt value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
24
|
+
const settings = new Settings({
|
|
25
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
26
|
+
});
|
|
27
|
+
expect(settings.get('key1'))
|
|
28
|
+
.rejects.toEqual(new Error('Can\'t get a key without defaultValue'));
|
|
29
|
+
}));
|
|
30
|
+
it('Can get 0 as default value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
31
|
+
const settings = new Settings({
|
|
32
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
33
|
+
});
|
|
34
|
+
expect(yield settings.get('key1', 0)).toBe(0);
|
|
35
|
+
}));
|
|
36
|
+
it('Can get false as default value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
37
|
+
const settings = new Settings({
|
|
38
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
39
|
+
});
|
|
40
|
+
expect(yield settings.get('key1', false)).toBe(false);
|
|
41
|
+
}));
|
|
42
|
+
it('Uses env.SETTING_MS_SERVICE_HOST as service host if no one defined', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
43
|
+
const m = mockSetting('key1', 'value1');
|
|
44
|
+
process.env.SETTING_MS_SERVICE_HOST = serviceUrl;
|
|
45
|
+
const settings = new Settings();
|
|
46
|
+
const value = yield settings.get('key1', 'dv');
|
|
47
|
+
expect(value).toEqual('value1');
|
|
48
|
+
expect(m.isDone()).toBeTruthy();
|
|
49
|
+
process.env.SETTING_MS_SERVICE_HOST = '';
|
|
50
|
+
}));
|
|
51
|
+
it('Get a key from server', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
52
|
+
const m = mockSetting('key1', 'value1');
|
|
53
|
+
const settings = new Settings({
|
|
54
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
55
|
+
});
|
|
56
|
+
const value = yield settings.get('key1', 'dv');
|
|
57
|
+
expect(value).toEqual('value1');
|
|
58
|
+
expect(m.isDone()).toBeTruthy();
|
|
59
|
+
}));
|
|
60
|
+
it('Cache from server', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
61
|
+
const m = mockSetting('key1', 'value1');
|
|
62
|
+
const settings = new Settings({
|
|
63
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
64
|
+
});
|
|
65
|
+
const value = yield settings.get('key1', 'dv');
|
|
66
|
+
expect(value).toEqual('value1');
|
|
67
|
+
expect(m.isDone()).toBeTruthy();
|
|
68
|
+
const value2 = yield settings.get('key1', 'dv');
|
|
69
|
+
expect(value2).toEqual('value1');
|
|
70
|
+
}));
|
|
71
|
+
it('Use only one request for similar keys', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
72
|
+
const m = mockSetting('key1EM', 'value1EM');
|
|
73
|
+
const settings = new Settings({
|
|
74
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
75
|
+
});
|
|
76
|
+
const [value, value2] = yield Promise.all([
|
|
77
|
+
settings.get('key1EM', 'dv'),
|
|
78
|
+
settings.get('key1EM', 'dv'),
|
|
79
|
+
]);
|
|
80
|
+
expect(value).toEqual('value1EM');
|
|
81
|
+
expect(m.isDone()).toBeTruthy();
|
|
82
|
+
expect(value2).toEqual('value1EM');
|
|
83
|
+
}));
|
|
84
|
+
it('Finds with labels', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
85
|
+
const labels = [{ fleetId: 'uuid' }];
|
|
86
|
+
const m = mockSetting('key1', 'value1', labels);
|
|
87
|
+
const settings = new Settings({
|
|
88
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
89
|
+
});
|
|
90
|
+
const value = yield settings.get('key1', 'dv', labels);
|
|
91
|
+
expect(value).toEqual('value1');
|
|
92
|
+
expect(m.isDone()).toBeTruthy();
|
|
93
|
+
}));
|
|
94
|
+
it('Returns default value when network error', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
95
|
+
const settings = new Settings({
|
|
96
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
97
|
+
});
|
|
98
|
+
const value = yield settings.get('key1', 'dv', [{ fleetId: 'uuid' }]);
|
|
99
|
+
expect(value).toEqual('dv');
|
|
100
|
+
}));
|
|
101
|
+
it('Throws an error if network error and no default value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
102
|
+
const settings = new Settings({
|
|
103
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
104
|
+
});
|
|
105
|
+
const f = () => settings.get('key1', settings.NEVER_DEFAULT_VALUE, [{ fleetId: 'uuid' }]);
|
|
106
|
+
expect(f())
|
|
107
|
+
.rejects.toEqual(new Error('Cannot find value from network or cache, default value is set to never'));
|
|
108
|
+
}));
|
|
109
|
+
it('Throws an error if network error and no default value and success the next time', (done) => __awaiter(void 0, void 0, void 0, function* () {
|
|
110
|
+
const settings = new Settings({
|
|
111
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
112
|
+
});
|
|
113
|
+
const f = () => settings.get('key1', settings.NEVER_DEFAULT_VALUE);
|
|
114
|
+
expect(f())
|
|
115
|
+
.rejects.toEqual(new Error('Cannot find value from network or cache, default value is set to never'));
|
|
116
|
+
setTimeout(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
117
|
+
const m = mockSetting('key1', 'value1');
|
|
118
|
+
expect(yield settings.get('key1', settings.NEVER_DEFAULT_VALUE)).toEqual('value1');
|
|
119
|
+
expect(m.isDone());
|
|
120
|
+
done();
|
|
121
|
+
}), 100);
|
|
122
|
+
}));
|
|
123
|
+
it('Values can be flushed', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
124
|
+
const m1 = mockSetting('key1', 'value1');
|
|
125
|
+
const settings = new Settings({
|
|
126
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
127
|
+
});
|
|
128
|
+
settings.flush();
|
|
129
|
+
const value = yield settings.get('key1', 'dv');
|
|
130
|
+
expect(value).toEqual('value1');
|
|
131
|
+
expect(m1.isDone()).toBeTruthy();
|
|
132
|
+
settings.flush();
|
|
133
|
+
const m2 = mockSetting('key1', 'value2');
|
|
134
|
+
const value2 = yield settings.get('key1', 'dv');
|
|
135
|
+
expect(value2).toEqual('value2');
|
|
136
|
+
expect(m2.isDone()).toBeTruthy();
|
|
137
|
+
}));
|
|
138
|
+
it('when NODE_ENV === test return the default value', () => __awaiter(void 0, void 0, void 0, function* () {
|
|
139
|
+
process.env.NODE_ENV = 'test';
|
|
140
|
+
const settings = new Settings({
|
|
141
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
142
|
+
});
|
|
143
|
+
const value = yield settings.get('key1', 'dv');
|
|
144
|
+
expect(value).toEqual('dv');
|
|
145
|
+
process.env.NODE_ENV = 'node-common-test';
|
|
146
|
+
}));
|
|
147
|
+
});
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
roots: ['<rootDir>/src'],
|
|
4
|
+
transform: {
|
|
5
|
+
'^.+\\.tsx?$': 'ts-jest',
|
|
6
|
+
},
|
|
7
|
+
testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.(tsx?$|jsx?$)',
|
|
8
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
9
|
+
coverageThreshold: {
|
|
10
|
+
global: {
|
|
11
|
+
lines: 75,
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
}
|
package/nodemon.json
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@autofleet/settings",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "ts-node src/index.ts",
|
|
8
|
+
"example": "ts-node src/example.ts",
|
|
9
|
+
"prepublish": "tsc",
|
|
10
|
+
"linter": "./node_modules/.bin/eslint .",
|
|
11
|
+
"test": "jest --forceExit",
|
|
12
|
+
"coverage": "jest --coverage --forceExit --runInBand && rm -rf ./coverage",
|
|
13
|
+
"dev": "nodemon"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"@autofleet/logger": "^1.0.0",
|
|
17
|
+
"@autofleet/network": "^1.2.1",
|
|
18
|
+
"@types/jest": "^22.0.0",
|
|
19
|
+
"bluebird": "^3.7.2",
|
|
20
|
+
"dotenv": "^8.2.0",
|
|
21
|
+
"jest": "^22.4.4",
|
|
22
|
+
"node-cache": "^5.1.2",
|
|
23
|
+
"nock": "^10.0.2"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@typescript-eslint/eslint-plugin": "^4.8.1",
|
|
27
|
+
"eslint": "^7.13.0",
|
|
28
|
+
"eslint-config-airbnb-typescript": "^12.0.0",
|
|
29
|
+
"eslint-plugin-import": "^2.22.1",
|
|
30
|
+
"ts-jest": "^25.4.0",
|
|
31
|
+
"ts-node": "^8.6.2",
|
|
32
|
+
"typescript": "^3.8.3"
|
|
33
|
+
},
|
|
34
|
+
"author": "",
|
|
35
|
+
"license": "ISC"
|
|
36
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
const nock = require('nock');
|
|
2
|
+
const Settings = require('./index');
|
|
3
|
+
|
|
4
|
+
process.env.NODE_ENV = 'node-common-test';
|
|
5
|
+
|
|
6
|
+
const serviceUrl = 'localhost:8085';
|
|
7
|
+
|
|
8
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
9
|
+
const mockSetting = (key: string, value: string | boolean | number | never, labels: any = undefined, response = 200) => {
|
|
10
|
+
const n = nock(`http://${serviceUrl}`)
|
|
11
|
+
.get(`/api/v1/settings/get-setting/${key}`)
|
|
12
|
+
.query(labels ? { labels } : undefined);
|
|
13
|
+
|
|
14
|
+
return n.reply(response, { value });
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
describe('Settings', () => {
|
|
18
|
+
it('Throws exeption if there is no defualt value', async () => {
|
|
19
|
+
const settings = new Settings({
|
|
20
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
expect(settings.get('key1'))
|
|
24
|
+
.rejects.toEqual(new Error('Can\'t get a key without defaultValue'));
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('Can get 0 as default value', async () => {
|
|
28
|
+
const settings = new Settings({
|
|
29
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
expect(await settings.get('key1', 0)).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('Can get false as default value', async () => {
|
|
36
|
+
const settings = new Settings({
|
|
37
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
expect(await settings.get('key1', false)).toBe(false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('Uses env.SETTING_MS_SERVICE_HOST as service host if no one defined', async () => {
|
|
44
|
+
const m = mockSetting('key1', 'value1');
|
|
45
|
+
process.env.SETTING_MS_SERVICE_HOST = serviceUrl;
|
|
46
|
+
const settings = new Settings();
|
|
47
|
+
|
|
48
|
+
const value = await settings.get('key1', 'dv');
|
|
49
|
+
expect(value).toEqual('value1');
|
|
50
|
+
expect(m.isDone()).toBeTruthy();
|
|
51
|
+
process.env.SETTING_MS_SERVICE_HOST = '';
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('Get a key from server', async () => {
|
|
55
|
+
const m = mockSetting('key1', 'value1');
|
|
56
|
+
const settings = new Settings({
|
|
57
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const value = await settings.get('key1', 'dv');
|
|
61
|
+
expect(value).toEqual('value1');
|
|
62
|
+
expect(m.isDone()).toBeTruthy();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it('Cache from server', async () => {
|
|
66
|
+
const m = mockSetting('key1', 'value1');
|
|
67
|
+
const settings = new Settings({
|
|
68
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const value = await settings.get('key1', 'dv');
|
|
72
|
+
expect(value).toEqual('value1');
|
|
73
|
+
expect(m.isDone()).toBeTruthy();
|
|
74
|
+
|
|
75
|
+
const value2 = await settings.get('key1', 'dv');
|
|
76
|
+
expect(value2).toEqual('value1');
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('Use only one request for similar keys', async () => {
|
|
80
|
+
const m = mockSetting('key1EM', 'value1EM');
|
|
81
|
+
const settings = new Settings({
|
|
82
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const [value, value2] = await Promise.all([
|
|
86
|
+
settings.get('key1EM', 'dv'),
|
|
87
|
+
settings.get('key1EM', 'dv'),
|
|
88
|
+
]);
|
|
89
|
+
expect(value).toEqual('value1EM');
|
|
90
|
+
expect(m.isDone()).toBeTruthy();
|
|
91
|
+
|
|
92
|
+
expect(value2).toEqual('value1EM');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('Finds with labels', async () => {
|
|
96
|
+
const labels = [{ fleetId: 'uuid' }];
|
|
97
|
+
const m = mockSetting('key1', 'value1', labels);
|
|
98
|
+
const settings = new Settings({
|
|
99
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const value = await settings.get('key1', 'dv', labels);
|
|
103
|
+
expect(value).toEqual('value1');
|
|
104
|
+
expect(m.isDone()).toBeTruthy();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('Returns default value when network error', async () => {
|
|
108
|
+
const settings = new Settings({
|
|
109
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const value = await settings.get('key1', 'dv', [{ fleetId: 'uuid' }]);
|
|
113
|
+
expect(value).toEqual('dv');
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('Throws an error if network error and no default value', async () => {
|
|
117
|
+
const settings = new Settings({
|
|
118
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const f = () => settings.get('key1', settings.NEVER_DEFAULT_VALUE, [{ fleetId: 'uuid' }]);
|
|
122
|
+
expect(f())
|
|
123
|
+
.rejects.toEqual(new Error('Cannot find value from network or cache, default value is set to never'));
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
it('Throws an error if network error and no default value and success the next time', async (done) => {
|
|
127
|
+
const settings = new Settings({
|
|
128
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const f = () => settings.get('key1', settings.NEVER_DEFAULT_VALUE);
|
|
132
|
+
expect(f())
|
|
133
|
+
.rejects.toEqual(new Error('Cannot find value from network or cache, default value is set to never'));
|
|
134
|
+
|
|
135
|
+
setTimeout(async () => {
|
|
136
|
+
const m = mockSetting('key1', 'value1');
|
|
137
|
+
expect(await settings.get('key1', settings.NEVER_DEFAULT_VALUE)).toEqual('value1');
|
|
138
|
+
expect(m.isDone());
|
|
139
|
+
done();
|
|
140
|
+
}, 100);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('Values can be flushed', async () => {
|
|
144
|
+
const m1 = mockSetting('key1', 'value1');
|
|
145
|
+
const settings = new Settings({
|
|
146
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
147
|
+
});
|
|
148
|
+
settings.flush();
|
|
149
|
+
|
|
150
|
+
const value = await settings.get('key1', 'dv');
|
|
151
|
+
expect(value).toEqual('value1');
|
|
152
|
+
expect(m1.isDone()).toBeTruthy();
|
|
153
|
+
|
|
154
|
+
settings.flush();
|
|
155
|
+
const m2 = mockSetting('key1', 'value2');
|
|
156
|
+
|
|
157
|
+
const value2 = await settings.get('key1', 'dv');
|
|
158
|
+
expect(value2).toEqual('value2');
|
|
159
|
+
expect(m2.isDone()).toBeTruthy();
|
|
160
|
+
});
|
|
161
|
+
it('when NODE_ENV === test return the default value', async () => {
|
|
162
|
+
process.env.NODE_ENV = 'test';
|
|
163
|
+
const settings = new Settings({
|
|
164
|
+
serviceUrl: `http://${serviceUrl}/`,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const value = await settings.get('key1', 'dv');
|
|
168
|
+
expect(value).toEqual('dv');
|
|
169
|
+
process.env.NODE_ENV = 'node-common-test';
|
|
170
|
+
});
|
|
171
|
+
});
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// eslint-disable-next-line max-classes-per-file
|
|
2
|
+
import dotenv from 'dotenv';
|
|
3
|
+
import Network from '@autofleet/network';
|
|
4
|
+
import Logger from '@autofleet/logger';
|
|
5
|
+
import NodeCache from 'node-cache';
|
|
6
|
+
import EventEmitter from 'events';
|
|
7
|
+
import util from 'util';
|
|
8
|
+
|
|
9
|
+
dotenv.config();
|
|
10
|
+
|
|
11
|
+
const nextTick = util.promisify(process.nextTick);
|
|
12
|
+
const logger = Logger();
|
|
13
|
+
|
|
14
|
+
const fiveMinutes = 60 * 5;
|
|
15
|
+
const waitingToNetwork = 'waitingToNetwork';
|
|
16
|
+
|
|
17
|
+
const findUrl = (serviceUrl?: string) => serviceUrl
|
|
18
|
+
|| (process.env.SETTING_MS_SERVICE_HOST && process.env.SETTING_MS_SERVICE_HOST.length > 0 ? `http://${process.env.SETTING_MS_SERVICE_HOST}/` : undefined)
|
|
19
|
+
|| (process.env.NODE_ENV !== 'test' ? 'http://setting-ms.autofleet.io/' : 'http://localhost:9999/');
|
|
20
|
+
|
|
21
|
+
interface SettingsClassOptions {
|
|
22
|
+
serviceUrl?: string;
|
|
23
|
+
ttl?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type SettingValue = string | boolean | number | never;
|
|
27
|
+
|
|
28
|
+
interface Label {
|
|
29
|
+
businessModelId?: string;
|
|
30
|
+
demandSourceId?: string;
|
|
31
|
+
fleetId?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
type LabelsArray = Label[];
|
|
35
|
+
|
|
36
|
+
interface GetSettingOption {
|
|
37
|
+
timeout?: number;
|
|
38
|
+
rejectOnFail?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
class ConnotGetNetworkValueError extends Error {}
|
|
42
|
+
|
|
43
|
+
class SettingsManager {
|
|
44
|
+
ttl: number;
|
|
45
|
+
|
|
46
|
+
settingsCache: NodeCache;
|
|
47
|
+
|
|
48
|
+
networkEvents: EventEmitter;
|
|
49
|
+
|
|
50
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
51
|
+
network: any;
|
|
52
|
+
|
|
53
|
+
NEVER_DEFAULT_VALUE: string;
|
|
54
|
+
|
|
55
|
+
static get NEVER_DEFAULT_VALUE() {
|
|
56
|
+
return 'NEVER_DEFAULT_VALUE';
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static readNetworkValue(networkValue: SettingValue, defaultValue: SettingValue) {
|
|
60
|
+
const returnValue = typeof networkValue !== 'undefined' ? networkValue : defaultValue;
|
|
61
|
+
|
|
62
|
+
if (returnValue === SettingsManager.NEVER_DEFAULT_VALUE) {
|
|
63
|
+
throw new Error('Cannot find value from network or cache, default value is set to never');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return returnValue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
constructor({ serviceUrl, ttl = fiveMinutes }:SettingsClassOptions = {}) {
|
|
70
|
+
const localServiceUrl = findUrl(serviceUrl);
|
|
71
|
+
|
|
72
|
+
this.NEVER_DEFAULT_VALUE = SettingsManager.NEVER_DEFAULT_VALUE;
|
|
73
|
+
this.ttl = ttl;
|
|
74
|
+
this.settingsCache = new NodeCache();
|
|
75
|
+
this.networkEvents = new EventEmitter();
|
|
76
|
+
this.network = new Network({
|
|
77
|
+
serviceUrl: localServiceUrl,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async get(
|
|
82
|
+
key: SettingValue,
|
|
83
|
+
defaultValue: SettingValue,
|
|
84
|
+
labels:LabelsArray = [],
|
|
85
|
+
{ timeout = 5000, rejectOnFail = false } : GetSettingOption = {},
|
|
86
|
+
) {
|
|
87
|
+
if (typeof defaultValue === 'undefined') {
|
|
88
|
+
throw new Error('Can\'t get a key without defaultValue');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cacheKey = `${key}_${JSON.stringify(labels)}`;
|
|
92
|
+
const cacheValue = this.settingsCache.get(cacheKey);
|
|
93
|
+
if (cacheValue !== undefined) {
|
|
94
|
+
if (waitingToNetwork === cacheValue) {
|
|
95
|
+
await nextTick();
|
|
96
|
+
return new Promise((resolve) => {
|
|
97
|
+
this.networkEvents.once(cacheKey, (networkValue) => {
|
|
98
|
+
resolve(SettingsManager.readNetworkValue(networkValue, defaultValue));
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
return cacheValue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (process.env.NODE_ENV === 'test') {
|
|
106
|
+
return defaultValue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let networkValue;
|
|
110
|
+
try {
|
|
111
|
+
this.settingsCache.set(cacheKey, waitingToNetwork, this.ttl);
|
|
112
|
+
const networkResponse = await this.network.get(`/api/v1/settings/get-setting/${key}`, {
|
|
113
|
+
params: {
|
|
114
|
+
labels,
|
|
115
|
+
},
|
|
116
|
+
timeout,
|
|
117
|
+
});
|
|
118
|
+
networkValue = networkResponse.data.value;
|
|
119
|
+
} catch (error) {
|
|
120
|
+
logger.error('Cant get setting from network');
|
|
121
|
+
if (rejectOnFail) {
|
|
122
|
+
throw new ConnotGetNetworkValueError();
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
this.networkEvents.emit(cacheKey, networkValue);
|
|
127
|
+
let returnValue;
|
|
128
|
+
try {
|
|
129
|
+
returnValue = SettingsManager.readNetworkValue(networkValue, defaultValue);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
this.settingsCache.del(cacheKey);
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
this.settingsCache.set(cacheKey, returnValue, this.ttl);
|
|
135
|
+
return returnValue;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
setLocal(key: string, labels: LabelsArray, value: SettingValue) {
|
|
139
|
+
const cacheKey = `${key}_${JSON.stringify(labels)}`;
|
|
140
|
+
this.settingsCache.set(cacheKey, value, this.ttl);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
flush() {
|
|
144
|
+
return this.settingsCache.flushAll();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = SettingsManager;
|