Skip to content

Commit 7e64b78

Browse files
author
Lee Richmond
committed
Merge pull request #4 from wtandy/collection_proxy
CollectionProxy, Scope#stats
2 parents e195998 + 2217d6f commit 7e64b78

13 files changed

+327
-29
lines changed

gulpfile.js

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const mocha = require('gulp-mocha');
33
const webpack = require('webpack-stream');
44
const ts = require('gulp-typescript');
55
const del = require('del');
6+
const watch = require('gulp-watch');
67

78
var tsProject = ts.createProject('tsconfig.json');
89

@@ -18,6 +19,12 @@ gulp.task('test', ['clean:test'], () =>
1819
.pipe(mocha())
1920
);
2021

22+
gulp.task('watch', function () {
23+
return watch(['src/**/*', 'test/**/*'], function () {
24+
gulp.start('test')
25+
})
26+
});
27+
2128
gulp.task('test-browser', function () {
2229
gulp
2330
.src(['./index.d.ts.', './src/main.ts', './src/**/*.ts', './test/fixtures.ts', './test/test-helper.ts', './test/**/*-test.ts'], { base: '.' })

index.d.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,8 @@ declare module NodeJS {
1313

1414
interface japiDoc {
1515
data: any; // can't do Array | japiResource
16-
included: Array<japiResource>;
16+
included?: Array<japiResource>;
17+
meta?: any;
1718
}
1819

1920
interface japiResourceIdentifier {
@@ -27,3 +28,9 @@ interface japiResource extends japiResourceIdentifier {
2728
meta?: Object;
2829
links?: Object;
2930
}
31+
32+
interface IResultProxy<T> {
33+
data: any
34+
meta: Object
35+
raw: japiDoc
36+
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"gulp-clean": "^0.3.2",
3131
"gulp-mocha": "^3.0.1",
3232
"gulp-typescript": "^3.1.3",
33+
"gulp-watch": "4.3.11",
3334
"json-loader": "^0.5.4",
3435
"mocha": "^3.2.0",
3536
"sinon": "^1.17.6",

src/model.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import Scope from './scope';
44
import Config from './configuration';
55
import Attribute from './attribute';
66
import deserialize from './util/deserialize';
7+
import { CollectionProxy, RecordProxy } from './proxies';
78
import _extend from './util/extend';
89
import { camelize } from './util/string';
910

@@ -41,11 +42,11 @@ export default class Model {
4142
this.attributes = attributes;
4243
}
4344

44-
static all() : Promise<Array<Model>> {
45+
static all() : Promise<CollectionProxy<Model>> {
4546
return this.scope().all();
4647
}
4748

48-
static find(id : string | number) : Promise<Model> {
49+
static find(id : string | number) : Promise<RecordProxy<Model>> {
4950
return this.scope().find(id);
5051
}
5152

src/proxies.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import CollectionProxy from './proxies/collection-proxy'
2+
import RecordProxy from './proxies/record-proxy'
3+
4+
export {
5+
CollectionProxy,
6+
RecordProxy
7+
}

src/proxies/collection-proxy.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Model from '../model';
2+
3+
class CollectionProxy<T> implements IResultProxy<T> {
4+
private _raw_json : japiDoc;
5+
private _array : Array<T>;
6+
7+
constructor (raw_json : japiDoc = { data: [] }) {
8+
this.setRaw(raw_json);
9+
}
10+
11+
get raw () : japiDoc {
12+
return this._raw_json;
13+
}
14+
15+
get data () : Array<T> {
16+
return this._array;
17+
}
18+
19+
get meta () : Object {
20+
return this.raw.meta || {};
21+
}
22+
23+
private setRaw = (json_payload : japiDoc) => {
24+
this._raw_json = json_payload;
25+
26+
this._array = [];
27+
28+
this.raw.data.map((datum : japiResource) => {
29+
this._array.push(Model.fromJsonapi(datum, this.raw));
30+
});
31+
}
32+
}
33+
34+
export default CollectionProxy;

src/proxies/record-proxy.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import Model from '../model';
2+
3+
class RecordProxy<T> implements IResultProxy<T> {
4+
private _raw_json : japiDoc;
5+
private _model : T;
6+
7+
constructor (raw_json : japiDoc = { data: [] }) {
8+
this.setRaw(raw_json);
9+
}
10+
11+
get raw () : japiDoc {
12+
return this._raw_json;
13+
}
14+
15+
get data () : T {
16+
return this._model;
17+
}
18+
19+
get meta () : Object {
20+
return this.raw.meta || {};
21+
}
22+
23+
private setRaw = (json_payload : japiDoc) => {
24+
this._raw_json = json_payload;
25+
26+
if (this.raw.data) {
27+
this._model = Model.fromJsonapi(this.raw.data, this.raw);
28+
} else {
29+
this._model = null
30+
}
31+
}
32+
}
33+
34+
export default RecordProxy;

src/scope.ts

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import Model from './model';
22
import Config from './configuration';
33
import parameterize from './util/parameterize';
44
import IncludeDirective from './util/include-directive';
5+
import { CollectionProxy, RecordProxy } from './proxies';
56
import Request from './request';
67
import colorize from './util/colorize';
78

@@ -13,29 +14,30 @@ export default class Scope {
1314
_fields: Object = {};
1415
_extra_fields: Object = {};
1516
_include: Object = {};
17+
_stats: Object = {};
1618

1719
constructor(model : typeof Model) {
1820
this.model = model;
1921
}
2022

21-
all() : Promise<Array<Model>> {
23+
all() : Promise<CollectionProxy<Model>> {
2224
return this._fetch(this.model.url()).then((json : japiDoc) => {
23-
return json.data.map((datum : japiResource) => {
24-
return Model.fromJsonapi(datum, json);
25-
});
25+
let collection = new CollectionProxy(json);
26+
27+
return collection;
2628
});
2729
}
2830

29-
find(id : string | number) : Promise<Model> {
31+
find(id : string | number) : Promise<RecordProxy<Model>> {
3032
return this._fetch(this.model.url(id)).then((json : japiDoc) => {
31-
return Model.fromJsonapi(json.data, json);
33+
return new RecordProxy(json)
3234
});
3335
}
3436

3537
// TODO: paginate 1
3638
first() : Promise<Model> {
37-
return this.per(1).all().then((models : Array<Model>) => {
38-
return models[0];
39+
return this.per(1).all().then((models : CollectionProxy<Model>) => {
40+
return models.data[0];
3941
});
4042
}
4143

@@ -56,6 +58,13 @@ export default class Scope {
5658
return this;
5759
}
5860

61+
stats(clause: Object) : Scope {
62+
for (let key in clause) {
63+
this._stats[key] = clause[key];
64+
}
65+
return this;
66+
}
67+
5968
order(clause: Object | string) : Scope {
6069
if (typeof clause == "object") {
6170
for (let key in clause) {
@@ -95,6 +104,13 @@ export default class Scope {
95104
return this;
96105
}
97106

107+
// The `Model` class has a `scope()` method to return the scope for it.
108+
// This method makes it possible for methods to expect either a model or
109+
// a scope and reliably cast them to a scope for use via `scope()`
110+
scope() : Scope {
111+
return this;
112+
}
113+
98114
asQueryParams() : Object {
99115
let qp = {};
100116

@@ -103,6 +119,7 @@ export default class Scope {
103119
qp['sort'] = this._sortParam(this._sort);
104120
qp['fields'] = this._fields;
105121
qp['extra_fields'] = this._extra_fields;
122+
qp['stats'] = this._stats;
106123
qp['include'] = new IncludeDirective(this._include).toString();
107124

108125
return qp;

test/integration/finders-test.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ after(function () {
77
fetchMock.restore();
88
});
99

10+
let resultData = function(promise) {
11+
return promise.then(function(proxyObject) {
12+
return proxyObject.data
13+
})
14+
}
15+
1016
describe('Model finders', function() {
1117
describe('#find()', function() {
1218
before(function () {
@@ -22,13 +28,13 @@ describe('Model finders', function() {
2228
});
2329

2430
it('returns a promise that resolves the correct instance', function() {
25-
return expect(Person.find(1)).to.eventually
31+
return expect(resultData(Person.find(1))).to.eventually
2632
.be.instanceof(Person).and
2733
.have.property('id', '1');
2834
});
2935

3036
it('assigns attributes correctly', function() {
31-
return expect(Person.find(1)).to.eventually
37+
return expect(resultData(Person.find(1))).to.eventually
3238
.have.property('name', 'John')
3339
});
3440

@@ -44,7 +50,7 @@ describe('Model finders', function() {
4450
});
4551

4652
it('resolves to the correct class', function() {
47-
return expect(Person.find(1)).to.eventually
53+
return expect(resultData(Person.find(1))).to.eventually
4854
.be.instanceof(Author);
4955
});
5056
});
@@ -71,7 +77,8 @@ describe('Model finders', function() {
7177
});
7278

7379
describe('#all()', function() {
74-
before(function () {
80+
beforeEach(function () {
81+
fetchMock.restore();
7582
fetchMock.get('http://example.com/api/v1/people', {
7683
data: [
7784
{ id: '1', type: 'people' }
@@ -80,10 +87,33 @@ describe('Model finders', function() {
8087
});
8188

8289
it('returns a promise that resolves the correct instances', function() {
83-
return expect(Person.all()).to.eventually
90+
return expect(resultData(Person.all())).to.eventually
8491
.all.be.instanceof(Person)
8592
.all.have.property('id', '1')
8693
});
94+
95+
describe('response includes a meta payload', function() {
96+
beforeEach(function () {
97+
fetchMock.restore();
98+
fetchMock.get('http://example.com/api/v1/people', {
99+
data: [
100+
{ id: '1', type: 'people' }
101+
],
102+
meta: {
103+
stats: {
104+
total: {
105+
count: 45
106+
},
107+
}
108+
}
109+
});
110+
});
111+
112+
it('includes meta payload in the resulting collection', function() {
113+
return expect(Person.all()).to.eventually
114+
.have.deep.property('meta.stats.total.count', 45)
115+
});
116+
});
87117
});
88118

89119
describe('#page', function() {
@@ -96,25 +126,24 @@ describe('Model finders', function() {
96126
});
97127

98128
it('queries correctly', function() {
99-
return expect(Person.page(2).all()).to.eventually
129+
return expect(resultData(Person.page(2).all())).to.eventually
100130
.all.be.instanceof(Person)
101131
.all.have.property('id', '2')
102132
});
103133
});
104134

105135
describe('#per', function() {
106136
before(function () {
107-
fetchMock.get('http://example.com/api/v1/people?page[size]=10', {
137+
fetchMock.get('http://example.com/api/v1/people?page[size]=2', {
108138
data: [
109-
{ id: '2', type: 'people' }
139+
{ id: '1', type: 'people' }
110140
]
111141
});
112142
});
113143

114144
it('queries correctly', function() {
115-
return expect(Person.page(2).all()).to.eventually
145+
return expect(resultData(Person.per(2).all())).to.eventually
116146
.all.be.instanceof(Person)
117-
.all.have.property('id', '2')
118147
});
119148
});
120149

@@ -128,7 +157,7 @@ describe('Model finders', function() {
128157
});
129158

130159
it('queries correctly', function() {
131-
return expect(Person.order('foo').order({ bar: 'desc' }).all()).to.eventually
160+
return expect(resultData(Person.order('foo').order({ bar: 'desc' }).all())).to.eventually
132161
.all.be.instanceof(Person)
133162
.all.have.property('id', '2')
134163
});
@@ -144,7 +173,7 @@ describe('Model finders', function() {
144173
});
145174

146175
it('queries correctly', function() {
147-
return expect(Person.where({ id: 2 }).where({ a: 'b' }).all()).to.eventually
176+
return expect(resultData(Person.where({ id: 2 }).where({ a: 'b' }).all())).to.eventually
148177
.all.be.instanceof(Person)
149178
.all.have.property('id', '2')
150179
});
@@ -160,7 +189,7 @@ describe('Model finders', function() {
160189
});
161190

162191
it('queries correctly', function() {
163-
return expect(Person.select({ people: ['name', 'age'] }).all()).to.eventually
192+
return expect(resultData(Person.select({ people: ['name', 'age'] }).all())).to.eventually
164193
.all.be.instanceof(Person)
165194
.all.have.property('id', '2')
166195
});
@@ -176,7 +205,7 @@ describe('Model finders', function() {
176205
});
177206

178207
it('queries correctly', function() {
179-
return expect(Person.selectExtra({ people: ['net_worth', 'best_friend'] }).all()).to.eventually
208+
return expect(resultData(Person.selectExtra({ people: ['net_worth', 'best_friend'] }).all())).to.eventually
180209
.all.be.instanceof(Person)
181210
.all.have.property('id', '2')
182211
});
@@ -195,7 +224,7 @@ describe('Model finders', function() {
195224
});
196225

197226
it('queries correctly', function() {
198-
return expect(Person.includes({ a: ['b', { c: 'd' }] }).all()).to.eventually
227+
return expect(resultData(Person.includes({ a: ['b', { c: 'd' }] }).all())).to.eventually
199228
.all.be.instanceof(Person)
200229
.all.have.property('id', '2')
201230
});

0 commit comments

Comments
 (0)