spice-js 2.7.0 → 2.7.2

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/build/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  exports.__esModule = true;
4
- exports.default = exports.status = exports.addTask = exports.MapType = exports.SpiceCache = exports.MailFile = exports.AI = exports.Storage = exports.LocalStorage = exports.DebugStorage = exports.MailDebug = exports.DataType = exports.Mail = exports.RestHelper = exports.Serializer = exports.Crypt = exports.SpiceModel = exports.ResourceLifecycleTriggered = exports.SocketEventDriver = exports.SpiceEventDriver = exports.EventDebugger = exports.Event = void 0;
4
+ exports.default = exports.status = exports.addTask = exports.MapType = exports.SpiceCache = exports.MailFile = exports.AI = exports.Storage = exports.LocalStorage = exports.DebugStorage = exports.MailDebug = exports.DataType = exports.Mail = exports.ResourceReloader = exports.RestHelper = exports.Serializer = exports.Crypt = exports.SpiceModel = exports.ResourceLifecycleTriggered = exports.SocketEventDriver = exports.SpiceEventDriver = exports.EventDebugger = exports.Event = void 0;
5
5
 
6
6
  var _Event = _interopRequireDefault(require("./events/Event"));
7
7
 
@@ -39,6 +39,10 @@ var _RestHelper = _interopRequireDefault(require("./utility/RestHelper"));
39
39
 
40
40
  exports.RestHelper = _RestHelper.default;
41
41
 
42
+ var _ResourceReloader = _interopRequireDefault(require("./utility/ResourceReloader"));
43
+
44
+ exports.ResourceReloader = _ResourceReloader.default;
45
+
42
46
  var _Status = _interopRequireDefault(require("./utility/Status"));
43
47
 
44
48
  var _Mail = _interopRequireDefault(require("./mail/Mail"));
@@ -1924,52 +1924,24 @@ class SpiceModel {
1924
1924
  var requestedColumns = _this18.parseRequestedColumns(args == null ? void 0 : args.columns); // Cache the modifiers lookup for the specified type.
1925
1925
 
1926
1926
 
1927
- var modifiers = ((_this18$_serializers = _this18[_serializers]) == null ? void 0 : (_this18$_serializers$ = _this18$_serializers[type]) == null ? void 0 : _this18$_serializers$.modifiers) || []; // OPTIMIZED: Separate field-specific modifiers (can run in parallel) from generic ones (serial)
1928
-
1929
- var fieldModifiers = [];
1930
- var genericModifiers = [];
1927
+ var modifiers = ((_this18$_serializers = _this18[_serializers]) == null ? void 0 : (_this18$_serializers$ = _this18$_serializers[type]) == null ? void 0 : _this18$_serializers$.modifiers) || []; // Run modifiers serially
1931
1928
 
1932
1929
  for (var modifier of modifiers) {
1933
- // Skip field-specific modifiers if columns specified and neither source nor destination field is requested
1930
+ // Skip field-specific modifiers if columns specified and field is not requested
1934
1931
  if (requestedColumns && modifier.field && !requestedColumns.has(modifier.field) && !(modifier.sourceField && requestedColumns.has(modifier.sourceField))) {
1935
1932
  continue;
1936
- } // Field modifiers have a .field property and .execute function - they can run in parallel
1937
-
1938
-
1939
- if (modifier.field && typeof modifier.execute === "function") {
1940
- fieldModifiers.push(modifier);
1941
- } else {
1942
- genericModifiers.push(modifier);
1943
1933
  }
1944
- } // Run generic modifiers serially first (they may transform the data structure)
1945
1934
 
1946
-
1947
- for (var _modifier of genericModifiers) {
1948
1935
  try {
1949
- var executeFn = typeof _modifier === "function" ? _modifier : _modifier.execute;
1936
+ // Handle both function modifiers and object modifiers with .execute
1937
+ var executeFn = typeof modifier === "function" ? modifier : modifier.execute;
1950
1938
  var result = yield executeFn(data, old_data, _this18[_ctx], _this18.type); // Only assign if modifier returned a value to prevent data corruption
1951
1939
 
1952
1940
  if (result !== undefined) {
1953
1941
  data = result;
1954
1942
  }
1955
1943
  } catch (error) {
1956
- console.error("Modifier error in do_serialize (generic):", error.stack);
1957
- }
1958
- } // Run field-specific modifiers SERIALLY to allow proper deduplication
1959
- // of nested relation lookups (parallel execution causes duplicate fetches
1960
- // when related entities have circular references like user fields)
1961
-
1962
-
1963
- for (var _modifier2 of fieldModifiers) {
1964
- try {
1965
- var _result = yield _modifier2.execute(data, old_data, _this18[_ctx], _this18.type); // Only assign if modifier returned a value to prevent data corruption
1966
-
1967
-
1968
- if (_result !== undefined) {
1969
- data = _result;
1970
- }
1971
- } catch (error) {
1972
- console.error("Modifier error for field " + _modifier2.field + ":", error.stack);
1944
+ console.error("Modifier error in do_serialize:", error.stack);
1973
1945
  }
1974
1946
  } // Ensure data is always an array for consistent processing.
1975
1947
 
@@ -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.0",
3
+ "version": "2.7.2",
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,
@@ -1727,12 +1727,9 @@ export default class SpiceModel {
1727
1727
  // Cache the modifiers lookup for the specified type.
1728
1728
  const modifiers = this[_serializers]?.[type]?.modifiers || [];
1729
1729
 
1730
- // OPTIMIZED: Separate field-specific modifiers (can run in parallel) from generic ones (serial)
1731
- const fieldModifiers = [];
1732
- const genericModifiers = [];
1733
-
1730
+ // Run modifiers serially
1734
1731
  for (const modifier of modifiers) {
1735
- // Skip field-specific modifiers if columns specified and neither source nor destination field is requested
1732
+ // Skip field-specific modifiers if columns specified and field is not requested
1736
1733
  if (
1737
1734
  requestedColumns &&
1738
1735
  modifier.field &&
@@ -1742,17 +1739,8 @@ export default class SpiceModel {
1742
1739
  continue;
1743
1740
  }
1744
1741
 
1745
- // Field modifiers have a .field property and .execute function - they can run in parallel
1746
- if (modifier.field && typeof modifier.execute === "function") {
1747
- fieldModifiers.push(modifier);
1748
- } else {
1749
- genericModifiers.push(modifier);
1750
- }
1751
- }
1752
-
1753
- // Run generic modifiers serially first (they may transform the data structure)
1754
- for (const modifier of genericModifiers) {
1755
1742
  try {
1743
+ // Handle both function modifiers and object modifiers with .execute
1756
1744
  const executeFn =
1757
1745
  typeof modifier === "function" ? modifier : modifier.execute;
1758
1746
  const result = await executeFn(data, old_data, this[_ctx], this.type);
@@ -1761,33 +1749,7 @@ export default class SpiceModel {
1761
1749
  data = result;
1762
1750
  }
1763
1751
  } catch (error) {
1764
- console.error(
1765
- "Modifier error in do_serialize (generic):",
1766
- error.stack
1767
- );
1768
- }
1769
- }
1770
-
1771
- // Run field-specific modifiers SERIALLY to allow proper deduplication
1772
- // of nested relation lookups (parallel execution causes duplicate fetches
1773
- // when related entities have circular references like user fields)
1774
- for (const modifier of fieldModifiers) {
1775
- try {
1776
- const result = await modifier.execute(
1777
- data,
1778
- old_data,
1779
- this[_ctx],
1780
- this.type
1781
- );
1782
- // Only assign if modifier returned a value to prevent data corruption
1783
- if (result !== undefined) {
1784
- data = result;
1785
- }
1786
- } catch (error) {
1787
- console.error(
1788
- `Modifier error for field ${modifier.field}:`,
1789
- error.stack
1790
- );
1752
+ console.error("Modifier error in do_serialize:", error.stack);
1791
1753
  }
1792
1754
  }
1793
1755
 
@@ -0,0 +1,333 @@
1
+ import path from "path";
2
+ import fs from "fs";
3
+
4
+ /**
5
+ * ResourceReloader - Utility for dynamically reloading resources at runtime
6
+ * without requiring a full application restart.
7
+ *
8
+ * This enables zero-downtime updates for:
9
+ * - Schema updates
10
+ * - Resource creation
11
+ * - Resource deletion (removes from memory, routes orphaned until restart)
12
+ */
13
+
14
+ /**
15
+ * Clear the Node.js require cache for a specific file path
16
+ * @param {string} filePath - The full path to the file
17
+ */
18
+ function clearRequireCache(filePath) {
19
+ // Resolve the file path to handle different require formats
20
+ const resolvedPath = require.resolve(filePath);
21
+ if (require.cache[resolvedPath]) {
22
+ delete require.cache[resolvedPath];
23
+ return true;
24
+ }
25
+ return false;
26
+ }
27
+
28
+ /**
29
+ * Reload a schema into memory
30
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
31
+ * @returns {object} Result with success status and message
32
+ */
33
+ export async function reloadSchema(resourceName) {
34
+ try {
35
+ const schemaPath = path.join(
36
+ spice.root_path,
37
+ "schemas",
38
+ `${resourceName}.js`
39
+ );
40
+
41
+ if (!fs.existsSync(schemaPath)) {
42
+ return { success: false, error: `Schema file not found: ${schemaPath}` };
43
+ }
44
+
45
+ clearRequireCache(schemaPath);
46
+ const imported = require(schemaPath);
47
+ const schema = imported.default || imported;
48
+
49
+ // Store with both original and lowercase keys
50
+ spice.schemas[resourceName] = schema;
51
+ spice.schemas[resourceName.toLowerCase()] = schema;
52
+
53
+ return { success: true, message: `Schema ${resourceName} reloaded` };
54
+ } catch (error) {
55
+ return {
56
+ success: false,
57
+ error: `Failed to reload schema: ${error.message}`,
58
+ };
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Reload a model into memory
64
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
65
+ * @returns {object} Result with success status and message
66
+ */
67
+ export async function reloadModel(resourceName) {
68
+ try {
69
+ const modelPath = path.join(
70
+ spice.root_path,
71
+ "models",
72
+ `${resourceName}.js`
73
+ );
74
+
75
+ if (!fs.existsSync(modelPath)) {
76
+ return { success: false, error: `Model file not found: ${modelPath}` };
77
+ }
78
+
79
+ clearRequireCache(modelPath);
80
+ const imported = require(modelPath);
81
+ const model = imported.default || imported;
82
+
83
+ // Store with both original and lowercase keys
84
+ spice.models[resourceName] = model;
85
+ spice.models[resourceName.toLowerCase()] = model;
86
+
87
+ return { success: true, message: `Model ${resourceName} reloaded` };
88
+ } catch (error) {
89
+ return {
90
+ success: false,
91
+ error: `Failed to reload model: ${error.message}`,
92
+ };
93
+ }
94
+ }
95
+
96
+ /**
97
+ * Reload a controller into memory
98
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
99
+ * @returns {object} Result with success status and message
100
+ */
101
+ export async function reloadController(resourceName) {
102
+ try {
103
+ const controllerPath = path.join(
104
+ spice.root_path,
105
+ "controllers",
106
+ `${resourceName}.js`
107
+ );
108
+
109
+ if (!fs.existsSync(controllerPath)) {
110
+ return {
111
+ success: false,
112
+ error: `Controller file not found: ${controllerPath}`,
113
+ };
114
+ }
115
+
116
+ clearRequireCache(controllerPath);
117
+ const imported = require(controllerPath);
118
+ const controller = imported.default || imported;
119
+
120
+ // Store with both original and lowercase keys
121
+ spice.controllers[resourceName] = controller;
122
+ spice.controllers[resourceName.toLowerCase()] = controller;
123
+
124
+ return { success: true, message: `Controller ${resourceName} reloaded` };
125
+ } catch (error) {
126
+ return {
127
+ success: false,
128
+ error: `Failed to reload controller: ${error.message}`,
129
+ };
130
+ }
131
+ }
132
+
133
+ /**
134
+ * Reload a cache into memory
135
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
136
+ * @returns {object} Result with success status and message
137
+ */
138
+ export async function reloadCache(resourceName) {
139
+ try {
140
+ const cachePath = path.join(spice.root_path, "cache", `${resourceName}.js`);
141
+
142
+ if (!fs.existsSync(cachePath)) {
143
+ return { success: false, error: `Cache file not found: ${cachePath}` };
144
+ }
145
+
146
+ clearRequireCache(cachePath);
147
+ const imported = require(cachePath);
148
+ const cache = imported.default || imported;
149
+
150
+ // Store with both original and lowercase keys
151
+ spice.cache[resourceName] = cache;
152
+ spice.cache[resourceName.toLowerCase()] = cache;
153
+
154
+ return { success: true, message: `Cache ${resourceName} reloaded` };
155
+ } catch (error) {
156
+ return {
157
+ success: false,
158
+ error: `Failed to reload cache: ${error.message}`,
159
+ };
160
+ }
161
+ }
162
+
163
+ /**
164
+ * Register a new route with the Koa app
165
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
166
+ * @returns {object} Result with success status and message
167
+ */
168
+ export async function registerRoute(resourceName) {
169
+ try {
170
+ const routePath = path.join(
171
+ spice.root_path,
172
+ "routes",
173
+ `${resourceName}.js`
174
+ );
175
+
176
+ if (!fs.existsSync(routePath)) {
177
+ return { success: false, error: `Route file not found: ${routePath}` };
178
+ }
179
+
180
+ clearRequireCache(routePath);
181
+ let router = require(routePath);
182
+
183
+ // Handle ESM default exports
184
+ if (router.default) {
185
+ router = router.default;
186
+ }
187
+
188
+ // Store the router
189
+ spice.routers[resourceName] = router;
190
+ spice.routers[resourceName.toLowerCase()] = router;
191
+
192
+ // Register with Koa app
193
+ spice.app.use(router.routes()).use(router.allowedMethods());
194
+
195
+ return { success: true, message: `Route ${resourceName} registered` };
196
+ } catch (error) {
197
+ return {
198
+ success: false,
199
+ error: `Failed to register route: ${error.message}`,
200
+ };
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Reload all components of a resource (schema, model, controller, cache, route)
206
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
207
+ * @returns {object} Result with success status, messages, and any errors
208
+ */
209
+ export async function reloadResource(resourceName) {
210
+ const results = {
211
+ success: true,
212
+ messages: [],
213
+ errors: [],
214
+ };
215
+
216
+ // Reload in order: schema first (dependencies might rely on it)
217
+ const operations = [
218
+ { name: "schema", fn: reloadSchema },
219
+ { name: "model", fn: reloadModel },
220
+ { name: "controller", fn: reloadController },
221
+ { name: "cache", fn: reloadCache },
222
+ { name: "route", fn: registerRoute },
223
+ ];
224
+
225
+ for (const op of operations) {
226
+ const result = await op.fn(resourceName);
227
+ if (result.success) {
228
+ results.messages.push(result.message);
229
+ } else {
230
+ results.errors.push(`${op.name}: ${result.error}`);
231
+ // Continue anyway - some resources might not have all components
232
+ }
233
+ }
234
+
235
+ // Consider success if at least some components loaded
236
+ results.success = results.messages.length > 0;
237
+
238
+ return results;
239
+ }
240
+
241
+ /**
242
+ * Remove a resource from memory
243
+ * This removes the resource from spice.models, spice.controllers, spice.schemas, spice.cache
244
+ * Note: Routes cannot be unregistered from Koa, they will remain orphaned until restart
245
+ * @param {string} resourceName - The resource name (e.g., "User", "Product")
246
+ * @returns {object} Result with success status and message
247
+ */
248
+ export function removeResourceFromMemory(resourceName) {
249
+ const removed = [];
250
+
251
+ // Remove from schemas
252
+ if (spice.schemas) {
253
+ if (spice.schemas[resourceName]) {
254
+ delete spice.schemas[resourceName];
255
+ removed.push("schema");
256
+ }
257
+ if (spice.schemas[resourceName.toLowerCase()]) {
258
+ delete spice.schemas[resourceName.toLowerCase()];
259
+ }
260
+ }
261
+
262
+ // Remove from models
263
+ if (spice.models) {
264
+ if (spice.models[resourceName]) {
265
+ delete spice.models[resourceName];
266
+ removed.push("model");
267
+ }
268
+ if (spice.models[resourceName.toLowerCase()]) {
269
+ delete spice.models[resourceName.toLowerCase()];
270
+ }
271
+ }
272
+
273
+ // Remove from controllers
274
+ if (spice.controllers) {
275
+ if (spice.controllers[resourceName]) {
276
+ delete spice.controllers[resourceName];
277
+ removed.push("controller");
278
+ }
279
+ if (spice.controllers[resourceName.toLowerCase()]) {
280
+ delete spice.controllers[resourceName.toLowerCase()];
281
+ }
282
+ }
283
+
284
+ // Remove from cache
285
+ if (spice.cache) {
286
+ if (spice.cache[resourceName]) {
287
+ delete spice.cache[resourceName];
288
+ removed.push("cache");
289
+ }
290
+ if (spice.cache[resourceName.toLowerCase()]) {
291
+ delete spice.cache[resourceName.toLowerCase()];
292
+ }
293
+ }
294
+
295
+ // Remove from routers (the route middleware remains in Koa but won't have backing)
296
+ if (spice.routers) {
297
+ if (spice.routers[resourceName]) {
298
+ delete spice.routers[resourceName];
299
+ removed.push("router");
300
+ }
301
+ if (spice.routers[resourceName.toLowerCase()]) {
302
+ delete spice.routers[resourceName.toLowerCase()];
303
+ }
304
+ }
305
+
306
+ // Also clear from Node.js require cache to prevent stale imports
307
+ const resourceTypes = ["schemas", "models", "controllers", "cache", "routes"];
308
+ for (const type of resourceTypes) {
309
+ const filePath = path.join(spice.root_path, type, `${resourceName}.js`);
310
+ try {
311
+ clearRequireCache(filePath);
312
+ } catch (e) {
313
+ // File might not exist, that's ok
314
+ }
315
+ }
316
+
317
+ return {
318
+ success: true,
319
+ message: `Resource ${resourceName} removed from memory`,
320
+ removed,
321
+ note: "Routes remain in Koa middleware stack but will fail without backing controller/model. Full cleanup on restart.",
322
+ };
323
+ }
324
+
325
+ export default {
326
+ reloadSchema,
327
+ reloadModel,
328
+ reloadController,
329
+ reloadCache,
330
+ registerRoute,
331
+ reloadResource,
332
+ removeResourceFromMemory,
333
+ };