alchemymvc 1.4.0-alpha.9 → 1.4.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.
- package/lib/app/behaviour/sluggable_behaviour.js +72 -1
- package/lib/app/conduit/electron_conduit.js +1 -1
- package/lib/app/conduit/socket_conduit.js +62 -8
- package/lib/app/controller/alchemy_info_controller.js +2 -2
- package/lib/app/datasource/mongo_datasource.js +84 -104
- package/lib/app/element/al_time.js +126 -0
- package/lib/app/helper/enum_values.js +9 -0
- package/lib/app/helper/router_helper.js +37 -5
- package/lib/app/helper/socket_helper.js +71 -14
- package/lib/app/helper/syncable.js +253 -30
- package/lib/app/helper_datasource/00-nosql_datasource.js +403 -53
- package/lib/app/helper_datasource/05-fallback_datasource.js +5 -2
- package/lib/app/helper_datasource/idb_datasource.js +75 -59
- package/lib/app/helper_datasource/indexed_db.js +41 -33
- package/lib/app/helper_datasource/read_operational_context.js +0 -17
- package/lib/app/helper_field/00-objectid_field.js +16 -0
- package/lib/app/helper_field/11-date_field.js +13 -0
- package/lib/app/helper_field/datetime_field.js +13 -0
- package/lib/app/helper_field/enum_field.js +27 -0
- package/lib/app/helper_field/geopoint_field.js +61 -0
- package/lib/app/helper_field/html_field.js +14 -1
- package/lib/app/helper_field/integer_field.js +14 -0
- package/lib/app/helper_field/local_date_field.js +13 -0
- package/lib/app/helper_field/local_date_time_field.js +13 -0
- package/lib/app/helper_field/local_time_field.js +13 -0
- package/lib/app/helper_field/password_field.js +24 -0
- package/lib/app/helper_field/schema_field.js +85 -2
- package/lib/app/helper_field/url_field.js +22 -0
- package/lib/app/helper_model/00-base_criteria.js +67 -6
- package/lib/app/helper_model/05-criteria_expressions.js +23 -4
- package/lib/app/helper_model/10-model_criteria.js +39 -18
- package/lib/app/helper_model/document.js +12 -7
- package/lib/app/helper_model/field_config.js +9 -2
- package/lib/app/helper_model/model.js +9 -8
- package/lib/app/model/system_task_history_model.js +11 -1
- package/lib/class/conduit.js +112 -12
- package/lib/class/datasource.js +30 -2
- package/lib/class/document.js +1 -1
- package/lib/class/field.js +170 -7
- package/lib/class/inode_file.js +2 -2
- package/lib/class/model.js +12 -11
- package/lib/class/operational_context.js +37 -0
- package/lib/class/route.js +34 -3
- package/lib/class/router.js +14 -7
- package/lib/class/schema.js +1 -1
- package/lib/class/schema_client.js +141 -14
- package/lib/class/session.js +1 -1
- package/lib/class/task.js +30 -1
- package/lib/class/task_service.js +83 -14
- package/lib/core/alchemy.js +78 -4
- package/lib/core/alchemy_functions.js +7 -11
- package/lib/core/alchemy_load_functions.js +37 -5
- package/lib/core/client_alchemy.js +9 -1
- package/lib/core/middleware.js +52 -20
- package/lib/core/setting.js +75 -6
- package/lib/scripts/create_settings.js +44 -0
- package/lib/scripts/setup_ai_devmode.js +324 -0
- package/lib/scripts/setup_devwatch.js +0 -0
- package/lib/stages/00-load_core.js +1 -1
- package/lib/stages/50-routes.js +6 -1
- package/lib/stages/90-server.js +0 -1
- package/package.json +18 -18
|
@@ -282,15 +282,86 @@ Sluggable.setMethod(function createSlug(record, new_value, callback) {
|
|
|
282
282
|
|
|
283
283
|
return Function.parallel(tasks, callback);
|
|
284
284
|
} else {
|
|
285
|
+
let existing_slug = record[that.target_field.name];
|
|
286
|
+
|
|
285
287
|
// Only generate a new slug if it doesn't exist yet
|
|
286
|
-
if (!
|
|
288
|
+
if (!existing_slug) {
|
|
287
289
|
return that.generateSlug(record[that.source_field.name], false, record, callback);
|
|
288
290
|
}
|
|
291
|
+
|
|
292
|
+
// Check if the existing slug is a duplicate (belongs to another record)
|
|
293
|
+
return that.checkSlugIsDuplicate(existing_slug, false, record, function checked(err, is_duplicate) {
|
|
294
|
+
if (err) {
|
|
295
|
+
return callback(err);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// If the slug is a duplicate, regenerate from the source field
|
|
299
|
+
if (is_duplicate) {
|
|
300
|
+
return that.generateSlug(record[that.source_field.name], false, record, callback);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Slug is not a duplicate, keep it
|
|
304
|
+
callback(null, existing_slug);
|
|
305
|
+
});
|
|
289
306
|
}
|
|
290
307
|
|
|
291
308
|
callback();
|
|
292
309
|
});
|
|
293
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Check if a slug already exists (belongs to another record)
|
|
313
|
+
*
|
|
314
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
315
|
+
* @since 1.4.0
|
|
316
|
+
* @version 1.4.0
|
|
317
|
+
*
|
|
318
|
+
* @param {string} slug
|
|
319
|
+
* @param {string} key The translation key (or false)
|
|
320
|
+
* @param {Document} record
|
|
321
|
+
* @param {Function} callback
|
|
322
|
+
*/
|
|
323
|
+
Sluggable.setMethod(function checkSlugIsDuplicate(slug, key, record, callback) {
|
|
324
|
+
|
|
325
|
+
let that = this,
|
|
326
|
+
for_record_id = record?._id,
|
|
327
|
+
model = Model.get(this.model.name),
|
|
328
|
+
path = this.target_field.name;
|
|
329
|
+
|
|
330
|
+
if (key) {
|
|
331
|
+
path += '.' + key;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
let conditions = {};
|
|
335
|
+
conditions[path] = slug;
|
|
336
|
+
|
|
337
|
+
// Also include unique_modifier_fields in the check
|
|
338
|
+
if (this.unique_modifier_fields.length && record) {
|
|
339
|
+
for (let i = 0; i < this.unique_modifier_fields.length; i++) {
|
|
340
|
+
let field = this.unique_modifier_fields[i];
|
|
341
|
+
conditions[field] = record[field];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
model.find('first', {conditions: conditions}, function gotFirst(err, found_item) {
|
|
346
|
+
if (err) {
|
|
347
|
+
return callback(err);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// If no item found, it's not a duplicate
|
|
351
|
+
if (!found_item) {
|
|
352
|
+
return callback(null, false);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// If the found item is the same as the current record, it's not a duplicate
|
|
356
|
+
if (for_record_id && String(for_record_id) == String(found_item._id)) {
|
|
357
|
+
return callback(null, false);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// It's a duplicate
|
|
361
|
+
callback(null, true);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
|
|
294
365
|
/**
|
|
295
366
|
* Actually generate the slug from the given string,
|
|
296
367
|
* and look for existing slugs in the path
|
|
@@ -6,7 +6,7 @@ var iostream = alchemy.use('socket.io-stream'),
|
|
|
6
6
|
*
|
|
7
7
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
8
8
|
* @since 0.2.0
|
|
9
|
-
* @version 1.
|
|
9
|
+
* @version 1.4.0
|
|
10
10
|
*
|
|
11
11
|
* @param {Socker} socket
|
|
12
12
|
* @param {Object} announcement
|
|
@@ -25,6 +25,10 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
|
|
|
25
25
|
// Store the announcement data
|
|
26
26
|
this.announcement = announcement;
|
|
27
27
|
|
|
28
|
+
if (!this.canCreateSocketConnection()) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
28
32
|
// Detect node clients
|
|
29
33
|
if (this.headers['user-agent'] == 'node-XMLHttpRequest') {
|
|
30
34
|
this.isNodeClient = true;
|
|
@@ -84,8 +88,8 @@ var SocketConduit = Function.inherits('Alchemy.Conduit', function Socket(socket,
|
|
|
84
88
|
|
|
85
89
|
// Listen for responses on the stream socket
|
|
86
90
|
this.stream.on('response', function onStreamResponse(stream, data) {
|
|
87
|
-
|
|
88
|
-
that.onPayload(
|
|
91
|
+
data.stream = stream;
|
|
92
|
+
that.onPayload(data);
|
|
89
93
|
});
|
|
90
94
|
|
|
91
95
|
// Listen to data subscriptions
|
|
@@ -145,6 +149,22 @@ SocketConduit.setProperty(function is_connected() {
|
|
|
145
149
|
return this.socket?.connected || false;
|
|
146
150
|
});
|
|
147
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Is this client allowed to create a socket connection?
|
|
154
|
+
*
|
|
155
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
156
|
+
* @since 1.4.0
|
|
157
|
+
* @version 1.4.0
|
|
158
|
+
*/
|
|
159
|
+
SocketConduit.setMethod(function canCreateSocketConnection() {
|
|
160
|
+
|
|
161
|
+
if (this.isCrawler()) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return true;
|
|
166
|
+
});
|
|
167
|
+
|
|
148
168
|
/**
|
|
149
169
|
* Parse the request, get information from the url
|
|
150
170
|
*
|
|
@@ -166,7 +186,7 @@ SocketConduit.setMethod(function parseRequest() {
|
|
|
166
186
|
*
|
|
167
187
|
* @param {Object} data
|
|
168
188
|
*/
|
|
169
|
-
SocketConduit.setMethod(function parseAnnouncement() {
|
|
189
|
+
SocketConduit.setMethod(async function parseAnnouncement() {
|
|
170
190
|
|
|
171
191
|
var connections,
|
|
172
192
|
data = this.announcement;
|
|
@@ -179,6 +199,10 @@ SocketConduit.setMethod(function parseAnnouncement() {
|
|
|
179
199
|
// Register the connection in the user's session
|
|
180
200
|
this.getSession().registerConnection(this);
|
|
181
201
|
|
|
202
|
+
// Allow plugins to restore session state (e.g., persistent login cookies)
|
|
203
|
+
// This is critical after server restarts when the session exists but has no user data
|
|
204
|
+
await alchemy.emit('restoring_websocket_session', this);
|
|
205
|
+
|
|
182
206
|
// Tell the client we're ready
|
|
183
207
|
this.websocket.emit('ready');
|
|
184
208
|
|
|
@@ -192,7 +216,7 @@ SocketConduit.setMethod(function parseAnnouncement() {
|
|
|
192
216
|
*
|
|
193
217
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
194
218
|
* @since 0.2.0
|
|
195
|
-
* @version
|
|
219
|
+
* @version 1.4.0
|
|
196
220
|
*
|
|
197
221
|
* @param {Object} packet
|
|
198
222
|
*/
|
|
@@ -237,9 +261,39 @@ SocketConduit.setMethod(function onLinkup(packet) {
|
|
|
237
261
|
router = Router;
|
|
238
262
|
}
|
|
239
263
|
|
|
264
|
+
// Helper to send permission denied error and clean up
|
|
265
|
+
const send_permission_denied = (required_permission) => {
|
|
266
|
+
let error_linkup = new Linkup(this, type, id, packet.data);
|
|
267
|
+
error_linkup.submit('error', {
|
|
268
|
+
code: 'PERMISSION_DENIED',
|
|
269
|
+
message: 'Permission denied',
|
|
270
|
+
required_permission: required_permission
|
|
271
|
+
});
|
|
272
|
+
error_linkup.destroy();
|
|
273
|
+
};
|
|
274
|
+
|
|
240
275
|
// See if any socket routes have been set
|
|
241
276
|
if (router.linkupRoutes[type]) {
|
|
242
|
-
|
|
277
|
+
let route_config = router.linkupRoutes[type];
|
|
278
|
+
|
|
279
|
+
// Support both old format (just fnc) and new format ({fnc, permission})
|
|
280
|
+
if (typeof route_config === 'function' || typeof route_config === 'string') {
|
|
281
|
+
fnc = route_config;
|
|
282
|
+
} else {
|
|
283
|
+
fnc = route_config.fnc;
|
|
284
|
+
|
|
285
|
+
// Check the linkup route's permission
|
|
286
|
+
if (route_config.permission && !this.hasPermission(route_config.permission)) {
|
|
287
|
+
send_permission_denied(route_config.permission);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Check the router section's permissions
|
|
293
|
+
if (!router.checkPermission(this)) {
|
|
294
|
+
send_permission_denied(router.permissions);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
243
297
|
|
|
244
298
|
// Create the new linkup instance
|
|
245
299
|
linkup = new Linkup(this, type, id, packet.data);
|
|
@@ -335,7 +389,7 @@ SocketConduit.setMethod(function onPayload(packet) {
|
|
|
335
389
|
let test = linkup.simpleListeners.get(packet.type);
|
|
336
390
|
|
|
337
391
|
if (!test) {
|
|
338
|
-
|
|
392
|
+
log.warn('Linkup has no listener for', packet.type);
|
|
339
393
|
}
|
|
340
394
|
|
|
341
395
|
if (packet.stream) {
|
|
@@ -679,6 +733,6 @@ Linkup.setMethod(function createStream() {
|
|
|
679
733
|
* @param {Function} callback
|
|
680
734
|
*/
|
|
681
735
|
Linkup.setMethod(function error(err, callback) {
|
|
682
|
-
|
|
736
|
+
log.error('Linkup error:', err);
|
|
683
737
|
this.submit('error', {stack: err.stack, message: err.message}, callback);
|
|
684
738
|
});
|
|
@@ -149,8 +149,8 @@ Info.setAction(function postponed(conduit, id) {
|
|
|
149
149
|
*
|
|
150
150
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
151
151
|
* @since 1.3.10
|
|
152
|
-
* @version 1.
|
|
152
|
+
* @version 1.4.0
|
|
153
153
|
*/
|
|
154
154
|
Info.setAction(function syncable(conduit, linkup, config) {
|
|
155
|
-
Classes.Alchemy.Syncable.handleLink(conduit, linkup, config);
|
|
155
|
+
Classes.Alchemy.Syncable.Syncable.handleLink(conduit, linkup, config);
|
|
156
156
|
});
|
|
@@ -84,7 +84,7 @@ Mongo.setSupport('querying_associations', true);
|
|
|
84
84
|
*
|
|
85
85
|
* @return {Object}
|
|
86
86
|
*/
|
|
87
|
-
Mongo.setProperty('allowed_find_options', [
|
|
87
|
+
Mongo.setProperty('allowed_find_options', new Set([
|
|
88
88
|
'limit',
|
|
89
89
|
'sort',
|
|
90
90
|
'projection',
|
|
@@ -111,7 +111,7 @@ Mongo.setProperty('allowed_find_options', [
|
|
|
111
111
|
'maxTimeMS',
|
|
112
112
|
'collation',
|
|
113
113
|
'session'
|
|
114
|
-
]);
|
|
114
|
+
]));
|
|
115
115
|
|
|
116
116
|
/**
|
|
117
117
|
* Convert the given value to a BigInt
|
|
@@ -230,7 +230,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
for (key in options) {
|
|
233
|
-
if (this.allowed_find_options.
|
|
233
|
+
if (!this.allowed_find_options.has(key)) {
|
|
234
234
|
continue;
|
|
235
235
|
}
|
|
236
236
|
|
|
@@ -354,90 +354,69 @@ Mongo.setMethod(function _read(context) {
|
|
|
354
354
|
// Sorting should happen in the pipeline
|
|
355
355
|
if (options.sort && options.sort.length) {
|
|
356
356
|
let sort_object = {};
|
|
357
|
-
|
|
357
|
+
|
|
358
358
|
for (let entry of options.sort) {
|
|
359
|
-
|
|
359
|
+
let field_name = entry[0];
|
|
360
|
+
let assoc_name = entry[2];
|
|
361
|
+
|
|
362
|
+
// If there's an association, prefix the field with the association alias
|
|
363
|
+
if (assoc_name) {
|
|
364
|
+
field_name = assoc_name + '.' + field_name;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
sort_object[field_name] = entry[1];
|
|
360
368
|
}
|
|
361
|
-
|
|
362
|
-
|
|
369
|
+
|
|
370
|
+
// Add $sort at the END of the pipeline (after $lookup stages)
|
|
371
|
+
// so that we can sort by fields from associated models
|
|
372
|
+
compiled.pipeline.push({$sort: sort_object});
|
|
363
373
|
}
|
|
364
|
-
|
|
365
|
-
//
|
|
374
|
+
|
|
375
|
+
// Use $facet to combine count and data retrieval in a single query
|
|
376
|
+
// This avoids running the expensive $lookup stages twice
|
|
377
|
+
// Note: $skip and $limit go INSIDE the data facet so count reflects total matches
|
|
378
|
+
let data_pipeline = [];
|
|
379
|
+
|
|
366
380
|
if (options.skip) {
|
|
367
|
-
|
|
381
|
+
data_pipeline.push({$skip: options.skip});
|
|
368
382
|
}
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
let pipeline = JSON.clone(compiled.pipeline),
|
|
380
|
-
cloned_options = JSON.clone(aggregate_options);
|
|
381
|
-
|
|
382
|
-
pipeline.push({$count: 'available'});
|
|
383
|
-
|
|
384
|
-
// Expensive aggregate just to get the available count...
|
|
385
|
-
Pledge.done(collection.aggregate(pipeline, cloned_options), function gotAggregate(err, cursor) {
|
|
386
|
-
|
|
387
|
-
if (err) {
|
|
388
|
-
return next(err);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
Pledge.done(cursor.toArray(), function gotAvailableArray(err, items) {
|
|
392
|
-
|
|
393
|
-
if (err) {
|
|
394
|
-
return next(err);
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
if (!items || !items.length) {
|
|
398
|
-
return next(null, null);
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
let available = items[0].available;
|
|
402
|
-
|
|
403
|
-
if (options.skip) {
|
|
404
|
-
available += options.skip;
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
return next(null, available);
|
|
408
|
-
});
|
|
409
|
-
});
|
|
410
|
-
},
|
|
411
|
-
rows: function getRows(next) {
|
|
412
|
-
|
|
413
|
-
let pipeline = JSON.clone(compiled.pipeline);
|
|
414
|
-
|
|
415
|
-
// Limits also have to be set in the pipeline now
|
|
416
|
-
// (We have to do it here, so the `available` count is correct)
|
|
417
|
-
if (options.limit) {
|
|
418
|
-
pipeline.push({$limit: options.limit});
|
|
419
|
-
}
|
|
420
|
-
|
|
421
|
-
Pledge.done(collection.aggregate(pipeline, aggregate_options), function gotAggregate(err, cursor) {
|
|
422
|
-
|
|
423
|
-
if (err) {
|
|
424
|
-
return next(err);
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
Pledge.done(cursor.toArray(), next);
|
|
428
|
-
});
|
|
383
|
+
|
|
384
|
+
if (options.limit) {
|
|
385
|
+
data_pipeline.push({$limit: options.limit});
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
let facet_stage = {
|
|
389
|
+
$facet: {
|
|
390
|
+
data: data_pipeline,
|
|
429
391
|
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
// Only include count facet if available is requested
|
|
395
|
+
if (criteria.options.available !== false) {
|
|
396
|
+
facet_stage.$facet.count = [{$count: 'total'}];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
compiled.pipeline.push(facet_stage);
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
let cursor = await collection.aggregate(compiled.pipeline, {});
|
|
403
|
+
let results = await cursor.toArray();
|
|
404
|
+
|
|
405
|
+
let facet_result = results[0] || {};
|
|
406
|
+
let rows = facet_result.data || [];
|
|
407
|
+
let available = null;
|
|
408
|
+
|
|
409
|
+
if (criteria.options.available !== false) {
|
|
410
|
+
available = facet_result.count?.[0]?.total || 0;
|
|
434
411
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
pledge.resolve(
|
|
439
|
-
})
|
|
440
|
-
|
|
412
|
+
|
|
413
|
+
rows = that.organizeResultItems(model, rows);
|
|
414
|
+
|
|
415
|
+
pledge.resolve({rows, available});
|
|
416
|
+
} catch (err) {
|
|
417
|
+
pledge.reject(err);
|
|
418
|
+
}
|
|
419
|
+
|
|
441
420
|
return pledge;
|
|
442
421
|
}
|
|
443
422
|
|
|
@@ -457,13 +436,13 @@ Mongo.setMethod(function _read(context) {
|
|
|
457
436
|
Pledge.done(cursor.toArray(), next);
|
|
458
437
|
}
|
|
459
438
|
}, function done(err, data) {
|
|
460
|
-
|
|
439
|
+
|
|
461
440
|
if (err) {
|
|
462
|
-
return
|
|
441
|
+
return pledge.reject(err);
|
|
463
442
|
}
|
|
464
|
-
|
|
443
|
+
|
|
465
444
|
data.rows = that.organizeResultItems(model, data.rows);
|
|
466
|
-
|
|
445
|
+
|
|
467
446
|
pledge.resolve(data);
|
|
468
447
|
});
|
|
469
448
|
|
|
@@ -506,38 +485,39 @@ Mongo.setMethod(function _create(context) {
|
|
|
506
485
|
data[key] = val;
|
|
507
486
|
}
|
|
508
487
|
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
// Clear the cache
|
|
512
|
-
model.nukeCache();
|
|
513
|
-
|
|
514
|
-
if (err != null) {
|
|
515
|
-
return pledge.reject(err);
|
|
516
|
-
}
|
|
488
|
+
Pledge.done(collection.insertOne(data, {w: 1}), function afterInsert(err, result) {
|
|
517
489
|
|
|
518
|
-
|
|
519
|
-
|
|
490
|
+
// Clear the cache
|
|
491
|
+
model.nukeCache();
|
|
520
492
|
|
|
521
|
-
|
|
493
|
+
if (err != null) {
|
|
494
|
+
// In MongoDB driver 6.x, write errors are thrown as MongoServerError
|
|
495
|
+
// Check if this is a write error that should be converted to validation violations
|
|
496
|
+
if (err.code || err.writeErrors) {
|
|
522
497
|
let violations = new Classes.Alchemy.Error.Validation.Violations();
|
|
523
498
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
for (entry of write_errors) {
|
|
499
|
+
// Handle bulk write errors (array of errors)
|
|
500
|
+
if (err.writeErrors && err.writeErrors.length) {
|
|
501
|
+
for (let entry of err.writeErrors) {
|
|
528
502
|
let violation = new Classes.Alchemy.Error.Validation.Violation();
|
|
529
|
-
violation.message = entry.errmsg || entry.message || entry.code;
|
|
503
|
+
violation.message = entry.errmsg || entry.message || String(entry.code);
|
|
530
504
|
violations.add(violation);
|
|
531
505
|
}
|
|
532
506
|
} else {
|
|
533
|
-
|
|
507
|
+
// Single error (e.g., duplicate key)
|
|
508
|
+
let violation = new Classes.Alchemy.Error.Validation.Violation();
|
|
509
|
+
violation.message = err.errmsg || err.message || String(err.code);
|
|
510
|
+
violations.add(violation);
|
|
534
511
|
}
|
|
535
512
|
|
|
536
513
|
return pledge.reject(violations);
|
|
537
514
|
}
|
|
538
515
|
|
|
539
|
-
pledge.
|
|
540
|
-
}
|
|
516
|
+
return pledge.reject(err);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
pledge.resolve(Object.assign({}, data));
|
|
520
|
+
});
|
|
541
521
|
|
|
542
522
|
return pledge;
|
|
543
523
|
}
|
|
@@ -645,7 +625,7 @@ const performUpdate = (collection, model, context) => {
|
|
|
645
625
|
}
|
|
646
626
|
|
|
647
627
|
if (options.debug) {
|
|
648
|
-
|
|
628
|
+
log.debug('Updating with obj', id, update_object);
|
|
649
629
|
}
|
|
650
630
|
|
|
651
631
|
let promise;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const DATETIME = Symbol('datetime');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The custom al-time element
|
|
5
|
+
*
|
|
6
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
7
|
+
* @since 1.4.0
|
|
8
|
+
* @version 1.4.0
|
|
9
|
+
*/
|
|
10
|
+
const AlTime = Function.inherits('Alchemy.Element', 'AlTime');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* The preferred format
|
|
14
|
+
*
|
|
15
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
16
|
+
* @since 0.3.0
|
|
17
|
+
* @version 0.3.0
|
|
18
|
+
*/
|
|
19
|
+
AlTime.setAttribute('format', null, function setFormat(format) {
|
|
20
|
+
this._populate({value: this.datetime, format: format});
|
|
21
|
+
return format;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Get/set the datetime value
|
|
26
|
+
*
|
|
27
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
28
|
+
* @since 0.3.0
|
|
29
|
+
* @version 0.3.0
|
|
30
|
+
*/
|
|
31
|
+
AlTime.setAttribute('datetime', null, function setDatetime(value) {
|
|
32
|
+
this._populate({value: value});
|
|
33
|
+
return this[DATETIME];
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Set the value with a function call
|
|
38
|
+
*
|
|
39
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
40
|
+
* @since 0.3.0
|
|
41
|
+
* @version 0.3.0
|
|
42
|
+
*/
|
|
43
|
+
AlTime.setMethod(function setDatetime(value) {
|
|
44
|
+
this.datetime = value;
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Populate the element
|
|
49
|
+
*
|
|
50
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
51
|
+
* @since 0.3.0
|
|
52
|
+
* @version 0.3.0
|
|
53
|
+
*
|
|
54
|
+
* @return {Object[]}
|
|
55
|
+
*/
|
|
56
|
+
AlTime.setMethod(function populate() {
|
|
57
|
+
let iso_string = this._populate({value: this.value, format: this.format});
|
|
58
|
+
this[DATETIME] = iso_string;
|
|
59
|
+
return iso_string;
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Populate the element
|
|
64
|
+
*
|
|
65
|
+
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
66
|
+
* @since 0.3.0
|
|
67
|
+
* @version 0.3.0
|
|
68
|
+
*/
|
|
69
|
+
AlTime.setMethod(function _populate(input) {
|
|
70
|
+
|
|
71
|
+
if (!input) {
|
|
72
|
+
input = {};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let {value, format} = input;
|
|
76
|
+
|
|
77
|
+
if (value == null) {
|
|
78
|
+
value = this[DATETIME];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (format == null) {
|
|
82
|
+
format = this.format;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let iso_date,
|
|
86
|
+
date;
|
|
87
|
+
|
|
88
|
+
if (value) {
|
|
89
|
+
date = Date.create(value);
|
|
90
|
+
iso_date = date.toISOString();
|
|
91
|
+
} else {
|
|
92
|
+
iso_date = '';
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
value = iso_date;
|
|
96
|
+
|
|
97
|
+
if (Blast.isServer) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
let target_element = this.children?.[0];
|
|
102
|
+
|
|
103
|
+
if (!target_element) {
|
|
104
|
+
target_element = this.createElement('time');
|
|
105
|
+
this.append(target_element);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
let tag_name = target_element.tagName;
|
|
109
|
+
let formatted;
|
|
110
|
+
|
|
111
|
+
if (format) {
|
|
112
|
+
formatted = date.format(format);
|
|
113
|
+
} else {
|
|
114
|
+
formatted = date + '';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (tag_name == 'INPUT') {
|
|
118
|
+
target_element.value = formatted;
|
|
119
|
+
} else {
|
|
120
|
+
if (tag_name == 'TIME') {
|
|
121
|
+
target_element.setAttribute('datetime', iso_date);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
target_element.textContent = formatted;
|
|
125
|
+
}
|
|
126
|
+
});
|
|
@@ -70,17 +70,23 @@ EnumMap.setMethod(function set(name, value) {
|
|
|
70
70
|
result = {
|
|
71
71
|
name : value.name,
|
|
72
72
|
title : value.title,
|
|
73
|
+
short_title : value.short_title,
|
|
73
74
|
};
|
|
74
75
|
} else {
|
|
75
76
|
result = {
|
|
76
77
|
name : value.name,
|
|
77
78
|
title : value.title || value.name,
|
|
79
|
+
short_title : value.short_title,
|
|
78
80
|
};
|
|
79
81
|
|
|
80
82
|
if (value.icon) {
|
|
81
83
|
result.icon = value.icon;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
if (value.icon_style) {
|
|
87
|
+
result.icon_style = value.icon_style;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
if (value.color) {
|
|
85
91
|
result.color = value.color;
|
|
86
92
|
}
|
|
@@ -96,6 +102,9 @@ EnumMap.setMethod(function set(name, value) {
|
|
|
96
102
|
}
|
|
97
103
|
|
|
98
104
|
result.number = this.local.size + 1;
|
|
105
|
+
|
|
106
|
+
// "value" does not mean the value that is stored in the database
|
|
107
|
+
// it can be the function that houses info
|
|
99
108
|
result.value = value;
|
|
100
109
|
result.is_enumified = true;
|
|
101
110
|
|