alchemymvc 1.1.7 → 1.2.0

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.
Files changed (39) hide show
  1. package/lib/app/behaviour/sluggable_behaviour.js +2 -4
  2. package/lib/app/conduit/http_conduit.js +173 -192
  3. package/lib/app/conduit/loopback_conduit.js +0 -0
  4. package/lib/app/conduit/socket_conduit.js +620 -620
  5. package/lib/app/datasource/mongo_datasource.js +75 -10
  6. package/lib/app/element/time_ago.js +0 -0
  7. package/lib/app/helper/socket_helper.js +613 -613
  8. package/lib/app/helper_datasource/00-nosql_datasource.js +73 -19
  9. package/lib/app/helper_datasource/remote_datasource.js +0 -0
  10. package/lib/app/helper_field/05-string_field.js +0 -0
  11. package/lib/app/helper_field/schema_field.js +7 -2
  12. package/lib/app/helper_model/criteria.js +76 -18
  13. package/lib/app/helper_model/field_config.js +24 -5
  14. package/lib/app/helper_model/model.js +65 -7
  15. package/lib/app/model/alchemy_migration_model.js +33 -0
  16. package/lib/bootstrap.js +9 -0
  17. package/lib/class/conduit.js +2474 -2412
  18. package/lib/class/datasource.js +8 -8
  19. package/lib/class/field.js +7 -1
  20. package/lib/class/inode.js +62 -0
  21. package/lib/class/inode_dir.js +115 -0
  22. package/lib/class/inode_file.js +112 -111
  23. package/lib/class/migration.js +138 -0
  24. package/lib/class/model.js +1765 -1772
  25. package/lib/class/reciprocal.js +8 -2
  26. package/lib/class/router.js +0 -0
  27. package/lib/class/schema.js +14 -12
  28. package/lib/core/base.js +37 -10
  29. package/lib/core/client_base.js +1 -9
  30. package/lib/core/middleware.js +4 -8
  31. package/lib/core/socket.js +159 -159
  32. package/lib/init/alchemy.js +1779 -1779
  33. package/lib/init/devwatch.js +1 -1
  34. package/lib/init/functions.js +2 -56
  35. package/lib/init/load_functions.js +8 -2
  36. package/lib/init/requirements.js +101 -96
  37. package/lib/stages.js +12 -0
  38. package/package.json +74 -76
  39. package/CHANGELOG.md +0 -445
@@ -1,1773 +1,1766 @@
1
- var nameCache = {},
2
- mongo = alchemy.use('mongodb'),
3
- all_prefixes = alchemy.shared('Routing.prefixes'),
4
- fs = alchemy.use('fs'),
5
- createdModel;
6
-
7
- /**
8
- * The Model class
9
- *
10
- * @constructor
11
- *
12
- * @author Jelle De Loecker <jelle@develry.be>
13
- * @since 0.0.1
14
- * @version 1.1.0
15
- */
16
- var Model = Function.inherits('Alchemy.Base', 'Alchemy.Model', function Model(options) {
17
- this.init(options);
18
- });
19
-
20
- /**
21
- * Set the modelName property after class creation
22
- *
23
- * @author Jelle De Loecker <jelle@develry.be>
24
- * @since 0.2.0
25
- * @version 1.1.0
26
- */
27
- Model.constitute(function setModelName() {
28
- this.model_name = this.name;
29
- this.setProperty('model_name', this.model_name);
30
-
31
- if (!this.table) {
32
- this.table = this.model_name.tableize();
33
- }
34
- });
35
-
36
- /**
37
- * This is a model constructor
38
- *
39
- * @type {Boolean}
40
- */
41
- Model.setStaticProperty('model', true);
42
-
43
- /**
44
- * The cache duration static getter/setter
45
- *
46
- * @author Jelle De Loecker <jelle@develry.be>
47
- * @since 0.1.0
48
- * @version 1.0.3
49
- *
50
- * @property cache_duration
51
- * @type {String}
52
- */
53
- Model.setStaticProperty(function cache_duration() {
54
-
55
- if (this._cache_duration == null) {
56
- this._cache_duration = alchemy.settings.model_query_cache_duration;
57
- }
58
-
59
- return this._cache_duration;
60
- }, function setCacheDuration(duration) {
61
- this._cache_duration = duration;
62
-
63
- // @todo: reset cache
64
- });
65
-
66
- /**
67
- * Get the cache object
68
- *
69
- * @author Jelle De Loecker <jelle@develry.be>
70
- * @since 0.1.0
71
- * @version 1.0.3
72
- *
73
- * @property cache
74
- * @type {Object}
75
- */
76
- Model.setStaticProperty(function cache() {
77
-
78
- if (this.cache_duration) {
79
-
80
- if (this._cache) {
81
- return this._cache;
82
- }
83
-
84
- this._cache = alchemy.getCache(this.name, this.cache_duration);
85
- return this._cache;
86
- }
87
-
88
- return false;
89
- }, function setCache(value) {
90
- return this._cache = value;
91
- });
92
-
93
-
94
- /**
95
- * Is this an abstract model?
96
- *
97
- * @author Jelle De Loecker <jelle@develry.be>
98
- * @since 1.1.0
99
- * @version 1.1.0
100
- *
101
- * @type {Boolean}
102
- */
103
- Model.setStaticProperty(function is_abstract() {
104
-
105
- // Do simple is_abstract_class check
106
- if (this.is_abstract_class != null) {
107
- return !!this.is_abstract_class;
108
- }
109
-
110
- // If we need to load an external schema, it's also not abstract
111
- if (this.prototype.load_external_schema) {
112
- return false;
113
- }
114
-
115
- // See if this model has other fields than the default ones
116
- let field_count = this.schema.array.length;
117
-
118
- if (this.schema.has('_id')) {
119
- field_count--;
120
- }
121
-
122
- if (this.schema.has('created')) {
123
- field_count--;
124
- }
125
-
126
- if (this.schema.has('updated')) {
127
- field_count--;
128
- }
129
-
130
- return field_count < 1;
131
- });
132
-
133
-
134
- /**
135
- * Get the document class constructor
136
- *
137
- * @type {Alchemy.Document}
138
- */
139
- Model.prepareStaticProperty('Document', function getDocumentClass() {
140
- return Classes.Alchemy.Document.Document.getDocumentClass(this);
141
- });
142
-
143
- /**
144
- * Get the client document class constructor
145
- *
146
- * @type {Hawkejs.Document}
147
- */
148
- Model.prepareStaticProperty('ClientDocument', function getClientDocumentClass() {
149
- return this.Document.getClientDocumentClass();
150
- });
151
-
152
- /**
153
- * Set the static per-model schema
154
- *
155
- * @version 1.1.0
156
- *
157
- * @type {Schema}
158
- */
159
- Model.staticCompose('schema', function createSchema(doNext) {
160
-
161
- var that = this,
162
- model = this.compositorParent,
163
- schema = new Classes.Alchemy.Schema();
164
-
165
- // The base Model does not have a schema
166
- if (model.name == 'Model') {
167
- return false;
168
- } else {
169
-
170
- // Link the schema to this model
171
- schema.setModel(model);
172
-
173
- // Set the schema name
174
- schema.setName(model.name);
175
-
176
- if (model.prototype.add_basic_fields !== false) {
177
-
178
- // Set default model fields immediately after this function ends
179
- // This has to be scheduled next, because addField would call createSchema
180
- // again, resulting in an infinite loop
181
- doNext(function addSchemaBasics() {
182
- model.addField('_id', 'ObjectId', {default: Field.createPathEvaluator('alchemy.ObjectId')});
183
- model.addField('created', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
184
- model.addField('updated', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
185
- });
186
- }
187
- }
188
-
189
- return schema;
190
- }, [
191
- 'addEnumValues',
192
- 'setEnumValues',
193
- 'belongsTo',
194
- 'hasOneParent',
195
- 'hasAndBelongsToMany',
196
- 'hasMany',
197
- 'hasOneChild',
198
- 'addIndex',
199
- 'addRule',
200
- ]);
201
-
202
- Model.setDeprecatedProperty('modelName', 'model_name');
203
- Model.setDeprecatedProperty('blueprint', 'schema');
204
-
205
- /**
206
- * The default database config to use
207
- *
208
- * @type {String}
209
- */
210
- Model.setProperty('dbConfig', 'default');
211
-
212
- /**
213
- * The default field to use as display
214
- *
215
- * @type {String}
216
- */
217
- Model.setProperty('displayField', 'title');
218
-
219
- /**
220
- * Translate is on by default
221
- *
222
- * @type {Boolean}
223
- */
224
- Model.setProperty('translate', true);
225
-
226
- /**
227
- * Set the name of the primary key field
228
- *
229
- * @author Jelle De Loecker <jelle@develry.be>
230
- * @since 0.1.0
231
- * @version 0.1.0
232
- */
233
- Model.setProperty('primary_key', '_id');
234
-
235
- /**
236
- * Should we load the schema from the database?
237
- *
238
- * @author Jelle De Loecker <jelle@develry.be>
239
- * @since 1.1.0
240
- * @version 1.1.0
241
- */
242
- Model.setProperty('load_external_schema', false);
243
-
244
- /**
245
- * Object where behaviours are stored
246
- *
247
- * @type {Object}
248
- */
249
- Model.prepareProperty('behaviours', Object);
250
-
251
- /**
252
- * Associations
253
- *
254
- * @type {Object}
255
- */
256
- Model.setProperty(function associations() {
257
- return this.schema.associations;
258
- });
259
-
260
- /**
261
- * Instance access to static cache
262
- *
263
- * @type {Expirable}
264
- */
265
- Model.prepareProperty('cache', function cache() {
266
- return this.constructor.cache;
267
- });
268
-
269
- /**
270
- * Instance access to static schema
271
- *
272
- * @type {Schema}
273
- */
274
- Model.setProperty(function schema() {
275
- return this.constructor.schema;
276
- });
277
-
278
- /**
279
- * Is this an abstract model?
280
- *
281
- * @type {Boolean}
282
- */
283
- Model.setProperty(function is_abstract() {
284
- return this.constructor.is_abstract;
285
- });
286
-
287
- /**
288
- * This is a wrapper class
289
- */
290
- Model.makeAbstractClass();
291
-
292
- /**
293
- * This wrapper class starts a new group
294
- */
295
- Model.startNewGroup();
296
-
297
- /**
298
- * The connection
299
- *
300
- * @type {Object}
301
- */
302
- Model.prepareProperty('datasource', function datasource() {
303
- if (this.table) return Datasource.get(this.dbConfig);
304
- });
305
-
306
- /**
307
- * The default sort options
308
- *
309
- * @type {Object}
310
- */
311
- Model.prepareProperty('sort', function sort() {
312
- return {created: 1};
313
- });
314
-
315
- /**
316
- * Check a url value
317
- *
318
- * @author Jelle De Loecker <jelle@develry.be>
319
- * @since 1.0.0
320
- * @version 1.1.0
321
- *
322
- * @param {String} value The value in the url
323
- * @param {String} name The name of the url parameter
324
- * @param {String} field_name The name of the field to check
325
- * @param {Conduit} conduit The optional conduit
326
- *
327
- * @return {Pledge}
328
- */
329
- Model.setStatic(function checkPathValue(value, name, field_name, conduit) {
330
-
331
- var instance,
332
- pledge,
333
- crit;
334
-
335
- if (!field_name) {
336
- if (name == 'id') {
337
- field_name = this.prototype.primary_key;
338
- } else {
339
- field_name = name;
340
- }
341
- }
342
-
343
- if (conduit) {
344
- instance = conduit.getModel(this);
345
- } else {
346
- instance = new this;
347
- }
348
-
349
- // Create new criteria instance
350
- crit = instance.find();
351
-
352
- // Look for the wanted field
353
- crit.where(field_name).equals(value);
354
-
355
- pledge = instance.find('first', crit);
356
-
357
- return pledge;
358
- });
359
-
360
- /**
361
- * Add a field to this model's schema
362
- *
363
- * @author Jelle De Loecker <jelle@develry.be>
364
- * @since 0.2.0
365
- * @version 1.0.7
366
- *
367
- * @return {Alchemy.Field}
368
- */
369
- Model.setStatic(function addField(name, type, options) {
370
-
371
- var field,
372
- is_new;
373
-
374
- is_new = !this.schema.has(name);
375
-
376
- // Add it to the schema
377
- field = this.schema.addField(name, type, options);
378
-
379
- if (is_new) {
380
- // Add it to the Document class
381
- this.Document.setFieldGetter(name);
382
-
383
- // Add the field to the client document too, if it's not private
384
- if (!field.is_private) {
385
- // False means it should not be set on the server implementation
386
- // (because that's where it's coming from)
387
- this.ClientDocument.setFieldGetter(name, null, null, false);
388
- }
389
- }
390
-
391
- return field;
392
- });
393
-
394
-
395
- /**
396
- * Add a behaviour to this model
397
- *
398
- * @author Jelle De Loecker <jelle@develry.be>
399
- * @since 0.2.0
400
- * @version 0.2.0
401
- */
402
- Model.setStatic(function addBehaviour(behaviour_name, options) {
403
- return this.schema.addBehaviour(behaviour_name, options);
404
- });
405
-
406
- /**
407
- * Add an association to this model's schema
408
- * and set it on the Document as a getter
409
- *
410
- * @author Jelle De Loecker <jelle@develry.be>
411
- * @since 0.2.0
412
- * @version 0.2.0
413
- */
414
- Model.setStatic(function addAssociation(type, alias, model_name, options) {
415
- var data = this.schema.addAssociation(type, alias, model_name, options);
416
- this.Document.setAliasGetter(data.alias);
417
-
418
- // Add the alias to the client document too, if it's not private
419
- if (!options || !options.is_private) {
420
- // False means it should not be set on the server implementation
421
- // (because that's where it's coming from)
422
- this.ClientDocument.setAliasGetter(name);
423
- }
424
- });
425
-
426
- /**
427
- * Set a method on the document class
428
- *
429
- * @author Jelle De Loecker <jelle@develry.be>
430
- * @since 0.2.0
431
- * @version 1.0.6
432
- */
433
- Model.setStatic(function setDocumentMethod(name, fnc) {
434
-
435
- var that = this;
436
-
437
- if (typeof name == 'function') {
438
- fnc = name;
439
- name = fnc.name;
440
- }
441
-
442
- Blast.loaded(function whenLoaded() {
443
- that.Document.setMethod(name, fnc);
444
- });
445
- });
446
-
447
- /**
448
- * Set a property on the document class
449
- *
450
- * @author Jelle De Loecker <jelle@develry.be>
451
- * @since 0.2.0
452
- * @version 1.0.6
453
- */
454
- Model.setStatic(function setDocumentProperty(name, fnc) {
455
-
456
- var that = this,
457
- args = arguments;
458
-
459
- Blast.loaded(function whenLoaded() {
460
- that.Document.setProperty.apply(that.Document, args);
461
- });
462
- });
463
-
464
- /**
465
- * Get a field
466
- *
467
- * @author Jelle De Loecker <jelle@develry.be>
468
- * @since 0.2.0
469
- * @version 0.2.0
470
- *
471
- * @return {FieldType}
472
- */
473
- Model.setStatic(function getField(name) {
474
-
475
- var fieldPath,
476
- alias,
477
- model,
478
- split;
479
-
480
- if (name.indexOf('.') > -1) {
481
- split = name.split('.');
482
-
483
- alias = name[0];
484
-
485
- if (this.schema.associations[alias] == null) {
486
- model = this;
487
- fieldPath = name;
488
- } else {
489
- model = Model.get(this.schema.associations[alias].modelName).constructor;
490
- split.shift();
491
- fieldPath = split.join('.');
492
- }
493
- } else {
494
- model = this;
495
- fieldPath = name;
496
- }
497
-
498
- return model.schema.get(fieldPath);
499
- });
500
-
501
- /**
502
- * Get the model's public configuration
503
- * (This is used to create the client-side Model instances)
504
- *
505
- * @author Jelle De Loecker <jelle@develry.be>
506
- * @since 1.0.0
507
- * @version 1.1.0
508
- */
509
- Model.setStatic(function getClientConfig() {
510
-
511
- var result = {
512
- name : this.model_name,
513
- schema : this.schema,
514
- primary_key : this.prototype.primary_key
515
- };
516
-
517
- if (this.super.name != 'Model') {
518
- result.parent = this.super.name;
519
- }
520
-
521
- return result;
522
- });
523
-
524
- /**
525
- * Initialize behaviours
526
- *
527
- * @author Jelle De Loecker <jelle@develry.be>
528
- * @since 0.2.0
529
- * @version 0.2.0
530
- *
531
- * @return {Document}
532
- */
533
- Model.setMethod(function initBehaviours() {
534
-
535
- var behaviour,
536
- key;
537
-
538
- this.behaviours = {};
539
-
540
- for (key in this.schema.behaviours) {
541
- behaviour = this.schema.behaviours[key];
542
-
543
- this.behaviours[key] = new behaviour.constructor(this, behaviour.options);
544
- }
545
-
546
- });
547
-
548
- /**
549
- * Enable a behaviour on-the-fly
550
- *
551
- * @author Jelle De Loecker <jelle@develry.be>
552
- * @since 0.0.1
553
- * @version 0.2.0
554
- */
555
- Model.setMethod(function addBehaviour(behaviourname, options) {
556
-
557
- var instance;
558
-
559
- if (!options) {
560
- options = {};
561
- }
562
-
563
- instance = Behaviour.get(behaviourname, this, options);
564
- this.behaviours[behaviourname] = instance;
565
-
566
- return instance;
567
- });
568
-
569
- /**
570
- * Get a behaviour instance
571
- *
572
- * @author Jelle De Loecker <jelle@develry.be>
573
- * @since 1.0.3
574
- * @version 1.0.3
575
- *
576
- * @param {String} name
577
- *
578
- * @return {Behaviour}
579
- */
580
- Model.setMethod(function getBehaviour(name) {
581
-
582
- name = name.camelize();
583
-
584
- if (!name.endsWith('Behaviour')) {
585
- name += 'Behaviour';
586
- }
587
-
588
- return this.behaviours[name];
589
- });
590
-
591
- /**
592
- * Enable translations
593
- *
594
- * @author Jelle De Loecker <jelle@develry.be>
595
- * @since 0.2.0
596
- * @version 0.2.0
597
- */
598
- Model.setMethod(function enableTranslations() {
599
- this.translate = true;
600
- });
601
-
602
- /**
603
- * Disable translations
604
- *
605
- * @author Jelle De Loecker <jelle@develry.be>
606
- * @since 0.2.0
607
- * @version 0.2.0
608
- */
609
- Model.setMethod(function disableTranslations() {
610
- this.translate = false;
611
- });
612
-
613
- /**
614
- * Aggregate
615
- *
616
- * @author Jelle De Loecker <jelle@develry.be>
617
- * @since 0.5.0
618
- * @version 0.5.0
619
- *
620
- * @param {Array} pipeline
621
- * @param {Function} callback
622
- */
623
- Model.setMethod(function aggregate(pipeline, callback) {
624
-
625
- this.datasource.collection(this.table, function gotCollection(err, collection) {
626
-
627
- if (err) {
628
- return callback(err);
629
- }
630
-
631
- collection.aggregate(pipeline, callback);
632
- });
633
- });
634
-
635
- /**
636
- * Translate the given records
637
- *
638
- * @author Jelle De Loecker <jelle@develry.be>
639
- * @since 0.2.0
640
- * @version 1.1.4
641
- *
642
- * @param {Array} items
643
- * @param {Object} options Optional options object
644
- * @param {Function} callback
645
- */
646
- Model.setMethod(function translateItems(items, options, callback) {
647
-
648
- var collection,
649
- fieldName,
650
- prefixes,
651
- conduit,
652
- prefix,
653
- record,
654
- alias,
655
- found,
656
- item,
657
- key,
658
- i,
659
- j;
660
-
661
- if (options && options instanceof Classes.Alchemy.Criteria.Criteria) {
662
- options = options.options;
663
- }
664
-
665
- // No items to translate
666
- if (!items.length) {
667
- return callback();
668
- }
669
-
670
- // No fields in this schema are translatable
671
- if (!this.schema.hasTranslations) {
672
- return callback();
673
- }
674
-
675
- // Do nothing if there are no translatable fields
676
- // or translate is disabled
677
- if (!this.translate || (!this.conduit && !options.locale)) {
678
- return callback();
679
- }
680
-
681
- // Get the alias we need to translate
682
- alias = options.forAlias || this.name;
683
-
684
- // Get the (optional) attached conduit
685
- conduit = this.conduit;
686
-
687
- // If prefixes are given as an option, only use those
688
- if (options.prefixes) {
689
- prefix = options.prefixes;
690
- } else {
691
- // Possible prefixes
692
- prefix = [];
693
-
694
- // Prefixes set in the options get precedence
695
- if (options.locale && options.locale !== true) {
696
- prefix.include(options.locale);
697
- }
698
-
699
- // Append the visited prefix after that (if there is one)
700
- if (conduit && conduit.prefix) {
701
- prefix.include(conduit.prefix);
702
- }
703
-
704
- // Append all the allowed locales after that
705
- if (conduit && conduit.locales) {
706
- prefix.include(conduit.locales);
707
- }
708
-
709
- // Add all available prefixes last
710
- for (key in all_prefixes) {
711
- prefix.push(key);
712
- }
713
-
714
- // The fallback prefix
715
- prefix.push('__');
716
-
717
- // @DEPRECATED: empty keys should no longer be allowed
718
- prefix.push('');
719
- }
720
-
721
- for (i = 0; i < items.length; i++) {
722
- item = items[i];
723
-
724
- if (!options.ungrouped_items) {
725
- item = item[alias];
726
- }
727
-
728
- collection = Array.cast(item);
729
-
730
- // Clone the prefixes
731
- prefixes = prefix.slice(0);
732
-
733
- // If one of the query conditions searched through a translatable field,
734
- // the prefix found should get preference
735
- if (options.use_found_prefix && items.item_prefixes && items.item_prefixes[i]) {
736
- prefixes.unshift(items.item_prefixes[i]);
737
- }
738
-
739
- let field;
740
-
741
- for (j = 0; j < collection.length; j++) {
742
- record = collection[j];
743
-
744
- for (fieldName in this.schema.translatableFields) {
745
- field = this.schema.translatableFields[fieldName];
746
- field.translateRecord(prefixes, record, options.allow_empty);
747
- }
748
- }
749
- }
750
-
751
- callback();
752
- });
753
-
754
- /**
755
- * Create the given record if the id does not exist in the database
756
- *
757
- * @author Jelle De Loecker <jelle@develry.be>
758
- * @since 0.0.1
759
- * @version 1.1.0
760
- *
761
- * @param {Array} list A list of all the records that need to be in the db
762
- * @param {Function} callback
763
- */
764
- Model.setMethod(function ensureIds(list, callback) {
765
-
766
- var that = this;
767
-
768
- list = Array.cast(list);
769
-
770
- return Function.forEach.parallel(list, function checkEntry(entry, key, next) {
771
- var id;
772
-
773
- id = entry[that.primary_key];
774
-
775
- if (!id && entry[that.name]) {
776
- id = entry[that.name][that.primary_key];
777
- }
778
-
779
- if (!id) {
780
- return next(new Classes.Alchemy.Error.Model('`Model#ensureIds()` can\'t ensure an entry without an _id'));
781
- }
782
-
783
- that.findById(id, function gotItem(err, result) {
784
-
785
- if (err) {
786
- return next(err);
787
- }
788
-
789
- if (result) {
790
- return next();
791
- }
792
-
793
- that.save(entry, {create: true, document: false}, next);
794
- });
795
- }, callback);
796
- });
797
-
798
- /**
799
- * Save one record
800
- *
801
- * @author Jelle De Loecker <jelle@develry.be>
802
- * @since 0.0.1
803
- * @version 1.1.0
804
- *
805
- * @param {Document} document
806
- * @param {Object} options
807
- * @param {Function} callback
808
- *
809
- * @return {Pledge}
810
- */
811
- Model.setMethod(function saveRecord(document, options, callback) {
812
-
813
- var that = this,
814
- saved_record,
815
- creating,
816
- results,
817
- pledge,
818
- main,
819
- iter;
820
-
821
- if (!document) {
822
- pledge = Pledge.reject(new Error('Unable to save record: given document is undefined'));
823
- pledge.done(callback);
824
- return pledge;
825
- }
826
-
827
- // Normalize the arguments
828
- if (typeof options == 'function') {
829
- callback = options;
830
- }
831
-
832
- if (typeof options !== 'object') {
833
- options = {};
834
- }
835
-
836
- pledge = Function.series(function doAudit(next) {
837
-
838
- if (Object.isPlainObject(document)) {
839
- return next(new Error('Model#saveRecord() expects a Document, not a plain object'));
840
- }
841
-
842
- // Look through unique indexes if no _id is present
843
- that.auditRecord(document, options, function afterAudit(err, doc) {
844
-
845
- if (err) {
846
- return next(err);
847
- }
848
-
849
- // Is a new record being created?
850
- creating = options.create || doc[that.primary_key] == null;
851
- next();
852
- });
853
- }, function doBeforeSave(next) {
854
-
855
- if (typeof that.beforeSave == 'function') {
856
- that.beforeSave(document, options, next);
857
- } else {
858
- next();
859
- }
860
-
861
- }, function emitSavingEvent(next) {
862
- that.emit('saving', document, options, creating, function afterSavingEvent(err, stopped) {
863
- return next(err);
864
- });
865
- }, function doDatabase(next) {
866
-
867
- if (options.debug) {
868
- console.log('Saving document', document, 'Creating?', creating);
869
- }
870
-
871
- function gotRecord(err, result) {
872
- if (err) {
873
- return next(err);
874
- }
875
-
876
- saved_record = result;
877
- next();
878
- }
879
-
880
- if (creating) {
881
- that.createRecord(document, options, gotRecord);
882
- } else {
883
- that.updateRecord(document, options, gotRecord);
884
- }
885
- }, function doAssociated(next) {
886
-
887
- var tasks = [],
888
- assoc,
889
- entry,
890
- key;
891
-
892
- Object.each(document.$record, function eachEntry(entry, key) {
893
-
894
- // Skip our own record
895
- if (key == that.name) {
896
- return;
897
- }
898
-
899
- // Get the association configuration
900
- assoc = that.schema.associations[key];
901
-
902
- // If the association doesn't exist, do nothing
903
- if (!assoc) {
904
- return;
905
- }
906
-
907
- // Add the saved _id
908
- entry[assoc.options.foreignKey] = saved_record[assoc.options.localKey];
909
-
910
- // Add the task
911
- tasks.push(function doSave(next) {
912
- var a_model = that.getModel(assoc.modelName);
913
- a_model.save(entry, next);
914
- });
915
- });
916
-
917
- Function.parallel(tasks, next);
918
- }, function done(err) {
919
-
920
- if (err) {
921
- return;
922
- }
923
-
924
- return saved_record;
925
- });
926
-
927
- pledge.handleCallback(callback);
928
-
929
- return pledge;
930
- });
931
-
932
- /**
933
- * Look for the record id by checking the indexes
934
- *
935
- * @author Jelle De Loecker <jelle@develry.be>
936
- * @since 0.1.0
937
- * @version 1.1.0
938
- *
939
- * @param {Document} document
940
- * @param {Object} options
941
- * @param {Function} callback
942
- */
943
- Model.setMethod(function auditRecord(document, options, callback) {
944
-
945
- var that = this,
946
- results,
947
- schema,
948
- tasks;
949
-
950
- if (!document) {
951
- return callback(new Error('No record was given to audit'));
952
- }
953
-
954
- schema = this.schema;
955
-
956
- if (schema && document[this.primary_key] == null && options.audit !== false) {
957
- tasks = {};
958
- results = {};
959
-
960
- if (options.debug) {
961
- console.log('Pre-save audit record', document);
962
- }
963
-
964
- schema.eachAlternateIndex(document, function iterIndex(index, indexName) {
965
-
966
- if (options.debug) {
967
- console.log('Checking alternate index', indexName);
968
- }
969
-
970
- tasks[indexName] = function auditIndex(next) {
971
- var query = {},
972
- fieldName;
973
-
974
- for (fieldName in index.fields) {
975
- if (document[fieldName] != null) {
976
- query[fieldName] = document[fieldName];
977
-
978
- // @todo: should run through the FieldType instance
979
- if (String(query[fieldName]).isObjectId()) {
980
- query[fieldName] = alchemy.castObjectId(query[fieldName]);
981
- }
982
- }
983
- }
984
-
985
- that.datasource.read(that, query, {}, function gotRecordInfo(err, records) {
986
-
987
- if (err != null) {
988
- return next(err);
989
- }
990
-
991
- if (records[0] != null) {
992
- results[indexName] = records[0];
993
- }
994
-
995
- next();
996
- });
997
-
998
- };
999
- });
1000
-
1001
- Function.parallel(tasks, function doneAudit(err) {
1002
-
1003
- var indexName,
1004
- record,
1005
- count,
1006
- ids;
1007
-
1008
- if (err != null) {
1009
- return callback(err);
1010
- }
1011
-
1012
- if (!Object.isEmpty(results)) {
1013
-
1014
- count = 0;
1015
- ids = {};
1016
-
1017
- for (indexName in results) {
1018
- record = results[indexName];
1019
-
1020
- // First make sure this index is allowed during the audit
1021
- // If it's not, this means it should be considered a duplicate
1022
- if (options.allowedIndexes != null && !Object.hasValue(options.allowedIndexes, indexName)) {
1023
- if (callback) callback(new Error('Duplicate index found other than _id: ' + indexName), null);
1024
- return;
1025
- }
1026
-
1027
- // Add the id a first time
1028
- if (ids[record[that.primary_key]] == null) {
1029
- count++;
1030
- ids[record[that.primary_key]] = true;
1031
- }
1032
- }
1033
-
1034
- // If more than 1 ids are found, we can't update the item
1035
- // because we don't know which record is the actual owner
1036
- if (count > 1) {
1037
- if (callback) callback(new Error('Multiple unique records found'));
1038
- return;
1039
- }
1040
-
1041
- // Use the last found record to get the id
1042
- document[that.primary_key] = record[that.primary_key];
1043
- }
1044
-
1045
- if (options.debug) {
1046
- console.log('Audit done, found pk:', document[that.primary_key]);
1047
- }
1048
-
1049
- callback(null, document);
1050
- });
1051
-
1052
- return;
1053
- }
1054
-
1055
- setImmediate(function skippedAudit() {
1056
- callback(null, document);
1057
- });
1058
- });
1059
-
1060
- /**
1061
- * Turn a record into something the database will understand
1062
- *
1063
- * @author Jelle De Loecker <jelle@develry.be>
1064
- * @since 1.0.3
1065
- * @version 1.0.4
1066
- *
1067
- * @param {Document} record
1068
- * @param {Object} options
1069
- * @param {Function} callback
1070
- *
1071
- * @return {Pledge}
1072
- */
1073
- Model.setMethod(function convertRecordToDatasourceFormat(record, options, callback) {
1074
-
1075
- var that = this,
1076
- pledge,
1077
- data;
1078
-
1079
- if (typeof options == 'function') {
1080
- callback = options;
1081
- options = {};
1082
- }
1083
-
1084
- if (!options) {
1085
- options = {};
1086
- }
1087
-
1088
- data = record[this.name] || record;
1089
-
1090
- // Normalize the data
1091
- data = this.compose(data, options);
1092
-
1093
- pledge = this.datasource.toDatasource(this, data);
1094
-
1095
- pledge.handleCallback(callback);
1096
-
1097
- return pledge;
1098
- });
1099
-
1100
- /**
1101
- * Process an object of datasource format
1102
- *
1103
- * @author Jelle De Loecker <jelle@develry.be>
1104
- * @since 1.0.3
1105
- * @version 1.0.3
1106
- *
1107
- * @param {Object} ds_data
1108
- * @param {Object} options
1109
- * @param {Function} callback
1110
- *
1111
- * @return {Pledge}
1112
- */
1113
- Model.setMethod(function processDatasourceFormat(ds_data, options, callback) {
1114
-
1115
- var that = this,
1116
- pledge,
1117
- data;
1118
-
1119
- if (typeof options == 'function') {
1120
- callback = options;
1121
- options = {};
1122
- }
1123
-
1124
- if (!options) {
1125
- options = {};
1126
- }
1127
-
1128
- pledge = this.datasource.toApp(this.schema, {}, options, ds_data);
1129
-
1130
- pledge.handleCallback(callback);
1131
-
1132
- return pledge;
1133
- });
1134
-
1135
- /**
1136
- * Get a field instance from the schema
1137
- *
1138
- * @author Jelle De Loecker <jelle@develry.be>
1139
- * @since 0.2.0
1140
- * @version 0.2.0
1141
- *
1142
- * @param {String} name The name of the field
1143
- *
1144
- * @return {FieldType}
1145
- */
1146
- Model.setMethod(function getField(name) {
1147
- return this.schema.getField(name);
1148
- });
1149
-
1150
- /**
1151
- * Get the title to display for this record
1152
- *
1153
- * @author Jelle De Loecker <jelle@develry.be>
1154
- * @since 0.0.1
1155
- * @version 0.1.0
1156
- *
1157
- * @param {Object} item The record item of this model
1158
- * @param {String|Array} fallbacks Extra fallbacks to use
1159
- *
1160
- * @return {String} The display title to use
1161
- */
1162
- Model.setMethod(function getDisplayTitle(item, fallbacks) {
1163
-
1164
- var fields,
1165
- field,
1166
- main,
1167
- val,
1168
- i;
1169
-
1170
- if (!item) {
1171
- return 'Undefined item';
1172
- }
1173
-
1174
- if (item[this.modelName]) {
1175
- main = item[this.modelName];
1176
- } else {
1177
- main = item;
1178
- }
1179
-
1180
- if (!main) {
1181
- return 'Undefined item';
1182
- }
1183
-
1184
- fields = Array.cast(this.displayField);
1185
-
1186
- if (fallbacks) {
1187
- fields = fields.concat(fallbacks);
1188
- }
1189
-
1190
- for (i = 0; i < fields.length; i++) {
1191
- val = main[fields[i]];
1192
-
1193
- if (Object.isObject(val)) {
1194
- field = this.getField(fields[i]);
1195
-
1196
- if (field && field.isTranslatable) {
1197
- val = alchemy.pickTranslation(this.conduit, val).result;
1198
- }
1199
- }
1200
-
1201
- if (val && typeof val == 'string') {
1202
- return val;
1203
- }
1204
- }
1205
-
1206
- return main[this.primary_key] || '';
1207
- });
1208
-
1209
- /**
1210
- * Clear the cache of this and all associated models
1211
- *
1212
- * @author Jelle De Loecker <jelle@develry.be>
1213
- * @since 0.0.1
1214
- * @version 1.1.6
1215
- *
1216
- * @param {Boolean} associated Also nuke associated models
1217
- * @param {Branch} parent
1218
- */
1219
- Model.setMethod(function nukeCache(associated, parent) {
1220
-
1221
- let model_name,
1222
- branch,
1223
- alias;
1224
-
1225
- // Nuke associated caches by default
1226
- if (typeof associated == 'undefined') {
1227
- associated = true;
1228
- }
1229
-
1230
- // Create the parent branch object
1231
- if (!parent) {
1232
- branch = parent = new Classes.Branch.Data(this.name);
1233
- }
1234
-
1235
- if (branch || !parent.root.seen(this.name)) {
1236
-
1237
- if (!branch) {
1238
- branch = parent.append(this.name);
1239
- }
1240
-
1241
- if (this.cache) {
1242
- this.cache.reset();
1243
- }
1244
-
1245
- // Also nuke the cache of the client model, if it exists
1246
- if (Classes.Alchemy.Client.Model[this.constructor.name] && Classes.Alchemy.Client.Model[this.constructor.name].cache) {
1247
- Classes.Alchemy.Client.Model[this.constructor.name].cache.reset();
1248
- }
1249
- }
1250
-
1251
- // Return if we don't need to nuke associated models
1252
- if (!associated) {
1253
- return;
1254
- }
1255
-
1256
- for (alias in this.associations) {
1257
- model_name = this.associations[alias].modelName;
1258
-
1259
- if (!parent.root.seen(model_name)) {
1260
- let assoc_model = this.getModel(model_name);
1261
- assoc_model.nukeCache(true, branch);
1262
- }
1263
- }
1264
- });
1265
-
1266
- /**
1267
- * Delete the given record id
1268
- *
1269
- * @author Jelle De Loecker <jelle@develry.be>
1270
- * @since 0.0.1
1271
- * @version 1.0.7
1272
- *
1273
- * @param {String} id The object id
1274
- * @param {Function} callback
1275
- *
1276
- * @return {Pledge}
1277
- */
1278
- Model.setMethod(function remove(id, callback) {
1279
-
1280
- var that = this,
1281
- pledge = new Pledge(),
1282
- id;
1283
-
1284
- pledge.handleCallback(callback);
1285
-
1286
- if (!id) {
1287
- pledge.reject(new Error('Invalid id given!'));
1288
- return pledge;
1289
- }
1290
-
1291
- if (this.datasource.supports('objectid')) {
1292
- id = alchemy.castObjectId(id);
1293
- } else {
1294
- id = String(id);
1295
- }
1296
-
1297
- let query = {};
1298
- query[this.primary_key] = id;
1299
-
1300
- this.datasource.remove(this, query, {}, function afterRemove(err, result) {
1301
-
1302
- if (err != null) {
1303
- return pledge.reject(err);
1304
- }
1305
-
1306
- that.emit('removed', result, {}, false, function afterRemovedEvent() {
1307
- pledge.resolve(result);
1308
- });
1309
- });
1310
-
1311
- return pledge;
1312
- });
1313
-
1314
- /**
1315
- * Get all the records and perform the given task on them
1316
- *
1317
- * @author Jelle De Loecker <jelle@develry.be>
1318
- * @since 0.5.0
1319
- * @version 1.0.5
1320
- *
1321
- * @param {Object} options Find options
1322
- * @param {Function} task Task to perform on each record
1323
- * @param {Function} callback Function to call when done
1324
- */
1325
- Model.setMethod(function eachRecord(options, task, callback) {
1326
-
1327
- var that = this,
1328
- parallel_limit,
1329
- available = null,
1330
- last_id = null,
1331
- pledge = new Classes.Pledge(),
1332
- index = 0;
1333
-
1334
- if (typeof options == 'function') {
1335
- callback = task;
1336
- task = options;
1337
- options = {};
1338
- } else if (!options) {
1339
- options = {};
1340
- }
1341
-
1342
- if (!callback) {
1343
- callback = Function.thrower;
1344
- }
1345
-
1346
- // Apply default limit of 50 records per fetch
1347
- options = Object.assign({}, {limit: 50}, options);
1348
-
1349
- // Get amount of tasks to do in parallel
1350
- parallel_limit = options.parallel_limit || 8;
1351
-
1352
- // Sort by _id ascending
1353
- if (!options.sort) {
1354
- options.sort = {};
1355
- options.sort[this.primary_key] = 1;
1356
- }
1357
-
1358
- // Make sure there is a conditions object
1359
- if (!options.conditions) {
1360
- options.conditions = {};
1361
- }
1362
-
1363
- this.find('all', options, function gotRecords(err, result) {
1364
-
1365
- var tasks = [];
1366
-
1367
- if (!result.length) {
1368
- pledge.reportProgress(100);
1369
- pledge.resolve();
1370
- return callback(null);
1371
- }
1372
-
1373
- if (available == null) {
1374
- available = result.available;
1375
- options.available = false;
1376
- } else {
1377
- result.available = available;
1378
- }
1379
-
1380
- result.forEach(function eachRecord(record) {
1381
-
1382
- var record_index = index++;
1383
-
1384
- last_id = record[that.primary_key];
1385
-
1386
- tasks.push(function doSave(next) {
1387
- pledge.reportProgress(((record_index - 1) / available) * 100);
1388
- task.call(that, record, record_index, next);
1389
- });
1390
- });
1391
-
1392
- Function.parallel(parallel_limit, tasks, function done(err) {
1393
-
1394
- if (err) {
1395
- pledge.reject(err);
1396
- return callback(err);
1397
- }
1398
-
1399
- let next_options = Object.assign({}, options);
1400
-
1401
- // Get records with a bigger _id than the last found
1402
- next_options.conditions[that.primary_key] = {$gt: last_id};
1403
-
1404
- that.find('all', next_options, gotRecords);
1405
- });
1406
- });
1407
-
1408
- return pledge;
1409
- });
1410
-
1411
- /**
1412
- * Strip out private fields
1413
- *
1414
- * @author Jelle De Loecker <jelle@develry.be>
1415
- * @since 1.0.0
1416
- * @version 1.0.0
1417
- *
1418
- * @param {Array} records
1419
- */
1420
- Model.setMethod(function removePrivateFields(records) {
1421
-
1422
- var has_private_fields,
1423
- fields = this.schema.getSorted(false),
1424
- record,
1425
- field,
1426
- i,
1427
- j;
1428
-
1429
- records = Array.cast(records);
1430
-
1431
- for (i = 0; i < records.length; i++) {
1432
- record = records[i];
1433
-
1434
- for (j = 0; j < fields.length; j++) {
1435
- field = fields[j];
1436
-
1437
- if (field.is_private) {
1438
- has_private_fields = true;
1439
- delete record[field.name];
1440
- }
1441
- }
1442
-
1443
- // If there are no private fields, break loop
1444
- if (!has_private_fields) {
1445
- break;
1446
- }
1447
- }
1448
-
1449
- return records;
1450
- });
1451
-
1452
- /**
1453
- * Create an export stream
1454
- *
1455
- * @author Jelle De Loecker <jelle@develry.be>
1456
- * @since 1.0.5
1457
- * @version 1.0.5
1458
- *
1459
- * @param {Stream} output
1460
- * @param {Object} options
1461
- *
1462
- * @return {Pledge}
1463
- */
1464
- Model.setMethod(function exportToStream(output, options) {
1465
-
1466
- if (!alchemy.isStream(output)) {
1467
- if (!options) {
1468
- options = output;
1469
- output = null;
1470
- }
1471
-
1472
- output = options.output;
1473
- }
1474
-
1475
- if (!output) {
1476
- return Pledge.reject(new Error('No target output stream has been given'));
1477
- }
1478
-
1479
- if (!options) {
1480
- options = {};
1481
- }
1482
-
1483
- // Only allow 1 task to run at a time
1484
- options.parallel_limit = 1;
1485
-
1486
- let that = this,
1487
- name_buf = Buffer.from(this.model_name),
1488
- head_buf;
1489
-
1490
- // 0x01 is a model
1491
- head_buf = Buffer.concat([Buffer.from([0x01, name_buf.length]), name_buf]);
1492
-
1493
- output.write(head_buf);
1494
-
1495
- return this.eachRecord(options, function eachRecord(record, index, next) {
1496
- record.exportToStream(output).done(next);
1497
- }, function done(err) {
1498
-
1499
- });
1500
- });
1501
-
1502
- /**
1503
- * Import from a stream
1504
- *
1505
- * @author Jelle De Loecker <jelle@develry.be>
1506
- * @since 1.0.5
1507
- * @version 1.0.5
1508
- *
1509
- * @param {Stream} input
1510
- * @param {Object} options
1511
- *
1512
- * @return {Pledge}
1513
- */
1514
- Model.setMethod(function importFromStream(input, options) {
1515
-
1516
- if (!alchemy.isStream(input)) {
1517
- if (!options) {
1518
- options = input;
1519
- input = null;
1520
- }
1521
-
1522
- input = options.input;
1523
- }
1524
-
1525
- if (!input) {
1526
- return Pledge.reject(new Error('No source input stream has been given'));
1527
- }
1528
-
1529
- let that = this,
1530
- current_type = null,
1531
- extra_stream,
1532
- pledge = new Pledge(),
1533
- stopped,
1534
- paused,
1535
- buffer,
1536
- value,
1537
- seen = 0,
1538
- left,
1539
- size,
1540
- doc;
1541
-
1542
- input.on('data', function onData(data) {
1543
-
1544
- if (stopped) {
1545
- return;
1546
- }
1547
-
1548
- if (buffer) {
1549
- buffer = Buffer.concat([buffer, data]);
1550
- } else {
1551
- buffer = data;
1552
- }
1553
-
1554
- handleBuffer();
1555
- });
1556
-
1557
- function handleBuffer() {
1558
-
1559
- if (paused) {
1560
- return;
1561
- }
1562
-
1563
- if (!current_type && buffer.length < 2) {
1564
- return;
1565
- }
1566
-
1567
- if (!current_type) {
1568
- current_type = buffer.readUInt8(0);
1569
-
1570
- if (current_type == 0x01) {
1571
- size = buffer.readUInt8(1);
1572
- buffer = buffer.slice(2);
1573
- } else if (current_type == 0x02 && buffer.length >= 5) {
1574
- size = buffer.readUInt32BE(1);
1575
- buffer = buffer.slice(5);
1576
- } else if (current_type == 0xFF) {
1577
- size = buffer.readUInt32BE(1);
1578
- buffer = buffer.slice(5);
1579
- seen = 0;
1580
-
1581
- if (!doc) {
1582
- stopped = true;
1583
- pledge.reject(new Error('Found extra import data, but no active document'));
1584
- } else {
1585
- extra_stream = new require('stream').PassThrough();
1586
- doc.extraImportFromStream(extra_stream);
1587
- }
1588
- } else {
1589
- // Not enough data? Wait
1590
- current_type = null;
1591
- return;
1592
- }
1593
- }
1594
-
1595
- handleRest();
1596
- }
1597
-
1598
- function handleRest() {
1599
-
1600
- if (current_type == 0xFF) {
1601
- left = size - seen;
1602
- value = buffer.slice(0, left);
1603
-
1604
- seen += value.length;
1605
-
1606
- if (value.length == buffer.length) {
1607
- buffer = null;
1608
- } else if (value.length < buffer.length) {
1609
- buffer = buffer.slice(left);
1610
- }
1611
-
1612
- extra_stream.write(value);
1613
-
1614
- if (value.length == left) {
1615
- extra_stream.end();
1616
- current_type = null;
1617
-
1618
- if (buffer) {
1619
- handleBuffer();
1620
- }
1621
- }
1622
-
1623
- return;
1624
- }
1625
-
1626
- if (buffer.length >= size) {
1627
- value = buffer.slice(0, size);
1628
- buffer = buffer.slice(size);
1629
- } else {
1630
- // Wait for next call
1631
- return;
1632
- }
1633
-
1634
- if (current_type == 0x01) {
1635
- value = value.toString();
1636
-
1637
- if (value == that.model_name) {
1638
- // Found name!
1639
- current_type = null;
1640
- size = 0;
1641
- } else {
1642
- stopped = true;
1643
- return pledge.reject(new Error('Model names do not match'));
1644
- }
1645
- } else if (current_type == 0x02) {
1646
- doc = that.createDocument();
1647
- input.pause();
1648
- paused = true;
1649
-
1650
- doc.importFromBuffer(value).done(function done(err, result) {
1651
-
1652
- if (err) {
1653
- stopped = true;
1654
- return pledge.reject(err);
1655
- }
1656
-
1657
- current_type = null;
1658
- paused = false;
1659
- input.resume();
1660
-
1661
- handleBuffer();
1662
- });
1663
-
1664
- return;
1665
- }
1666
-
1667
- if (buffer && buffer.length) {
1668
- handleBuffer();
1669
- }
1670
- }
1671
-
1672
- return pledge;
1673
- });
1674
-
1675
- /**
1676
- * Get a model
1677
- *
1678
- * @author Jelle De Loecker <jelle@develry.be>
1679
- * @since 0.0.1
1680
- * @version 1.1.0
1681
- *
1682
- * @param {String} name
1683
- * @param {Boolean} init
1684
- *
1685
- * @return {Model}
1686
- */
1687
- Model.get = function get(name, init, options) {
1688
-
1689
- var constructor,
1690
- namespace,
1691
- pieces,
1692
- path,
1693
- obj;
1694
-
1695
- if (typeof name == 'function') {
1696
- name = name.name;
1697
- }
1698
-
1699
- if (!name) {
1700
- throw new TypeError('Model name should be a non-empty string');
1701
- }
1702
-
1703
- if (init && typeof init == 'object') {
1704
- options = init;
1705
- init = true;
1706
- }
1707
-
1708
- if (!options) {
1709
- options = {};
1710
- }
1711
-
1712
- if (nameCache[name]) {
1713
- if (init === false) {
1714
- return nameCache[name];
1715
- } else {
1716
- return new nameCache[name];
1717
- }
1718
- }
1719
-
1720
- pieces = name.split('.');
1721
-
1722
- if (pieces.length > 1) {
1723
- // The first part is the namespace
1724
- namespace = pieces.shift();
1725
-
1726
- // The rest should be the path
1727
- path = pieces.join('.');
1728
-
1729
- obj = Classes[namespace];
1730
-
1731
- if (!obj) {
1732
- if (init === false) {
1733
- return null;
1734
- }
1735
-
1736
- throw new TypeError('Namespace "' + namespace + '" could not be found');
1737
- }
1738
- } else {
1739
- path = name;
1740
- obj = Classes.Alchemy.Model;
1741
- }
1742
-
1743
- constructor = Object.path(obj, path) || obj[String(path).modelName()];
1744
-
1745
- if (constructor == null) {
1746
- if (init === false) {
1747
- return null;
1748
- }
1749
-
1750
- throw new Error('Could not find model "' + name + '"');
1751
- }
1752
-
1753
- // Store this name in the cache,
1754
- // so we don't have to perform the expensive #modelName() method again
1755
- nameCache[name] = constructor;
1756
-
1757
- if (init === false) {
1758
- return constructor;
1759
- } else {
1760
- return new constructor;
1761
- }
1762
- };
1763
-
1764
- /**
1765
- * Make the base Model class a global
1766
- *
1767
- * @author Jelle De Loecker <jelle@develry.be>
1768
- * @since 0.0.1
1769
- * @version 0.2.0
1770
- *
1771
- * @type {Object}
1772
- */
1
+ var nameCache = {},
2
+ mongo = alchemy.use('mongodb'),
3
+ all_prefixes = alchemy.shared('Routing.prefixes'),
4
+ fs = alchemy.use('fs'),
5
+ createdModel;
6
+
7
+ /**
8
+ * The Model class
9
+ *
10
+ * @constructor
11
+ *
12
+ * @author Jelle De Loecker <jelle@develry.be>
13
+ * @since 0.0.1
14
+ * @version 1.1.0
15
+ */
16
+ var Model = Function.inherits('Alchemy.Base', 'Alchemy.Model', function Model(options) {
17
+ this.init(options);
18
+ });
19
+
20
+ /**
21
+ * Set the modelName property after class creation
22
+ *
23
+ * @author Jelle De Loecker <jelle@develry.be>
24
+ * @since 0.2.0
25
+ * @version 1.1.0
26
+ */
27
+ Model.constitute(function setModelName() {
28
+ this.model_name = this.name;
29
+ this.setProperty('model_name', this.model_name);
30
+
31
+ if (!this.table) {
32
+ this.table = this.model_name.tableize();
33
+ }
34
+ });
35
+
36
+ /**
37
+ * This is a model constructor
38
+ *
39
+ * @type {Boolean}
40
+ */
41
+ Model.setStaticProperty('model', true);
42
+
43
+ /**
44
+ * The cache duration static getter/setter
45
+ *
46
+ * @author Jelle De Loecker <jelle@develry.be>
47
+ * @since 0.1.0
48
+ * @version 1.0.3
49
+ *
50
+ * @property cache_duration
51
+ * @type {String}
52
+ */
53
+ Model.setStaticProperty(function cache_duration() {
54
+
55
+ if (this._cache_duration == null) {
56
+ this._cache_duration = alchemy.settings.model_query_cache_duration;
57
+ }
58
+
59
+ return this._cache_duration;
60
+ }, function setCacheDuration(duration) {
61
+ this._cache_duration = duration;
62
+
63
+ // @todo: reset cache
64
+ });
65
+
66
+ /**
67
+ * Get the cache object
68
+ *
69
+ * @author Jelle De Loecker <jelle@develry.be>
70
+ * @since 0.1.0
71
+ * @version 1.0.3
72
+ *
73
+ * @property cache
74
+ * @type {Object}
75
+ */
76
+ Model.setStaticProperty(function cache() {
77
+
78
+ if (this.cache_duration) {
79
+
80
+ if (this._cache) {
81
+ return this._cache;
82
+ }
83
+
84
+ this._cache = alchemy.getCache(this.name, this.cache_duration);
85
+ return this._cache;
86
+ }
87
+
88
+ return false;
89
+ }, function setCache(value) {
90
+ return this._cache = value;
91
+ });
92
+
93
+
94
+ /**
95
+ * Is this an abstract model?
96
+ *
97
+ * @author Jelle De Loecker <jelle@develry.be>
98
+ * @since 1.1.0
99
+ * @version 1.1.0
100
+ *
101
+ * @type {Boolean}
102
+ */
103
+ Model.setStaticProperty(function is_abstract() {
104
+
105
+ // Do simple is_abstract_class check
106
+ if (this.is_abstract_class != null) {
107
+ return !!this.is_abstract_class;
108
+ }
109
+
110
+ // If we need to load an external schema, it's also not abstract
111
+ if (this.prototype.load_external_schema) {
112
+ return false;
113
+ }
114
+
115
+ // See if this model has other fields than the default ones
116
+ let field_count = this.schema.array.length;
117
+
118
+ if (this.schema.has('_id')) {
119
+ field_count--;
120
+ }
121
+
122
+ if (this.schema.has('created')) {
123
+ field_count--;
124
+ }
125
+
126
+ if (this.schema.has('updated')) {
127
+ field_count--;
128
+ }
129
+
130
+ return field_count < 1;
131
+ });
132
+
133
+
134
+ /**
135
+ * Get the document class constructor
136
+ *
137
+ * @type {Alchemy.Document}
138
+ */
139
+ Model.prepareStaticProperty('Document', function getDocumentClass() {
140
+ return Classes.Alchemy.Document.Document.getDocumentClass(this);
141
+ });
142
+
143
+ /**
144
+ * Get the client document class constructor
145
+ *
146
+ * @type {Hawkejs.Document}
147
+ */
148
+ Model.prepareStaticProperty('ClientDocument', function getClientDocumentClass() {
149
+ return this.Document.getClientDocumentClass();
150
+ });
151
+
152
+ /**
153
+ * Set the static per-model schema
154
+ *
155
+ * @version 1.1.0
156
+ *
157
+ * @type {Schema}
158
+ */
159
+ Model.staticCompose('schema', function createSchema(doNext) {
160
+
161
+ var that = this,
162
+ model = this.compositorParent,
163
+ schema = new Classes.Alchemy.Schema();
164
+
165
+ // The base Model does not have a schema
166
+ if (model.name == 'Model') {
167
+ return false;
168
+ } else {
169
+
170
+ // Link the schema to this model
171
+ schema.setModel(model);
172
+
173
+ // Set the schema name
174
+ schema.setName(model.name);
175
+
176
+ if (model.prototype.add_basic_fields !== false) {
177
+
178
+ // Set default model fields immediately after this function ends
179
+ // This has to be scheduled next, because addField would call createSchema
180
+ // again, resulting in an infinite loop
181
+ doNext(function addSchemaBasics() {
182
+ model.addField('_id', 'ObjectId', {default: Field.createPathEvaluator('alchemy.ObjectId')});
183
+ model.addField('created', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
184
+ model.addField('updated', 'Datetime', {default: Field.createPathEvaluator('Date.create')});
185
+ });
186
+ }
187
+ }
188
+
189
+ return schema;
190
+ }, [
191
+ 'addEnumValues',
192
+ 'setEnumValues',
193
+ 'belongsTo',
194
+ 'hasOneParent',
195
+ 'hasAndBelongsToMany',
196
+ 'hasMany',
197
+ 'hasOneChild',
198
+ 'addIndex',
199
+ 'addRule',
200
+ ]);
201
+
202
+ Model.setDeprecatedProperty('modelName', 'model_name');
203
+ Model.setDeprecatedProperty('blueprint', 'schema');
204
+
205
+ /**
206
+ * The default database config to use
207
+ *
208
+ * @type {String}
209
+ */
210
+ Model.setProperty('dbConfig', 'default');
211
+
212
+ /**
213
+ * The default field to use as display
214
+ *
215
+ * @type {String}
216
+ */
217
+ Model.setProperty('displayField', 'title');
218
+
219
+ /**
220
+ * Translate is on by default
221
+ *
222
+ * @type {Boolean}
223
+ */
224
+ Model.setProperty('translate', true);
225
+
226
+ /**
227
+ * Set the name of the primary key field
228
+ *
229
+ * @author Jelle De Loecker <jelle@develry.be>
230
+ * @since 0.1.0
231
+ * @version 0.1.0
232
+ */
233
+ Model.setProperty('primary_key', '_id');
234
+
235
+ /**
236
+ * Should we load the schema from the database?
237
+ *
238
+ * @author Jelle De Loecker <jelle@develry.be>
239
+ * @since 1.1.0
240
+ * @version 1.1.0
241
+ */
242
+ Model.setProperty('load_external_schema', false);
243
+
244
+ /**
245
+ * Object where behaviours are stored
246
+ *
247
+ * @type {Object}
248
+ */
249
+ Model.prepareProperty('behaviours', Object);
250
+
251
+ /**
252
+ * Associations
253
+ *
254
+ * @type {Object}
255
+ */
256
+ Model.setProperty(function associations() {
257
+ return this.schema.associations;
258
+ });
259
+
260
+ /**
261
+ * Instance access to static cache
262
+ *
263
+ * @type {Expirable}
264
+ */
265
+ Model.prepareProperty('cache', function cache() {
266
+ return this.constructor.cache;
267
+ });
268
+
269
+ /**
270
+ * Instance access to static schema
271
+ *
272
+ * @type {Schema}
273
+ */
274
+ Model.setProperty(function schema() {
275
+ return this.constructor.schema;
276
+ });
277
+
278
+ /**
279
+ * Is this an abstract model?
280
+ *
281
+ * @type {Boolean}
282
+ */
283
+ Model.setProperty(function is_abstract() {
284
+ return this.constructor.is_abstract;
285
+ });
286
+
287
+ /**
288
+ * This is a wrapper class
289
+ */
290
+ Model.makeAbstractClass();
291
+
292
+ /**
293
+ * This wrapper class starts a new group
294
+ */
295
+ Model.startNewGroup();
296
+
297
+ /**
298
+ * The connection
299
+ *
300
+ * @type {Object}
301
+ */
302
+ Model.prepareProperty('datasource', function datasource() {
303
+ if (this.table) return Datasource.get(this.dbConfig);
304
+ });
305
+
306
+ /**
307
+ * The default sort options
308
+ *
309
+ * @type {Object}
310
+ */
311
+ Model.prepareProperty('sort', function sort() {
312
+ return {created: 1};
313
+ });
314
+
315
+ /**
316
+ * Check a url value
317
+ *
318
+ * @author Jelle De Loecker <jelle@develry.be>
319
+ * @since 1.0.0
320
+ * @version 1.1.0
321
+ *
322
+ * @param {String} value The value in the url
323
+ * @param {String} name The name of the url parameter
324
+ * @param {String} field_name The name of the field to check
325
+ * @param {Conduit} conduit The optional conduit
326
+ *
327
+ * @return {Pledge}
328
+ */
329
+ Model.setStatic(function checkPathValue(value, name, field_name, conduit) {
330
+
331
+ var instance,
332
+ pledge,
333
+ crit;
334
+
335
+ if (!field_name) {
336
+ if (name == 'id') {
337
+ field_name = this.prototype.primary_key;
338
+ } else {
339
+ field_name = name;
340
+ }
341
+ }
342
+
343
+ if (conduit) {
344
+ instance = conduit.getModel(this);
345
+ } else {
346
+ instance = new this;
347
+ }
348
+
349
+ // Create new criteria instance
350
+ crit = instance.find();
351
+
352
+ // Look for the wanted field
353
+ crit.where(field_name).equals(value);
354
+
355
+ pledge = instance.find('first', crit);
356
+
357
+ return pledge;
358
+ });
359
+
360
+ /**
361
+ * Add a field to this model's schema
362
+ *
363
+ * @author Jelle De Loecker <jelle@develry.be>
364
+ * @since 0.2.0
365
+ * @version 1.0.7
366
+ *
367
+ * @return {Alchemy.Field}
368
+ */
369
+ Model.setStatic(function addField(name, type, options) {
370
+
371
+ var field,
372
+ is_new;
373
+
374
+ is_new = !this.schema.has(name);
375
+
376
+ // Add it to the schema
377
+ field = this.schema.addField(name, type, options);
378
+
379
+ if (is_new) {
380
+ // Add it to the Document class
381
+ this.Document.setFieldGetter(name);
382
+
383
+ // Add the field to the client document too, if it's not private
384
+ if (!field.is_private) {
385
+ // False means it should not be set on the server implementation
386
+ // (because that's where it's coming from)
387
+ this.ClientDocument.setFieldGetter(name, null, null, false);
388
+ }
389
+ }
390
+
391
+ return field;
392
+ });
393
+
394
+
395
+ /**
396
+ * Add a behaviour to this model
397
+ *
398
+ * @author Jelle De Loecker <jelle@develry.be>
399
+ * @since 0.2.0
400
+ * @version 0.2.0
401
+ */
402
+ Model.setStatic(function addBehaviour(behaviour_name, options) {
403
+ return this.schema.addBehaviour(behaviour_name, options);
404
+ });
405
+
406
+ /**
407
+ * Add an association to this model's schema
408
+ * and set it on the Document as a getter
409
+ *
410
+ * @author Jelle De Loecker <jelle@develry.be>
411
+ * @since 0.2.0
412
+ * @version 0.2.0
413
+ */
414
+ Model.setStatic(function addAssociation(type, alias, model_name, options) {
415
+ var data = this.schema.addAssociation(type, alias, model_name, options);
416
+ this.Document.setAliasGetter(data.alias);
417
+
418
+ // Add the alias to the client document too, if it's not private
419
+ if (!options || !options.is_private) {
420
+ // False means it should not be set on the server implementation
421
+ // (because that's where it's coming from)
422
+ this.ClientDocument.setAliasGetter(name);
423
+ }
424
+ });
425
+
426
+ /**
427
+ * Set a method on the document class
428
+ *
429
+ * @author Jelle De Loecker <jelle@develry.be>
430
+ * @since 0.2.0
431
+ * @version 1.0.6
432
+ */
433
+ Model.setStatic(function setDocumentMethod(name, fnc) {
434
+
435
+ var that = this;
436
+
437
+ if (typeof name == 'function') {
438
+ fnc = name;
439
+ name = fnc.name;
440
+ }
441
+
442
+ Blast.loaded(function whenLoaded() {
443
+ that.Document.setMethod(name, fnc);
444
+ });
445
+ });
446
+
447
+ /**
448
+ * Set a property on the document class
449
+ *
450
+ * @author Jelle De Loecker <jelle@develry.be>
451
+ * @since 0.2.0
452
+ * @version 1.0.6
453
+ */
454
+ Model.setStatic(function setDocumentProperty(name, fnc) {
455
+
456
+ var that = this,
457
+ args = arguments;
458
+
459
+ Blast.loaded(function whenLoaded() {
460
+ that.Document.setProperty.apply(that.Document, args);
461
+ });
462
+ });
463
+
464
+ /**
465
+ * Get a field
466
+ *
467
+ * @author Jelle De Loecker <jelle@develry.be>
468
+ * @since 0.2.0
469
+ * @version 0.2.0
470
+ *
471
+ * @return {FieldType}
472
+ */
473
+ Model.setStatic(function getField(name) {
474
+
475
+ var fieldPath,
476
+ alias,
477
+ model,
478
+ split;
479
+
480
+ if (name.indexOf('.') > -1) {
481
+ split = name.split('.');
482
+
483
+ alias = name[0];
484
+
485
+ if (this.schema.associations[alias] == null) {
486
+ model = this;
487
+ fieldPath = name;
488
+ } else {
489
+ model = Model.get(this.schema.associations[alias].modelName).constructor;
490
+ split.shift();
491
+ fieldPath = split.join('.');
492
+ }
493
+ } else {
494
+ model = this;
495
+ fieldPath = name;
496
+ }
497
+
498
+ return model.schema.get(fieldPath);
499
+ });
500
+
501
+ /**
502
+ * Get the model's public configuration
503
+ * (This is used to create the client-side Model instances)
504
+ *
505
+ * @author Jelle De Loecker <jelle@develry.be>
506
+ * @since 1.0.0
507
+ * @version 1.1.0
508
+ */
509
+ Model.setStatic(function getClientConfig() {
510
+
511
+ var result = {
512
+ name : this.model_name,
513
+ schema : this.schema,
514
+ primary_key : this.prototype.primary_key
515
+ };
516
+
517
+ if (this.super.name != 'Model') {
518
+ result.parent = this.super.name;
519
+ }
520
+
521
+ return result;
522
+ });
523
+
524
+ /**
525
+ * Initialize behaviours
526
+ *
527
+ * @author Jelle De Loecker <jelle@develry.be>
528
+ * @since 0.2.0
529
+ * @version 0.2.0
530
+ *
531
+ * @return {Document}
532
+ */
533
+ Model.setMethod(function initBehaviours() {
534
+
535
+ var behaviour,
536
+ key;
537
+
538
+ this.behaviours = {};
539
+
540
+ for (key in this.schema.behaviours) {
541
+ behaviour = this.schema.behaviours[key];
542
+
543
+ this.behaviours[key] = new behaviour.constructor(this, behaviour.options);
544
+ }
545
+
546
+ });
547
+
548
+ /**
549
+ * Enable a behaviour on-the-fly
550
+ *
551
+ * @author Jelle De Loecker <jelle@develry.be>
552
+ * @since 0.0.1
553
+ * @version 0.2.0
554
+ */
555
+ Model.setMethod(function addBehaviour(behaviourname, options) {
556
+
557
+ var instance;
558
+
559
+ if (!options) {
560
+ options = {};
561
+ }
562
+
563
+ instance = Behaviour.get(behaviourname, this, options);
564
+ this.behaviours[behaviourname] = instance;
565
+
566
+ return instance;
567
+ });
568
+
569
+ /**
570
+ * Get a behaviour instance
571
+ *
572
+ * @author Jelle De Loecker <jelle@develry.be>
573
+ * @since 1.0.3
574
+ * @version 1.0.3
575
+ *
576
+ * @param {String} name
577
+ *
578
+ * @return {Behaviour}
579
+ */
580
+ Model.setMethod(function getBehaviour(name) {
581
+
582
+ name = name.camelize();
583
+
584
+ if (!name.endsWith('Behaviour')) {
585
+ name += 'Behaviour';
586
+ }
587
+
588
+ return this.behaviours[name];
589
+ });
590
+
591
+ /**
592
+ * Enable translations
593
+ *
594
+ * @author Jelle De Loecker <jelle@develry.be>
595
+ * @since 0.2.0
596
+ * @version 0.2.0
597
+ */
598
+ Model.setMethod(function enableTranslations() {
599
+ this.translate = true;
600
+ });
601
+
602
+ /**
603
+ * Disable translations
604
+ *
605
+ * @author Jelle De Loecker <jelle@develry.be>
606
+ * @since 0.2.0
607
+ * @version 0.2.0
608
+ */
609
+ Model.setMethod(function disableTranslations() {
610
+ this.translate = false;
611
+ });
612
+
613
+ /**
614
+ * Aggregate
615
+ *
616
+ * @author Jelle De Loecker <jelle@develry.be>
617
+ * @since 0.5.0
618
+ * @version 0.5.0
619
+ *
620
+ * @param {Array} pipeline
621
+ * @param {Function} callback
622
+ */
623
+ Model.setMethod(function aggregate(pipeline, callback) {
624
+
625
+ this.datasource.collection(this.table, function gotCollection(err, collection) {
626
+
627
+ if (err) {
628
+ return callback(err);
629
+ }
630
+
631
+ collection.aggregate(pipeline, callback);
632
+ });
633
+ });
634
+
635
+ /**
636
+ * Translate the given records
637
+ *
638
+ * @author Jelle De Loecker <jelle@develry.be>
639
+ * @since 0.2.0
640
+ * @version 1.1.4
641
+ *
642
+ * @param {Array} items
643
+ * @param {Object} options Optional options object
644
+ * @param {Function} callback
645
+ */
646
+ Model.setMethod(function translateItems(items, options, callback) {
647
+
648
+ var collection,
649
+ fieldName,
650
+ prefixes,
651
+ conduit,
652
+ prefix,
653
+ record,
654
+ alias,
655
+ found,
656
+ item,
657
+ key,
658
+ i,
659
+ j;
660
+
661
+ if (options && options instanceof Classes.Alchemy.Criteria.Criteria) {
662
+ options = options.options;
663
+ }
664
+
665
+ // No items to translate
666
+ if (!items.length) {
667
+ return callback();
668
+ }
669
+
670
+ // No fields in this schema are translatable
671
+ if (!this.schema.hasTranslations) {
672
+ return callback();
673
+ }
674
+
675
+ // Do nothing if there are no translatable fields
676
+ // or translate is disabled
677
+ if (!this.translate || (!this.conduit && !options.locale)) {
678
+ return callback();
679
+ }
680
+
681
+ // Get the alias we need to translate
682
+ alias = options.forAlias || this.name;
683
+
684
+ // Get the (optional) attached conduit
685
+ conduit = this.conduit;
686
+
687
+ // If prefixes are given as an option, only use those
688
+ if (options.prefixes) {
689
+ prefix = options.prefixes;
690
+ } else {
691
+ // Possible prefixes
692
+ prefix = [];
693
+
694
+ // Prefixes set in the options get precedence
695
+ if (options.locale && options.locale !== true) {
696
+ prefix.include(options.locale);
697
+ }
698
+
699
+ // Append the visited prefix after that (if there is one)
700
+ if (conduit && conduit.prefix) {
701
+ prefix.include(conduit.prefix);
702
+ }
703
+
704
+ // Append all the allowed locales after that
705
+ if (conduit && conduit.locales) {
706
+ prefix.include(conduit.locales);
707
+ }
708
+
709
+ // Add all available prefixes last
710
+ for (key in all_prefixes) {
711
+ prefix.push(key);
712
+ }
713
+
714
+ // The fallback prefix
715
+ prefix.push('__');
716
+
717
+ // @DEPRECATED: empty keys should no longer be allowed
718
+ prefix.push('');
719
+ }
720
+
721
+ for (i = 0; i < items.length; i++) {
722
+ item = items[i];
723
+
724
+ if (!options.ungrouped_items) {
725
+ item = item[alias];
726
+ }
727
+
728
+ collection = Array.cast(item);
729
+
730
+ // Clone the prefixes
731
+ prefixes = prefix.slice(0);
732
+
733
+ // If one of the query conditions searched through a translatable field,
734
+ // the prefix found should get preference
735
+ if (options.use_found_prefix && items.item_prefixes && items.item_prefixes[i]) {
736
+ prefixes.unshift(items.item_prefixes[i]);
737
+ }
738
+
739
+ let field;
740
+
741
+ for (j = 0; j < collection.length; j++) {
742
+ record = collection[j];
743
+
744
+ for (fieldName in this.schema.translatableFields) {
745
+ field = this.schema.translatableFields[fieldName];
746
+ field.translateRecord(prefixes, record, options.allow_empty);
747
+ }
748
+ }
749
+ }
750
+
751
+ callback();
752
+ });
753
+
754
+ /**
755
+ * Create the given record if the id does not exist in the database
756
+ *
757
+ * @author Jelle De Loecker <jelle@develry.be>
758
+ * @since 0.0.1
759
+ * @version 1.1.0
760
+ *
761
+ * @param {Array} list A list of all the records that need to be in the db
762
+ * @param {Function} callback
763
+ */
764
+ Model.setMethod(function ensureIds(list, callback) {
765
+
766
+ var that = this;
767
+
768
+ list = Array.cast(list);
769
+
770
+ return Function.forEach.parallel(list, function checkEntry(entry, key, next) {
771
+ var id;
772
+
773
+ id = entry[that.primary_key];
774
+
775
+ if (!id && entry[that.name]) {
776
+ id = entry[that.name][that.primary_key];
777
+ }
778
+
779
+ if (!id) {
780
+ return next(new Classes.Alchemy.Error.Model('`Model#ensureIds()` can\'t ensure an entry without an _id'));
781
+ }
782
+
783
+ that.findById(id, function gotItem(err, result) {
784
+
785
+ if (err) {
786
+ return next(err);
787
+ }
788
+
789
+ if (result) {
790
+ return next();
791
+ }
792
+
793
+ that.save(entry, {create: true, document: false}, next);
794
+ });
795
+ }, callback);
796
+ });
797
+
798
+ /**
799
+ * Save one record
800
+ *
801
+ * @author Jelle De Loecker <jelle@develry.be>
802
+ * @since 0.0.1
803
+ * @version 1.2.0
804
+ *
805
+ * @param {Document} document
806
+ * @param {Object} options
807
+ * @param {Function} callback
808
+ *
809
+ * @return {Pledge}
810
+ */
811
+ Model.setMethod(function saveRecord(document, options, callback) {
812
+
813
+ var that = this,
814
+ saved_record,
815
+ creating,
816
+ results,
817
+ pledge,
818
+ main,
819
+ iter;
820
+
821
+ if (!document) {
822
+ pledge = Pledge.reject(new Error('Unable to save record: given document is undefined'));
823
+ pledge.done(callback);
824
+ return pledge;
825
+ }
826
+
827
+ // Normalize the arguments
828
+ if (typeof options == 'function') {
829
+ callback = options;
830
+ }
831
+
832
+ if (typeof options !== 'object') {
833
+ options = {};
834
+ }
835
+
836
+ pledge = Function.series(function doAudit(next) {
837
+
838
+ if (Object.isPlainObject(document)) {
839
+ return next(new Error('Model#saveRecord() expects a Document, not a plain object'));
840
+ }
841
+
842
+ // Look through unique indexes if no _id is present
843
+ that.auditRecord(document, options, function afterAudit(err, doc) {
844
+
845
+ if (err) {
846
+ return next(err);
847
+ }
848
+
849
+ // Is a new record being created?
850
+ creating = options.create || doc[that.primary_key] == null;
851
+ next();
852
+ });
853
+ }, function doBeforeSave(next) {
854
+
855
+ if (typeof that.beforeSave == 'function') {
856
+ let promise = that.beforeSave(document, options, next);
857
+
858
+ if (promise) {
859
+ Pledge.done(promise, next);
860
+ } else if (that.beforeSave.length < 3) {
861
+ // If the method accepts no `next` callback, call it now
862
+ next();
863
+ }
864
+ } else {
865
+ next();
866
+ }
867
+
868
+ }, function emitSavingEvent(next) {
869
+ that.emit('saving', document, options, creating, function afterSavingEvent(err, stopped) {
870
+ return next(err);
871
+ });
872
+ }, function doDatabase(next) {
873
+
874
+ if (options.debug) {
875
+ console.log('Saving document', document, 'Creating?', creating);
876
+ }
877
+
878
+ function gotRecord(err, result) {
879
+ if (err) {
880
+ return next(err);
881
+ }
882
+
883
+ saved_record = result;
884
+ next();
885
+ }
886
+
887
+ if (creating) {
888
+ that.createRecord(document, options, gotRecord);
889
+ } else {
890
+ that.updateRecord(document, options, gotRecord);
891
+ }
892
+ }, function doAssociated(next) {
893
+
894
+ var tasks = [],
895
+ assoc,
896
+ entry,
897
+ key;
898
+
899
+ Object.each(document.$record, function eachEntry(entry, key) {
900
+
901
+ // Skip our own record
902
+ if (key == that.name) {
903
+ return;
904
+ }
905
+
906
+ // Get the association configuration
907
+ assoc = that.schema.associations[key];
908
+
909
+ // If the association doesn't exist, do nothing
910
+ if (!assoc) {
911
+ return;
912
+ }
913
+
914
+ // Add the saved _id
915
+ //@TODO: this throws an error sometimes.
916
+ entry[assoc.options.foreignKey] = saved_record[assoc.options.localKey];
917
+
918
+ // Add the task
919
+ tasks.push(function doSave(next) {
920
+ var a_model = that.getModel(assoc.modelName);
921
+ a_model.save(entry, next);
922
+ });
923
+ });
924
+
925
+ Function.parallel(tasks, next);
926
+ }, function done(err) {
927
+
928
+ if (err) {
929
+ return;
930
+ }
931
+
932
+ return saved_record;
933
+ });
934
+
935
+ pledge.handleCallback(callback);
936
+
937
+ return pledge;
938
+ });
939
+
940
+ /**
941
+ * Look for the record id by checking the indexes
942
+ *
943
+ * @author Jelle De Loecker <jelle@develry.be>
944
+ * @since 0.1.0
945
+ * @version 1.1.0
946
+ *
947
+ * @param {Document} document
948
+ * @param {Object} options
949
+ * @param {Function} callback
950
+ */
951
+ Model.setMethod(function auditRecord(document, options, callback) {
952
+
953
+ var that = this,
954
+ results,
955
+ schema,
956
+ tasks;
957
+
958
+ if (!document) {
959
+ return callback(new Error('No record was given to audit'));
960
+ }
961
+
962
+ schema = this.schema;
963
+
964
+ if (schema && document[this.primary_key] == null && options.audit !== false) {
965
+ tasks = {};
966
+ results = {};
967
+
968
+ if (options.debug) {
969
+ console.log('Pre-save audit record', document);
970
+ }
971
+
972
+ schema.eachAlternateIndex(document, function iterIndex(index, indexName) {
973
+
974
+ if (options.debug) {
975
+ console.log('Checking alternate index', indexName);
976
+ }
977
+
978
+ tasks[indexName] = function auditIndex(next) {
979
+ var query = {},
980
+ fieldName;
981
+
982
+ for (fieldName in index.fields) {
983
+ if (document[fieldName] != null) {
984
+ query[fieldName] = document[fieldName];
985
+
986
+ // @todo: should run through the FieldType instance
987
+ if (String(query[fieldName]).isObjectId()) {
988
+ query[fieldName] = alchemy.castObjectId(query[fieldName]);
989
+ }
990
+ }
991
+ }
992
+
993
+ that.datasource.read(that, query, {}, function gotRecordInfo(err, records) {
994
+
995
+ if (err != null) {
996
+ return next(err);
997
+ }
998
+
999
+ if (records[0] != null) {
1000
+ results[indexName] = records[0];
1001
+ }
1002
+
1003
+ next();
1004
+ });
1005
+
1006
+ };
1007
+ });
1008
+
1009
+ Function.parallel(tasks, function doneAudit(err) {
1010
+
1011
+ var indexName,
1012
+ record,
1013
+ count,
1014
+ ids;
1015
+
1016
+ if (err != null) {
1017
+ return callback(err);
1018
+ }
1019
+
1020
+ if (!Object.isEmpty(results)) {
1021
+
1022
+ count = 0;
1023
+ ids = {};
1024
+
1025
+ for (indexName in results) {
1026
+ record = results[indexName];
1027
+
1028
+ // First make sure this index is allowed during the audit
1029
+ // If it's not, this means it should be considered a duplicate
1030
+ if (options.allowedIndexes != null && !Object.hasValue(options.allowedIndexes, indexName)) {
1031
+ if (callback) callback(new Error('Duplicate index found other than _id: ' + indexName), null);
1032
+ return;
1033
+ }
1034
+
1035
+ // Add the id a first time
1036
+ if (ids[record[that.primary_key]] == null) {
1037
+ count++;
1038
+ ids[record[that.primary_key]] = true;
1039
+ }
1040
+ }
1041
+
1042
+ // If more than 1 ids are found, we can't update the item
1043
+ // because we don't know which record is the actual owner
1044
+ if (count > 1) {
1045
+ if (callback) callback(new Error('Multiple unique records found'));
1046
+ return;
1047
+ }
1048
+
1049
+ // Use the last found record to get the id
1050
+ document[that.primary_key] = record[that.primary_key];
1051
+ }
1052
+
1053
+ if (options.debug) {
1054
+ console.log('Audit done, found pk:', document[that.primary_key]);
1055
+ }
1056
+
1057
+ callback(null, document);
1058
+ });
1059
+
1060
+ return;
1061
+ }
1062
+
1063
+ setImmediate(function skippedAudit() {
1064
+ callback(null, document);
1065
+ });
1066
+ });
1067
+
1068
+ /**
1069
+ * Turn a record into something the database will understand
1070
+ *
1071
+ * @author Jelle De Loecker <jelle@develry.be>
1072
+ * @since 1.0.3
1073
+ * @version 1.0.4
1074
+ *
1075
+ * @param {Document} record
1076
+ * @param {Object} options
1077
+ * @param {Function} callback
1078
+ *
1079
+ * @return {Pledge}
1080
+ */
1081
+ Model.setMethod(function convertRecordToDatasourceFormat(record, options, callback) {
1082
+
1083
+ var that = this,
1084
+ pledge,
1085
+ data;
1086
+
1087
+ if (typeof options == 'function') {
1088
+ callback = options;
1089
+ options = {};
1090
+ }
1091
+
1092
+ if (!options) {
1093
+ options = {};
1094
+ }
1095
+
1096
+ data = record[this.name] || record;
1097
+
1098
+ // Normalize the data
1099
+ data = this.compose(data, options);
1100
+
1101
+ pledge = this.datasource.toDatasource(this, data);
1102
+
1103
+ pledge.handleCallback(callback);
1104
+
1105
+ return pledge;
1106
+ });
1107
+
1108
+ /**
1109
+ * Process an object of datasource format
1110
+ *
1111
+ * @author Jelle De Loecker <jelle@develry.be>
1112
+ * @since 1.0.3
1113
+ * @version 1.0.3
1114
+ *
1115
+ * @param {Object} ds_data
1116
+ * @param {Object} options
1117
+ * @param {Function} callback
1118
+ *
1119
+ * @return {Pledge}
1120
+ */
1121
+ Model.setMethod(function processDatasourceFormat(ds_data, options, callback) {
1122
+
1123
+ var that = this,
1124
+ pledge,
1125
+ data;
1126
+
1127
+ if (typeof options == 'function') {
1128
+ callback = options;
1129
+ options = {};
1130
+ }
1131
+
1132
+ if (!options) {
1133
+ options = {};
1134
+ }
1135
+
1136
+ pledge = this.datasource.toApp(this.schema, {}, options, ds_data);
1137
+
1138
+ pledge.handleCallback(callback);
1139
+
1140
+ return pledge;
1141
+ });
1142
+
1143
+ /**
1144
+ * Get the title to display for this record
1145
+ *
1146
+ * @author Jelle De Loecker <jelle@develry.be>
1147
+ * @since 0.0.1
1148
+ * @version 0.1.0
1149
+ *
1150
+ * @param {Object} item The record item of this model
1151
+ * @param {String|Array} fallbacks Extra fallbacks to use
1152
+ *
1153
+ * @return {String} The display title to use
1154
+ */
1155
+ Model.setMethod(function getDisplayTitle(item, fallbacks) {
1156
+
1157
+ var fields,
1158
+ field,
1159
+ main,
1160
+ val,
1161
+ i;
1162
+
1163
+ if (!item) {
1164
+ return 'Undefined item';
1165
+ }
1166
+
1167
+ if (item[this.modelName]) {
1168
+ main = item[this.modelName];
1169
+ } else {
1170
+ main = item;
1171
+ }
1172
+
1173
+ if (!main) {
1174
+ return 'Undefined item';
1175
+ }
1176
+
1177
+ fields = Array.cast(this.displayField);
1178
+
1179
+ if (fallbacks) {
1180
+ fields = fields.concat(fallbacks);
1181
+ }
1182
+
1183
+ for (i = 0; i < fields.length; i++) {
1184
+ val = main[fields[i]];
1185
+
1186
+ if (Object.isObject(val)) {
1187
+ field = this.getField(fields[i]);
1188
+
1189
+ if (field && field.isTranslatable) {
1190
+ val = alchemy.pickTranslation(this.conduit, val).result;
1191
+ }
1192
+ }
1193
+
1194
+ if (val && typeof val == 'string') {
1195
+ return val;
1196
+ }
1197
+ }
1198
+
1199
+ return main[this.primary_key] || '';
1200
+ });
1201
+
1202
+ /**
1203
+ * Clear the cache of this and all associated models
1204
+ *
1205
+ * @author Jelle De Loecker <jelle@develry.be>
1206
+ * @since 0.0.1
1207
+ * @version 1.1.6
1208
+ *
1209
+ * @param {Boolean} associated Also nuke associated models
1210
+ * @param {Branch} parent
1211
+ */
1212
+ Model.setMethod(function nukeCache(associated, parent) {
1213
+
1214
+ let model_name,
1215
+ branch,
1216
+ alias;
1217
+
1218
+ // Nuke associated caches by default
1219
+ if (typeof associated == 'undefined') {
1220
+ associated = true;
1221
+ }
1222
+
1223
+ // Create the parent branch object
1224
+ if (!parent) {
1225
+ branch = parent = new Classes.Branch.Data(this.name);
1226
+ }
1227
+
1228
+ if (branch || !parent.root.seen(this.name)) {
1229
+
1230
+ if (!branch) {
1231
+ branch = parent.append(this.name);
1232
+ }
1233
+
1234
+ if (this.cache) {
1235
+ this.cache.reset();
1236
+ }
1237
+
1238
+ // Also nuke the cache of the client model, if it exists
1239
+ if (Classes.Alchemy.Client.Model[this.constructor.name] && Classes.Alchemy.Client.Model[this.constructor.name].cache) {
1240
+ Classes.Alchemy.Client.Model[this.constructor.name].cache.reset();
1241
+ }
1242
+ }
1243
+
1244
+ // Return if we don't need to nuke associated models
1245
+ if (!associated) {
1246
+ return;
1247
+ }
1248
+
1249
+ for (alias in this.associations) {
1250
+ model_name = this.associations[alias].modelName;
1251
+
1252
+ if (!parent.root.seen(model_name)) {
1253
+ let assoc_model = this.getModel(model_name);
1254
+ assoc_model.nukeCache(true, branch);
1255
+ }
1256
+ }
1257
+ });
1258
+
1259
+ /**
1260
+ * Delete the given record id
1261
+ *
1262
+ * @author Jelle De Loecker <jelle@develry.be>
1263
+ * @since 0.0.1
1264
+ * @version 1.0.7
1265
+ *
1266
+ * @param {String} id The object id
1267
+ * @param {Function} callback
1268
+ *
1269
+ * @return {Pledge}
1270
+ */
1271
+ Model.setMethod(function remove(id, callback) {
1272
+
1273
+ var that = this,
1274
+ pledge = new Pledge(),
1275
+ id;
1276
+
1277
+ pledge.handleCallback(callback);
1278
+
1279
+ if (!id) {
1280
+ pledge.reject(new Error('Invalid id given!'));
1281
+ return pledge;
1282
+ }
1283
+
1284
+ if (this.datasource.supports('objectid')) {
1285
+ id = alchemy.castObjectId(id);
1286
+ } else {
1287
+ id = String(id);
1288
+ }
1289
+
1290
+ let query = {};
1291
+ query[this.primary_key] = id;
1292
+
1293
+ this.datasource.remove(this, query, {}, function afterRemove(err, result) {
1294
+
1295
+ if (err != null) {
1296
+ return pledge.reject(err);
1297
+ }
1298
+
1299
+ that.emit('removed', result, {}, false, function afterRemovedEvent() {
1300
+ pledge.resolve(result);
1301
+ });
1302
+ });
1303
+
1304
+ return pledge;
1305
+ });
1306
+
1307
+ /**
1308
+ * Get all the records and perform the given task on them
1309
+ *
1310
+ * @author Jelle De Loecker <jelle@develry.be>
1311
+ * @since 0.5.0
1312
+ * @version 1.2.0
1313
+ *
1314
+ * @param {Object} options Find options
1315
+ * @param {Function} task Task to perform on each record
1316
+ * @param {Function} callback Function to call when done
1317
+ */
1318
+ Model.setMethod(function eachRecord(options, task, callback) {
1319
+
1320
+ var that = this,
1321
+ parallel_limit,
1322
+ available = null,
1323
+ last_id = null,
1324
+ pledge = new Classes.Pledge(),
1325
+ index = 0;
1326
+
1327
+ if (typeof options == 'function') {
1328
+ callback = task;
1329
+ task = options;
1330
+ options = {};
1331
+ } else if (!options) {
1332
+ options = {};
1333
+ }
1334
+
1335
+ if (!callback) {
1336
+ callback = Function.thrower;
1337
+ }
1338
+
1339
+ // Apply default limit of 50 records per fetch
1340
+ options = Object.assign({}, {limit: 50}, options);
1341
+
1342
+ // Get amount of tasks to do in parallel
1343
+ parallel_limit = options.parallel_limit || 8;
1344
+
1345
+ // Sort by _id ascending
1346
+ if (!options.sort) {
1347
+ options.sort = {};
1348
+ options.sort[this.primary_key] = 1;
1349
+ }
1350
+
1351
+ // Make sure there is a conditions object
1352
+ if (!options.conditions) {
1353
+ options.conditions = {};
1354
+ }
1355
+
1356
+ this.find('all', options, function gotRecords(err, result) {
1357
+
1358
+ var tasks = [];
1359
+
1360
+ if (!result.length) {
1361
+ pledge.reportProgress(100);
1362
+ pledge.resolve();
1363
+ return callback(null);
1364
+ }
1365
+
1366
+ if (available == null) {
1367
+ available = result.available;
1368
+ options.available = false;
1369
+ } else {
1370
+ result.available = available;
1371
+ }
1372
+
1373
+ result.forEach(function eachRecord(record) {
1374
+
1375
+ var record_index = index++;
1376
+
1377
+ last_id = record[that.model_name][that.primary_key];
1378
+
1379
+ tasks.push(function doSave(next) {
1380
+ pledge.reportProgress(((record_index - 1) / available) * 100);
1381
+ task.call(that, record, record_index, next);
1382
+ });
1383
+ });
1384
+
1385
+ Function.parallel(parallel_limit, tasks, function done(err) {
1386
+
1387
+ if (err) {
1388
+ pledge.reject(err);
1389
+ return callback(err);
1390
+ }
1391
+
1392
+ let next_options = Object.assign({}, options);
1393
+
1394
+ // Get records with a bigger _id than the last found
1395
+ next_options.conditions[that.primary_key] = {$gt: last_id};
1396
+
1397
+ that.find('all', next_options, gotRecords);
1398
+ });
1399
+ });
1400
+
1401
+ return pledge;
1402
+ });
1403
+
1404
+ /**
1405
+ * Strip out private fields
1406
+ *
1407
+ * @author Jelle De Loecker <jelle@develry.be>
1408
+ * @since 1.0.0
1409
+ * @version 1.0.0
1410
+ *
1411
+ * @param {Array} records
1412
+ */
1413
+ Model.setMethod(function removePrivateFields(records) {
1414
+
1415
+ var has_private_fields,
1416
+ fields = this.schema.getSorted(false),
1417
+ record,
1418
+ field,
1419
+ i,
1420
+ j;
1421
+
1422
+ records = Array.cast(records);
1423
+
1424
+ for (i = 0; i < records.length; i++) {
1425
+ record = records[i];
1426
+
1427
+ for (j = 0; j < fields.length; j++) {
1428
+ field = fields[j];
1429
+
1430
+ if (field.is_private) {
1431
+ has_private_fields = true;
1432
+ delete record[field.name];
1433
+ }
1434
+ }
1435
+
1436
+ // If there are no private fields, break loop
1437
+ if (!has_private_fields) {
1438
+ break;
1439
+ }
1440
+ }
1441
+
1442
+ return records;
1443
+ });
1444
+
1445
+ /**
1446
+ * Create an export stream
1447
+ *
1448
+ * @author Jelle De Loecker <jelle@develry.be>
1449
+ * @since 1.0.5
1450
+ * @version 1.0.5
1451
+ *
1452
+ * @param {Stream} output
1453
+ * @param {Object} options
1454
+ *
1455
+ * @return {Pledge}
1456
+ */
1457
+ Model.setMethod(function exportToStream(output, options) {
1458
+
1459
+ if (!alchemy.isStream(output)) {
1460
+ if (!options) {
1461
+ options = output;
1462
+ output = null;
1463
+ }
1464
+
1465
+ output = options.output;
1466
+ }
1467
+
1468
+ if (!output) {
1469
+ return Pledge.reject(new Error('No target output stream has been given'));
1470
+ }
1471
+
1472
+ if (!options) {
1473
+ options = {};
1474
+ }
1475
+
1476
+ // Only allow 1 task to run at a time
1477
+ options.parallel_limit = 1;
1478
+
1479
+ let that = this,
1480
+ name_buf = Buffer.from(this.model_name),
1481
+ head_buf;
1482
+
1483
+ // 0x01 is a model
1484
+ head_buf = Buffer.concat([Buffer.from([0x01, name_buf.length]), name_buf]);
1485
+
1486
+ output.write(head_buf);
1487
+
1488
+ return this.eachRecord(options, function eachRecord(record, index, next) {
1489
+ record.exportToStream(output).done(next);
1490
+ }, function done(err) {
1491
+
1492
+ });
1493
+ });
1494
+
1495
+ /**
1496
+ * Import from a stream
1497
+ *
1498
+ * @author Jelle De Loecker <jelle@develry.be>
1499
+ * @since 1.0.5
1500
+ * @version 1.0.5
1501
+ *
1502
+ * @param {Stream} input
1503
+ * @param {Object} options
1504
+ *
1505
+ * @return {Pledge}
1506
+ */
1507
+ Model.setMethod(function importFromStream(input, options) {
1508
+
1509
+ if (!alchemy.isStream(input)) {
1510
+ if (!options) {
1511
+ options = input;
1512
+ input = null;
1513
+ }
1514
+
1515
+ input = options.input;
1516
+ }
1517
+
1518
+ if (!input) {
1519
+ return Pledge.reject(new Error('No source input stream has been given'));
1520
+ }
1521
+
1522
+ let that = this,
1523
+ current_type = null,
1524
+ extra_stream,
1525
+ pledge = new Pledge(),
1526
+ stopped,
1527
+ paused,
1528
+ buffer,
1529
+ value,
1530
+ seen = 0,
1531
+ left,
1532
+ size,
1533
+ doc;
1534
+
1535
+ input.on('data', function onData(data) {
1536
+
1537
+ if (stopped) {
1538
+ return;
1539
+ }
1540
+
1541
+ if (buffer) {
1542
+ buffer = Buffer.concat([buffer, data]);
1543
+ } else {
1544
+ buffer = data;
1545
+ }
1546
+
1547
+ handleBuffer();
1548
+ });
1549
+
1550
+ function handleBuffer() {
1551
+
1552
+ if (paused) {
1553
+ return;
1554
+ }
1555
+
1556
+ if (!current_type && buffer.length < 2) {
1557
+ return;
1558
+ }
1559
+
1560
+ if (!current_type) {
1561
+ current_type = buffer.readUInt8(0);
1562
+
1563
+ if (current_type == 0x01) {
1564
+ size = buffer.readUInt8(1);
1565
+ buffer = buffer.slice(2);
1566
+ } else if (current_type == 0x02 && buffer.length >= 5) {
1567
+ size = buffer.readUInt32BE(1);
1568
+ buffer = buffer.slice(5);
1569
+ } else if (current_type == 0xFF) {
1570
+ size = buffer.readUInt32BE(1);
1571
+ buffer = buffer.slice(5);
1572
+ seen = 0;
1573
+
1574
+ if (!doc) {
1575
+ stopped = true;
1576
+ pledge.reject(new Error('Found extra import data, but no active document'));
1577
+ } else {
1578
+ extra_stream = new require('stream').PassThrough();
1579
+ doc.extraImportFromStream(extra_stream);
1580
+ }
1581
+ } else {
1582
+ // Not enough data? Wait
1583
+ current_type = null;
1584
+ return;
1585
+ }
1586
+ }
1587
+
1588
+ handleRest();
1589
+ }
1590
+
1591
+ function handleRest() {
1592
+
1593
+ if (current_type == 0xFF) {
1594
+ left = size - seen;
1595
+ value = buffer.slice(0, left);
1596
+
1597
+ seen += value.length;
1598
+
1599
+ if (value.length == buffer.length) {
1600
+ buffer = null;
1601
+ } else if (value.length < buffer.length) {
1602
+ buffer = buffer.slice(left);
1603
+ }
1604
+
1605
+ extra_stream.write(value);
1606
+
1607
+ if (value.length == left) {
1608
+ extra_stream.end();
1609
+ current_type = null;
1610
+
1611
+ if (buffer) {
1612
+ handleBuffer();
1613
+ }
1614
+ }
1615
+
1616
+ return;
1617
+ }
1618
+
1619
+ if (buffer.length >= size) {
1620
+ value = buffer.slice(0, size);
1621
+ buffer = buffer.slice(size);
1622
+ } else {
1623
+ // Wait for next call
1624
+ return;
1625
+ }
1626
+
1627
+ if (current_type == 0x01) {
1628
+ value = value.toString();
1629
+
1630
+ if (value == that.model_name) {
1631
+ // Found name!
1632
+ current_type = null;
1633
+ size = 0;
1634
+ } else {
1635
+ stopped = true;
1636
+ return pledge.reject(new Error('Model names do not match'));
1637
+ }
1638
+ } else if (current_type == 0x02) {
1639
+ doc = that.createDocument();
1640
+ input.pause();
1641
+ paused = true;
1642
+
1643
+ doc.importFromBuffer(value).done(function done(err, result) {
1644
+
1645
+ if (err) {
1646
+ stopped = true;
1647
+ return pledge.reject(err);
1648
+ }
1649
+
1650
+ current_type = null;
1651
+ paused = false;
1652
+ input.resume();
1653
+
1654
+ handleBuffer();
1655
+ });
1656
+
1657
+ return;
1658
+ }
1659
+
1660
+ if (buffer && buffer.length) {
1661
+ handleBuffer();
1662
+ }
1663
+ }
1664
+
1665
+ return pledge;
1666
+ });
1667
+
1668
+ /**
1669
+ * Get a model
1670
+ *
1671
+ * @author Jelle De Loecker <jelle@develry.be>
1672
+ * @since 0.0.1
1673
+ * @version 1.1.0
1674
+ *
1675
+ * @param {String} name
1676
+ * @param {Boolean} init
1677
+ *
1678
+ * @return {Model}
1679
+ */
1680
+ Model.get = function get(name, init, options) {
1681
+
1682
+ var constructor,
1683
+ namespace,
1684
+ pieces,
1685
+ path,
1686
+ obj;
1687
+
1688
+ if (typeof name == 'function') {
1689
+ name = name.name;
1690
+ }
1691
+
1692
+ if (!name) {
1693
+ throw new TypeError('Model name should be a non-empty string');
1694
+ }
1695
+
1696
+ if (init && typeof init == 'object') {
1697
+ options = init;
1698
+ init = true;
1699
+ }
1700
+
1701
+ if (!options) {
1702
+ options = {};
1703
+ }
1704
+
1705
+ if (nameCache[name]) {
1706
+ if (init === false) {
1707
+ return nameCache[name];
1708
+ } else {
1709
+ return new nameCache[name];
1710
+ }
1711
+ }
1712
+
1713
+ pieces = name.split('.');
1714
+
1715
+ if (pieces.length > 1) {
1716
+ // The first part is the namespace
1717
+ namespace = pieces.shift();
1718
+
1719
+ // The rest should be the path
1720
+ path = pieces.join('.');
1721
+
1722
+ obj = Classes[namespace];
1723
+
1724
+ if (!obj) {
1725
+ if (init === false) {
1726
+ return null;
1727
+ }
1728
+
1729
+ throw new TypeError('Namespace "' + namespace + '" could not be found');
1730
+ }
1731
+ } else {
1732
+ path = name;
1733
+ obj = Classes.Alchemy.Model;
1734
+ }
1735
+
1736
+ constructor = Object.path(obj, path) || obj[String(path).modelName()];
1737
+
1738
+ if (constructor == null) {
1739
+ if (init === false) {
1740
+ return null;
1741
+ }
1742
+
1743
+ throw new Error('Could not find model "' + name + '"');
1744
+ }
1745
+
1746
+ // Store this name in the cache,
1747
+ // so we don't have to perform the expensive #modelName() method again
1748
+ nameCache[name] = constructor;
1749
+
1750
+ if (init === false) {
1751
+ return constructor;
1752
+ } else {
1753
+ return new constructor;
1754
+ }
1755
+ };
1756
+
1757
+ /**
1758
+ * Make the base Model class a global
1759
+ *
1760
+ * @author Jelle De Loecker <jelle@develry.be>
1761
+ * @since 0.0.1
1762
+ * @version 0.2.0
1763
+ *
1764
+ * @type {Object}
1765
+ */
1773
1766
  global.Model = Model;