alchemymvc 1.4.0-alpha.9 → 1.4.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.
- package/lib/app/behaviour/revision_behaviour.js +1 -1
- package/lib/app/behaviour/sluggable_behaviour.js +74 -3
- 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 +103 -107
- package/lib/app/element/al_time.js +126 -0
- package/lib/app/helper/cron.js +2 -2
- 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 +410 -54
- package/lib/app/helper_datasource/05-fallback_datasource.js +7 -2
- package/lib/app/helper_datasource/idb_datasource.js +82 -64
- 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_datasource/remote_datasource.js +1 -1
- 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 +28 -2
- package/lib/app/helper_field/schema_field.js +88 -4
- package/lib/app/helper_field/url_field.js +22 -0
- package/lib/app/helper_model/00-base_criteria.js +81 -6
- package/lib/app/helper_model/05-criteria_expressions.js +53 -11
- package/lib/app/helper_model/10-model_criteria.js +82 -22
- package/lib/app/helper_model/document.js +23 -9
- package/lib/app/helper_model/field_config.js +9 -2
- package/lib/app/helper_model/model.js +15 -11
- package/lib/app/model/system_task_history_model.js +11 -1
- package/lib/class/conduit.js +116 -13
- package/lib/class/controller.js +1 -0
- package/lib/class/datasource.js +30 -2
- package/lib/class/document.js +40 -12
- package/lib/class/field.js +170 -7
- package/lib/class/import_stream_parser.js +299 -0
- package/lib/class/inode_file.js +2 -2
- package/lib/class/migration.js +5 -2
- package/lib/class/model.js +22 -151
- package/lib/class/operational_context.js +37 -0
- package/lib/class/plugin.js +32 -3
- package/lib/class/route.js +34 -3
- package/lib/class/router.js +38 -34
- package/lib/class/schema.js +1 -1
- package/lib/class/schema_client.js +172 -14
- package/lib/class/session.js +1 -1
- package/lib/class/sitemap.js +2 -2
- package/lib/class/task.js +30 -1
- package/lib/class/task_service.js +83 -14
- package/lib/core/alchemy.js +188 -166
- package/lib/core/alchemy_functions.js +7 -11
- package/lib/core/alchemy_load_functions.js +101 -10
- package/lib/core/base.js +2 -2
- package/lib/core/client_alchemy.js +9 -1
- package/lib/core/middleware.js +83 -25
- package/lib/core/setting.js +86 -14
- package/lib/scripts/create_constants.js +5 -1
- 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 +9 -3
- package/lib/stages/50-routes.js +6 -1
- package/lib/stages/90-server.js +0 -1
- package/lib/testing/browser.js +1164 -0
- package/lib/testing/harness.js +840 -0
- package/package.json +26 -19
- package/testing/browser.js +27 -0
- package/testing.js +37 -0
|
@@ -320,7 +320,7 @@ Revision.setMethod(function afterSave(record, options, created) {
|
|
|
320
320
|
[that.revision_model.model_name] : revision_data
|
|
321
321
|
};
|
|
322
322
|
|
|
323
|
-
// Save the data
|
|
323
|
+
// Save the data (but do not wait for it)
|
|
324
324
|
that.revision_model.save(revision_data, {allowFields: true});
|
|
325
325
|
}
|
|
326
326
|
}
|
|
@@ -141,7 +141,7 @@ Sluggable.setStatic(function attached(schema, new_options) {
|
|
|
141
141
|
*
|
|
142
142
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
143
143
|
* @since 0.1.0
|
|
144
|
-
* @version 1.
|
|
144
|
+
* @version 1.4.1
|
|
145
145
|
*
|
|
146
146
|
* @param {Object} data The data that is to be saved
|
|
147
147
|
* @param {Object} options Behaviour options
|
|
@@ -172,7 +172,7 @@ Sluggable.setMethod(async function beforeSave(data, options, creating) {
|
|
|
172
172
|
if (!creating) {
|
|
173
173
|
old_record = await this.model.findById(data._id);
|
|
174
174
|
|
|
175
|
-
if (
|
|
175
|
+
if (old_record) {
|
|
176
176
|
old_value = old_record[that.target_field.name];
|
|
177
177
|
}
|
|
178
178
|
}
|
|
@@ -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
|
});
|
|
@@ -3,6 +3,7 @@ const mongo = alchemy.use('mongodb'),
|
|
|
3
3
|
|
|
4
4
|
const CONNECTION = Symbol('connection'),
|
|
5
5
|
CONNECTION_ERROR = Symbol('connection_error'),
|
|
6
|
+
CONNECTION_ERROR_TIME = Symbol('connection_error_time'),
|
|
6
7
|
MONGO_CLIENT = Symbol('mongo_client');
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -10,7 +11,7 @@ const CONNECTION = Symbol('connection'),
|
|
|
10
11
|
*
|
|
11
12
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
12
13
|
* @since 0.2.0
|
|
13
|
-
* @version 1.
|
|
14
|
+
* @version 1.4.1
|
|
14
15
|
*/
|
|
15
16
|
const Mongo = Function.inherits('Alchemy.Datasource.Nosql', function Mongo(name, _options) {
|
|
16
17
|
|
|
@@ -20,6 +21,9 @@ const Mongo = Function.inherits('Alchemy.Datasource.Nosql', function Mongo(name,
|
|
|
20
21
|
// Possible connection error
|
|
21
22
|
this[CONNECTION_ERROR] = null;
|
|
22
23
|
|
|
24
|
+
// Time of the connection error (for retry logic)
|
|
25
|
+
this[CONNECTION_ERROR_TIME] = null;
|
|
26
|
+
|
|
23
27
|
// The actual DB connection
|
|
24
28
|
this[CONNECTION] = null;
|
|
25
29
|
|
|
@@ -84,7 +88,7 @@ Mongo.setSupport('querying_associations', true);
|
|
|
84
88
|
*
|
|
85
89
|
* @return {Object}
|
|
86
90
|
*/
|
|
87
|
-
Mongo.setProperty('allowed_find_options', [
|
|
91
|
+
Mongo.setProperty('allowed_find_options', new Set([
|
|
88
92
|
'limit',
|
|
89
93
|
'sort',
|
|
90
94
|
'projection',
|
|
@@ -111,7 +115,7 @@ Mongo.setProperty('allowed_find_options', [
|
|
|
111
115
|
'maxTimeMS',
|
|
112
116
|
'collation',
|
|
113
117
|
'session'
|
|
114
|
-
]);
|
|
118
|
+
]));
|
|
115
119
|
|
|
116
120
|
/**
|
|
117
121
|
* Convert the given value to a BigInt
|
|
@@ -230,7 +234,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
|
|
|
230
234
|
}
|
|
231
235
|
|
|
232
236
|
for (key in options) {
|
|
233
|
-
if (this.allowed_find_options.
|
|
237
|
+
if (!this.allowed_find_options.has(key)) {
|
|
234
238
|
continue;
|
|
235
239
|
}
|
|
236
240
|
|
|
@@ -245,7 +249,7 @@ Mongo.setMethod(function normalizeFindOptions(options) {
|
|
|
245
249
|
*
|
|
246
250
|
* @author Jelle De Loecker <jelle@elevenways.be>
|
|
247
251
|
* @since 0.2.0
|
|
248
|
-
* @version 1.4.
|
|
252
|
+
* @version 1.4.1
|
|
249
253
|
*
|
|
250
254
|
* @param {Pledge}
|
|
251
255
|
*/
|
|
@@ -256,7 +260,18 @@ Mongo.setMethod(function connect() {
|
|
|
256
260
|
}
|
|
257
261
|
|
|
258
262
|
if (this[CONNECTION_ERROR]) {
|
|
259
|
-
|
|
263
|
+
// Allow retry after 5 seconds (configurable via options.retry_delay)
|
|
264
|
+
let retry_delay = this.options.retry_delay ?? 5000;
|
|
265
|
+
let error_age = Date.now() - this[CONNECTION_ERROR_TIME];
|
|
266
|
+
|
|
267
|
+
if (error_age < retry_delay) {
|
|
268
|
+
throw this[CONNECTION_ERROR];
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Clear the cached error to allow retry
|
|
272
|
+
this[CONNECTION_ERROR] = null;
|
|
273
|
+
this[CONNECTION_ERROR_TIME] = null;
|
|
274
|
+
log.info('Retrying connection to Mongo datasource', this.name);
|
|
260
275
|
}
|
|
261
276
|
|
|
262
277
|
let pledge = this[CONNECTION] = new Swift();
|
|
@@ -268,6 +283,7 @@ Mongo.setMethod(function connect() {
|
|
|
268
283
|
|
|
269
284
|
if (err) {
|
|
270
285
|
this[CONNECTION_ERROR] = err;
|
|
286
|
+
this[CONNECTION_ERROR_TIME] = Date.now();
|
|
271
287
|
alchemy.printLog(alchemy.SEVERE, 'Could not create connection to Mongo server', {err: err});
|
|
272
288
|
return pledge.reject(err);
|
|
273
289
|
} else {
|
|
@@ -354,90 +370,69 @@ Mongo.setMethod(function _read(context) {
|
|
|
354
370
|
// Sorting should happen in the pipeline
|
|
355
371
|
if (options.sort && options.sort.length) {
|
|
356
372
|
let sort_object = {};
|
|
357
|
-
|
|
373
|
+
|
|
358
374
|
for (let entry of options.sort) {
|
|
359
|
-
|
|
375
|
+
let field_name = entry[0];
|
|
376
|
+
let assoc_name = entry[2];
|
|
377
|
+
|
|
378
|
+
// If there's an association, prefix the field with the association alias
|
|
379
|
+
if (assoc_name) {
|
|
380
|
+
field_name = assoc_name + '.' + field_name;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
sort_object[field_name] = entry[1];
|
|
360
384
|
}
|
|
361
|
-
|
|
362
|
-
|
|
385
|
+
|
|
386
|
+
// Add $sort at the END of the pipeline (after $lookup stages)
|
|
387
|
+
// so that we can sort by fields from associated models
|
|
388
|
+
compiled.pipeline.push({$sort: sort_object});
|
|
363
389
|
}
|
|
364
|
-
|
|
365
|
-
//
|
|
390
|
+
|
|
391
|
+
// Use $facet to combine count and data retrieval in a single query
|
|
392
|
+
// This avoids running the expensive $lookup stages twice
|
|
393
|
+
// Note: $skip and $limit go INSIDE the data facet so count reflects total matches
|
|
394
|
+
let data_pipeline = [];
|
|
395
|
+
|
|
366
396
|
if (options.skip) {
|
|
367
|
-
|
|
397
|
+
data_pipeline.push({$skip: options.skip});
|
|
368
398
|
}
|
|
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
|
-
});
|
|
399
|
+
|
|
400
|
+
if (options.limit) {
|
|
401
|
+
data_pipeline.push({$limit: options.limit});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
let facet_stage = {
|
|
405
|
+
$facet: {
|
|
406
|
+
data: data_pipeline,
|
|
429
407
|
}
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
408
|
+
};
|
|
409
|
+
|
|
410
|
+
// Only include count facet if available is requested
|
|
411
|
+
if (criteria.options.available !== false) {
|
|
412
|
+
facet_stage.$facet.count = [{$count: 'total'}];
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
compiled.pipeline.push(facet_stage);
|
|
416
|
+
|
|
417
|
+
try {
|
|
418
|
+
let cursor = await collection.aggregate(compiled.pipeline, {});
|
|
419
|
+
let results = await cursor.toArray();
|
|
420
|
+
|
|
421
|
+
let facet_result = results[0] || {};
|
|
422
|
+
let rows = facet_result.data || [];
|
|
423
|
+
let available = null;
|
|
424
|
+
|
|
425
|
+
if (criteria.options.available !== false) {
|
|
426
|
+
available = facet_result.count?.[0]?.total || 0;
|
|
434
427
|
}
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
pledge.resolve(
|
|
439
|
-
})
|
|
440
|
-
|
|
428
|
+
|
|
429
|
+
rows = that.organizeResultItems(model, rows);
|
|
430
|
+
|
|
431
|
+
pledge.resolve({rows, available});
|
|
432
|
+
} catch (err) {
|
|
433
|
+
pledge.reject(err);
|
|
434
|
+
}
|
|
435
|
+
|
|
441
436
|
return pledge;
|
|
442
437
|
}
|
|
443
438
|
|
|
@@ -457,13 +452,13 @@ Mongo.setMethod(function _read(context) {
|
|
|
457
452
|
Pledge.done(cursor.toArray(), next);
|
|
458
453
|
}
|
|
459
454
|
}, function done(err, data) {
|
|
460
|
-
|
|
455
|
+
|
|
461
456
|
if (err) {
|
|
462
|
-
return
|
|
457
|
+
return pledge.reject(err);
|
|
463
458
|
}
|
|
464
|
-
|
|
459
|
+
|
|
465
460
|
data.rows = that.organizeResultItems(model, data.rows);
|
|
466
|
-
|
|
461
|
+
|
|
467
462
|
pledge.resolve(data);
|
|
468
463
|
});
|
|
469
464
|
|
|
@@ -506,38 +501,39 @@ Mongo.setMethod(function _create(context) {
|
|
|
506
501
|
data[key] = val;
|
|
507
502
|
}
|
|
508
503
|
|
|
509
|
-
|
|
504
|
+
Pledge.done(collection.insertOne(data, {w: 1}), function afterInsert(err, result) {
|
|
510
505
|
|
|
511
|
-
|
|
512
|
-
|
|
506
|
+
// Clear the cache
|
|
507
|
+
model.nukeCache();
|
|
513
508
|
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
// @TODO: fix because of mongodb 6
|
|
519
|
-
let write_errors = result.message?.documents?.[0]?.writeErrors;
|
|
520
|
-
|
|
521
|
-
if (write_errors) {
|
|
509
|
+
if (err != null) {
|
|
510
|
+
// In MongoDB driver 6.x, write errors are thrown as MongoServerError
|
|
511
|
+
// Check if this is a write error that should be converted to validation violations
|
|
512
|
+
if (err.code || err.writeErrors) {
|
|
522
513
|
let violations = new Classes.Alchemy.Error.Validation.Violations();
|
|
523
514
|
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
for (entry of write_errors) {
|
|
515
|
+
// Handle bulk write errors (array of errors)
|
|
516
|
+
if (err.writeErrors && err.writeErrors.length) {
|
|
517
|
+
for (let entry of err.writeErrors) {
|
|
528
518
|
let violation = new Classes.Alchemy.Error.Validation.Violation();
|
|
529
|
-
violation.message = entry.errmsg || entry.message || entry.code;
|
|
519
|
+
violation.message = entry.errmsg || entry.message || String(entry.code);
|
|
530
520
|
violations.add(violation);
|
|
531
521
|
}
|
|
532
522
|
} else {
|
|
533
|
-
|
|
523
|
+
// Single error (e.g., duplicate key)
|
|
524
|
+
let violation = new Classes.Alchemy.Error.Validation.Violation();
|
|
525
|
+
violation.message = err.errmsg || err.message || String(err.code);
|
|
526
|
+
violations.add(violation);
|
|
534
527
|
}
|
|
535
528
|
|
|
536
529
|
return pledge.reject(violations);
|
|
537
530
|
}
|
|
538
531
|
|
|
539
|
-
pledge.
|
|
540
|
-
}
|
|
532
|
+
return pledge.reject(err);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
pledge.resolve(Object.assign({}, data));
|
|
536
|
+
});
|
|
541
537
|
|
|
542
538
|
return pledge;
|
|
543
539
|
}
|
|
@@ -645,7 +641,7 @@ const performUpdate = (collection, model, context) => {
|
|
|
645
641
|
}
|
|
646
642
|
|
|
647
643
|
if (options.debug) {
|
|
648
|
-
|
|
644
|
+
log.debug('Updating with obj', id, update_object);
|
|
649
645
|
}
|
|
650
646
|
|
|
651
647
|
let promise;
|