alchemymvc 1.3.21 → 1.4.0-alpha.1

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 (155) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +3 -3
  3. package/lib/app/behaviour/publishable_behaviour.js +5 -5
  4. package/lib/app/behaviour/revision_behaviour.js +10 -10
  5. package/lib/app/behaviour/sluggable_behaviour.js +14 -14
  6. package/lib/app/conduit/electron_conduit.js +9 -9
  7. package/lib/app/conduit/http_conduit.js +13 -13
  8. package/lib/app/conduit/loopback_conduit.js +15 -15
  9. package/lib/app/conduit/socket_conduit.js +43 -43
  10. package/lib/app/config/routes.js +26 -0
  11. package/lib/app/controller/00-default_app_controller.js +21 -0
  12. package/lib/app/controller/alchemy_info_controller.js +12 -12
  13. package/lib/app/datasource/mongo_datasource.js +16 -16
  14. package/lib/app/element/00-default_app_element.js +19 -0
  15. package/lib/app/element/time_ago.js +5 -5
  16. package/lib/app/helper/00-default_app_helper.js +11 -0
  17. package/lib/app/helper/alchemy_helper.js +22 -22
  18. package/lib/app/helper/backed_map.js +1 -1
  19. package/lib/app/helper/breadcrumb.js +10 -10
  20. package/lib/app/helper/client_collection.js +3 -3
  21. package/lib/app/helper/cron.js +29 -29
  22. package/lib/app/helper/enum_values.js +6 -6
  23. package/lib/app/helper/pagination_helper.js +36 -36
  24. package/lib/app/helper/router_helper.js +35 -35
  25. package/lib/app/helper/socket_helper.js +57 -57
  26. package/lib/app/helper/syncable.js +84 -59
  27. package/lib/app/helper_component/paginate_component.js +9 -9
  28. package/lib/app/helper_controller/component.js +1 -1
  29. package/lib/app/helper_controller/conduit.js +31 -31
  30. package/lib/app/helper_controller/controller.js +54 -39
  31. package/lib/app/helper_datasource/00-nosql_datasource.js +624 -70
  32. package/lib/app/helper_datasource/05-fallback_datasource.js +10 -10
  33. package/lib/app/helper_datasource/idb_datasource.js +6 -6
  34. package/lib/app/helper_datasource/indexed_db.js +22 -22
  35. package/lib/app/helper_datasource/remote_datasource.js +5 -5
  36. package/lib/app/helper_error/http_error.js +4 -4
  37. package/lib/app/helper_error/model_error.js +2 -2
  38. package/lib/app/helper_error/validation_error.js +12 -12
  39. package/lib/app/helper_field/00-objectid_field.js +7 -7
  40. package/lib/app/helper_field/05-string_field.js +16 -12
  41. package/lib/app/helper_field/06-text_field.js +2 -4
  42. package/lib/app/helper_field/10-number_field.js +9 -12
  43. package/lib/app/helper_field/11-date_field.js +15 -15
  44. package/lib/app/helper_field/15-local_temporal_field.js +10 -10
  45. package/lib/app/helper_field/20-decimal_field.js +8 -9
  46. package/lib/app/helper_field/belongsto_field.js +1 -1
  47. package/lib/app/helper_field/big_int_field.js +8 -8
  48. package/lib/app/helper_field/boolean_field.js +9 -11
  49. package/lib/app/helper_field/datetime_field.js +3 -3
  50. package/lib/app/helper_field/enum_field.js +13 -8
  51. package/lib/app/helper_field/fixed_decimal_field.js +6 -7
  52. package/lib/app/helper_field/geopoint_field.js +9 -10
  53. package/lib/app/helper_field/habtm_field.js +3 -3
  54. package/lib/app/helper_field/hasoneparent_field.js +1 -1
  55. package/lib/app/helper_field/html_field.js +2 -4
  56. package/lib/app/helper_field/integer_field.js +8 -11
  57. package/lib/app/helper_field/local_date_field.js +5 -5
  58. package/lib/app/helper_field/local_date_time_field.js +5 -5
  59. package/lib/app/helper_field/local_time_field.js +5 -5
  60. package/lib/app/helper_field/mixed_field.js +5 -5
  61. package/lib/app/helper_field/object_field.js +8 -8
  62. package/lib/app/helper_field/password_field.js +3 -3
  63. package/lib/app/helper_field/regexp_field.js +7 -9
  64. package/lib/app/helper_field/schema_field.js +91 -88
  65. package/lib/app/helper_field/settings_field.js +92 -0
  66. package/lib/app/helper_field/time_field.js +6 -6
  67. package/lib/app/helper_field/url_field.js +2 -4
  68. package/lib/app/helper_model/00-base_criteria.js +662 -0
  69. package/lib/app/helper_model/05-criteria_expressions.js +605 -0
  70. package/lib/app/helper_model/10-model_criteria.js +1182 -0
  71. package/lib/app/helper_model/data_provider.js +2 -2
  72. package/lib/app/helper_model/document.js +103 -92
  73. package/lib/app/helper_model/document_list.js +14 -14
  74. package/lib/app/helper_model/field_config.js +11 -11
  75. package/lib/app/helper_model/field_set.js +17 -17
  76. package/lib/app/helper_model/model.js +203 -124
  77. package/lib/app/helper_model/remote_data_provider.js +2 -2
  78. package/lib/app/helper_validator/00_validator.js +16 -16
  79. package/lib/app/helper_validator/not_empty_validator.js +9 -9
  80. package/lib/app/model/00-default_app_model.js +18 -0
  81. package/lib/app/model/05-system_model.js +27 -0
  82. package/lib/app/model/{alchemy_migration_model.js → system_migration_model.js} +4 -4
  83. package/lib/app/model/system_setting_model.js +154 -0
  84. package/lib/app/model/{alchemy_task_history_model.js → system_task_history_model.js} +7 -7
  85. package/lib/app/model/{alchemy_task_model.js → system_task_model.js} +11 -11
  86. package/lib/bootstrap.js +22 -312
  87. package/lib/class/accumulator.js +5 -5
  88. package/lib/class/behaviour.js +5 -5
  89. package/lib/class/component.js +3 -3
  90. package/lib/class/conduit.js +203 -163
  91. package/lib/class/controller.js +42 -42
  92. package/lib/class/datasource.js +74 -79
  93. package/lib/class/document.js +74 -95
  94. package/lib/class/document_list.js +5 -5
  95. package/lib/class/element.js +17 -17
  96. package/lib/class/error.js +3 -3
  97. package/lib/class/field.js +169 -91
  98. package/lib/class/field_value.js +6 -6
  99. package/lib/class/helper.js +3 -3
  100. package/lib/class/inode.js +17 -17
  101. package/lib/class/inode_dir.js +12 -12
  102. package/lib/class/inode_file.js +50 -25
  103. package/lib/class/inode_list.js +4 -4
  104. package/lib/class/migration.js +4 -4
  105. package/lib/class/model.js +182 -168
  106. package/lib/class/path_definition.js +22 -22
  107. package/lib/class/path_evaluator.js +5 -5
  108. package/lib/class/path_param_definition.js +7 -7
  109. package/lib/class/plugin.js +312 -0
  110. package/lib/class/postponement.js +29 -29
  111. package/lib/class/reciprocal.js +8 -8
  112. package/lib/class/route.js +33 -33
  113. package/lib/class/router.js +73 -73
  114. package/lib/class/schema.js +21 -21
  115. package/lib/class/schema_client.js +73 -67
  116. package/lib/class/session.js +63 -29
  117. package/lib/class/session_scene.js +4 -4
  118. package/lib/class/sitemap.js +16 -16
  119. package/lib/class/task.js +39 -39
  120. package/lib/class/task_service.js +43 -47
  121. package/lib/{init → core}/alchemy.js +413 -374
  122. package/lib/{init/functions.js → core/alchemy_functions.js} +171 -108
  123. package/lib/core/alchemy_load_functions.js +715 -0
  124. package/lib/core/base.js +50 -62
  125. package/lib/core/client_alchemy.js +144 -152
  126. package/lib/core/client_base.js +39 -52
  127. package/lib/core/discovery.js +16 -18
  128. package/lib/core/middleware.js +54 -43
  129. package/lib/core/{routing.js → prefix.js} +14 -16
  130. package/lib/core/setting.js +1684 -0
  131. package/lib/core/stage.js +758 -0
  132. package/lib/scripts/create_constants.js +119 -0
  133. package/lib/{init/languages.js → scripts/create_languages.js} +5 -5
  134. package/lib/scripts/create_settings.js +449 -0
  135. package/lib/scripts/create_shared_constants.js +95 -0
  136. package/lib/scripts/create_stages.js +55 -0
  137. package/lib/scripts/init_alchemy.js +51 -0
  138. package/lib/{init/requirements.js → scripts/preload_modules.js} +15 -2
  139. package/lib/scripts/setup_devwatch.js +238 -0
  140. package/lib/stages/00-load_core.js +342 -0
  141. package/lib/stages/05-load_app.js +57 -0
  142. package/lib/stages/10-datasource.js +61 -0
  143. package/lib/stages/15-tasks.js +27 -0
  144. package/lib/stages/20-settings.js +68 -0
  145. package/lib/stages/50-routes.js +218 -0
  146. package/lib/stages/90-server.js +347 -0
  147. package/package.json +5 -7
  148. package/lib/app/helper_model/criteria.js +0 -2294
  149. package/lib/app/helper_model/db_query.js +0 -1488
  150. package/lib/app/routes.js +0 -11
  151. package/lib/core/socket.js +0 -171
  152. package/lib/init/constants.js +0 -158
  153. package/lib/init/devwatch.js +0 -238
  154. package/lib/init/load_functions.js +0 -973
  155. package/lib/stages.js +0 -513
@@ -0,0 +1,1182 @@
1
+ const CriteriaNS = Function.getNamespace('Alchemy.Criteria'),
2
+ Expressions = Function.getNamespace('Alchemy.Criteria.Expression');
3
+
4
+ /**
5
+ * The Criteria class
6
+ *
7
+ * @author Jelle De Loecker <jelle@elevenways.be>
8
+ * @since 1.1.0
9
+ * @version 1.4.0
10
+ *
11
+ * @param {Model} model
12
+ */
13
+ const Criteria = Function.inherits('Alchemy.Criteria.Criteria', function Model(model) {
14
+
15
+ // The model
16
+ this.model = model;
17
+
18
+ Model.super.call(this, {
19
+ select : new Select(this),
20
+ document : true,
21
+ document_list : true,
22
+ });
23
+ });
24
+
25
+ /**
26
+ * Make sure to get a criteria
27
+ *
28
+ * @author Jelle De Loecker <jelle@elevenways.be>
29
+ * @since 1.2.5
30
+ * @version 1.4.0
31
+ *
32
+ * @param {Object} conditions The thing that should be a criteria
33
+ * @param {Object} options The options to apply
34
+ * @param {Model} model The model that it probably belongs to
35
+ *
36
+ * @return {Criteria}
37
+ */
38
+ Criteria.setStatic(function cast(conditions, options, model) {
39
+
40
+ if (Criteria.isCriteria(conditions)) {
41
+ return conditions;
42
+ }
43
+
44
+ if (arguments.length == 2) {
45
+ model = options;
46
+ options = conditions;
47
+ conditions = null;
48
+ }
49
+
50
+ let instance = new Criteria(model);
51
+
52
+ if (options) {
53
+ instance.applyOldOptions(options);
54
+ }
55
+
56
+ if (conditions) {
57
+ instance.applyConditions(conditions);
58
+ }
59
+
60
+ return instance;
61
+ });
62
+
63
+ /**
64
+ * Undry the given object
65
+ *
66
+ * @author Jelle De Loecker <jelle@elevenways.be>
67
+ * @since 1.1.0
68
+ * @version 1.2.3
69
+ *
70
+ * @param {Object} data
71
+ *
72
+ * @return {Criteria}
73
+ */
74
+ Criteria.setStatic(function unDry(data) {
75
+
76
+ var criteria = new Criteria();
77
+
78
+ if (data.model) {
79
+ try {
80
+ criteria.model = alchemy.getModel(data.model);
81
+ } catch (err) {
82
+ // Ignore
83
+ console.warn('Failed to find "' + data.model + '" model');
84
+ }
85
+ }
86
+
87
+ // Revive the group instance
88
+ criteria.group = Expressions.Group.revive(data.group, criteria);
89
+
90
+ if (!data.options) {
91
+ data.options = {};
92
+ }
93
+
94
+ // Revive the select
95
+ data.options.select = Select.revive(data.options.select, criteria);
96
+
97
+ criteria.options = data.options || {};
98
+
99
+ return criteria;
100
+ });
101
+
102
+ /**
103
+ * Parse a path to an object
104
+ *
105
+ * @author Jelle De Loecker <jelle@elevenways.be>
106
+ * @since 1.1.0
107
+ * @version 1.1.0
108
+ *
109
+ * @param {string} path
110
+ * @param {Criteria} criteria
111
+ *
112
+ * @return {Object}
113
+ */
114
+ Criteria.setStatic(function parsePath(path, criteria) {
115
+
116
+ var target_path,
117
+ result = {},
118
+ pieces,
119
+ piece,
120
+ alias,
121
+ i;
122
+
123
+ if (path.indexOf('.') > -1) {
124
+ pieces = path.split('.');
125
+ } else {
126
+ pieces = [path];
127
+ }
128
+
129
+ for (i = 0; i < pieces.length; i++) {
130
+ piece = pieces[i];
131
+ alias = null;
132
+
133
+ if (criteria && criteria.model) {
134
+ if (criteria.model.associations[piece]) {
135
+ alias = piece;
136
+ }
137
+ }
138
+
139
+ if (!alias && piece[0].isUpperCase()) {
140
+ alias = piece;
141
+ }
142
+
143
+ if (alias) {
144
+ if (!result.association) {
145
+ result.association = [];
146
+ }
147
+
148
+ result.association.push(alias);
149
+ continue;
150
+ }
151
+
152
+ target_path = pieces.slice(i).join('.');
153
+ break;
154
+ }
155
+
156
+ if (target_path) {
157
+ result.target_path = target_path;
158
+ }
159
+
160
+ return result;
161
+ });
162
+
163
+ /**
164
+ * Create a reference to the datasource
165
+ *
166
+ * @author Jelle De Loecker <jelle@elevenways.be>
167
+ * @since 1.1.0
168
+ * @version 1.1.0
169
+ *
170
+ * @type {Datasource}
171
+ */
172
+ Criteria.setProperty(function datasource() {
173
+
174
+ if (this._datasource) {
175
+ return this._datasource;
176
+ }
177
+
178
+ if (this.model) {
179
+ return this.model.datasource;
180
+ }
181
+ }, function setDatasource(ds) {
182
+ this._datasource = ds;
183
+ return this._datasource;
184
+ });
185
+
186
+ /**
187
+ * The recursiveness of this criteria
188
+ *
189
+ * @author Jelle De Loecker <jelle@elevenways.be>
190
+ * @since 1.1.0
191
+ * @version 1.1.0
192
+ *
193
+ * @type {number}
194
+ */
195
+ Criteria.setProperty(function recursive_level() {
196
+
197
+ if (this.options.recursive) {
198
+ return this.options.recursive;
199
+ }
200
+
201
+ return 0;
202
+ });
203
+
204
+ /**
205
+ * Allow the criteria to be used in a for wait loop
206
+ *
207
+ * @author Jelle De Loecker <jelle@elevenways.be>
208
+ * @since 1.1.0
209
+ * @version 1.1.0
210
+ */
211
+ Criteria.setMethod(Symbol.asyncIterator, function asyncIterator() {
212
+
213
+ const that = this,
214
+ model = this.model;
215
+
216
+ if (!model) {
217
+ throw new Error('Unable to iterate over a criteria without a model');
218
+ }
219
+
220
+ // Clone it
221
+ let criteria = this.clone();
222
+
223
+ // Set the limit to 1
224
+ criteria.limit(1);
225
+
226
+ // Create the iterator context
227
+ let context = {
228
+ index : 0,
229
+ next : async function next() {
230
+
231
+ criteria.skip(this.index++);
232
+
233
+ let record = await model.find('first', criteria);
234
+
235
+ if (!record) {
236
+ return {done: true};
237
+ }
238
+
239
+ return {value: record, done: false};
240
+ }
241
+ };
242
+
243
+ return context;
244
+ });
245
+
246
+ /**
247
+ * Return object for jsonifying
248
+ *
249
+ * @author Jelle De Loecker <jelle@elevenways.be>
250
+ * @since 1.1.0
251
+ * @version 1.2.3
252
+ *
253
+ * @return {Object}
254
+ */
255
+ Criteria.setMethod(function toJSON() {
256
+
257
+ let result = {},
258
+ options;
259
+
260
+ if (this.model && this.model.name) {
261
+ result.model = this.model.name;
262
+ }
263
+
264
+ if (this.options) {
265
+ let key;
266
+ options = {};
267
+
268
+ for (key in this.options) {
269
+ if (key == 'assoc_cache' || key == 'init_record') {
270
+ continue;
271
+ }
272
+
273
+ options[key] = this.options[key];
274
+ }
275
+ }
276
+
277
+ result.group = this.group;
278
+ result.options = options;
279
+
280
+ return result;
281
+ });
282
+
283
+ /**
284
+ * Clone this instance
285
+ *
286
+ * @author Jelle De Loecker <jelle@elevenways.be>
287
+ * @since 1.1.0
288
+ * @version 1.1.0
289
+ *
290
+ * @return {Criteria}
291
+ */
292
+ Criteria.setMethod(function clone() {
293
+
294
+ var data = JSON.toDryObject(this),
295
+ result;
296
+
297
+ data.model = null;
298
+ result = JSON.undry(data);
299
+
300
+ result.model = this.model;
301
+
302
+ return result;
303
+ });
304
+
305
+ /**
306
+ * Get the main fields to select
307
+ *
308
+ * @author Jelle De Loecker <jelle@elevenways.be>
309
+ * @since 1.1.0
310
+ * @version 1.2.3
311
+ *
312
+ * @return {string[]}
313
+ */
314
+ Criteria.setMethod(function getFieldsToSelect() {
315
+
316
+ let result;
317
+
318
+ if (this.options?.select?.fields?.length) {
319
+ result = this.options.select.fields.slice(0);
320
+ }
321
+
322
+ // Fields can sometimes be required for a query (like in a join) but they
323
+ // won't be selected if other fields are explicitly set.
324
+ // So in that case: add these special fields to the projection
325
+ if (result && this.options?.select?.query_fields) {
326
+ result.push(...this.options.select.query_fields);
327
+ }
328
+
329
+ return result || [];
330
+ });
331
+
332
+ /**
333
+ * Get the association selects, if any
334
+ *
335
+ * @author Jelle De Loecker <jelle@elevenways.be>
336
+ * @since 1.1.0
337
+ * @version 1.1.0
338
+ *
339
+ * @return {Object}
340
+ */
341
+ Criteria.setMethod(function getAssociationsToSelect() {
342
+
343
+ let result = this.options.select.associations;
344
+
345
+ return result;
346
+ });
347
+
348
+ /**
349
+ * Should the given association be queried?
350
+ *
351
+ * @author Jelle De Loecker <jelle@elevenways.be>
352
+ * @since 1.1.0
353
+ * @version 1.1.0
354
+ *
355
+ * @param {string} name
356
+ *
357
+ * @return {boolean}
358
+ */
359
+ Criteria.setMethod(function shouldQueryAssociation(name) {
360
+
361
+ var result = false;
362
+
363
+ // If there are explicit associations selected,
364
+ // then this one has to be in it!
365
+ if (this.getAssociationsToSelect()) {
366
+ result = this.options.select.shouldQueryAssociation(name);
367
+ } else {
368
+ // There are no explicit associations, look at the recursive level
369
+ result = this.recursive_level > 0;
370
+ }
371
+
372
+ return result;
373
+ });
374
+
375
+ /**
376
+ * Get association configuration in the current active model
377
+ * or in the options
378
+ *
379
+ * @author Jelle De Loecker <jelle@elevenways.be>
380
+ * @since 1.1.0
381
+ * @version 1.1.0
382
+ *
383
+ * @param {string} alias
384
+ *
385
+ * @return {Object}
386
+ */
387
+ Criteria.setMethod(function getAssociationConfiguration(alias) {
388
+
389
+ if (this.options.associations && this.options.associations[alias]) {
390
+ return this.options.associations[alias];
391
+ }
392
+
393
+ return this.model.getAssociation(alias);
394
+ });
395
+
396
+ /**
397
+ * Get a new criteria for adding associated data
398
+ *
399
+ * @author Jelle De Loecker <jelle@elevenways.be>
400
+ * @since 1.1.0
401
+ * @version 1.2.3
402
+ *
403
+ * @param {string} name
404
+ * @param {Object} item
405
+ *
406
+ * @return {Criteria}
407
+ */
408
+ Criteria.setMethod(function getCriteriaForAssociation(name, item) {
409
+
410
+ if (!this.model) {
411
+ throw new Error('Unable to create criteria for association "' + name + '" without originating model instance');
412
+ }
413
+
414
+ let assoc_model = this.model.getAliasModel(name),
415
+ data = item[this.model.name];
416
+
417
+ // @TODO: For deadlock reasons we don't query self-referencing links!
418
+ // (Implemented in Schema fields, BelongsTo and such could still pose problems!)
419
+ if (assoc_model.name == this.options.init_model) {
420
+ return;
421
+ }
422
+
423
+ let association = this.getAssociationConfiguration(name);
424
+
425
+ let value = data[association.options.localKey];
426
+
427
+ // If no valid value is found for the associated key, do nothing
428
+ if (value == null) {
429
+ return;
430
+ }
431
+
432
+ let assoc_crit = assoc_model.find(),
433
+ assoc_key = association.options.foreignKey,
434
+ options = this.options,
435
+ select;
436
+
437
+ if (Array.isArray(value)) {
438
+ assoc_crit.where(assoc_key).in(value);
439
+ } else {
440
+ assoc_crit.where(assoc_key).equals(value);
441
+ }
442
+
443
+ assoc_crit.setOption('assoc_key', assoc_key);
444
+ assoc_crit.setOption('assoc_value', value);
445
+
446
+ // Make the assoc_cache if it doesn't exist yet
447
+ if (options.create_references !== false && !options.assoc_cache) {
448
+ options.assoc_cache = {};
449
+ }
450
+
451
+ // Add the assoc_cache
452
+ if (options.assoc_cache) {
453
+ assoc_crit.setOption('assoc_cache', options.assoc_cache);
454
+ }
455
+
456
+ // Take over the locale option
457
+ if (options.locale) {
458
+ assoc_crit.setOption('locale', options.locale);
459
+ }
460
+
461
+ // The debug object, if there is one
462
+ if (options._debugObject) {
463
+ assoc_crit.setOption('_debugObject', options._debugObject);
464
+ }
465
+
466
+ // Don't get the available count
467
+ assoc_crit.setOption('available', false);
468
+
469
+ if (options.select.associations && options.select.associations[name]) {
470
+ select = options.select.associations[name];
471
+ assoc_crit.options.select = select.cloneForCriteria(assoc_crit);
472
+ }
473
+
474
+ // Sort the results
475
+ // @TODO: add sorts
476
+ // if (query.sort && query.sort[alias]) {
477
+ // assocOpts.sort = query.sort[alias];
478
+ // }
479
+
480
+ if (Number.isSafeInteger(options.recursive) && options.recursive > 0) {
481
+ assoc_crit.recursive(options.recursive - 1);
482
+ } else {
483
+ // Disable recursiveness for the next level
484
+ assoc_crit.recursive(0);
485
+ }
486
+
487
+ // Add the model name from where we're adding associated data
488
+ assoc_crit.setOption('init_model', options.init_model || this.model.name);
489
+ assoc_crit.setOption('init_record', options.init_record || item);
490
+
491
+ assoc_crit.setOption('from_alias', options.for_alias);
492
+ assoc_crit.setOption('from_model', options.for_model);
493
+
494
+ assoc_crit.setOption('for_alias', name);
495
+ assoc_crit.setOption('for_model', assoc_model.name);
496
+
497
+ // Honor the original document option
498
+ assoc_crit.setOption('document', options.document);
499
+
500
+ if (options.debug) {
501
+ assoc_crit.setOption('debug', true);
502
+ console.log('Associated criteria:', assoc_model.name, assoc_crit);
503
+ }
504
+
505
+ return assoc_crit;
506
+ });
507
+
508
+ /**
509
+ * Limit the amount of records to get
510
+ *
511
+ * @author Jelle De Loecker <jelle@elevenways.be>
512
+ * @since 1.1.0
513
+ * @version 1.2.7
514
+ *
515
+ * @param {number} amount
516
+ *
517
+ * @return {Criteria}
518
+ */
519
+ Criteria.setMethod(function limit(amount) {
520
+
521
+ if (typeof amount != 'number') {
522
+ amount = parseInt(amount);
523
+ }
524
+
525
+ this.options.limit = amount;
526
+ return this;
527
+ });
528
+
529
+ /**
530
+ * Skip an amount of records
531
+ *
532
+ * @author Jelle De Loecker <jelle@elevenways.be>
533
+ * @since 1.1.0
534
+ * @version 1.2.7
535
+ *
536
+ * @param {number} amount
537
+ *
538
+ * @return {Criteria}
539
+ */
540
+ Criteria.setMethod(function skip(amount) {
541
+
542
+ if (typeof amount != 'number') {
543
+ amount = parseInt(amount);
544
+ }
545
+
546
+ this.options.skip = amount;
547
+ return this;
548
+ });
549
+
550
+ /**
551
+ * Get a specific page
552
+ *
553
+ * @author Jelle De Loecker <jelle@elevenways.be>
554
+ * @since 1.1.3
555
+ * @version 1.2.7
556
+ *
557
+ * @param {number} page A 1-indexed page number
558
+ * @param {number} page_size
559
+ *
560
+ * @return {Criteria}
561
+ */
562
+ Criteria.setMethod(function page(page, page_size) {
563
+
564
+ if (typeof page != 'number') {
565
+ page = parseInt(page);
566
+ }
567
+
568
+ if (page_size && typeof page_size != 'number') {
569
+ page_size = parseInt(page_size);
570
+ }
571
+
572
+ if (!page) {
573
+ throw new Error('A page number is required');
574
+ }
575
+
576
+ if (!page_size || !isFinite(page_size)) {
577
+ page_size = 10;
578
+ }
579
+
580
+ let skip = (page - 1) * page_size;
581
+
582
+ this.options.page = page;
583
+ this.options.page_size = page_size;
584
+
585
+ this.skip(skip);
586
+ return this.limit(page_size);
587
+ });
588
+
589
+ /**
590
+ * Select a specific field or association
591
+ *
592
+ * @author Jelle De Loecker <jelle@elevenways.be>
593
+ * @since 1.1.0
594
+ * @version 1.2.0
595
+ *
596
+ * @param {string|Array} field
597
+ *
598
+ * @return {Criteria}
599
+ */
600
+ Criteria.setMethod(function select(field) {
601
+
602
+ var context;
603
+
604
+ if (Object.isIterable(field)) {
605
+ let entry;
606
+
607
+ for (entry of field) {
608
+ context = this.select(entry);
609
+ }
610
+ } else {
611
+
612
+ if (this._select) {
613
+ context = this._select.parse(field);
614
+ } else {
615
+ context = this.options.select.parse(field);
616
+ }
617
+ }
618
+
619
+ // Selects don't always change the context
620
+ if (context) {
621
+ return context;
622
+ }
623
+
624
+ return this;
625
+ });
626
+
627
+ /**
628
+ * Add a specific association
629
+ *
630
+ * @author Jelle De Loecker <jelle@elevenways.be>
631
+ * @since 1.1.0
632
+ * @version 1.1.0
633
+ *
634
+ * @param {string} alias
635
+ *
636
+ * @return {Criteria}
637
+ */
638
+ Criteria.setMethod(['contain', 'populate'], function populate(alias) {
639
+
640
+ if (Array.isArray(alias)) {
641
+ let i;
642
+
643
+ for (i = 0; i < alias.length; i++) {
644
+ this.populate(alias[i]);
645
+ }
646
+ } else {
647
+ let select = this._select || this.options.select;
648
+
649
+ select.addAssociation(alias);
650
+ }
651
+
652
+ return this;
653
+ });
654
+
655
+ /**
656
+ * How deep can we go?
657
+ *
658
+ * @author Jelle De Loecker <jelle@elevenways.be>
659
+ * @since 1.1.0
660
+ * @version 1.1.0
661
+ *
662
+ * @param {number} amount
663
+ *
664
+ * @return {Criteria}
665
+ */
666
+ Criteria.setMethod(function recursive(amount) {
667
+ this.options.recursive = amount;
668
+ return this;
669
+ });
670
+
671
+
672
+ /**
673
+ * Set the sort
674
+ *
675
+ * @author Jelle De Loecker <jelle@elevenways.be>
676
+ * @since 1.1.0
677
+ * @version 1.1.4
678
+ *
679
+ * @param {Array} value
680
+ *
681
+ * @return {Criteria}
682
+ */
683
+ Criteria.setMethod(function sort(value) {
684
+
685
+ var result;
686
+
687
+ if (value) {
688
+ result = [];
689
+
690
+ // Parse strings
691
+ if (typeof value == 'string') {
692
+ // When it contains a space, we expect something
693
+ // like "_id asc"
694
+ if (value.indexOf(' ') > -1) {
695
+ result.push(value.split(' '));
696
+ } else {
697
+ // Sort ascending by default
698
+ result.push([value, 1]);
699
+ }
700
+ } else if (Array.isArray(value)) {
701
+ if (Array.isArray(value[0])) {
702
+ result = value;
703
+ } else {
704
+ result.push(value);
705
+ }
706
+ } else {
707
+ let keys = Object.keys(value),
708
+ key;
709
+
710
+ if (keys.length == 2 && ~keys.indexOf('dir') && ~keys.indexOf('field')) {
711
+
712
+ if (value.field && value.dir) {
713
+ result.push([value.field, value.dir]);
714
+ }
715
+
716
+ } else {
717
+ for (key in value) {
718
+ result.push([key, value[key]]);
719
+ }
720
+ }
721
+ }
722
+
723
+ let entry,
724
+ i;
725
+
726
+ for (i = 0; i < result.length; i++) {
727
+ entry = result[i];
728
+
729
+ if (typeof entry[1] == 'string') {
730
+ entry[1] = entry[1].toLowerCase();
731
+
732
+ if (entry[1] == 'asc') {
733
+ entry[1] = 1;
734
+ } else if (entry[1] == 'desc') {
735
+ entry[1] = -1;
736
+ } else {
737
+ throw new Error('Unable to parse sort specification "' + entry[1] + '"');
738
+ }
739
+ }
740
+ }
741
+
742
+ // @TODO: implement better handling of ModelName.field sort stuff
743
+ // (Because at this moment, it's just ignored!)
744
+ for (entry of result) {
745
+ if (entry[0].indexOf('.') > -1) {
746
+ let pieces = entry[0].split('.'),
747
+ char = pieces[0][0];
748
+
749
+ if (char == char.toUpperCase()) {
750
+ pieces = pieces.slice(1);
751
+ }
752
+
753
+ entry[0] = pieces.join('.');
754
+ }
755
+ }
756
+
757
+ } else {
758
+ result = null;
759
+ }
760
+
761
+ this.options.sort = result;
762
+
763
+ return this;
764
+ });
765
+
766
+ /**
767
+ * Normalize the criteria by filling in some values on datasources without joins
768
+ *
769
+ * @author Jelle De Loecker <jelle@elevenways.be>
770
+ * @since 1.1.0
771
+ * @version 1.1.0
772
+ *
773
+ * @return {Pledge}
774
+ */
775
+ Criteria.setMethod(function normalize() {
776
+
777
+ if (!this.model) {
778
+ return Pledge.reject(new Error('Unable to normalize criteria without model instance'));
779
+ }
780
+
781
+ let that = this,
782
+ tasks = [],
783
+ i;
784
+
785
+ for (i = 0; i < this.all_expressions.length; i++) {
786
+ let expression = this.all_expressions[i];
787
+
788
+ if (!expression) {
789
+ continue;
790
+ }
791
+
792
+ // Do we need to normalize association values?
793
+ if (expression.requires_association_normalization) {
794
+ tasks.push(function doNormalize(next) {
795
+ expression.normalizeAssociationValues().done(next);
796
+ });
797
+
798
+ continue;
799
+ }
800
+
801
+ let pledge = expression.normalize();
802
+
803
+ if (pledge) {
804
+ tasks.push(pledge);
805
+ }
806
+ }
807
+
808
+ return Function.parallel(4, tasks);
809
+ });
810
+
811
+ /**
812
+ * Compile to MongoDB-like query
813
+ *
814
+ * @author Jelle De Loecker <jelle@elevenways.be>
815
+ * @since 1.1.0
816
+ * @version 1.1.0
817
+ *
818
+ * @return {Object}
819
+ */
820
+ Criteria.setMethod(function compile() {
821
+
822
+ if (!this.datasource) {
823
+ throw new Error('Unable to compile criteria without a datasource target');
824
+ }
825
+
826
+ return this.datasource.compileCriteria(this);
827
+ });
828
+
829
+ /**
830
+ * Parse an old, mongodb specific options object
831
+ *
832
+ * @author Jelle De Loecker <jelle@elevenways.be>
833
+ * @since 1.1.0
834
+ * @version 1.1.0
835
+ *
836
+ * @param {Object} options
837
+ *
838
+ * @return {Criteria}
839
+ */
840
+ Criteria.setMethod(function applyOldOptions(options) {
841
+
842
+ if (!options || Object.isEmpty(options)) {
843
+ return this;
844
+ }
845
+
846
+ let entry,
847
+ key;
848
+
849
+ for (key in options) {
850
+ entry = options[key];
851
+
852
+ switch (key) {
853
+ case 'sort' : this.sort(entry); break;
854
+ case 'limit' : this.limit(entry); break;
855
+ case 'fields' : this.select(entry); break;
856
+ case 'select' : this.select(entry); break;
857
+ case 'recursive' : this.recursive(entry); break;
858
+ case 'offset' : this.skip(entry); break;
859
+ case 'populate' : this.populate(entry); break;
860
+
861
+ case 'conditions':
862
+ this.applyConditions(entry);
863
+ break;
864
+
865
+ default:
866
+ this.setOption(key, entry);
867
+ }
868
+ }
869
+
870
+ return this;
871
+ });
872
+
873
+ /**
874
+ * The Criteria Select class
875
+ *
876
+ * @author Jelle De Loecker <jelle@elevenways.be>
877
+ * @since 1.1.0
878
+ * @version 1.1.0
879
+ */
880
+ var Select = Function.inherits('Alchemy.Base', 'Alchemy.Criteria', function Select(criteria) {
881
+ // The parent criteria instance
882
+ this.criteria = criteria;
883
+ });
884
+
885
+ /**
886
+ * Revive the given object
887
+ *
888
+ * @author Jelle De Loecker <jelle@elevenways.be>
889
+ * @since 1.1.0
890
+ * @version 1.2.3
891
+ *
892
+ * @return {Select}
893
+ */
894
+ Select.setStatic(function revive(data, criteria) {
895
+
896
+ if (!data) {
897
+ return;
898
+ }
899
+
900
+ let result = new Select(criteria),
901
+ key;
902
+
903
+ result.fields = data.fields;
904
+
905
+ if (data.associations) {
906
+ result.associations = {};
907
+
908
+ for (key in data.associations) {
909
+ result.associations[key] = Select.revive(data.associations[key], criteria);
910
+ }
911
+ }
912
+
913
+ if (data.association_name) {
914
+ result.association_name = data.association_name;
915
+ }
916
+
917
+ return result;
918
+ });
919
+
920
+ /**
921
+ * Return object to jsonify
922
+ *
923
+ * @author Jelle De Loecker <jelle@elevenways.be>
924
+ * @since 1.1.0
925
+ * @version 1.1.0
926
+ *
927
+ * @return {Object}
928
+ */
929
+ Select.setMethod(function toJSON() {
930
+ return {
931
+ association_name : this.association_name,
932
+ associations : this.associations,
933
+ fields : this.fields
934
+ };
935
+ });
936
+
937
+ /**
938
+ * Return the elements to checksum in place of this object
939
+ *
940
+ * @author Jelle De Loecker <jelle@elevenways.be>
941
+ * @since 1.1.0
942
+ * @version 1.1.0
943
+ */
944
+ Select.setMethod(Blast.checksumSymbol, function toChecksum() {
945
+
946
+ var result = [];
947
+
948
+ if (this.associations) {
949
+ result.push(this.associations);
950
+ }
951
+
952
+ if (this.fields) {
953
+ result.push(this.fields);
954
+ }
955
+
956
+ if (this.association_name) {
957
+ result.push(this.association_name);
958
+ }
959
+
960
+ if (!result.length) {
961
+ return null;
962
+ }
963
+
964
+ return result;
965
+ });
966
+
967
+ /**
968
+ * Add an association
969
+ *
970
+ * @author Jelle De Loecker <jelle@elevenways.be>
971
+ * @since 1.1.0
972
+ * @version 1.3.4
973
+ *
974
+ * @param {string} name
975
+ *
976
+ * @return {Select} This creates a new Select instance
977
+ */
978
+ Select.setMethod(function addAssociation(name) {
979
+
980
+ if (!this.criteria?.model) {
981
+ throw new Error('Unable to select an association: this Criteria has no model info');
982
+ }
983
+
984
+ var pieces;
985
+
986
+ if (!this.associations) {
987
+ this.associations = {};
988
+ }
989
+
990
+ if (Array.isArray(name)) {
991
+ pieces = name;
992
+ } else if (name.indexOf('.') > -1) {
993
+ pieces = name.split('.');
994
+ }
995
+
996
+ if (pieces && pieces.length) {
997
+ let context = this;
998
+
999
+ while (pieces.length) {
1000
+ name = pieces.shift();
1001
+ context = context.addAssociation(name);
1002
+ }
1003
+
1004
+ return context;
1005
+ }
1006
+
1007
+ if (!this.associations[name]) {
1008
+ this.associations[name] = new Select(this.criteria);
1009
+ this.associations[name].association_name = name;
1010
+ }
1011
+
1012
+ // Get the association data
1013
+ try {
1014
+ let info = this.criteria.model.getAssociation(name);
1015
+
1016
+ if (info) {
1017
+ // Make sure the localkey is added to the resultset
1018
+ this.requireFieldForQuery(info.options.localKey);
1019
+ }
1020
+ } catch (err) {
1021
+ console.warn('Failed to find "' + name + '" association for ' + this.criteria.model.model_name);
1022
+ }
1023
+
1024
+ return this.associations[name];
1025
+ });
1026
+
1027
+ /**
1028
+ * Require a field for query purposes
1029
+ *
1030
+ * @author Jelle De Loecker <jelle@elevenways.be>
1031
+ * @since 1.2.0
1032
+ * @version 1.2.0
1033
+ *
1034
+ * @param {string} path
1035
+ */
1036
+ Select.setMethod(function requireFieldForQuery(path) {
1037
+
1038
+ if (!this.query_fields) {
1039
+ this.query_fields = [];
1040
+ }
1041
+
1042
+ this.query_fields.push(path);
1043
+ });
1044
+
1045
+ /**
1046
+ * Add a field
1047
+ *
1048
+ * @author Jelle De Loecker <jelle@elevenways.be>
1049
+ * @since 1.1.0
1050
+ * @version 1.1.0
1051
+ *
1052
+ * @param {string} path
1053
+ */
1054
+ Select.setMethod(function addField(path) {
1055
+
1056
+ if (!this.fields) {
1057
+ this.fields = [];
1058
+ }
1059
+
1060
+ this.fields.push(path);
1061
+ });
1062
+
1063
+ /**
1064
+ * Parse a path meant to add as a selection
1065
+ *
1066
+ * @author Jelle De Loecker <jelle@elevenways.be>
1067
+ * @since 1.1.0
1068
+ * @version 1.2.0
1069
+ *
1070
+ * @param {string|Object} path
1071
+ *
1072
+ * @return {Criteria|Null} A criteria object if the context has changed
1073
+ */
1074
+ Select.setMethod(function parse(path) {
1075
+
1076
+ let context,
1077
+ select = this,
1078
+ parsed;
1079
+
1080
+ if (typeof path == 'object' && path && path.name) {
1081
+
1082
+ if (path.path) {
1083
+ path = path.path;
1084
+ } else {
1085
+ let obj = path;
1086
+ path = obj.name;
1087
+
1088
+ if (obj.association) {
1089
+ path = obj.association + '.' + path;
1090
+ }
1091
+ }
1092
+ }
1093
+
1094
+ parsed = Criteria.parsePath(path, this.criteria);
1095
+
1096
+ // Associations were found,
1097
+ // like "Comment._id" or "Comment.User"
1098
+ if (parsed.association) {
1099
+ let name,
1100
+ i;
1101
+
1102
+ for (i = 0; i < parsed.association.length; i++) {
1103
+ name = parsed.association[i];
1104
+
1105
+ if (this.model && this.model.name == name) {
1106
+ continue;
1107
+ }
1108
+
1109
+ select = select.addAssociation(name);
1110
+ }
1111
+ }
1112
+
1113
+ if (parsed.target_path) {
1114
+ select.addField(parsed.target_path);
1115
+ } else if (parsed.association) {
1116
+ // When only an association was given, then the context changes
1117
+ context = this.criteria.augment('select');
1118
+ context._select = select;
1119
+ return context;
1120
+ }
1121
+ });
1122
+
1123
+ /**
1124
+ * Clone this select for the given criteria
1125
+ *
1126
+ * @author Jelle De Loecker <jelle@elevenways.be>
1127
+ * @since 1.1.0
1128
+ * @version 1.2.0
1129
+ *
1130
+ * @param {Criteria} criteria
1131
+ *
1132
+ * @return {Select}
1133
+ */
1134
+ Select.setMethod(function cloneForCriteria(criteria) {
1135
+
1136
+ var clone = new Select(criteria);
1137
+
1138
+ if (this.association_name) {
1139
+ clone.association_name = this.association_name;
1140
+ }
1141
+
1142
+ if (this.fields && this.fields.length) {
1143
+ clone.fields = this.fields.slice(0);
1144
+ }
1145
+
1146
+ if (this.query_fields && this.query_fields.length) {
1147
+ clone.query_fields = this.query_fields.slice(0);
1148
+ }
1149
+
1150
+ if (this.associations) {
1151
+ let key;
1152
+
1153
+ clone.associations = {};
1154
+
1155
+ for (key in this.associations) {
1156
+ clone.associations[key] = this.associations[key].cloneForCriteria(criteria);
1157
+ }
1158
+ }
1159
+
1160
+ return clone;
1161
+ });
1162
+
1163
+ /**
1164
+ * Should the given association be queried according to this select?
1165
+ * (The Criteria instance can also have a recursive level set)
1166
+ *
1167
+ * @author Jelle De Loecker <jelle@elevenways.be>
1168
+ * @since 1.1.0
1169
+ * @version 1.1.0
1170
+ *
1171
+ * @param {string} name
1172
+ *
1173
+ * @return {boolean}
1174
+ */
1175
+ Select.setMethod(function shouldQueryAssociation(name) {
1176
+
1177
+ if (this.associations) {
1178
+ return !!this.associations[name];
1179
+ }
1180
+
1181
+ return false;
1182
+ });