webspresso 0.0.65 → 0.0.66

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,78 @@
1
+ /**
2
+ * Admin API for ORM cache metrics and purge
3
+ * @module plugins/orm-cache-admin/api-handlers
4
+ */
5
+
6
+ /**
7
+ * @param {object} options
8
+ * @param {import('../../core/orm/types').DatabaseInstance} options.db
9
+ */
10
+ function createOrmCacheAdminHandlers({ db }) {
11
+ function requireCache(res) {
12
+ if (!db.cache) {
13
+ res.status(503).json({ error: 'ORM cache is not enabled on this database' });
14
+ return null;
15
+ }
16
+ return db.cache;
17
+ }
18
+
19
+ async function getStats(req, res) {
20
+ try {
21
+ const cache = requireCache(res);
22
+ if (!cache) return;
23
+ res.json(cache.getMetrics());
24
+ } catch (e) {
25
+ res.status(500).json({ error: e.message });
26
+ }
27
+ }
28
+
29
+ async function postPurge(req, res) {
30
+ try {
31
+ const cache = requireCache(res);
32
+ if (!cache) return;
33
+ cache.purge();
34
+ res.json({ ok: true });
35
+ } catch (e) {
36
+ res.status(500).json({ error: e.message });
37
+ }
38
+ }
39
+
40
+ async function postInvalidate(req, res) {
41
+ try {
42
+ const cache = requireCache(res);
43
+ if (!cache) return;
44
+ const body = req.body && typeof req.body === 'object' ? req.body : {};
45
+ if (Array.isArray(body.tags) && body.tags.length > 0) {
46
+ cache.invalidateTags(body.tags.map(String));
47
+ return res.json({ ok: true, mode: 'tags' });
48
+ }
49
+ if (typeof body.model === 'string' && body.model.trim()) {
50
+ cache.invalidateModel(body.model.trim());
51
+ return res.json({ ok: true, mode: 'model' });
52
+ }
53
+ res.status(400).json({ error: 'Provide { model: string } or { tags: string[] }' });
54
+ } catch (e) {
55
+ res.status(500).json({ error: e.message });
56
+ }
57
+ }
58
+
59
+ async function postResetMetrics(req, res) {
60
+ try {
61
+ const cache = requireCache(res);
62
+ if (!cache) return;
63
+ cache.resetMetrics();
64
+ res.json({ ok: true });
65
+ } catch (e) {
66
+ res.status(500).json({ error: e.message });
67
+ }
68
+ }
69
+
70
+ return {
71
+ getStats,
72
+ postPurge,
73
+ postInvalidate,
74
+ postResetMetrics,
75
+ };
76
+ }
77
+
78
+ module.exports = { createOrmCacheAdminHandlers };
@@ -0,0 +1,72 @@
1
+ /**
2
+ * ORM Cache Admin — metrics, purge, invalidate (requires admin-panel + db.cache)
3
+ * @module plugins/orm-cache-admin
4
+ */
5
+
6
+ const { createOrmCacheAdminHandlers } = require('./api-handlers');
7
+ const { generateOrmCacheAdminComponent } = require('./admin-component');
8
+
9
+ /**
10
+ * @param {object} options
11
+ * @param {import('../../core/orm/types').DatabaseInstance} options.db - From createDatabase({ cache: true })
12
+ */
13
+ function ormCacheAdminPlugin(options = {}) {
14
+ const { db } = options;
15
+
16
+ if (!db) {
17
+ throw new Error('orm-cache-admin plugin requires `db` (database instance from createDatabase)');
18
+ }
19
+
20
+ return {
21
+ name: 'orm-cache-admin',
22
+ version: '1.0.0',
23
+ description: 'Admin UI for ORM query cache metrics and purge',
24
+ dependencies: { 'admin-panel': '*' },
25
+
26
+ register() {},
27
+
28
+ onRoutesReady(ctx) {
29
+ const adminApi = ctx.usePlugin('admin-panel');
30
+ if (!adminApi) {
31
+ console.warn('[orm-cache-admin] admin-panel not found, skipping registration');
32
+ return;
33
+ }
34
+
35
+ if (!db.cache) {
36
+ console.warn('[orm-cache-admin] db.cache is disabled; enable createDatabase({ cache: true })');
37
+ return;
38
+ }
39
+
40
+ const handlers = createOrmCacheAdminHandlers({ db });
41
+
42
+ adminApi.registerModule({
43
+ id: 'orm-cache',
44
+ pages: [{
45
+ id: 'orm-cache',
46
+ title: 'ORM Cache',
47
+ path: '/orm-cache',
48
+ icon: 'database',
49
+ component: generateOrmCacheAdminComponent(),
50
+ }],
51
+ menu: [{
52
+ id: 'orm-cache',
53
+ label: 'ORM Cache',
54
+ path: '/orm-cache',
55
+ icon: 'database',
56
+ order: 15,
57
+ }],
58
+ api: {
59
+ prefix: '/orm-cache',
60
+ routes: [
61
+ { method: 'get', path: '/stats', handler: handlers.getStats },
62
+ { method: 'post', path: '/purge', handler: handlers.postPurge },
63
+ { method: 'post', path: '/invalidate', handler: handlers.postInvalidate },
64
+ { method: 'post', path: '/metrics/reset', handler: handlers.postResetMetrics },
65
+ ],
66
+ },
67
+ });
68
+ },
69
+ };
70
+ }
71
+
72
+ module.exports = ormCacheAdminPlugin;
@@ -276,9 +276,58 @@ function detectLocale(req) {
276
276
  }
277
277
 
278
278
  /**
279
- * Resolve middleware from config - supports both functions and named strings
280
- * @param {Array} middlewareConfig - Array of middleware functions or names
281
- * @param {Object} middlewareRegistry - Named middleware registry
279
+ * True when the registry entry is (options) => (req, res, next) => …
280
+ * Express handlers typically have length >= 2 (req, res) or 3 (req, res, next).
281
+ */
282
+ function isMiddlewareFactory(fn) {
283
+ return typeof fn === 'function' && fn.length <= 1;
284
+ }
285
+
286
+ /**
287
+ * Resolve a named middleware from createApp({ middlewares }).
288
+ * @param {string} name
289
+ * @param {Function} entry
290
+ * @param {boolean} fromTuple - true when route used ['name', options]
291
+ * @param {unknown} tupleOptions - second element of the tuple (only when fromTuple)
292
+ * @param {Object} middlewareRegistry - for error messages
293
+ * @returns {Function} Express middleware
294
+ */
295
+ function resolveNamedMiddleware(name, entry, fromTuple, tupleOptions, middlewareRegistry) {
296
+ if (!entry) {
297
+ throw new Error(`Middleware "${name}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
298
+ }
299
+ if (typeof entry !== 'function') {
300
+ throw new Error(`Middleware "${name}" must be a function`);
301
+ }
302
+
303
+ if (fromTuple) {
304
+ if (!isMiddlewareFactory(entry)) {
305
+ throw new Error(
306
+ `Middleware "${name}" must be a factory (options) => (req, res, next) => … when using ["${name}", options] tuple form`
307
+ );
308
+ }
309
+ const produced = entry(tupleOptions);
310
+ if (typeof produced !== 'function') {
311
+ throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
312
+ }
313
+ return produced;
314
+ }
315
+
316
+ if (isMiddlewareFactory(entry)) {
317
+ const produced = entry({});
318
+ if (typeof produced !== 'function') {
319
+ throw new Error(`Middleware factory "${name}" must return an Express middleware function`);
320
+ }
321
+ return produced;
322
+ }
323
+
324
+ return entry;
325
+ }
326
+
327
+ /**
328
+ * Resolve middleware from config — functions, string names, or [name, options] tuples
329
+ * @param {Array} middlewareConfig - middleware functions, names, or ['name', options] tuples
330
+ * @param {Object} middlewareRegistry - Named middleware registry (plain handlers or option factories)
282
331
  * @returns {Array} Array of resolved middleware functions
283
332
  */
284
333
  function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
@@ -292,17 +341,17 @@ function resolveMiddlewares(middlewareConfig, middlewareRegistry = {}) {
292
341
  }
293
342
 
294
343
  if (typeof mw === 'string') {
295
- const resolved = middlewareRegistry[mw];
296
- if (!resolved) {
297
- throw new Error(`Middleware "${mw}" not found in registry. Available: ${Object.keys(middlewareRegistry).join(', ') || 'none'}`);
298
- }
299
- if (typeof resolved !== 'function') {
300
- throw new Error(`Middleware "${mw}" must be a function`);
301
- }
302
- return resolved;
344
+ return resolveNamedMiddleware(mw, middlewareRegistry[mw], false, undefined, middlewareRegistry);
345
+ }
346
+
347
+ if (Array.isArray(mw) && mw.length === 2 && typeof mw[0] === 'string') {
348
+ const name = mw[0];
349
+ return resolveNamedMiddleware(name, middlewareRegistry[name], true, mw[1], middlewareRegistry);
303
350
  }
304
351
 
305
- throw new Error(`Invalid middleware at index ${index}: must be a function or string name`);
352
+ throw new Error(
353
+ `Invalid middleware at index ${index}: must be a function, string name, or [name, options] tuple`
354
+ );
306
355
  });
307
356
  }
308
357
 
@@ -164,6 +164,8 @@ Analytics plugin adds `fsy.analyticsHead`, `fsy.verificationTags`, etc., when co
164
164
 
165
165
  **Transactions:** `db.transaction(async (trx) => { trx.getRepository('User') })`.
166
166
 
167
+ **Query cache (optional):** `createDatabase({ ..., cache: true })` or `cache: { defaultStrategy: 'auto'|'smart', memory: { maxEntries, defaultTtlMs }, provider?: custom }`. Opt-in per model: `defineModel({ ..., cache: 'auto'|'smart'|true })`. API: `db.cache` → `getMetrics()`, `purge()`, `invalidateModel(name)`, `invalidateTags(tags[])`, `resetMetrics()`. Reads bypass cache when using a transaction knex. Admin UI: `ormCacheAdminPlugin({ db })` (needs `admin-panel` and `cache` enabled).
168
+
167
169
  Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and plugins. **`pages/api/`** handlers receive **`req.db`** (and route **`middleware`** runs after it). Outside requests, use **`getDb()`** / **`hasDb()`**; for **`setupRoutes`**-only routes, use **`attachDbMiddleware`**.
168
170
 
169
171
  ---
@@ -180,6 +182,7 @@ Pass **`db`** into **`createApp({ db })`** so **`ctx.db`** works in pages and pl
180
182
  | `auditLogPlugin` | Admin mutation audit trail |
181
183
  | `recaptchaPlugin` | v2/v3 + middleware |
182
184
  | `seoCheckerPlugin` | Dev SEO panel |
185
+ | `ormCacheAdminPlugin` | Admin page for ORM cache metrics / purge / invalidate (`db.cache` required) |
183
186
 
184
187
  **Custom plugin:** `name`, `version`, `register(ctx)`, `onRoutesReady(ctx)` — use `ctx.app`, `ctx.db`, `ctx.addHelper`, `ctx.addRoute`, `ctx.usePlugin('other')`. Plugin failures **warn**; app keeps running.
185
188