spice-js 2.7.1 → 2.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,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
+ };