spice-js 2.7.1 → 2.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/index.js +5 -1
- package/build/models/SpiceModel.js +156 -83
- package/build/utility/ResourceReloader.js +426 -0
- package/package.json +1 -1
- package/src/index.js +17 -15
- package/src/models/SpiceModel.js +79 -34
- package/src/utility/ResourceReloader.js +333 -0
|
@@ -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
|
+
};
|