Skip to content

Commit bc2a927

Browse files
author
Lee Richmond
committed
initial pass at relationships
1 parent 9f5d22b commit bc2a927

File tree

11 files changed

+260
-66
lines changed

11 files changed

+260
-66
lines changed

gulpfile.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ gulp.task('clean:test', function () {
1212

1313
gulp.task('test', ['clean:test'], () =>
1414
gulp
15-
.src(['./index.d.ts.', './src/**/*.ts', './test/fixtures.ts', './test/test-helper.ts', './test/**/*-test.ts'], { base: '.' })
15+
.src(['./index.d.ts.', './src/main.ts', './src/**/*.ts', './test/fixtures.ts', './test/test-helper.ts', './test/**/*-test.ts'], { base: '.' })
1616
.pipe(tsProject())
1717
.pipe(gulp.dest('tmp/test'))
1818
.pipe(mocha())

index.d.ts

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -11,25 +11,9 @@ declare module NodeJS {
1111
}
1212
}
1313

14-
interface IModel {
15-
id: string;
16-
[propName: string]: any;
17-
}
18-
19-
interface anyObject {
20-
[propName: string]: any;
21-
}
22-
23-
interface modelsConfig {
24-
[key: string]: any;
25-
}
26-
27-
interface japiDocArray {
28-
data: Array<japiResource>;
29-
}
30-
3114
interface japiDoc {
32-
data: japiResource;
15+
data: any; // can't do Array | japiResource
16+
included: Array<japiResource>;
3317
}
3418

3519
interface japiResourceIdentifier {

src/associations.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import Attribute from './attribute';
2+
import Model from './model';
3+
4+
class Base extends Attribute {
5+
getter(context: Model) {
6+
return context.relationships[this.name];
7+
}
8+
9+
setter(context: Model, val: any) : void {
10+
context.relationships[this.name] = val;
11+
}
12+
}
13+
14+
class HasMany extends Base {
15+
getter(context: Model) {
16+
let gotten = super.getter(context);
17+
if (!gotten) {
18+
this.setter(context, []);
19+
return super.getter(context);
20+
} else {
21+
return gotten;
22+
}
23+
}
24+
}
25+
26+
class HasOne extends Base {
27+
}
28+
29+
class BelongsTo extends Base {
30+
}
31+
32+
const hasMany = function() : HasMany {
33+
return new HasMany();
34+
}
35+
36+
const hasOne = function() : HasOne {
37+
return new HasOne();
38+
}
39+
40+
const belongsTo = function() : BelongsTo {
41+
return new BelongsTo();
42+
}
43+
44+
export { hasMany, hasOne, belongsTo };

src/attribute.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,43 +5,52 @@ import Model from './model';
55

66
export default class Attribute {
77
name: string;
8-
private _value: any;
98

109
isAttr: boolean = true;
1110

1211
static applyAll(klass: typeof Model) : void {
1312
this._eachAttribute(klass, (attr) => {
1413
klass.attributeList.push(attr.name);
1514

16-
Object.defineProperty(klass.prototype, attr.name, {
17-
get() : any {
18-
return attr.getter(this);
19-
},
20-
21-
set(value) : void {
22-
attr.setter(this, value);
23-
}
24-
});
15+
Object.defineProperty(klass.prototype, attr.name, attr.getSet());
2516
});
2617
}
2718

2819
private static _eachAttribute(klass: typeof Model, callback: Function) : void {
2920
let instance = new klass();
3021
for (let propName in instance) {
3122
if (instance[propName] && instance[propName].hasOwnProperty('isAttr')) {
23+
3224
let attrInstance = instance[propName];
3325
attrInstance.name = propName;
3426
callback(attrInstance);
3527
}
3628
}
3729
}
3830

39-
setter(context: Model, val: any) : void {
40-
if (!val.hasOwnProperty('isAttr')) {
41-
context.attributes[this.name] = val;
31+
// This returns the getters/setters for use on the *model*
32+
getSet() {
33+
let attr = this;
34+
35+
return {
36+
get() : any {
37+
return attr.getter(this);
38+
},
39+
40+
set(value) : void {
41+
if (!value.hasOwnProperty('isAttr')) {
42+
attr.setter(this, value);
43+
}
44+
}
4245
}
4346
}
4447

48+
// The model calls this setter
49+
setter(context: Model, val: any) : void {
50+
context.attributes[this.name] = val;
51+
}
52+
53+
// The model calls this getter
4554
getter(context: Model) : any {
4655
return context.attributes[this.name];
4756
}

src/main.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ global.__extends = (this && this.__extends) || function (d, b) {
1111
import Config from './configuration';
1212
import Model from './model';
1313
import Attribute from './attribute';
14+
import { hasMany, hasOne, belongsTo } from './associations';
1415

1516
const attr = function() : any {
1617
return new Attribute();
1718
}
1819

19-
export { Config, Model, attr };
20+
export { Config, Model, attr, hasMany, hasOne, belongsTo };

src/model.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,15 @@ import Attribute from './attribute';
66
import deserialize from './util/deserialize';
77
import _extend from './util/extend';
88

9-
export default class Model implements IModel {
9+
export default class Model {
1010
static baseUrl = process.env.BROWSER? '': 'http://localhost:9999'
1111
static endpoint = 'define-in-subclass';
1212
static apiNamespace = '/';
1313
static jsonapiType = 'define-in-subclass';
1414

1515
id: string;
1616
attributes: Object = {};
17+
relationships: Object = {};
1718
__meta__: Object | void = null;
1819
parentClass: typeof Model;
1920
klass: typeof Model;
@@ -35,7 +36,7 @@ export default class Model implements IModel {
3536
return this._scope || new Scope(this);
3637
}
3738

38-
constructor(attributes?: anyObject) {
39+
constructor(attributes?: Object) {
3940
this._assignAttributes(attributes);
4041
}
4142

@@ -85,8 +86,8 @@ export default class Model implements IModel {
8586
return base;
8687
}
8788

88-
static fromJsonapi(resource: japiResource) : any {
89-
return deserialize(resource);
89+
static fromJsonapi(resource: japiResource, payload: japiDoc) : any {
90+
return deserialize(resource, payload);
9091
}
9192

9293
private _assignAttributes(attrs: Object) : void {

src/scope.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,16 +16,16 @@ export default class Scope {
1616
}
1717

1818
all() : Promise<Array<Model>> {
19-
return this._fetch(this.model.url()).then((json : japiDocArray) => {
19+
return this._fetch(this.model.url()).then((json : japiDoc) => {
2020
return json.data.map((datum : japiResource) => {
21-
return Model.fromJsonapi(datum);
21+
return Model.fromJsonapi(datum, json);
2222
});
2323
});
2424
}
2525

2626
find(id : string | number) : Promise<Model> {
2727
return this._fetch(this.model.url(id)).then((json : japiDoc) => {
28-
return Model.fromJsonapi(json.data);
28+
return Model.fromJsonapi(json.data, json);
2929
});
3030
}
3131

src/util/deserialize.ts

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,82 @@
33
import Config from '../configuration';
44
import Model from '../model';
55

6-
export default function deserialize(resource : japiResource) : Model {
7-
let klass = Config.modelForType(resource.type);
6+
export default function deserialize(resource : japiResource, payload: japiDoc) : Model {
7+
let deserializer = new Deserializer(payload);
8+
return deserializer.deserialize(resource);
9+
}
10+
11+
class Deserializer {
12+
_models = [];
13+
_resources = [];
14+
payload: japiDoc;
15+
16+
constructor(payload) {
17+
this.payload = payload;
18+
this.addResources(payload.data);
19+
this.addResources(payload.included);
20+
}
21+
22+
addResources(data) {
23+
if (Array.isArray(data)) {
24+
for (let datum of data) {
25+
this._resources.push(datum);
26+
}
27+
} else {
28+
this._resources.push(data);
29+
}
30+
}
31+
32+
deserialize(resource: japiResource, isRelation?: boolean) : Model {
33+
let record = this.findModel(resource);
34+
if (!record) {
35+
if (isRelation) {
36+
resource = this.findResource(resource);
37+
}
38+
record = this._deserialize(resource);
39+
}
40+
41+
return record;
42+
}
43+
44+
_deserialize(resource: japiResource) : Model {
45+
let klass = Config.modelForType(resource.type);
46+
let instance = new klass({ id: resource.id });
47+
this._models.push(instance);
48+
49+
for (let key in resource.attributes) {
50+
instance[key] = resource.attributes[key];
51+
}
52+
this._processRelationships(instance, resource.relationships);
53+
instance.__meta__ = resource.meta;
54+
return instance;
55+
}
856

9-
let instance = new klass({ id: resource.id });
10-
for (let key in resource.attributes) {
11-
instance[key] = resource.attributes[key];
57+
_processRelationships(instance, relationships) {
58+
for (let key in relationships) {
59+
let relationData = relationships[key].data;
60+
61+
if (Array.isArray(relationData)) {
62+
for (let datum of relationData) {
63+
let relatedRecord = this.deserialize(datum, true);
64+
instance[key].push(relatedRecord);
65+
}
66+
} else {
67+
let relatedRecord = this.deserialize(relationData, true);
68+
instance[key] = relatedRecord;
69+
}
70+
}
71+
}
72+
73+
findModel(resourceIdentifier) {
74+
return this._models.filter((m) => {
75+
return m.id == resourceIdentifier.id && m.klass.jsonapiType == resourceIdentifier.type;
76+
})[0];
1277
}
13-
instance.__meta__ = resource.meta;
1478

15-
return instance;
79+
findResource(resourceIdentifier) {
80+
return this._resources.filter((r) => {
81+
return r.id == resourceIdentifier.id && r.type == resourceIdentifier.type;
82+
})[0];
83+
}
1684
}

test/fixtures.ts

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Model, Config, attr } from '../src/main';
1+
import { Model, Config, attr, hasMany, belongsTo } from '../src/main';
22

33
// typescript class
44
class Person extends Model {
@@ -15,9 +15,26 @@ class Person extends Model {
1515
let Author = Person.extend({
1616
static: {
1717
jsonapiType: 'authors'
18-
}
18+
},
19+
20+
books: hasMany(),
21+
genre: belongsTo()
1922
});
2023

24+
class Book extends Model {
25+
static jsonapiType = 'books';
26+
27+
title: string = attr();
28+
}
29+
30+
class Genre extends Model {
31+
static jsonapiType = 'genres';
32+
33+
authors: any = hasMany()
34+
35+
name: string = attr();
36+
}
37+
2138
Config.bootstrap();
2239

23-
export { Author, Person };
40+
export { Author, Person, Book, Genre };

0 commit comments

Comments
 (0)