hmpo-model 3.2.1 → 4.0.2
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/.circleci/config.yml +21 -0
- package/README.md +86 -40
- package/lib/local-model.js +38 -42
- package/lib/model-error.js +51 -0
- package/lib/remote-model.js +162 -97
- package/package.json +9 -11
- package/test/lib/spec.local-model.js +36 -18
- package/test/lib/spec.remote-model.js +377 -105
- package/.travis.yml +0 -8
package/lib/remote-model.js
CHANGED
|
@@ -1,91 +1,165 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const debug = require('debug')('hmpo:model:remote');
|
|
3
4
|
const LocalModel = require('./local-model');
|
|
4
|
-
const
|
|
5
|
-
const _ = require('underscore');
|
|
5
|
+
const got = require('got');
|
|
6
6
|
const kebabCase = require('lodash.kebabcase');
|
|
7
|
-
const
|
|
7
|
+
const { URL } = require('url');
|
|
8
|
+
const ModelError = require('./model-error');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_TIMEOUT = 60000;
|
|
8
11
|
|
|
9
12
|
class RemoteModel extends LocalModel {
|
|
10
13
|
constructor(attributes, options) {
|
|
11
14
|
super(attributes, options);
|
|
12
|
-
|
|
15
|
+
this.got = got;
|
|
13
16
|
this.options.label = this.options.label || kebabCase(this.constructor.name);
|
|
14
17
|
this.setLogger();
|
|
15
18
|
}
|
|
16
19
|
|
|
17
20
|
setLogger() {
|
|
18
|
-
|
|
21
|
+
try {
|
|
22
|
+
const hmpoLogger = require('hmpo-logger');
|
|
23
|
+
this.logger = hmpoLogger.get(':' + this.options.label);
|
|
24
|
+
} catch (e) {
|
|
25
|
+
console.error('Error setting logger, using console instead!', e);
|
|
26
|
+
this.logger = { outbound: console.log, trimHtml: html => html };
|
|
27
|
+
}
|
|
19
28
|
}
|
|
20
29
|
|
|
21
|
-
fetch(callback) {
|
|
22
|
-
|
|
30
|
+
fetch(args, callback) {
|
|
31
|
+
if (typeof args === 'function') {
|
|
32
|
+
callback = args;
|
|
33
|
+
args = undefined;
|
|
34
|
+
}
|
|
35
|
+
const config = this.requestConfig({method: 'GET'}, args);
|
|
23
36
|
this.request(config, callback);
|
|
24
37
|
}
|
|
25
38
|
|
|
26
|
-
save(callback) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
headers: {
|
|
36
|
-
'Content-Type': 'application/json',
|
|
37
|
-
'Content-Length': Buffer.byteLength(data)
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
this.request(config, data, callback);
|
|
42
|
-
|
|
39
|
+
save(args, callback) {
|
|
40
|
+
if (typeof args === 'function') {
|
|
41
|
+
callback = args;
|
|
42
|
+
args = undefined;
|
|
43
|
+
}
|
|
44
|
+
this.prepare((err, json) => {
|
|
45
|
+
if (err) return callback(err);
|
|
46
|
+
const config = this.requestConfig({method: 'POST', json}, args);
|
|
47
|
+
this.request(config, callback);
|
|
43
48
|
});
|
|
44
49
|
}
|
|
45
50
|
|
|
46
|
-
delete(callback) {
|
|
47
|
-
|
|
51
|
+
delete(args, callback) {
|
|
52
|
+
if (typeof args === 'function') {
|
|
53
|
+
callback = args;
|
|
54
|
+
args = undefined;
|
|
55
|
+
}
|
|
56
|
+
const config = this.requestConfig({method: 'DELETE'}, args);
|
|
48
57
|
this.request(config, callback);
|
|
49
58
|
}
|
|
50
59
|
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
prepare(callback) {
|
|
61
|
+
debug('prepare');
|
|
62
|
+
callback(null, this.toJSON());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
requestConfig(config, args) {
|
|
66
|
+
const retConfig = Object.assign({}, config);
|
|
53
67
|
|
|
54
|
-
retConfig.
|
|
68
|
+
retConfig.url = this.url(retConfig.url || retConfig.uri, args);
|
|
55
69
|
|
|
56
|
-
|
|
70
|
+
retConfig.timeout = this.timeout(retConfig.timeout);
|
|
57
71
|
|
|
72
|
+
const auth = this.auth(retConfig.auth);
|
|
58
73
|
if (auth) {
|
|
59
|
-
retConfig.
|
|
74
|
+
retConfig.username = auth.username || auth.user;
|
|
75
|
+
retConfig.password = auth.password || auth.pass;
|
|
60
76
|
}
|
|
77
|
+
delete retConfig.auth;
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
|
|
79
|
+
const agent = this.proxy(retConfig.proxy, retConfig.url);
|
|
80
|
+
if (agent) {
|
|
81
|
+
retConfig.agent = agent;
|
|
64
82
|
}
|
|
83
|
+
delete retConfig.proxy;
|
|
84
|
+
|
|
85
|
+
const headers = Object.assign({}, this.options.headers, retConfig.headers);
|
|
86
|
+
if (Object.keys(headers).length) retConfig.headers = headers;
|
|
87
|
+
|
|
88
|
+
debug('requestConfig', retConfig);
|
|
65
89
|
|
|
66
90
|
return retConfig;
|
|
67
91
|
}
|
|
68
92
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
93
|
+
url(url = this.options.url) {
|
|
94
|
+
return url;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
auth(auth = this.options.auth) {
|
|
98
|
+
if (typeof auth === 'string') {
|
|
99
|
+
const splitAuth = auth.split(':');
|
|
100
|
+
auth = {
|
|
101
|
+
username: splitAuth.shift(),
|
|
102
|
+
password: splitAuth.join(':')
|
|
103
|
+
};
|
|
73
104
|
}
|
|
105
|
+
return auth;
|
|
106
|
+
}
|
|
74
107
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
timeout(timeout = this.options.timeout || DEFAULT_TIMEOUT) {
|
|
109
|
+
if (typeof timeout === 'number') {
|
|
110
|
+
timeout = {
|
|
111
|
+
lookup: timeout,
|
|
112
|
+
connect: timeout,
|
|
113
|
+
secureConnect: timeout,
|
|
114
|
+
socket: timeout,
|
|
115
|
+
send: timeout,
|
|
116
|
+
response: timeout
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return timeout;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
proxy(proxy = this.options.proxy, url) {
|
|
123
|
+
if (!proxy || !url) return;
|
|
124
|
+
|
|
125
|
+
if (typeof proxy === 'string') proxy = { proxy };
|
|
126
|
+
|
|
127
|
+
const isHttps = (new URL(url).protocol === 'https:');
|
|
79
128
|
|
|
80
|
-
|
|
129
|
+
if (isHttps) {
|
|
130
|
+
const { HttpsProxyAgent } = require('hpagent');
|
|
131
|
+
return {
|
|
132
|
+
https: new HttpsProxyAgent(Object.assign({
|
|
133
|
+
keepAlive: false,
|
|
134
|
+
maxSockets: 1,
|
|
135
|
+
maxFreeSockets: 1,
|
|
136
|
+
}, proxy))
|
|
137
|
+
};
|
|
138
|
+
} else {
|
|
139
|
+
const { HttpProxyAgent } = require('hpagent');
|
|
140
|
+
return {
|
|
141
|
+
http: new HttpProxyAgent(Object.assign({
|
|
142
|
+
keepAlive: false,
|
|
143
|
+
maxSockets: 1,
|
|
144
|
+
maxFreeSockets: 1,
|
|
145
|
+
}, proxy))
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
|
81
149
|
|
|
82
|
-
|
|
150
|
+
request(settings, callback) {
|
|
151
|
+
this.hookSync({settings});
|
|
152
|
+
this.logSync({settings});
|
|
153
|
+
this.emit('sync', settings);
|
|
83
154
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
155
|
+
let responseTime;
|
|
156
|
+
const startTime = process.hrtime.bigint();
|
|
157
|
+
const setResponseTime = () => {
|
|
158
|
+
const endTime = process.hrtime.bigint();
|
|
159
|
+
responseTime = Number((Number(endTime - startTime) / 1000000).toFixed(3));
|
|
160
|
+
};
|
|
88
161
|
|
|
162
|
+
const _callback = (err, data, statusCode) => {
|
|
89
163
|
if (err) {
|
|
90
164
|
this.hookFail({settings, statusCode, responseTime, err, data});
|
|
91
165
|
this.logError({settings, statusCode, responseTime, err, data});
|
|
@@ -100,83 +174,74 @@ class RemoteModel extends LocalModel {
|
|
|
100
174
|
}
|
|
101
175
|
};
|
|
102
176
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
177
|
+
this.got(settings)
|
|
178
|
+
.then(response => {
|
|
179
|
+
debug('request got response', response);
|
|
180
|
+
setResponseTime();
|
|
181
|
+
this.handleResponse(response, _callback);
|
|
182
|
+
})
|
|
183
|
+
.catch(err => {
|
|
184
|
+
setResponseTime();
|
|
185
|
+
if (err.code === 'ERR_NON_2XX_3XX_RESPONSE' && err.response) {
|
|
186
|
+
return this.handleResponse(err.response, _callback);
|
|
108
187
|
}
|
|
109
|
-
err
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
this.hookSync({settings});
|
|
116
|
-
this.logSync({settings});
|
|
117
|
-
this.emit('sync', settings);
|
|
188
|
+
err = new ModelError(err);
|
|
189
|
+
debug('request got error', err);
|
|
190
|
+
_callback(err, null, err.status);
|
|
191
|
+
});
|
|
118
192
|
}
|
|
119
193
|
|
|
120
194
|
handleResponse(response, callback) {
|
|
121
|
-
|
|
195
|
+
debug('handleResponse', response);
|
|
196
|
+
let data;
|
|
122
197
|
try {
|
|
123
198
|
data = JSON.parse(response.body || '{}');
|
|
124
199
|
} catch (err) {
|
|
125
200
|
err.status = response.statusCode;
|
|
126
201
|
err.body = response.body;
|
|
127
|
-
return callback(err, null,
|
|
202
|
+
return callback(err, null, err.status);
|
|
128
203
|
}
|
|
129
204
|
this.parseResponse(response.statusCode, data, callback);
|
|
130
205
|
}
|
|
131
206
|
|
|
132
207
|
parseResponse(statusCode, data, callback) {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
callback(err, null, statusCode);
|
|
139
|
-
}
|
|
140
|
-
} else {
|
|
141
|
-
callback(this.parseError(statusCode, data), data, statusCode);
|
|
208
|
+
debug('parseResponse', statusCode, data);
|
|
209
|
+
|
|
210
|
+
if (statusCode >= 400) {
|
|
211
|
+
const error = this.parseError(statusCode, data);
|
|
212
|
+
return callback(error, data, statusCode);
|
|
142
213
|
}
|
|
143
|
-
}
|
|
144
214
|
|
|
145
|
-
|
|
146
|
-
|
|
215
|
+
try {
|
|
216
|
+
data = this.parse(data);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
return callback(err, null, statusCode);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
callback(null, data, statusCode);
|
|
147
222
|
}
|
|
148
223
|
|
|
149
224
|
parse(data) {
|
|
225
|
+
debug('parse', data);
|
|
226
|
+
if (data && typeof data === 'object') {
|
|
227
|
+
if (Array.isArray(data)) {
|
|
228
|
+
this.set('data', data);
|
|
229
|
+
} else {
|
|
230
|
+
this.set(data);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
150
233
|
return data;
|
|
151
234
|
}
|
|
152
235
|
|
|
153
236
|
parseError(statusCode, data) {
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
url() {
|
|
158
|
-
return this.options.url;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
auth(credentials) {
|
|
162
|
-
if (!credentials) return;
|
|
163
|
-
|
|
164
|
-
if (typeof credentials === 'string') {
|
|
165
|
-
let auth = credentials.split(':');
|
|
166
|
-
credentials = {
|
|
167
|
-
user: auth.shift(),
|
|
168
|
-
pass: auth.join(':'),
|
|
169
|
-
sendImmediately: true
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
return credentials;
|
|
237
|
+
debug('parseError, statusCode, data');
|
|
238
|
+
return Object.assign({ status: statusCode }, data);
|
|
174
239
|
}
|
|
175
240
|
|
|
176
241
|
logMeta(tokenData) {
|
|
177
242
|
let data = {
|
|
178
243
|
outVerb: tokenData.settings.method,
|
|
179
|
-
outRequest: tokenData.settings.
|
|
244
|
+
outRequest: tokenData.settings.url
|
|
180
245
|
};
|
|
181
246
|
|
|
182
247
|
if (tokenData.statusCode) {
|
|
@@ -194,7 +259,7 @@ class RemoteModel extends LocalModel {
|
|
|
194
259
|
data.outErrorBody = this.logger.trimHtml(tokenData.err.body);
|
|
195
260
|
}
|
|
196
261
|
|
|
197
|
-
|
|
262
|
+
Object.assign(data, this.options.logging);
|
|
198
263
|
|
|
199
264
|
return data;
|
|
200
265
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "hmpo-model",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.2",
|
|
4
4
|
"description": "Simple model for interacting with http/rest apis.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -25,22 +25,20 @@
|
|
|
25
25
|
},
|
|
26
26
|
"homepage": "https://github.com/UKHomeOffice/passports-model",
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"
|
|
28
|
+
"debug": "^4.3.3",
|
|
29
|
+
"got": "^11.8.3",
|
|
30
|
+
"hpagent": "^0.1.2",
|
|
31
|
+
"lodash.kebabcase": "^4.1.1"
|
|
31
32
|
},
|
|
32
33
|
"devDependencies": {
|
|
33
34
|
"chai": "^4.3.4",
|
|
34
|
-
"eslint": "^
|
|
35
|
+
"eslint": "^8.3.0",
|
|
35
36
|
"hmpo-logger": "^4.1.3",
|
|
36
|
-
"mocha": "^
|
|
37
|
+
"mocha": "^9.1.3",
|
|
37
38
|
"nyc": "^15.1.0",
|
|
38
39
|
"proxyquire": "^2.0.0",
|
|
39
|
-
"sinon": "^
|
|
40
|
-
"sinon-chai": "^3.
|
|
41
|
-
},
|
|
42
|
-
"peerDependencies": {
|
|
43
|
-
"hmpo-logger": ">= 4"
|
|
40
|
+
"sinon": "^12.0.1",
|
|
41
|
+
"sinon-chai": "^3.7.0"
|
|
44
42
|
},
|
|
45
43
|
"nyc": {
|
|
46
44
|
"all": true,
|
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const Model = require('../../lib/local-model');
|
|
4
4
|
|
|
5
|
+
describe('Local Model', () => {
|
|
5
6
|
let model;
|
|
6
7
|
|
|
7
8
|
beforeEach(() => {
|
|
8
|
-
|
|
9
|
-
let Model = require('../../lib/local-model');
|
|
10
|
-
|
|
11
9
|
model = new Model();
|
|
12
10
|
});
|
|
13
11
|
|
|
@@ -18,7 +16,7 @@ describe('Local Model', () => {
|
|
|
18
16
|
});
|
|
19
17
|
|
|
20
18
|
it('has an attributes property of type object', () => {
|
|
21
|
-
model.attributes.
|
|
19
|
+
expect(model.attributes).to.be.an('object');
|
|
22
20
|
});
|
|
23
21
|
|
|
24
22
|
describe('constructor', () => {
|
|
@@ -54,20 +52,28 @@ describe('Local Model', () => {
|
|
|
54
52
|
describe('set', () => {
|
|
55
53
|
|
|
56
54
|
beforeEach(() => {
|
|
57
|
-
model
|
|
58
|
-
name: 'Test name'
|
|
59
|
-
};
|
|
55
|
+
model = new Model({ name: 'Test name' });
|
|
60
56
|
});
|
|
61
57
|
|
|
62
58
|
it('adds a key to the model attributes if the key is a string', () => {
|
|
63
|
-
model.set('age', 20)
|
|
59
|
+
model.set('age', 20);
|
|
60
|
+
expect(model.attributes).to.eql({
|
|
64
61
|
name: 'Test name',
|
|
65
62
|
age: 20
|
|
66
63
|
});
|
|
67
64
|
});
|
|
68
65
|
|
|
69
66
|
it('accepts an object as the key', () => {
|
|
70
|
-
model.set( { placeOfBirth: 'London' } )
|
|
67
|
+
model.set( { placeOfBirth: 'London' } );
|
|
68
|
+
expect(model.attributes).to.eql({
|
|
69
|
+
name: 'Test name',
|
|
70
|
+
placeOfBirth: 'London'
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('accepts a Map as the key', () => {
|
|
75
|
+
model.set( new Map([['placeOfBirth', 'London']]));
|
|
76
|
+
expect(model.attributes).to.eql({
|
|
71
77
|
name: 'Test name',
|
|
72
78
|
placeOfBirth: 'London'
|
|
73
79
|
});
|
|
@@ -141,17 +147,17 @@ describe('Local Model', () => {
|
|
|
141
147
|
|
|
142
148
|
it('removes properties from model when passed a string', () => {
|
|
143
149
|
model.unset('a');
|
|
144
|
-
model.toJSON().
|
|
150
|
+
expect(model.toJSON()).to.eql({ b: 2, c: 3 });
|
|
145
151
|
});
|
|
146
152
|
|
|
147
153
|
it('removes properties from model when passed an array', () => {
|
|
148
154
|
model.unset(['a', 'b']);
|
|
149
|
-
model.toJSON().
|
|
155
|
+
expect(model.toJSON()).to.eql({ c: 3 });
|
|
150
156
|
});
|
|
151
157
|
|
|
152
158
|
it('does nothing if passed a property that does not exist', () => {
|
|
153
159
|
model.unset('foo');
|
|
154
|
-
model.toJSON().
|
|
160
|
+
expect(model.toJSON()).to.eql({ a: 1, b: 2, c: 3 });
|
|
155
161
|
});
|
|
156
162
|
|
|
157
163
|
it('emits a change event', () => {
|
|
@@ -225,7 +231,7 @@ describe('Local Model', () => {
|
|
|
225
231
|
|
|
226
232
|
it('clears model attributes', () => {
|
|
227
233
|
model.reset();
|
|
228
|
-
model.toJSON().
|
|
234
|
+
expect(model.toJSON()).to.eql({});
|
|
229
235
|
expect(model.get('name')).to.be.undefined;
|
|
230
236
|
expect(model.get('age')).to.be.undefined;
|
|
231
237
|
});
|
|
@@ -244,9 +250,9 @@ describe('Local Model', () => {
|
|
|
244
250
|
model.on('change:age', listener2);
|
|
245
251
|
model.reset();
|
|
246
252
|
listener1.should.have.been.calledOnce;
|
|
247
|
-
listener1.should.have.been.calledWithExactly(undefined);
|
|
253
|
+
listener1.should.have.been.calledWithExactly(undefined, 'John');
|
|
248
254
|
listener2.should.have.been.calledOnce;
|
|
249
|
-
listener2.should.have.been.calledWithExactly(undefined);
|
|
255
|
+
listener2.should.have.been.calledWithExactly(undefined, 30);
|
|
250
256
|
});
|
|
251
257
|
|
|
252
258
|
it('emits no events if called with silent: true', () => {
|
|
@@ -266,10 +272,22 @@ describe('Local Model', () => {
|
|
|
266
272
|
};
|
|
267
273
|
});
|
|
268
274
|
|
|
269
|
-
it('returns
|
|
270
|
-
model.toJSON()
|
|
275
|
+
it('returns a bare object that\'s the same as the attributes property', () => {
|
|
276
|
+
const result = model.toJSON(true);
|
|
277
|
+
expect(result).to.eql({
|
|
271
278
|
name: 'Test name'
|
|
272
279
|
});
|
|
280
|
+
|
|
281
|
+
expect(result.constructor).to.be.undefined;
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it('returns an object that\'s the same as the attributes property with object prototype', () => {
|
|
285
|
+
const result = model.toJSON();
|
|
286
|
+
expect(result).to.eql({
|
|
287
|
+
name: 'Test name'
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
expect(result.constructor).to.be.a('function');
|
|
273
291
|
});
|
|
274
292
|
});
|
|
275
293
|
|