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 +5 -1
- package/build/models/SpiceModel.js +5 -33
- package/build/utility/ResourceReloader.js +426 -0
- package/package.json +1 -1
- package/src/index.js +17 -15
- package/src/models/SpiceModel.js +4 -42
- package/src/utility/ResourceReloader.js +333 -0
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) || []; //
|
|
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
|
|
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
|
-
|
|
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
|
|
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
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
|
-
|
|
85
|
-
|
|
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,
|
package/src/models/SpiceModel.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
+
};
|