hmpo-model 4.0.0 → 4.0.3

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.
@@ -0,0 +1,27 @@
1
+ name: Node.js CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ build:
11
+
12
+ runs-on: ubuntu-latest
13
+
14
+ strategy:
15
+ matrix:
16
+ node-version: [12.x, 14.x, 15.x]
17
+
18
+ steps:
19
+ - uses: actions/checkout@v2
20
+ - name: Use Node.js ${{ matrix.node-version }}
21
+ uses: actions/setup-node@v2
22
+ with:
23
+ node-version: ${{ matrix.node-version }}
24
+ - name: Install dependencies
25
+ run: npm ci
26
+ - name: Run tests
27
+ run: npm test
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const _ = require('underscore');
4
3
  const EventEmitter = require('events').EventEmitter;
5
4
 
6
5
  class LocalModel extends EventEmitter {
@@ -9,8 +8,8 @@ class LocalModel extends EventEmitter {
9
8
  super();
10
9
 
11
10
  this.options = options || {};
12
- this.attributes = {};
13
- this.set(attributes, {silent: true});
11
+ this.attributes = Object.create(null);
12
+ if (attributes) this.set(attributes, {silent: true});
14
13
  }
15
14
 
16
15
  get(key) {
@@ -19,32 +18,35 @@ class LocalModel extends EventEmitter {
19
18
 
20
19
  set(key, value, options) {
21
20
 
22
- let attrs = {};
23
-
24
- if (typeof key === 'string') {
21
+ let attrs = Object.create(null);
22
+ if (key instanceof Map) {
23
+ Object.assign(attrs, Object.fromEntries(key));
24
+ options = value;
25
+ } else if (typeof key === 'string') {
25
26
  attrs[key] = value;
26
27
  } else {
27
- attrs = key;
28
+ Object.assign(attrs, key);
28
29
  options = value;
29
30
  }
30
- options = options || {};
31
31
 
32
- let old = this.toJSON(),
33
- changed = {};
32
+ // silent set
33
+ if (options && options.silent) {
34
+ Object.assign(this.attributes, attrs);
35
+ return this;
36
+ }
34
37
 
35
- _.each(attrs, (value, key) => {
36
- if (value !== old[key]) {
37
- changed[key] = value;
38
- }
39
- });
38
+ const changes = [];
39
+ for (let key in attrs) {
40
+ const value = attrs[key];
41
+ const old = this.attributes[key];
42
+ if (value !== old) changes.push([key, value, old]);
43
+ }
40
44
 
41
- _.extend(this.attributes, attrs);
45
+ Object.assign(this.attributes, attrs);
42
46
 
43
- if (!options.silent && !_.isEmpty(changed)) {
44
- _.each(changed, (value, key) => {
45
- this.emit('change:' + key, this.get(key), old[key]);
46
- });
47
- this.emit('change', changed);
47
+ if (changes.length) {
48
+ changes.forEach(([key, value, old]) => this.emit('change:' + key, value, old));
49
+ if (this.listenerCount('change')) this.emit('change', Object.fromEntries(changes));
48
50
  }
49
51
 
50
52
  return this;
@@ -52,38 +54,32 @@ class LocalModel extends EventEmitter {
52
54
 
53
55
 
54
56
  unset(fields, options) {
55
- options = options || {};
56
57
  if (typeof fields === 'string') {
57
58
  fields = [fields];
58
59
  }
59
- let old = this.toJSON(),
60
- changed = {};
61
60
 
62
- _.each(fields, (key) => {
63
- if (old[key] !== undefined) {
64
- changed[key] = undefined;
61
+ const changes = [];
62
+ for (let key of fields) {
63
+ const old = this.attributes[key];
64
+ if (old !== undefined) {
65
+ changes.push([key, undefined, old]);
65
66
  delete this.attributes[key];
66
67
  }
67
- });
68
+ }
68
69
 
69
- if (!options.silent && !_.isEmpty(changed)) {
70
- _.each(changed, (value, key) => {
71
- this.emit('change:' + key, undefined, old[key]);
72
- });
73
- this.emit('change', changed);
70
+ if ((!options || !options.silent) && changes.length) {
71
+ changes.forEach(([key, value, old]) => this.emit('change:' + key, value, old));
72
+ if (this.listenerCount('change')) this.emit('change', Object.fromEntries(changes));
74
73
  }
75
74
 
76
75
  return this;
77
76
  }
78
77
 
79
78
  reset(options) {
80
- options = options || {};
81
- let keys = Object.keys(this.attributes);
82
- this.attributes = {};
83
- if (!options.silent) {
84
- _.each(keys, (key) => {
85
- this.emit('change:' + key, undefined);
86
- });
79
+ const old = this.attributes;
80
+ this.attributes = Object.create(null);
81
+ if (!options || !options.silent) {
82
+ Object.keys(old).forEach(key => this.emit('change:' + key, undefined, old[key]));
87
83
  this.emit('reset');
88
84
  }
89
85
  }
@@ -97,8 +93,8 @@ class LocalModel extends EventEmitter {
97
93
  this.set(property, val + amount);
98
94
  }
99
95
 
100
- toJSON() {
101
- return Object.assign({}, this.attributes);
96
+ toJSON(bare = false) {
97
+ return Object.assign(bare ? Object.create(null) : {}, this.attributes);
102
98
  }
103
99
  }
104
100
 
@@ -0,0 +1,51 @@
1
+
2
+ class ModelError extends Error {
3
+ constructor(e) {
4
+ super(e);
5
+ Object.defineProperties(this, {
6
+ original: {
7
+ value: e,
8
+ enumerable: false,
9
+ writable: false,
10
+ configurable: true,
11
+ },
12
+ name: {
13
+ value: e.name,
14
+ enumerable: false,
15
+ writable: true,
16
+ configurable: true,
17
+ },
18
+ code: {
19
+ value: e.code,
20
+ enumerable: false,
21
+ writable: true,
22
+ configurable: true,
23
+ },
24
+ errno: {
25
+ value: e.errno,
26
+ enumerable: false,
27
+ writable: true,
28
+ configurable: true,
29
+ },
30
+ info: {
31
+ get: () => this.original.info,
32
+ enumerable: false,
33
+ configurable: true
34
+ },
35
+ stack: {
36
+ get: () => this.original.stack,
37
+ enumerable: false,
38
+ configurable: true
39
+ },
40
+ });
41
+ this.code = e.code;
42
+
43
+ if (this.code === 'ETIMEDOUT') {
44
+ this.message = 'Connection timed out';
45
+ this.status = 504;
46
+ }
47
+ this.status = this.status || (e.response && e.response.statusCode) || 503;
48
+ }
49
+ }
50
+
51
+ module.exports = ModelError;
@@ -5,6 +5,7 @@ const LocalModel = require('./local-model');
5
5
  const got = require('got');
6
6
  const kebabCase = require('lodash.kebabcase');
7
7
  const { URL } = require('url');
8
+ const ModelError = require('./model-error');
8
9
 
9
10
  const DEFAULT_TIMEOUT = 60000;
10
11
 
@@ -174,20 +175,19 @@ class RemoteModel extends LocalModel {
174
175
  };
175
176
 
176
177
  this.got(settings)
177
- .catch(err => {
178
- debug('request got error', err);
179
- setResponseTime();
180
- if (err.code === 'ETIMEDOUT') {
181
- err.message = 'Connection timed out';
182
- err.status = 504;
183
- }
184
- err.status = err.status || (err.response && err.response.statusCode) || 503;
185
- return _callback(err, null, err.status, err.response);
186
- })
187
178
  .then(response => {
188
179
  debug('request got response', response);
189
180
  setResponseTime();
190
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);
187
+ }
188
+ err = new ModelError(err);
189
+ debug('request got error', err);
190
+ _callback(err, null, err.status);
191
191
  });
192
192
  }
193
193
 
@@ -199,7 +199,7 @@ class RemoteModel extends LocalModel {
199
199
  } catch (err) {
200
200
  err.status = response.statusCode;
201
201
  err.body = response.body;
202
- return callback(err, null, response.statusCode);
202
+ return callback(err, null, err.status);
203
203
  }
204
204
  this.parseResponse(response.statusCode, data, callback);
205
205
  }
@@ -223,7 +223,13 @@ class RemoteModel extends LocalModel {
223
223
 
224
224
  parse(data) {
225
225
  debug('parse', data);
226
- if (data && typeof data === 'object') this.set(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
+ }
227
233
  return data;
228
234
  }
229
235
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hmpo-model",
3
- "version": "4.0.0",
3
+ "version": "4.0.3",
4
4
  "description": "Simple model for interacting with http/rest apis.",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -28,17 +28,16 @@
28
28
  "debug": "^4.3.3",
29
29
  "got": "^11.8.3",
30
30
  "hpagent": "^0.1.2",
31
- "lodash.kebabcase": "^4.1.1",
32
- "underscore": "^1.13.1"
31
+ "lodash.kebabcase": "^4.1.1"
33
32
  },
34
33
  "devDependencies": {
35
- "chai": "^4.3.4",
36
- "eslint": "^8.3.0",
37
- "hmpo-logger": "^4.1.3",
38
- "mocha": "^9.1.3",
34
+ "chai": "^4.3.6",
35
+ "eslint": "^8.10.0",
36
+ "hmpo-logger": "^4.1.4",
37
+ "mocha": "^9.2.1",
39
38
  "nyc": "^15.1.0",
40
39
  "proxyquire": "^2.0.0",
41
- "sinon": "^12.0.1",
40
+ "sinon": "^13.0.1",
42
41
  "sinon-chai": "^3.7.0"
43
42
  },
44
43
  "nyc": {
@@ -1,13 +1,11 @@
1
1
  'use strict';
2
2
 
3
- describe('Local Model', () => {
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.should.be.a('object');
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.attributes = {
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).attributes.should.eql({
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' } ).attributes.should.eql({
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().should.eql({ b: 2, c: 3 });
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().should.eql({ c: 3 });
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().should.eql({ a: 1, b: 2, c: 3 });
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().should.eql({});
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 an object that\'s the same as the attributes property', () => {
270
- model.toJSON().should.eql({
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
 
@@ -1,8 +1,8 @@
1
1
  'use strict';
2
2
 
3
3
  const Model = require('../../lib/remote-model');
4
+ const ModelError = require('../../lib/model-error');
4
5
  const BaseModel = require('../../lib/local-model');
5
- const _ = require('underscore');
6
6
  const logger = require('hmpo-logger');
7
7
 
8
8
  const { HttpProxyAgent, HttpsProxyAgent } = require('hpagent');
@@ -543,7 +543,7 @@ describe('Remote Model', () => {
543
543
  }
544
544
  };
545
545
 
546
- let savedConfig = _.clone(config);
546
+ let savedConfig = Object.assign({}, config);
547
547
 
548
548
  let returnedConfig = model.requestConfig(config);
549
549
 
@@ -573,7 +573,7 @@ describe('Remote Model', () => {
573
573
  method: 'VERB'
574
574
  };
575
575
 
576
- requestSettings = _.clone(settings);
576
+ requestSettings = Object.assign({}, settings);
577
577
  });
578
578
 
579
579
  afterEach(() => {
@@ -650,9 +650,16 @@ describe('Remote Model', () => {
650
650
  settings: settings,
651
651
  statusCode: 418,
652
652
  responseTime: sinon.match.number,
653
- err: error,
653
+ err: sinon.match.instanceOf(ModelError),
654
654
  data: null
655
655
  });
656
+ model.logError.args[0][0].err.should.include({
657
+ name: 'Error',
658
+ message: 'Error: Lorem Ipsum',
659
+ status: 418,
660
+ info: undefined,
661
+ });
662
+ model.logError.args[0][0].err.stack.should.be.a('string');
656
663
  });
657
664
 
658
665
  it('should emit a fail event', () => {
@@ -660,15 +667,17 @@ describe('Remote Model', () => {
660
667
 
661
668
  model.emit.should.have.been.calledWithExactly(
662
669
  'fail',
663
- sinon.match({
664
- message: error.message,
665
- status: 418
666
- }),
670
+ sinon.match.instanceOf(ModelError),
667
671
  null,
668
672
  settings,
669
673
  418,
670
674
  sinon.match.number
671
675
  );
676
+ model.emit.args[1][1].should.include({
677
+ name: 'Error',
678
+ message: 'Error: Lorem Ipsum',
679
+ status: 418,
680
+ });
672
681
  });
673
682
 
674
683
  it('should translate timeout errors with status codes', () => {
@@ -680,11 +689,14 @@ describe('Remote Model', () => {
680
689
  settings: settings,
681
690
  statusCode: 504,
682
691
  responseTime: sinon.match.number,
683
- err: sinon.match({
684
- message: 'Connection timed out'
685
- }),
692
+ err: sinon.match.instanceOf(ModelError),
686
693
  data: null
687
694
  });
695
+ model.logError.args[0][0].err.should.include({
696
+ name: 'Error',
697
+ message: 'Connection timed out',
698
+ status: 504
699
+ });
688
700
  });
689
701
 
690
702
  it('should translate errors without status codes', () => {
@@ -696,12 +708,14 @@ describe('Remote Model', () => {
696
708
  settings: settings,
697
709
  statusCode: 503,
698
710
  responseTime: sinon.match.number,
699
- err: sinon.match({
700
- message: error.message,
701
- status: 503,
702
- }),
711
+ err: sinon.match.instanceOf(ModelError),
703
712
  data: null
704
713
  });
714
+ model.logError.args[0][0].err.should.include({
715
+ name: 'Error',
716
+ message: 'Error: Lorem Ipsum',
717
+ status: 503
718
+ });
705
719
  });
706
720
 
707
721
  it('should fire fail hook', () => {
@@ -714,11 +728,44 @@ describe('Remote Model', () => {
714
728
  settings: settings,
715
729
  statusCode: 418,
716
730
  responseTime: sinon.match.number,
717
- err: error,
731
+ err: sinon.match.instanceOf(ModelError),
718
732
  data: null
719
733
  });
720
734
  hook.should.have.been.calledOn(model);
721
735
  });
736
+
737
+ it('should handle response for ERR_NON_2XX_3XX_RESPONSE errors', () => {
738
+ error.code = 'ERR_NON_2XX_3XX_RESPONSE';
739
+ error.response = {
740
+ 'body': JSON.stringify({'data': 'value'}),
741
+ 'statusCode': 404
742
+ };
743
+
744
+ model.request(settings, cb);
745
+ console.log(model.logError.args[0][0]);
746
+ model.logError.should.have.been.calledWithExactly({
747
+ settings: settings,
748
+ statusCode: 404,
749
+ responseTime: sinon.match.number,
750
+ err: {
751
+ status: 404,
752
+ data: 'value'
753
+ },
754
+ data: { data: 'value' }
755
+ });
756
+
757
+ cb.should.have.been.calledWithExactly(
758
+ {
759
+ status: 404,
760
+ data: 'value'
761
+ },
762
+ {
763
+ data: 'value'
764
+ },
765
+ sinon.match.number
766
+ );
767
+ });
768
+
722
769
  });
723
770
 
724
771
  context('on success', () => {
@@ -910,6 +957,11 @@ describe('Remote Model', () => {
910
957
  model.get('foo').should.equal('bar');
911
958
  });
912
959
 
960
+ it('sets the parsed array data to the model as "data"', () => {
961
+ model.parse([1, 2, 3, 4]);
962
+ model.get('data').should.eql([1, 2, 3, 4]);
963
+ });
964
+
913
965
  it('does not set if the data falsey', () => {
914
966
  model.set = sinon.stub();
915
967
  model.parse(null);
@@ -1,21 +0,0 @@
1
- # This config is equivalent to both the '.circleci/extended/orb-free.yml' and the base '.circleci/config.yml'
2
- version: 2.1
3
-
4
- # Orbs are reusable packages of CircleCI configuration that you may share across projects, enabling you to create encapsulated, parameterized commands, jobs, and executors that can be used across multiple projects.
5
- # See: https://circleci.com/docs/2.0/orb-intro/
6
- orbs:
7
- node: circleci/node@4.7
8
-
9
- # Invoke jobs via workflows
10
- # See: https://circleci.com/docs/2.0/configuration-reference/#workflows
11
- workflows:
12
- sample: # This is the name of the workflow, feel free to change it to better match your workflow.
13
- # Inside the workflow, you define the jobs you want to run.
14
- jobs:
15
- - node/test:
16
- # This is the node version to use for the `cimg/node` tag
17
- # Relevant tags can be found on the CircleCI Developer Hub
18
- # https://circleci.com/developer/images/image/cimg/node
19
- version: '16.10'
20
- # If you are using yarn, change the line below from "npm" to "yarn"
21
- pkg-manager: npm