spice-js 2.7.1 → 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/utility/ResourceReloader.js +426 -0
- package/package.json +1 -1
- package/src/index.js +17 -15
- 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"));
|
|
@@ -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,
|
|
@@ -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
|
+
};
|