| 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371 | 1x
1x
1x
206x
1x
1x
1x
206x
206x
1x
2x
1x
2x
1x
1x
1x
1x
1x
1x
1x
1x
1x
22x
8x
6x
6x
6x
6x
6x
4x
4x
4x
4x
4x
2x
2x
2x
2x
6x
1x
85x
223x
45x
45x
45x
45x
45x
45x
8x
8x
8x
5x
3x
8x
8x
8x
8x
45x
45x
45x
1x
16x
16x
16x
16x
1x
2x
2x
2x
2x
4x
2x
6x
6x
6x
2x
2x
4x
3x
3x
3x
2x
3x
1x
203x
1x
20x
20x
2x
18x
1x
| import {IModel, Model} from 'mobx-collection-store';
import IDictionary from './interfaces/IDictionary';
import IRequestOptions from './interfaces/IRequestOptions';
import * as JsonApi from './interfaces/JsonApi';
import {config, create, fetchLink, handleResponse, remove, update} from './NetworkUtils';
import {Response} from './Response';
import {Store} from './Store';
import {getValue, mapItems, objectForEach} from './utils';
interface IInternal {
relationships?: IDictionary<JsonApi.IRelationship>;
meta?: object;
links?: IDictionary<JsonApi.ILink>;
persisted?: boolean;
id: number|string;
type: string;
}
export class Record extends Model implements IModel {
/**
* Type property of the record class
*
* @static
*
* @memberOf Record
*/
public static typeAttribute = ['__internal', 'type'];
/**
* ID property of the record class
*
* @static
*
* @memberOf Record
*/
public static idAttribute = ['__internal', 'id'];
/**
* Should the autogenerated ID be sent to the server when creating a record
*
* @static
* @type {boolean}
* @memberOf Record
*/
public static useAutogeneratedIds: boolean = false;
/**
* Endpoint for API requests if there is no self link
*
* @static
* @type {string|() => string}
* @memberOf Record
*/
public static endpoint: string|(() => string);
public 'static': typeof Record;
/**
* Internal metadata
*
* @private
* @type {IInternal}
* @memberOf Record
*/
private __internal: IInternal;
/**
* Cache link fetch requests
*
* @private
* @type {IDictionary<Promise<Response>>}
* @memberOf Record
*/
private __relationshipLinkCache: IDictionary<IDictionary<Promise<Response>>> = {};
/**
* Cache link fetch requests
*
* @private
* @type {IDictionary<Promise<Response>>}
* @memberOf Record
*/
private __linkCache: IDictionary<Promise<Response>> = {};
/**
* Get record relationship links
*
* @returns {IDictionary<JsonApi.IRelationship>} Record relationship links
*
* @memberOf Record
*/
public getRelationshipLinks(): IDictionary<JsonApi.IRelationship> {
return this.__internal && this.__internal.relationships;
}
/**
* Fetch a relationship link
*
* @param {string} relationship Name of the relationship
* @param {string} name Name of the link
* @param {IRequestOptions} [options] Server options
* @param {boolean} [force=false] Ignore the existing cache
* @returns {Promise<Response>} Response promise
*
* @memberOf Record
*/
public fetchRelationshipLink(
relationship: string,
name: string,
options?: IRequestOptions,
Eforce: boolean = false,
): Promise<Response> {
this.__relationshipLinkCache[relationship] = this.__relationshipLinkCache[relationship] || {};
/* istanbul ignore else */
if (!(name in this.__relationshipLinkCache) || force) {
const link: JsonApi.ILink = (
'relationships' in this.__internal &&
relationship in this.__internal.relationships &&
name in this.__internal.relationships[relationship]
) ? this.__internal.relationships[relationship][name] : null;
const headers: IDictionary<string> = options && options.headers;
this.__relationshipLinkCache[relationship][name] = fetchLink(link, this.__collection as Store, headers, options);
}
return this.__relationshipLinkCache[relationship][name];
}
/**
* Get record metadata
*
* @returns {object} Record metadata
*
* @memberOf Record
*/
public getMeta(): object {
return this.__internal && this.__internal.meta;
}
/**
* Get record links
*
* @returns {IDictionary<JsonApi.ILink>} Record links
*
* @memberOf Record
*/
public getLinks(): IDictionary<JsonApi.ILink> {
return this.__internal && this.__internal.links;
}
/**
* Fetch a record link
*
* @param {string} name Name of the link
* @param {IRequestOptions} [options] Server options
* @param {boolean} [force=false] Ignore the existing cache
* @returns {Promise<Response>} Response promise
*
* @memberOf Record
*/
public fetchLink(name: string, options?: IRequestOptions, force: boolean = false): Promise<Response> {
Eif (!(name in this.__linkCache) || force) {
const link: JsonApi.ILink = ('links' in this.__internal && name in this.__internal.links) ?
this.__internal.links[name] : null;
this.__linkCache[name] = fetchLink(link, this.__collection as Store, options && options.headers, options);
}
let request: Promise<Response> = this.__linkCache[name];
if (this['__queue__']) {
request = this.__linkCache[name].then((response) => {
const related: Record = this['__related__'];
const prop: string = this['__prop__'];
const record: Record = response.data as Record;
if (record &&
record.getRecordType() !== this.getRecordType() &&
record.getRecordType() === related.getRecordType()
) {
/* istanbul ignore if */
if (prop) {
related[prop] = record;
return response;
}
related.__persisted = true;
return response.replaceData(related);
}
return response;
});
}
return request;
}
/**
* Get the persisted state
*
* @readonly
* @private
* @type {boolean}
* @memberOf Record
*/
private get __persisted(): boolean {
return (this.__internal && this.__internal.persisted) || false;
}
/**
* Set the persisted state
*
* @private
*
* @memberOf Record
*/
private set __persisted(state: boolean) {
this.__internal.persisted = state;
}
/**
* Serialize the record into JSON API format
*
* @returns {JsonApi.IRecord} JSON API formated record
*
* @memberOf Record
*/
public toJsonApi(): JsonApi.IRecord {
const attributes: IDictionary<any> = this.toJS();
const useAutogenerated: boolean = this.static['useAutogeneratedIds'];
const data: JsonApi.IRecord = {
attributes,
id: (this.__persisted || useAutogenerated) ? this.getRecordId() : undefined,
type: this.getRecordType() as string,
};
const refs: IDictionary<string> = this['__refs'];
objectForEach(refs, (key: string) => {
data.relationships = data.relationships || {};
const rel = mapItems(this[`${key}Id`], (id: number|string) => {
if (!id && id !== 0) {
return null;
}
return {id, type: refs[key]};
});
data.relationships[key] = {data: rel} as JsonApi.IRelationship;
delete data.attributes[key];
delete data.attributes[`${key}Id`];
delete data.attributes[`${key}Meta`];
});
delete data.attributes.__internal;
delete data.attributes.__type__;
return data;
}
/**
* Saves (creates or updates) the record to the server
*
* @param {IRequestOptions} [options] Server options
* @returns {Promise<Record>} Returns the record is successful or rejects with an error
*
* @memberOf Record
*/
public save(options?: IRequestOptions): Promise<Record> {
const store: Store = this.__collection as Store;
const data: JsonApi.IRecord = this.toJsonApi();
const requestMethod: Function = this.__persisted ? update : create;
return requestMethod(store, this.__getUrl(), {data}, options && options.headers)
.then(handleResponse(this));
}
public saveRelationship(relationship: string, options?: IRequestOptions): Promise<Record> {
const link: JsonApi.ILink = (
'relationships' in this.__internal &&
relationship in this.__internal.relationships &&
'self' in this.__internal.relationships[relationship]
) ? this.__internal.relationships[relationship]['self'] : null;
/* istanbul ignore if */
if (!link) {
throw new Error('The relationship doesn\'t have a defined link');
}
const store: Store = this.__collection as Store;
/* istanbul ignore next */
const href: string = typeof link === 'object' ? link.href : link;
const type: string = this['__refs'][relationship];
type ID = JsonApi.IIdentifier|Array<JsonApi.IIdentifier>;
const data: ID = mapItems(this[`${relationship}Id`], (id) => ({id, type})) as ID;
return update(store, href, {data}, options && options.headers)
.then(handleResponse(this, relationship));
}
/**
* Remove the records from the server and store
*
* @param {IRequestOptions} [options] Server options
* @returns {Promise<boolean>} Resolves true if successfull or rejects if there was an error
*
* @memberOf Record
*/
public remove(options?: IRequestOptions): Promise<boolean> {
const store: Store = this.__collection as Store;
if (!this.__persisted) {
this.__collection.remove(this.getRecordType(), this.getRecordId());
return Promise.resolve(true);
}
return remove(store, this.__getUrl(), options && options.headers)
.then((response: Response) => {
/* istanbul ignore if */
if (response.error) {
throw response.error;
}
this.__persisted = false;
if (this.__collection) {
this.__collection.remove(this.getRecordType(), this.getRecordId());
}
return true;
});
}
/**
* Set the persisted status of the record
*
* @param {boolean} state Is the record persisted on the server
*
* @memberOf Record
*/
public setPersisted(state: boolean): void {
this.__persisted = state;
}
/**
* Get the URL that should be used for the API calls
*
* @private
* @returns {string} API URL
*
* @memberOf Record
*/
private __getUrl(): string {
const links: IDictionary<JsonApi.ILink> = this.getLinks();
if (links && links.self) {
const self: JsonApi.ILink = links.self;
/* istanbul ignore next */
return typeof self === 'string' ? self : self.href;
}
/* istanbul ignore next */
const url = getValue<string>(this.static.endpoint) || this.getRecordType() || this.static.type;
return this.__persisted
? `${config.baseUrl}${url}/${this.getRecordId()}`
: `${config.baseUrl}${url}`;
}
}
|