forked from benjiec/curious-js
-
Notifications
You must be signed in to change notification settings - Fork 2
/
curious.js
1693 lines (1501 loc) · 54.4 KB
/
curious.js
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
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* curious.js - JavaScript consumer code for Curious APIs.
*
* Copyright (c) 2015 Ginkgo Bioworks, Inc.
* @license MIT
*/
/**
* Curious JavaScript client and query construction
*
* @module curious
*/
// QUERY TERMS
/**
* Abstract base class for query terms
*
* @private
* @abstract
* @class
* @alias module:curious~QueryTerm
*
* @param {string} term The internal term text to use
*/
function QueryTerm(term) {
/**
* The term contents
*
* @method term
* @readonly
* @public
*
* @return {string} The term contents
*/
this.term = function _term() { return term; };
return this;
}
/**
* Return the term contents as they belong in a query, wrapped with parens and
* other operators.
*
* @public
*
* @return {string} The term contents, formatted
*/
QueryTerm.prototype.toString = function toString() { return this.term(); };
/**
* Determine whether or not the terms sticks implicit joins into adjacent
* terms.
*
* @public
*
* @return {boolean}
* True if the term implicitly joins with its following term. True by
* default
*/
QueryTerm.prototype.leftJoin = function leftJoin() { return false; };
/**
* Determine whether or not the term is a conditional and does not affect
* returned results.
*
* @public
*
* @return {boolean}
* True if term is a conditional
*/
QueryTerm.prototype.conditional = function conditional() { return false; };
/**
* Make a term that follows the query chain
*
* @private
* @class
* @extends {module:curious~QueryTerm}
* @alias module:curious~QueryTermFollow
*
* @param {string} term The term contents
*/
function QueryTermFollow(term) {
QueryTerm.call(this, term);
return this;
}
QueryTermFollow.prototype = new QueryTerm();
/**
* Make a term that performs a filter.
*
* @private
* @class
* @extends {module:curious~QueryTerm}
* @alias module:curious~QueryTermHaving
*
* @param {string} term The term contents
*/
function QueryTermHaving(term) {
QueryTerm.call(this, term);
return this;
}
QueryTermHaving.prototype = new QueryTerm();
QueryTermHaving.prototype.conditional = function conditional() { return true; };
QueryTermHaving.prototype.toString = function toString() { return '+(' + this.term() + ')'; };
/**
* Make a term that performs a negative (exclusive) filter.
*
* @private
* @class
* @extends {module:curious~QueryTerm}
* @alias module:curious~QueryTermHaving
*
* @param {string} term The term contents
*/
function QueryTermNotHaving(term) {
QueryTerm.call(this, term);
return this;
}
QueryTermNotHaving.prototype = new QueryTerm();
QueryTermNotHaving.prototype.conditional = function conditional() { return true; };
QueryTermNotHaving.prototype.toString = function toString() { return '-(' + this.term() + ')'; };
/**
* Make a term that performs an outer join.
*
* @private
* @class
* @extends {module:curious~QueryTerm}
* @alias module:curious~QueryTermWith
*
* @param {string} term The term contents
*/
function QueryTermWith(term) {
QueryTerm.call(this, term);
return this;
}
QueryTermWith.prototype = new QueryTerm();
QueryTermWith.prototype.leftJoin = function leftJoin() { return true; };
QueryTermWith.prototype.toString = function toString() { return '?(' + this.term() + ')'; };
// QUERY OBJECT
/**
* <p>Make a Curious query from constituent parts, using a chain of method
* calls to a single object.</p>
*
* <p>CuriousQuery objects are an object-based representation of a Curious
* query string to make passing around parts of a query and assembling
* queries easier.</p>
*
* <p>The result of curious queries will be an object containing arrays of
* objects, as specified in the Curious query. This would be analogous to what
* a Django QuerySet might look like on the back end.</p>
*
* <p>If there is more than one kind of object returned by the query and the
* query specifies some kind of relationship between the data in the objects
* (for example, Reactions that have Datasets), the returned objects will have
* attributes that point to their related objects. The names of these
* relationships are provided as a user-specified parameter.</p>
*
* <p>You construct CuriousQuery objects with a repeated chain of function
* calls on a core object, CuriousQuery object, much like in jQuery, or
* <code>_.chain()</code>. Every stage of the chain specifies a new term in
* the query, and a relationship name as a string. The stages can also take
* an optional third parameter that will specify the class of the constructed
* objects (insead of just <code>CuriousObject</code>).</p>
*
* <p>The initial Curious term happens either by passing parameters directly
* to the construtor, or by calling <code>.start()</code>.</p>
*
* @class
* @alias module:curious.CuriousQuery
*
* @param {string=} initialTermString
* The string for the starting term
* @param {string=} initialRelationship
* The starting term's relationship
* @param {function(Object)=} initialObjectClass
* A custom object class constructor for the starting term
*
* @return {CuriousQuery} The newly constructed object
*
* @example
* // Explicitly set start, wrapWith classes
* var q = (new curious.CuriousQuery())
* .start('Experiment(id=302)', 'experiment')
* .follow('Experiment.reaction_set', 'reactions')
* .follow('Reaction.dataset_set', 'dataset').wrapWith(Dataset)
* .follow('Dataset.attachment_set');
*
* q.query() ==
* 'Experiment(id=302), Experiment.reaction_set, '
* + 'Reaction.dataset_set, Dataset.attachment_set'
*
* @example
* // Terser version of the same query above
* var q = new curious.CuriousQuery('Experiment(id=302)', 'experiment')
* .follow('Experiment.reaction_set', 'reactions')
* .follow('Reaction.dataset_set', 'dataset', Dataset)
* .follow('Dataset.attachment_set');
*/
function CuriousQuery(
initialTermString, initialRelationship, initialObjectClass
) {
this.terms = [];
this.relationships = [];
this.objectFactories = [];
this.params = null;
this.existingObjects = null; // array of object arrays
// then-style callback pairs to attach to the end of the promise when the query is performed
this.thens = [];
if (initialTermString && initialRelationship) {
this.start(initialTermString, initialRelationship, initialObjectClass);
}
return this;
}
/**
* Generate the constructed query string represented by this object.
*
* @return {string} The fully constructed query
*/
CuriousQuery.prototype.query = function query() {
var queryString = '';
var terms = [];
// Flatten all terms and arrays of terms into a single array
this.terms.forEach(function (term) {
terms = terms.concat(term);
});
terms.forEach(function (term, termIndex) {
// The first term just gets added directly: it's the starting model or
// object. The following terms either do or do not have an implicit inner
// join between them. If they do not have an implicit inner join,
// commas are inserted to ensure that the objects that correspond to
// those terms are returned
if (termIndex > 0) {
if (term.conditional()) {
queryString += ' ';
} else if (
!term.conditional()
&& !terms[termIndex - 1].leftJoin()
&& !term.leftJoin()
) {
queryString += ', ';
} else {
queryString += ' ';
}
}
queryString += term;
});
return queryString;
};
/**
* Convert this object to a string, returning the complete query string
*
* @return {string} The fully constructed query
*/
CuriousQuery.prototype.toString = function toString() { return this.query(); };
/**
* Convert this probject to its native value equivalent, returning the complete query string
*
* @return {string} The fully constructed query
*/
CuriousQuery.prototype.valueOf = function valueOf() { return this.query(); };
/**
* Convert this probject to a plain JavaScript object to allow it to be serialized.
*
* @return {string} The fully constructed query
*/
CuriousQuery.prototype.toJSON = function toJSON() {
return {
terms: this.terms,
relationships: this.relationships,
params: this.params,
// Object Factories can't be serialized directly: they're functions.
objectFactories: this.objectFactories.map(function (factory) {
return factory ? factory.toString() : null;
}),
// Existing objects are turned into plain objects if possible, or left alone otherwise.
existingObjects: this.existingObjects.map(function (objectArray) {
return objectArray.map(function (existingObject) {
return (
existingObject.toJSON
? existingObject.toJSON()
: existingObject
);
});
}),
};
};
/**
* Extend this query object with another query object: return a new query
* chain with the current query chain's terms followed
* by the other query chain's terms.
*
* @param {CuriousQuery} extensionQueryObject The query object being added
* @return {CuriousQuery} The combined query
*/
CuriousQuery.prototype.extend = function extend(extensionQueryObject) {
var queryObject = this;
extensionQueryObject.terms.forEach(function (term, termIndex) {
queryObject._addTerm(
term,
extensionQueryObject.relationships[termIndex],
extensionQueryObject.objectFactories[termIndex]
);
});
return queryObject;
};
/**
* Return a deep copy of the current query object.
*
* @return {CuriousQuery}
* A new CuriousQuery object constaining the same terms, relationships,
* constructors
*/
CuriousQuery.prototype.clone = function clone() {
var clonedObject;
clonedObject = new CuriousQuery();
clonedObject.extend(this);
// One-level-deep copies of params and existing objects
clonedObject.setParams(this.params);
clonedObject.setExistingObjects(this.existingObjects);
return clonedObject;
};
/**
* <p>Add another term to this query: generic method.</p>
*
* <p>Consumers should not use this method, as they do not have access to the
* {@link module:curious~QueryTerm} classes.</p>
*
* @private
*
* @param {!QueryTerm|Array<QuerryTerm>} termObject
* A {@link module:curious~QueryTerm} object to append to the term, or an
* array of them
* @param {!string} relationship
* The name of this term in inter-term relationships
* @param {?function(Object)=} customConstructor
* A custom constructor for the resulting objects, if this part of the
* query returns new objects
*
* @return {CuriousQuery} The query object, with the new term added
*/
CuriousQuery.prototype._addTerm = function _addTerm(
termObject, relationship, customConstructor
) {
// Ensure that objectFactories, relationships, and terms always have the
// same number of elements.
if (termObject && relationship) {
this.terms.push(termObject);
this.relationships.push(relationship);
} else {
throw new Error(
'Must specify a term and a relationship to append to: ('
+ this.query()
+ ')'
);
}
if (customConstructor) {
this.objectFactories.push(_makeObjectFactory(customConstructor));
} else {
this.objectFactories.push(null);
}
return this;
};
/**
* <p>Append more text to the end of the last term: generic method.</p>
*
* <p>Consumers should not use this method, as they do not have access to the
* {@link module:curious~QueryTerm} classes.</p>
*
* @private
*
* @param {!QueryTerm|Array<!QueryTerm>} termObject
* A {@link module:curious~QueryTerm} object (or an array of them), to
* append to the previous term
*
* @return {CuriousQuery}
* The query object, with the term object's string representation appended
* to the previous term
*/
CuriousQuery.prototype._appendToPreviousTerm = function _appendToPreviousTerm(termObject) {
var lastTerm;
if (this.terms.length) {
lastTerm = this.terms[this.terms.length - 1];
// If the last term has not already been turned into an array, prep it
// first
if (!(lastTerm instanceof Array)) {
lastTerm = [lastTerm];
}
lastTerm = lastTerm.concat(termObject);
// modify the actual terms of the object, since lastTerm is just a shallow
// reference copy
this.terms[this.terms.length - 1] = lastTerm;
} else {
throw new Error('Must add terms before appending "' + termObject + '" to them.');
}
return this;
};
/**
* Add a starting term to this query. Equivalent to passing parameters
* directly to the constructor.
*
* @param {!string} termString
* The contents of the starting term
* @param {!string} relationship
* The name of this term in inter-term relationships
* @param {?function(Object)=} customConstructor
* A custom constructor for the resulting objects, if this part of the
* query returns new objects
*
* @return {CuriousQuery} The query object, with the term appended
*/
CuriousQuery.prototype.start = function start(termString, relationship, customConstructor) {
return this._addTerm(new QueryTermFollow(termString), relationship, customConstructor);
};
/**
* Add an inner-join term to this query.
*
* @param {!string} termString
* The contents of the starting term
* @param {!string} relationship
* The name of this term in inter-term relationships
* @param {?function(Object)=} customConstructor
* A custom constructor function for the resulting objects
*
* @return {CuriousQuery} The query object, with the term appended
*/
CuriousQuery.prototype.follow = function follow(termString, relationship, customConstructor) {
return this._addTerm(new QueryTermFollow(termString), relationship, customConstructor);
};
/**
* Add a filter term to this query.
*
* @param {!string} termString
* The subquery to filter by
*
* @return {CuriousQuery} The query object, with the term appended
*/
CuriousQuery.prototype.having = function having(termString) {
return this._appendToPreviousTerm(new QueryTermHaving(termString));
};
/**
* Add an exclude filter term to this query.
*
* @param {!string} termString
* The subquery to filter by
*
* @return {CuriousQuery} The query object, with the term appended
*/
CuriousQuery.prototype.notHaving = function notHaving(termString) {
return this._appendToPreviousTerm(new QueryTermNotHaving(termString));
};
/**
* Add an outer-join term to this query.
*
* @param {!string} termString
* The contents of the starting term
* @param {!string} relationship
* The name of this term in inter-term relationships
* @param {?function(Object)=} customConstructor
* A custom constructor for the resulting objects, if this part of the
* query returns new objects
*
* @return {CuriousQuery} The query object, with the term appended
*/
CuriousQuery.prototype.with = function _with(termString, relationship, customConstructor) {
return this._addTerm(new QueryTermWith(termString), relationship, customConstructor);
};
/**
* Specify the object constructor to use for the preceding term in the query.
*
* @param {?function(Object)=} customConstructor
* A constructor to use when instantiating objects from the previous part of
* the query
*
* @return {CuriousQuery}
* The query object, with the new constructor data stored internally
*/
CuriousQuery.prototype.wrapWith = function wrapWith(customConstructor) {
return this.wrapDynamically(_makeObjectFactory(customConstructor));
};
/**
* Specify the object factory function to use for the preceding term in the
* query. Unlike wrapDynamically, which can work with traditional
* constructors that do not return a value by default, this will only work
* with factory functions that explicitly return an object.
*
* @param {function(Object)} factoryFunction
* A factory function that returns an object of the desired wrapping class
*
* @return {CuriousQuery}
* The query object, with the new constructor data stored internally
*/
CuriousQuery.prototype.wrapDynamically = function wrapDynamically(factoryFunction) {
if (this.objectFactories.length) {
this.objectFactories[this.objectFactories.length - 1] = factoryFunction;
} else {
throw new Error('Cannot specify custom object constructor before starting a query');
}
return this;
};
/**
* Set the parameters that this query will pass to its curious client when
* perform is called.
*
* @param {!Object} params
* An object of parameters to set--see
* {@link module:curious.CuriousClient#performQuery} for a full description of the
* parameters.
*
* @return {CuriousQuery}
* The query object with its curious client parameters updated
*/
CuriousQuery.prototype.setParams = function setParams(params) {
var queryObject = this;
if (params instanceof Object) {
if (!queryObject.params) {
queryObject.params = {};
}
Object.keys(params).forEach(function (key) {
queryObject.params[key] = params[key];
});
}
return queryObject;
};
/**
* Set the existing objects that this query will use to link the returned
* objects into.
*
* @param {!Array<!Object>} objs The existing objects to set
*
* @return {CuriousQuery} The query object with its existing object set
* updated
*/
CuriousQuery.prototype.setExistingObjects = function setExistingObjects(objs) {
var queryObject = this;
if (objs && objs.forEach) {
queryObject.existingObjects = [];
objs.forEach(function (existingObject, ix) {
queryObject.existingObjects[ix] = existingObject;
});
}
return queryObject;
};
/**
* Perform the query using a passed-in CuriousClient object.
*
* @param {!CuriousClient} curiousClient
* A CuriousClient object that will handle performing the actual query
*
* @return {Promise}
* A promise, as returned by {@link module:curious.CuriousClient#performQuery}
*
*/
CuriousQuery.prototype.perform = function perform(curiousClient) {
var promise;
var q = this.query();
promise = curiousClient.performQuery(
q, this.relationships, this.objectFactories, this.params, this.existingObjects
);
// Attach any thenable resolve/reject promise callback pairs to the promise that
// results from query execution
this.thens.forEach(function (thenPair) {
// thenPair looks like [resolved, rejected]
if (thenPair[0]) {
// Just like promise = promise.then(resolved, rejected);
promise = promise.then.apply(promise, thenPair);
} else if (thenPair.length > 1 && thenPair[1]) {
// If the first callback is null but the second one isn't, we're looking at a catch
// situation. We use the same data structure to store both situations, so that they're
// attached to the promise in the same order they were attached to the Query object
promise = promise.catch(thenPair[1]);
}
});
return promise;
};
/**
* Add a (pair of) callback(s) to be called when the promise to perform the query resolves.
*
* This can be useful for constructing a query object with known post-processing before
* actually executing it.
*
* @param {function} fulfilled
* A function to call when the promise is fulfilled (just like you would pass to
* Promise.prototype.then)
*
* @param {function=} rejected
* A function to call when the promise is rejected (just like you would pass as the
* second argument to Promise.prototype.then)
*
* @return {CuriousQuery}
* The query itself, to allow chaining <code>then</code>s, or any other methods
*/
CuriousQuery.prototype.then = function then(fulfilled, rejected) {
this.thens.push([fulfilled, rejected]);
return this;
};
/**
* Add a callback to be called if the promise to perform the query is rejected.
*
* This can be useful for constructing a query object with known error-handling before
* actually executing it.
*
* @param {function} rejected
* A function to call when the promise is rejected (just like you would pass to
* Promise.prototype.catch)
*
* @return {CuriousQuery}
* The query itself, to allow chaining <code>then</code>s <code>catch</code>es, or any other methods
*/
CuriousQuery.prototype.catch = function _catch(rejected) {
this.thens.push([null, rejected]);
return this;
};
/**
* Return a function that will always construct an object of the specified
* class, regardless of whether or not the passed-in constructor needs to
* be called with `new` or not.
*
* @private
*
* @param {function} customConstructor
* The constructor that will be called with the new keyword to construct
* the object
*
* @return {function} A factory function that will return a new object
* whenever called
*/
function _makeObjectFactory(customConstructor) {
var CustomConstructorClass = customConstructor;
return function CustomConstructorClassFactory() {
return new CustomConstructorClass();
};
}
// CURIOUS OBJECTS
/**
* Utilities for dealing with curious objects
* @namespace
* @alias module:curious.CuriousObjects
*/
var CuriousObjects = (function _curiousObjectsModule() {
/**
* Base (default) class for an object returned from a Curious query
*
* @class
* @static
* @alias module:curious.CuriousObjects.defaultType
*
* @param {Object} objectData
* A plain JavaScript object representing the query data, as parsed from
* the returned JSON
* @param {boolean=} camelCase
* If true, construct camel-cased versions the fields in objectData
*/
function CuriousObject(objectData, camelCase) {
var newObject = this;
// Special properties that aren't data-bearing, but are often convenient
newObject.__url = null;
newObject.__model = null;
// Copy over the object data to be properties of the new CuriousObject
if (objectData instanceof Object && !(objectData instanceof Array)) {
Object.keys(objectData).forEach(function (key) {
var newKey = key;
if (camelCase) {
newKey = CuriousObjects.makeCamelCase(key);
}
newObject[newKey] = objectData[key];
});
}
return newObject;
}
/**
* Serialize CuriousObject instances to JSON effectively if they are passed to JSON.stringify
*
* @return {Object} A plain JavaScript object containing the CuriousObject's data
*/
CuriousObject.prototype.toJSON = function toJSON() {
var curiousObject = this;
var serializableObject = {};
// Copy over the object data to be properties of the new CuriousObject
Object.keys(curiousObject).forEach(function (key) {
serializableObject[key] = curiousObject[key];
});
return serializableObject;
};
/**
* When parsing a JSON string into objects, instantiate any objects that look like
* CuriousObject instances as such, instead of plain JavaScript objects.
*
* @static
* @param {string} jsonString A string of JSON-encoded data
*
* @return {*} The instantiated JSON-encoded data, with CuriousObjects placed where
* appropriate
*/
CuriousObject.fromJSON = function fromJSON(jsonString) {
return JSON.parse(jsonString, function (key, value) {
var parsedValue = value;
// If a plain object has '__url' and '__model' fields, it's probably a CuriousObject
if (value && value.hasOwnProperty('__url') && value.hasOwnProperty('__model')) {
parsedValue = new CuriousObject(value);
}
return parsedValue;
});
};
/**
* When a Curious query is performed, the returned data comes in a set of 3
* arrays to save space: objects, fields, urls. Assemble that data into a
* single array of objects, each of which has the appropriate fields. This
* makes the data much more reasonable to work with.
*
* @private
* @memberof module:curious.CuriousObjects
*
* @param {Object<string, Array>} queryData
* A plain JavaScript object representing the query data, as parsed from
* the returned JSON--this format is not meant to be easy to use, but
* takes less space.
* @param {Array<string>} queryData.fields
* The fields every object has
* @param {Array<Array>} queryData.objects
* An array of arrays, where each array is the values of a single object's
* properties, in the order specified by <code>queryData.fields</code>
* @param {Array<string>} queryData.urls
* The url of every object, if they have one
* @param {string} model
* The name of the Django model these objects come from
* @param {?function(Object)} customConstructor
* A constructor to use instead of the default CuriousObject constructor
* @param {boolean=} camelCase
* If true, construct camel-cased versions of the JSON objects returned
* by the Curious server.
*
* @return {Array<CuriousObject|CustomConstructorClass>}
* An array of objects that contain the data described in queryData
*/
function _parseObjects(queryData, model, customConstructor, camelCase) {
var objects = [];
if (queryData.objects instanceof Array) {
queryData.objects.forEach(function (objectDataArray, objectIndex) {
var url = queryData.urls[objectIndex];
var objectData = {};
var obj; // the final constructed object
var CustomConstructorClass = customConstructor; // Make a properly-capped version
// Combine the data from the fields
queryData.fields.forEach(function (fieldName, fieldIndex) {
objectData[fieldName] = objectDataArray[fieldIndex];
});
if (customConstructor) {
obj = new CustomConstructorClass(objectData);
// We can't be sure that the custom constructor that was passed in
// got all the fields assigned, so we should do it ourselves just
// in case for any fields the constructor might have missed.
queryData.fields.forEach(function (fieldName) {
var newFieldName = fieldName;
if (camelCase) {
newFieldName = CuriousObjects.makeCamelCase(fieldName);
}
// NOTE: don't check for obj.hasOwnProperty - we actually want to
// override existing fields in obj
obj[newFieldName] = objectData[fieldName];
});
} else {
// The CuriousObject constructor does this automatically
obj = new CuriousObject(objectData, camelCase);
}
// Set the magic fields
obj.__url = url;
obj.__model = model;
objects.push(obj);
});
}
return objects;
}
/**
* Get objects associated with each subquery. For each subquery, build a
* hash of ID to object.
*
* If existing objects are specified, will build relationships using the
* existing objects.
*
* @memberof module:curious.CuriousObjects
*
* @param {Array<string>} relationships
* The names of the relationships objects will have to one another
* @param {Array<function(Object)>} customConstructors
* The custom constructors for curious object classes
* @param {Object} queryJSONResponse
* An object of fields holding the query response, as returned and parsed
* directly from JSON without any post-processing
* @param {string} queryJSONResponse.computed_on
* The query timestamp
* @param {string} queryJSONResponse.last_model
* The model name of the last set of objects returned
* @param {Array<Object>} queryJSONResponse.results
* An array of objects containing Django object ids and other
* meta-information about the query; one element per model
* @param {string} queryJSONResponse.results[].model
* The model name for this part of the query
* @param {number} queryJSONResponse.results[].join_index
* The index of the model this model joins to
* @param {Array<Array>} queryJSONResponse.results[].objects
* The IDs of the objects returned by the query
* @param {Array<Object>} queryJSONResponse.data
* An array of objects containing the other fields of the Django objects, more than just the
* IDs--see {@link module:curious.CuriousObjects~_parseObjects} for a description of this data
* in the queryData parameter.
* @param {Array<Object<number, Object>>} existingObjects
* The existing objects--each object in the array is a mapping of an id
* to its corresponding object.
* @param {boolean=} camelCase
* If true, construct camel-cased versions of the JSON objects returned
* by the Curious server.
*
* @return {{objects: Array<Object>, trees: Array<Object>}}
* The parsed objects--<code>trees</code> holds any hierarchical
* relationships, for recursive queries.
*/
function parse(
relationships, customConstructors, queryJSONResponse, existingObjects,
camelCase
) {
var combinedObjects = [];
var trees = [];
if (queryJSONResponse.data instanceof Array) {
queryJSONResponse.data.forEach(function (queryData, queryIndex) {
var queryObjects; // the objects parsed from this query
var objectsByID = {};
// Parse out the objects for this query, passing
queryObjects = _parseObjects(
queryData,
queryJSONResponse.results[queryIndex].model,
// Only pass in custom constructors if we need to
(customConstructors instanceof Array)
? customConstructors[queryIndex]
: null,
camelCase
);
queryObjects.forEach(function (object) {
var id = object.id;
if (
existingObjects instanceof Array
&& existingObjects[queryIndex]
&& existingObjects[queryIndex].hasOwnProperty(id)
) {
objectsByID[id] = existingObjects[queryIndex][id];
} else {
objectsByID[id] = object;
}
});
combinedObjects.push(objectsByID);
trees.push(null);
});
// For each subquery, add a relationship to the results of the next
// subquery and then a reverse relationship
queryJSONResponse.results.forEach(function (queryResult, queryIndex) {
// An array of pairs: [objectID, srcObjectID], where
// the srcObjectID points to the ID of the object that this
// object is joined from (the 'source' of the join)
var joinIDPairs = queryResult.objects;
// A model-level join-index: shows which models are joined to
// which other models.
// jscs:disable requireCamelCaseOrUpperCaseIdentifiers
var joinIndex = queryResult.join_index;
// jscs:enable
var forwardRelationshipName = relationships[queryIndex];
var reverseRelationshipName = relationships[joinIndex];
if (camelCase) {
forwardRelationshipName = CuriousObjects.makeCamelCase(forwardRelationshipName);
reverseRelationshipName = CuriousObjects.makeCamelCase(reverseRelationshipName);
}
var joinSourceObjects = combinedObjects[joinIndex];
var joinDestinationObjects = combinedObjects[queryIndex];
if (joinSourceObjects && joinDestinationObjects) {
// Initialize empty arrays for relationships
Object.keys(joinSourceObjects).forEach(function (id) {
joinSourceObjects[id][forwardRelationshipName] = [];
});
Object.keys(joinDestinationObjects).forEach(function (id) {
joinDestinationObjects[id][reverseRelationshipName] = [];
});
// Go through each of the join ID pairs, and make the equvalent
// reference links in the corresponding object
joinIDPairs.forEach(function (joinIDPair) {
var id = joinIDPair[0];
var srcID = joinIDPair[1]; // the ID of the parent
var obj;
var srcObj; // the corresponding objects