spice-js 2.7.1 → 2.7.3

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.
@@ -0,0 +1,426 @@
1
+ "use strict";
2
+
3
+ exports.__esModule = true;
4
+ exports.reloadSchema = reloadSchema;
5
+ exports.reloadModel = reloadModel;
6
+ exports.reloadController = reloadController;
7
+ exports.reloadCache = reloadCache;
8
+ exports.registerRoute = registerRoute;
9
+ exports.reloadResource = reloadResource;
10
+ exports.removeResourceFromMemory = removeResourceFromMemory;
11
+ exports.default = void 0;
12
+
13
+ var _path = _interopRequireDefault(require("path"));
14
+
15
+ var _fs = _interopRequireDefault(require("fs"));
16
+
17
+ function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
18
+
19
+ function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { Promise.resolve(value).then(_next, _throw); } }
20
+
21
+ function _asyncToGenerator(fn) { return function () { var self = this, args = arguments; return new Promise(function (resolve, reject) { var gen = fn.apply(self, args); function _next(value) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value); } function _throw(err) { asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err); } _next(undefined); }); }; }
22
+
23
+ /**
24
+ * ResourceReloader - Utility for dynamically reloading resources at runtime
25
+ * without requiring a full application restart.
26
+ *
27
+ * This enables zero-downtime updates for:
28
+ * - Schema updates
29
+ * - Resource creation
30
+ * - Resource deletion (removes from memory, routes orphaned until restart)
31
+ */
32
+
33
+ /**
34
+ * Clear the Node.js require cache for a specific file path
35
+ * @param {string} filePath - The full path to the file
36
+ */
37
+ function clearRequireCache(filePath) {
38
+ // Resolve the file path to handle different require formats
39
+ var resolvedPath = require.resolve(filePath);
40
+
41
+ if (require.cache[resolvedPath]) {
42
+ delete require.cache[resolvedPath];
43
+ return true;
44
+ }
45
+
46
+ return false;
47
+ }
48
+ /**
49
+ * Reload a schema into memory
50
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
51
+ * @returns {object} Result with success status and message
52
+ */
53
+
54
+
55
+ function reloadSchema(_x) {
56
+ return _reloadSchema.apply(this, arguments);
57
+ }
58
+ /**
59
+ * Reload a model into memory
60
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
61
+ * @returns {object} Result with success status and message
62
+ */
63
+
64
+
65
+ function _reloadSchema() {
66
+ _reloadSchema = _asyncToGenerator(function* (resourceName) {
67
+ try {
68
+ var schemaPath = _path.default.join(spice.root_path, "schemas", resourceName + ".js");
69
+
70
+ if (!_fs.default.existsSync(schemaPath)) {
71
+ return {
72
+ success: false,
73
+ error: "Schema file not found: " + schemaPath
74
+ };
75
+ }
76
+
77
+ clearRequireCache(schemaPath);
78
+
79
+ var imported = require(schemaPath);
80
+
81
+ var schema = imported.default || imported; // Store with both original and lowercase keys
82
+
83
+ spice.schemas[resourceName] = schema;
84
+ spice.schemas[resourceName.toLowerCase()] = schema;
85
+ return {
86
+ success: true,
87
+ message: "Schema " + resourceName + " reloaded"
88
+ };
89
+ } catch (error) {
90
+ return {
91
+ success: false,
92
+ error: "Failed to reload schema: " + error.message
93
+ };
94
+ }
95
+ });
96
+ return _reloadSchema.apply(this, arguments);
97
+ }
98
+
99
+ function reloadModel(_x2) {
100
+ return _reloadModel.apply(this, arguments);
101
+ }
102
+ /**
103
+ * Reload a controller into memory
104
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
105
+ * @returns {object} Result with success status and message
106
+ */
107
+
108
+
109
+ function _reloadModel() {
110
+ _reloadModel = _asyncToGenerator(function* (resourceName) {
111
+ try {
112
+ var modelPath = _path.default.join(spice.root_path, "models", resourceName + ".js");
113
+
114
+ if (!_fs.default.existsSync(modelPath)) {
115
+ return {
116
+ success: false,
117
+ error: "Model file not found: " + modelPath
118
+ };
119
+ }
120
+
121
+ clearRequireCache(modelPath);
122
+
123
+ var imported = require(modelPath);
124
+
125
+ var model = imported.default || imported; // Store with both original and lowercase keys
126
+
127
+ spice.models[resourceName] = model;
128
+ spice.models[resourceName.toLowerCase()] = model;
129
+ return {
130
+ success: true,
131
+ message: "Model " + resourceName + " reloaded"
132
+ };
133
+ } catch (error) {
134
+ return {
135
+ success: false,
136
+ error: "Failed to reload model: " + error.message
137
+ };
138
+ }
139
+ });
140
+ return _reloadModel.apply(this, arguments);
141
+ }
142
+
143
+ function reloadController(_x3) {
144
+ return _reloadController.apply(this, arguments);
145
+ }
146
+ /**
147
+ * Reload a cache into memory
148
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
149
+ * @returns {object} Result with success status and message
150
+ */
151
+
152
+
153
+ function _reloadController() {
154
+ _reloadController = _asyncToGenerator(function* (resourceName) {
155
+ try {
156
+ var controllerPath = _path.default.join(spice.root_path, "controllers", resourceName + ".js");
157
+
158
+ if (!_fs.default.existsSync(controllerPath)) {
159
+ return {
160
+ success: false,
161
+ error: "Controller file not found: " + controllerPath
162
+ };
163
+ }
164
+
165
+ clearRequireCache(controllerPath);
166
+
167
+ var imported = require(controllerPath);
168
+
169
+ var controller = imported.default || imported; // Store with both original and lowercase keys
170
+
171
+ spice.controllers[resourceName] = controller;
172
+ spice.controllers[resourceName.toLowerCase()] = controller;
173
+ return {
174
+ success: true,
175
+ message: "Controller " + resourceName + " reloaded"
176
+ };
177
+ } catch (error) {
178
+ return {
179
+ success: false,
180
+ error: "Failed to reload controller: " + error.message
181
+ };
182
+ }
183
+ });
184
+ return _reloadController.apply(this, arguments);
185
+ }
186
+
187
+ function reloadCache(_x4) {
188
+ return _reloadCache.apply(this, arguments);
189
+ }
190
+ /**
191
+ * Register a new route with the Koa app
192
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
193
+ * @returns {object} Result with success status and message
194
+ */
195
+
196
+
197
+ function _reloadCache() {
198
+ _reloadCache = _asyncToGenerator(function* (resourceName) {
199
+ try {
200
+ var cachePath = _path.default.join(spice.root_path, "cache", resourceName + ".js");
201
+
202
+ if (!_fs.default.existsSync(cachePath)) {
203
+ return {
204
+ success: false,
205
+ error: "Cache file not found: " + cachePath
206
+ };
207
+ }
208
+
209
+ clearRequireCache(cachePath);
210
+
211
+ var imported = require(cachePath);
212
+
213
+ var cache = imported.default || imported; // Store with both original and lowercase keys
214
+
215
+ spice.cache[resourceName] = cache;
216
+ spice.cache[resourceName.toLowerCase()] = cache;
217
+ return {
218
+ success: true,
219
+ message: "Cache " + resourceName + " reloaded"
220
+ };
221
+ } catch (error) {
222
+ return {
223
+ success: false,
224
+ error: "Failed to reload cache: " + error.message
225
+ };
226
+ }
227
+ });
228
+ return _reloadCache.apply(this, arguments);
229
+ }
230
+
231
+ function registerRoute(_x5) {
232
+ return _registerRoute.apply(this, arguments);
233
+ }
234
+ /**
235
+ * Reload all components of a resource (schema, model, controller, cache, route)
236
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
237
+ * @returns {object} Result with success status, messages, and any errors
238
+ */
239
+
240
+
241
+ function _registerRoute() {
242
+ _registerRoute = _asyncToGenerator(function* (resourceName) {
243
+ try {
244
+ var routePath = _path.default.join(spice.root_path, "routes", resourceName + ".js");
245
+
246
+ if (!_fs.default.existsSync(routePath)) {
247
+ return {
248
+ success: false,
249
+ error: "Route file not found: " + routePath
250
+ };
251
+ }
252
+
253
+ clearRequireCache(routePath);
254
+
255
+ var router = require(routePath); // Handle ESM default exports
256
+
257
+
258
+ if (router.default) {
259
+ router = router.default;
260
+ } // Store the router
261
+
262
+
263
+ spice.routers[resourceName] = router;
264
+ spice.routers[resourceName.toLowerCase()] = router; // Register with Koa app
265
+
266
+ spice.app.use(router.routes()).use(router.allowedMethods());
267
+ return {
268
+ success: true,
269
+ message: "Route " + resourceName + " registered"
270
+ };
271
+ } catch (error) {
272
+ return {
273
+ success: false,
274
+ error: "Failed to register route: " + error.message
275
+ };
276
+ }
277
+ });
278
+ return _registerRoute.apply(this, arguments);
279
+ }
280
+
281
+ function reloadResource(_x6) {
282
+ return _reloadResource.apply(this, arguments);
283
+ }
284
+ /**
285
+ * Remove a resource from memory
286
+ * This removes the resource from spice.models, spice.controllers, spice.schemas, spice.cache
287
+ * Note: Routes cannot be unregistered from Koa, they will remain orphaned until restart
288
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
289
+ * @returns {object} Result with success status and message
290
+ */
291
+
292
+
293
+ function _reloadResource() {
294
+ _reloadResource = _asyncToGenerator(function* (resourceName) {
295
+ var results = {
296
+ success: true,
297
+ messages: [],
298
+ errors: []
299
+ }; // Reload in order: schema first (dependencies might rely on it)
300
+
301
+ var operations = [{
302
+ name: "schema",
303
+ fn: reloadSchema
304
+ }, {
305
+ name: "model",
306
+ fn: reloadModel
307
+ }, {
308
+ name: "controller",
309
+ fn: reloadController
310
+ }, {
311
+ name: "cache",
312
+ fn: reloadCache
313
+ }, {
314
+ name: "route",
315
+ fn: registerRoute
316
+ }];
317
+
318
+ for (var op of operations) {
319
+ var result = yield op.fn(resourceName);
320
+
321
+ if (result.success) {
322
+ results.messages.push(result.message);
323
+ } else {
324
+ results.errors.push(op.name + ": " + result.error); // Continue anyway - some resources might not have all components
325
+ }
326
+ } // Consider success if at least some components loaded
327
+
328
+
329
+ results.success = results.messages.length > 0;
330
+ return results;
331
+ });
332
+ return _reloadResource.apply(this, arguments);
333
+ }
334
+
335
+ function removeResourceFromMemory(resourceName) {
336
+ var removed = []; // Remove from schemas
337
+
338
+ if (spice.schemas) {
339
+ if (spice.schemas[resourceName]) {
340
+ delete spice.schemas[resourceName];
341
+ removed.push("schema");
342
+ }
343
+
344
+ if (spice.schemas[resourceName.toLowerCase()]) {
345
+ delete spice.schemas[resourceName.toLowerCase()];
346
+ }
347
+ } // Remove from models
348
+
349
+
350
+ if (spice.models) {
351
+ if (spice.models[resourceName]) {
352
+ delete spice.models[resourceName];
353
+ removed.push("model");
354
+ }
355
+
356
+ if (spice.models[resourceName.toLowerCase()]) {
357
+ delete spice.models[resourceName.toLowerCase()];
358
+ }
359
+ } // Remove from controllers
360
+
361
+
362
+ if (spice.controllers) {
363
+ if (spice.controllers[resourceName]) {
364
+ delete spice.controllers[resourceName];
365
+ removed.push("controller");
366
+ }
367
+
368
+ if (spice.controllers[resourceName.toLowerCase()]) {
369
+ delete spice.controllers[resourceName.toLowerCase()];
370
+ }
371
+ } // Remove from cache
372
+
373
+
374
+ if (spice.cache) {
375
+ if (spice.cache[resourceName]) {
376
+ delete spice.cache[resourceName];
377
+ removed.push("cache");
378
+ }
379
+
380
+ if (spice.cache[resourceName.toLowerCase()]) {
381
+ delete spice.cache[resourceName.toLowerCase()];
382
+ }
383
+ } // Remove from routers (the route middleware remains in Koa but won't have backing)
384
+
385
+
386
+ if (spice.routers) {
387
+ if (spice.routers[resourceName]) {
388
+ delete spice.routers[resourceName];
389
+ removed.push("router");
390
+ }
391
+
392
+ if (spice.routers[resourceName.toLowerCase()]) {
393
+ delete spice.routers[resourceName.toLowerCase()];
394
+ }
395
+ } // Also clear from Node.js require cache to prevent stale imports
396
+
397
+
398
+ var resourceTypes = ["schemas", "models", "controllers", "cache", "routes"];
399
+
400
+ for (var type of resourceTypes) {
401
+ var filePath = _path.default.join(spice.root_path, type, resourceName + ".js");
402
+
403
+ try {
404
+ clearRequireCache(filePath);
405
+ } catch (e) {// File might not exist, that's ok
406
+ }
407
+ }
408
+
409
+ return {
410
+ success: true,
411
+ message: "Resource " + resourceName + " removed from memory",
412
+ removed,
413
+ note: "Routes remain in Koa middleware stack but will fail without backing controller/model. Full cleanup on restart."
414
+ };
415
+ }
416
+
417
+ var _default = {
418
+ reloadSchema,
419
+ reloadModel,
420
+ reloadController,
421
+ reloadCache,
422
+ registerRoute,
423
+ reloadResource,
424
+ removeResourceFromMemory
425
+ };
426
+ exports.default = _default;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spice-js",
3
- "version": "2.7.1",
3
+ "version": "2.7.3",
4
4
  "description": "spice",
5
5
  "main": "build/index.js",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -24,6 +24,8 @@ export { default as Crypt } from "./utility/Crypt";
24
24
  export { default as Serializer } from "./utility/Serializer";
25
25
 
26
26
  export { default as RestHelper } from "./utility/RestHelper";
27
+
28
+ export { default as ResourceReloader } from "./utility/ResourceReloader";
27
29
  import Status from "./utility/Status";
28
30
  export { default as Mail } from "./mail/Mail";
29
31
  export { default as DataType } from "./utility/DataType";
@@ -80,9 +82,9 @@ export default class Spice {
80
82
 
81
83
  spice.addModifier = function (resource, modifier) {
82
84
  spice.mofifiers[resource.toLowerCase()] =
83
- spice.mofifiers[resource] == undefined
84
- ? [modifier]
85
- : [...spice.mofifiers[resource], modifier];
85
+ spice.mofifiers[resource] == undefined ?
86
+ [modifier]
87
+ : [...spice.mofifiers[resource], modifier];
86
88
  };
87
89
 
88
90
  spice.getModifiers = function (resource) {
@@ -96,14 +98,14 @@ export default class Spice {
96
98
  /* app._io.on("connection", (sock) => {
97
99
  console.log("Connection Up", sock);
98
100
  }); */
99
-
101
+
100
102
  // ⚡ OPTIMIZED: Load routes and models first, then generate docs lazily
101
103
  await require("./loaders").load();
102
-
104
+
103
105
  // ⚡ LAZY DOCS: Generate docs in background after startup to not block server
104
106
  let docsCache = null;
105
107
  let docsGenerating = false;
106
-
108
+
107
109
  // Generate docs asynchronously after startup
108
110
  const generateDocsInBackground = async () => {
109
111
  if (docsCache || docsGenerating) return;
@@ -114,26 +116,26 @@ export default class Spice {
114
116
  console.log("API documentation generated successfully");
115
117
  } catch (error) {
116
118
  console.error("Error generating docs:", error);
117
- docsCache = {
118
- swagger: "2.0",
119
- info: { title: "API Docs", version: "1.0.0" },
120
- paths: {},
121
- definitions: {}
119
+ docsCache = {
120
+ swagger: "2.0",
121
+ info: { title: "API Docs", version: "1.0.0" },
122
+ paths: {},
123
+ definitions: {},
122
124
  };
123
125
  } finally {
124
126
  docsGenerating = false;
125
127
  }
126
128
  };
127
-
129
+
128
130
  // Start generating docs in background (non-blocking)
129
131
  setTimeout(generateDocsInBackground, 100);
130
-
132
+
131
133
  // Middleware to serve docs - will wait if still generating
132
134
  app.use(async (ctx, next) => {
133
135
  if (ctx.path === "/docs/spec" || ctx.path === "/docs/spec.json") {
134
136
  // Wait for docs to be generated if not ready
135
137
  while (docsGenerating && !docsCache) {
136
- await new Promise(resolve => setTimeout(resolve, 100));
138
+ await new Promise((resolve) => setTimeout(resolve, 100));
137
139
  }
138
140
  if (!docsCache) {
139
141
  await generateDocsInBackground();
@@ -143,7 +145,7 @@ export default class Spice {
143
145
  }
144
146
  await next();
145
147
  });
146
-
148
+
147
149
  app.use(
148
150
  koaSwagger({
149
151
  hideTopbar: true,
@@ -606,7 +606,10 @@ export default class SpiceModel {
606
606
  }
607
607
 
608
608
  async getMulti(args) {
609
- try {
609
+ // ⚡ Profiling: use track() for proper async context forking
610
+ const p = this[_ctx]?.profiler;
611
+
612
+ const doGetMulti = async () => {
610
613
  if (!args) {
611
614
  args = {};
612
615
  }
@@ -653,6 +656,15 @@ export default class SpiceModel {
653
656
  }
654
657
 
655
658
  return results;
659
+ };
660
+
661
+ try {
662
+ if (p) {
663
+ return await p.track(`${this.type}.getMulti`, doGetMulti, {
664
+ ids_count: args?.ids?.length || 0,
665
+ });
666
+ }
667
+ return await doGetMulti();
656
668
  } catch (e) {
657
669
  console.warn(e.stack);
658
670
  throw e;
@@ -1445,6 +1457,9 @@ export default class SpiceModel {
1445
1457
  }
1446
1458
 
1447
1459
  async mapToObject(data, Class, source_property, store_property, property) {
1460
+ // ⚡ Get profiler for proper async context forking
1461
+ const p = this[_ctx]?.profiler;
1462
+
1448
1463
  let original_is_array = _.isArray(data);
1449
1464
  if (!original_is_array) {
1450
1465
  data = Array.of(data);
@@ -1469,21 +1484,36 @@ export default class SpiceModel {
1469
1484
  `${this[_current_path]}.${source_property}`
1470
1485
  : source_property;
1471
1486
 
1472
- var returned_all = await Promise.allSettled(
1473
- _.map(classes, (obj) => {
1474
- return new obj({
1475
- ...this[_args],
1476
- skip_cache: this[_skip_cache],
1477
- _level: this[_level] + 1,
1478
- mapping_dept: this[_mapping_dept],
1479
- mapping_dept_exempt: this[_mapping_dept_exempt],
1480
- _current_path: childPath,
1481
- }).getMulti({
1482
- skip_hooks: true,
1483
- ids: ids,
1484
- });
1485
- })
1486
- );
1487
+ // Wrap in profiler track() to ensure proper async context for child operations
1488
+ const fetchRelated = async () => {
1489
+ return await Promise.allSettled(
1490
+ _.map(classes, (obj) => {
1491
+ return new obj({
1492
+ ...this[_args],
1493
+ skip_cache: this[_skip_cache],
1494
+ _level: this[_level] + 1,
1495
+ mapping_dept: this[_mapping_dept],
1496
+ mapping_dept_exempt: this[_mapping_dept_exempt],
1497
+ _current_path: childPath,
1498
+ }).getMulti({
1499
+ skip_hooks: true,
1500
+ ids: ids,
1501
+ });
1502
+ })
1503
+ );
1504
+ };
1505
+
1506
+ var returned_all;
1507
+ if (p && ids.length > 0) {
1508
+ returned_all = await p.track(
1509
+ `${this.type}.map.${source_property}`,
1510
+ fetchRelated,
1511
+ { ids_count: ids.length }
1512
+ );
1513
+ } else {
1514
+ returned_all = await fetchRelated();
1515
+ }
1516
+
1487
1517
  let ug = _.flatten(
1488
1518
  _.compact(
1489
1519
  _.map(returned_all, (returned_obj) => {
@@ -1511,6 +1541,9 @@ export default class SpiceModel {
1511
1541
  store_property,
1512
1542
  property
1513
1543
  ) {
1544
+ // ⚡ Get profiler for proper async context forking
1545
+ const p = this[_ctx]?.profiler;
1546
+
1514
1547
  let original_is_array = _.isArray(data);
1515
1548
  if (!original_is_array) {
1516
1549
  data = Array.of(data);
@@ -1540,21 +1573,36 @@ export default class SpiceModel {
1540
1573
  : source_property;
1541
1574
 
1542
1575
  let classes = _.compact(_.isArray(Class) ? Class : [Class]);
1543
- var returned_all = await Promise.allSettled(
1544
- _.map(classes, (obj) => {
1545
- return new obj({
1546
- ...this[_args],
1547
- skip_cache: this[_skip_cache],
1548
- _level: this[_level] + 1,
1549
- mapping_dept: this[_mapping_dept],
1550
- mapping_dept_exempt: this[_mapping_dept_exempt],
1551
- _current_path: childPath,
1552
- }).getMulti({
1553
- skip_hooks: true,
1554
- ids: ids,
1555
- });
1556
- })
1557
- );
1576
+
1577
+ // ⚡ Wrap in profiler track() to ensure proper async context for child operations
1578
+ const fetchRelated = async () => {
1579
+ return await Promise.allSettled(
1580
+ _.map(classes, (obj) => {
1581
+ return new obj({
1582
+ ...this[_args],
1583
+ skip_cache: this[_skip_cache],
1584
+ _level: this[_level] + 1,
1585
+ mapping_dept: this[_mapping_dept],
1586
+ mapping_dept_exempt: this[_mapping_dept_exempt],
1587
+ _current_path: childPath,
1588
+ }).getMulti({
1589
+ skip_hooks: true,
1590
+ ids: ids,
1591
+ });
1592
+ })
1593
+ );
1594
+ };
1595
+
1596
+ var returned_all;
1597
+ if (p && ids.length > 0) {
1598
+ returned_all = await p.track(
1599
+ `${this.type}.mapArray.${source_property}`,
1600
+ fetchRelated,
1601
+ { ids_count: ids.length }
1602
+ );
1603
+ } else {
1604
+ returned_all = await fetchRelated();
1605
+ }
1558
1606
 
1559
1607
  var returned_objects = _.flatten(
1560
1608
  _.compact(
@@ -1564,9 +1612,6 @@ export default class SpiceModel {
1564
1612
  )
1565
1613
  );
1566
1614
 
1567
- /* let returned_objects = await new Class().getMulti({
1568
- ids: ids,
1569
- }); */
1570
1615
  _.each(data, (result) => {
1571
1616
  if (_.isString(result[store_property])) {
1572
1617
  result[store_property] = [result[store_property]];