webspresso 0.0.97 → 0.0.98

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.
Files changed (100) hide show
  1. package/.agents/ADMIN.md +153 -5
  2. package/.agents/ARCHITECTURE.md +10 -0
  3. package/.agents/CLI.md +195 -1
  4. package/.agents/PLUGINS.md +153 -9
  5. package/.agents/QUEUE.md +43 -0
  6. package/bin/commands/add-esbuild.js +80 -0
  7. package/bin/commands/add-tailwind.js +76 -31
  8. package/bin/commands/analytics-prune.js +57 -0
  9. package/bin/commands/api.js +207 -15
  10. package/bin/commands/build.js +99 -0
  11. package/bin/commands/dev.js +64 -35
  12. package/bin/commands/doctor.js +26 -0
  13. package/bin/commands/email-prune.js +47 -0
  14. package/bin/commands/make-job.js +86 -0
  15. package/bin/commands/make-model.js +250 -0
  16. package/bin/commands/new.js +45 -28
  17. package/bin/commands/openapi.js +102 -0
  18. package/bin/commands/page.js +6 -3
  19. package/bin/commands/queue-prune.js +59 -0
  20. package/bin/commands/routes.js +52 -0
  21. package/bin/commands/service.js +6 -3
  22. package/bin/commands/typegen.js +106 -0
  23. package/bin/commands/upload-prune.js +79 -0
  24. package/bin/webspresso.js +22 -0
  25. package/core/applySchema.js +13 -6
  26. package/core/auth/hash.js +21 -12
  27. package/core/compression/middleware.js +23 -17
  28. package/core/openapi/build-from-api-routes.js +160 -29
  29. package/core/orm/cache/fingerprint.js +15 -1
  30. package/core/orm/cache/memory-provider.js +27 -2
  31. package/core/orm/index.js +12 -7
  32. package/core/orm/model.js +49 -7
  33. package/core/queue/adapters/database.js +94 -0
  34. package/core/queue/adapters/memory.js +62 -0
  35. package/core/queue/adapters/redis.js +80 -0
  36. package/core/queue/manager.js +123 -0
  37. package/core/ssr/stream.js +11 -0
  38. package/index.d.ts +660 -10
  39. package/index.js +44 -96
  40. package/package.json +1 -1
  41. package/plugins/admin-panel/api.js +279 -17
  42. package/plugins/admin-panel/app.js +114 -22
  43. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
  44. package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
  45. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +10 -5
  46. package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
  47. package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
  48. package/plugins/admin-panel/client/parts/08-record-list.js +243 -23
  49. package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
  50. package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
  51. package/plugins/admin-panel/core/admin-module.js +13 -7
  52. package/plugins/admin-panel/core/api-extensions.js +30 -2
  53. package/plugins/admin-panel/core/registry.js +96 -16
  54. package/plugins/admin-panel/index.js +96 -0
  55. package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
  56. package/plugins/admin-panel/modules/admin-users.js +16 -1
  57. package/plugins/admin-panel/modules/bulk-actions.js +13 -5
  58. package/plugins/admin-panel/modules/custom-pages.js +492 -3
  59. package/plugins/admin-panel/modules/dashboard.js +283 -0
  60. package/plugins/admin-panel/modules/menu.js +56 -18
  61. package/plugins/admin-panel/modules/user-management.js +24 -6
  62. package/plugins/audit-log/admin-component.js +11 -0
  63. package/plugins/audit-log/api-handlers.js +34 -0
  64. package/plugins/audit-log/index.js +129 -3
  65. package/plugins/audit-log/purge.js +60 -10
  66. package/plugins/dashboard/app.js +152 -27
  67. package/plugins/dashboard/index.js +143 -39
  68. package/plugins/dashboard/styles.js +78 -0
  69. package/plugins/data-exchange/export-xlsx.js +9 -2
  70. package/plugins/data-exchange/import.js +9 -2
  71. package/plugins/data-exchange/index.js +6 -3
  72. package/plugins/email/index.js +132 -2
  73. package/plugins/email/purge.js +60 -10
  74. package/plugins/index.js +4 -0
  75. package/plugins/orm-cache-admin/index.js +4 -2
  76. package/plugins/queue/index.js +11 -0
  77. package/plugins/schema-explorer/admin-component.js +714 -0
  78. package/plugins/schema-explorer/relations-graph.js +205 -0
  79. package/plugins/schema-explorer.js +248 -69
  80. package/plugins/site-analytics/admin-component.js +23 -0
  81. package/plugins/site-analytics/api-handlers.js +42 -0
  82. package/plugins/site-analytics/index.js +147 -2
  83. package/plugins/site-analytics/purge.js +168 -0
  84. package/plugins/swagger.js +261 -13
  85. package/plugins/upload/index.js +26 -0
  86. package/plugins/upload/orphan-pruner.js +292 -0
  87. package/src/api/api-loader.js +3 -1
  88. package/src/bundle/index.js +517 -0
  89. package/src/bundle/manifest.js +165 -0
  90. package/src/dev/banner.js +296 -0
  91. package/src/dev/colors.js +70 -0
  92. package/src/dev/index.js +62 -0
  93. package/src/dev/profiler.js +354 -0
  94. package/src/dev/typegen.js +497 -0
  95. package/src/dev-reload/index.js +611 -0
  96. package/src/file-router.js +210 -62
  97. package/src/helpers.js +958 -98
  98. package/src/njk-frontmatter.js +12 -0
  99. package/src/pages/page-loader.js +61 -11
  100. package/src/server.js +629 -63
@@ -3,6 +3,12 @@
3
3
  * Parses and validates request input against compiled schemas
4
4
  */
5
5
 
6
+ const EMPTY_INPUT = Object.freeze({
7
+ body: undefined,
8
+ params: undefined,
9
+ query: undefined,
10
+ });
11
+
6
12
  /**
7
13
  * Apply compiled schema to request
8
14
  * Parses body, params, and query against their respective schemas
@@ -14,18 +20,19 @@
14
20
  * @throws {ZodError} If validation fails
15
21
  */
16
22
  function applySchema(req, compiledSchema) {
23
+ // No schema means no validation
24
+ if (!compiledSchema) {
25
+ req.input = EMPTY_INPUT;
26
+ return;
27
+ }
28
+
17
29
  // Initialize req.input
18
30
  req.input = {
19
31
  body: undefined,
20
32
  params: undefined,
21
- query: undefined
33
+ query: undefined,
22
34
  };
23
35
 
24
- // No schema means no validation
25
- if (!compiledSchema) {
26
- return;
27
- }
28
-
29
36
  // Parse body if schema exists
30
37
  if (compiledSchema.body) {
31
38
  req.input.body = compiledSchema.body.parse(req.body);
package/core/auth/hash.js CHANGED
@@ -4,12 +4,18 @@
4
4
  * @module core/auth/hash
5
5
  */
6
6
 
7
- let bcrypt;
8
- try {
9
- bcrypt = require('bcrypt');
10
- } catch {
11
- // bcrypt is optional, will throw if used without installation
12
- bcrypt = null;
7
+ let bcrypt = null;
8
+ let bcryptChecked = false;
9
+ function getBcrypt() {
10
+ if (!bcryptChecked) {
11
+ bcryptChecked = true;
12
+ try {
13
+ bcrypt = require('bcrypt');
14
+ } catch {
15
+ bcrypt = null;
16
+ }
17
+ }
18
+ return bcrypt;
13
19
  }
14
20
 
15
21
  /**
@@ -25,7 +31,8 @@ const DEFAULT_ROUNDS = process.env.NODE_ENV === 'test' ? 1 : 12;
25
31
  * @returns {Promise<string>} Hashed password
26
32
  */
27
33
  async function hash(password, rounds = DEFAULT_ROUNDS) {
28
- if (!bcrypt) {
34
+ const b = getBcrypt();
35
+ if (!b) {
29
36
  throw new Error('bcrypt is required for password hashing. Install it with: npm install bcrypt');
30
37
  }
31
38
 
@@ -33,7 +40,7 @@ async function hash(password, rounds = DEFAULT_ROUNDS) {
33
40
  throw new Error('Password must be a non-empty string');
34
41
  }
35
42
 
36
- return bcrypt.hash(password, rounds);
43
+ return b.hash(password, rounds);
37
44
  }
38
45
 
39
46
  /**
@@ -43,7 +50,8 @@ async function hash(password, rounds = DEFAULT_ROUNDS) {
43
50
  * @returns {Promise<boolean>} True if password matches
44
51
  */
45
52
  async function verify(password, hashedPassword) {
46
- if (!bcrypt) {
53
+ const b = getBcrypt();
54
+ if (!b) {
47
55
  throw new Error('bcrypt is required for password verification. Install it with: npm install bcrypt');
48
56
  }
49
57
 
@@ -52,7 +60,7 @@ async function verify(password, hashedPassword) {
52
60
  }
53
61
 
54
62
  try {
55
- return await bcrypt.compare(password, hashedPassword);
63
+ return await b.compare(password, hashedPassword);
56
64
  } catch {
57
65
  return false;
58
66
  }
@@ -65,7 +73,8 @@ async function verify(password, hashedPassword) {
65
73
  * @returns {boolean} True if rehash is needed
66
74
  */
67
75
  function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
68
- if (!bcrypt) {
76
+ const b = getBcrypt();
77
+ if (!b) {
69
78
  throw new Error('bcrypt is required. Install it with: npm install bcrypt');
70
79
  }
71
80
 
@@ -74,7 +83,7 @@ function needsRehash(hashedPassword, rounds = DEFAULT_ROUNDS) {
74
83
  }
75
84
 
76
85
  try {
77
- const hashRounds = bcrypt.getRounds(hashedPassword);
86
+ const hashRounds = b.getRounds(hashedPassword);
78
87
  return hashRounds < rounds;
79
88
  } catch {
80
89
  return true;
@@ -42,6 +42,11 @@ function appendVary(res, field) {
42
42
  * @param {Object} [options.deflate] - Custom Deflate options
43
43
  * @returns {import('express').RequestHandler}
44
44
  */
45
+ function resCompress(enable = true) {
46
+ this._noCompression = !enable;
47
+ return this;
48
+ }
49
+
45
50
  function createCompressionMiddleware(options = {}) {
46
51
  const threshold = typeof options.threshold === 'number' && options.threshold >= 0 ? options.threshold : 1024;
47
52
  const supportedEncodings = Array.isArray(options.encodings) && options.encodings.length > 0
@@ -53,10 +58,7 @@ function createCompressionMiddleware(options = {}) {
53
58
  return function compressionMiddleware(req, res, next) {
54
59
  // Expose opt-out API on response object
55
60
  res._noCompression = false;
56
- res.compress = function(enable = true) {
57
- res._noCompression = !enable;
58
- return res;
59
- };
61
+ res.compress = resCompress;
60
62
 
61
63
  // Skip compression for HEAD requests
62
64
  if (req.method === 'HEAD') {
@@ -64,6 +66,10 @@ function createCompressionMiddleware(options = {}) {
64
66
  }
65
67
 
66
68
  const acceptEncoding = req.headers['accept-encoding'];
69
+ if (!acceptEncoding) {
70
+ return next();
71
+ }
72
+
67
73
  let selectedEncoding = null;
68
74
  let checkedEncoding = false;
69
75
 
@@ -75,10 +81,10 @@ function createCompressionMiddleware(options = {}) {
75
81
  return selectedEncoding;
76
82
  }
77
83
 
78
- // Original methods to wrap
79
- const origWrite = res.write.bind(res);
80
- const origEnd = res.end.bind(res);
81
- const origWriteHead = res.writeHead.bind(res);
84
+ // Original methods to wrap (store references without .bind() allocation)
85
+ const origWrite = res.write;
86
+ const origEnd = res.end;
87
+ const origWriteHead = res.writeHead;
82
88
 
83
89
  let compressStream = null;
84
90
  let isCompressing = false;
@@ -151,15 +157,15 @@ function createCompressionMiddleware(options = {}) {
151
157
 
152
158
  // Pipe compression stream chunks directly into underlying socket/response
153
159
  compressStream.on('data', (chunk) => {
154
- origWrite(chunk);
160
+ origWrite.call(res, chunk);
155
161
  });
156
162
 
157
163
  compressStream.on('end', () => {
158
- origEnd();
164
+ origEnd.call(res);
159
165
  });
160
166
 
161
167
  compressStream.on('error', (err) => {
162
- origEnd();
168
+ origEnd.call(res);
163
169
  });
164
170
  }
165
171
 
@@ -219,7 +225,7 @@ function createCompressionMiddleware(options = {}) {
219
225
  } else {
220
226
  // Send buffered chunks uncompressed
221
227
  for (const b of bufferedChunks) {
222
- origWrite(b);
228
+ origWrite.call(res, b);
223
229
  }
224
230
  bufferedChunks = [];
225
231
  }
@@ -232,7 +238,7 @@ function createCompressionMiddleware(options = {}) {
232
238
  return compressStream.write(buf, callback);
233
239
  }
234
240
 
235
- return origWrite(buf, callback);
241
+ return origWrite.call(res, buf, callback);
236
242
  };
237
243
 
238
244
  // Wrap end
@@ -253,7 +259,7 @@ function createCompressionMiddleware(options = {}) {
253
259
  } else if (isCompressing && compressStream) {
254
260
  compressStream.write(buf);
255
261
  } else {
256
- origWrite(buf);
262
+ origWrite.call(res, buf);
257
263
  }
258
264
  }
259
265
 
@@ -282,10 +288,10 @@ function createCompressionMiddleware(options = {}) {
282
288
  appendVary(res, 'Accept-Encoding');
283
289
  }
284
290
  for (const b of bufferedChunks) {
285
- origWrite(b);
291
+ origWrite.call(res, b);
286
292
  }
287
293
  bufferedChunks = [];
288
- return origEnd(callback);
294
+ return origEnd.call(res, callback);
289
295
  }
290
296
  }
291
297
 
@@ -297,7 +303,7 @@ function createCompressionMiddleware(options = {}) {
297
303
  return res;
298
304
  }
299
305
 
300
- return origEnd(callback);
306
+ return origEnd.call(res, callback);
301
307
  };
302
308
 
303
309
  next();
@@ -1,5 +1,8 @@
1
+ 'use strict';
2
+
1
3
  /**
2
4
  * Build OpenAPI 3 document from file-router API route metadata + Zod schemas
5
+ * @module core/openapi/build-from-api-routes
3
6
  */
4
7
 
5
8
  const fs = require('fs');
@@ -16,11 +19,81 @@ const OPENAPI_METHODS = new Set(['get', 'post', 'put', 'patch', 'delete', 'head'
16
19
  * @returns {string}
17
20
  */
18
21
  function expressPathToOpenApi(expressPath) {
22
+ if (!expressPath) return '';
19
23
  return String(expressPath)
20
24
  .replace(/:([A-Za-z0-9_]+)/g, '{$1}')
25
+ .replace(/\[([A-Za-z0-9_]+)\]/g, '{$1}')
21
26
  .replace(/\*/g, '{wildcard}');
22
27
  }
23
28
 
29
+ /**
30
+ * Resolves the physical file path of an API route descriptor
31
+ * @param {Object} route
32
+ * @param {string} [pagesDir]
33
+ * @returns {string|null}
34
+ */
35
+ function resolveRouteFilePath(route, pagesDir) {
36
+ const rawFile = route.file || route.source;
37
+ if (!rawFile) return null;
38
+
39
+ if (path.isAbsolute(rawFile) && fs.existsSync(rawFile)) {
40
+ return rawFile;
41
+ }
42
+
43
+ const fromCwd = path.resolve(process.cwd(), rawFile);
44
+ if (fs.existsSync(fromCwd)) {
45
+ return fromCwd;
46
+ }
47
+
48
+ if (pagesDir) {
49
+ const fromPages = path.join(path.resolve(pagesDir), rawFile);
50
+ if (fs.existsSync(fromPages)) {
51
+ return fromPages;
52
+ }
53
+ }
54
+
55
+ return null;
56
+ }
57
+
58
+ /**
59
+ * Infers an expressive operation summary from HTTP method and path
60
+ * @param {string} method
61
+ * @param {string} routePath
62
+ * @returns {string}
63
+ */
64
+ function inferOperationSummary(method, routePath) {
65
+ const clean = String(routePath || '').replace(/^\/api\/?/, '').replace(/\/$/, '');
66
+ const segments = clean.split('/').filter(Boolean);
67
+ const lastSeg = segments[segments.length - 1] || 'root';
68
+ const isParam = lastSeg.startsWith(':') || (lastSeg.startsWith('{') && lastSeg.endsWith('}')) || (lastSeg.startsWith('[') && lastSeg.endsWith(']'));
69
+ const resource = isParam && segments.length > 1 ? segments[segments.length - 2] : lastSeg;
70
+ const m = String(method || 'GET').toUpperCase();
71
+
72
+ if (m === 'GET') {
73
+ return isParam ? `Get ${resource} by ID` : `List ${resource}`;
74
+ }
75
+ if (m === 'POST') return `Create ${resource}`;
76
+ if (m === 'PUT' || m === 'PATCH') return `Update ${resource}`;
77
+ if (m === 'DELETE') return `Delete ${resource}`;
78
+ return `${m} /api/${clean}`;
79
+ }
80
+
81
+ /**
82
+ * Infers tag category from route path segments
83
+ * @param {string} routePath
84
+ * @param {string[]} [explicitTags]
85
+ * @returns {string[]}
86
+ */
87
+ function inferOperationTags(routePath, explicitTags) {
88
+ if (Array.isArray(explicitTags) && explicitTags.length > 0) return explicitTags;
89
+ const clean = String(routePath || '').replace(/^\/api\/?/, '').replace(/\/$/, '');
90
+ const firstSeg = clean.split('/').filter(Boolean)[0];
91
+ if (firstSeg && !firstSeg.startsWith(':') && !firstSeg.startsWith('{') && !firstSeg.startsWith('[')) {
92
+ return [firstSeg];
93
+ }
94
+ return ['api'];
95
+ }
96
+
24
97
  /**
25
98
  * @param {import('zod').ZodObject<any>} zodObj
26
99
  * @param {'path' | 'query'} where
@@ -38,19 +111,31 @@ function expandZodObjectToParameters(zodObj, where) {
38
111
  }
39
112
 
40
113
  /**
41
- * @param {object} route - { type, method, pattern, file }
114
+ * @param {object} route - { type, method, pattern, path, file }
42
115
  * @param {object|null} compiled - compileSchema result
116
+ * @param {object|null} [mod] - Raw route module
43
117
  * @returns {object} OpenAPI Operation Object
44
118
  */
45
- function buildOperation(route, compiled) {
46
- const filePath = route.file;
47
- const method = route.method.toLowerCase();
48
- const summary = `${method.toUpperCase()} ${filePath}`;
119
+ function buildOperation(route, compiled, mod = null) {
120
+ const filePath = route.file || route.source || '';
121
+ const method = (route.method || 'GET').toLowerCase();
122
+ const routePath = route.path || route.pattern || filePath;
123
+
124
+ let summary = (mod && (mod.summary || mod.openapi?.summary)) || route.summary;
125
+ if (!summary) {
126
+ if (filePath) {
127
+ summary = `${method.toUpperCase()} ${filePath}`;
128
+ } else {
129
+ summary = inferOperationSummary(method, routePath);
130
+ }
131
+ }
132
+
133
+ const tags = inferOperationTags(routePath, (mod && (mod.tags || mod.openapi?.tags)) || route.tags);
49
134
 
50
135
  const op = {
51
- tags: ['api'],
136
+ tags,
52
137
  summary,
53
- operationId: String(filePath)
138
+ operationId: String(filePath || routePath)
54
139
  .replace(/[^a-zA-Z0-9]+/g, '_')
55
140
  .replace(/^_|_$/g, ''),
56
141
  responses: {
@@ -60,6 +145,25 @@ function buildOperation(route, compiled) {
60
145
  },
61
146
  };
62
147
 
148
+ if (mod && mod.description) {
149
+ op.description = mod.description;
150
+ }
151
+
152
+ // Auth / Security detection
153
+ const middlewares = Array.isArray(route.middleware)
154
+ ? route.middleware
155
+ : (Array.isArray(mod?.middleware) ? mod.middleware : []);
156
+ const security = [];
157
+ if (middlewares.includes('jwt') || middlewares.includes('bearer')) {
158
+ security.push({ bearerAuth: [] });
159
+ }
160
+ if (middlewares.includes('auth') || middlewares.includes('session')) {
161
+ security.push({ sessionAuth: [] });
162
+ }
163
+ if (security.length > 0) {
164
+ op.security = security;
165
+ }
166
+
63
167
  if (compiled?.response) {
64
168
  op.responses['200'] = {
65
169
  description: 'OK',
@@ -94,16 +198,17 @@ function buildOperation(route, compiled) {
94
198
  }
95
199
 
96
200
  /**
201
+ * Builds an OpenAPI 3.0 document from application routes and ORM components
97
202
  * @param {object} opts
98
- * @param {object[]} opts.routes - route metadata from mountPages
99
- * @param {string} opts.pagesDir
100
- * @param {boolean} [opts.includeOrmSchemas]
101
- * @param {string[]} [opts.ormExclude]
203
+ * @param {object[]} opts.routes - Route metadata array
204
+ * @param {string} opts.pagesDir - Pages directory path
205
+ * @param {boolean} [opts.includeOrmSchemas=false]
206
+ * @param {string[]} [opts.ormExclude=[]]
102
207
  * @param {object} [opts.info]
103
208
  * @param {object[]} [opts.servers]
104
209
  * @returns {object} OpenAPI 3.0.x document
105
210
  */
106
- function buildOpenApiDocument(opts) {
211
+ function buildOpenApiDocument(opts = {}) {
107
212
  const {
108
213
  routes = [],
109
214
  pagesDir,
@@ -118,31 +223,39 @@ function buildOpenApiDocument(opts) {
118
223
  }
119
224
 
120
225
  const paths = Object.create(null);
121
- const apiRoutes = routes.filter((r) => r.type === 'api');
122
- const absPages = path.resolve(pagesDir);
226
+ const apiRoutes = routes.filter((r) => r.type === 'api' || (r.path && r.path.startsWith('/api')));
123
227
 
124
228
  for (const route of apiRoutes) {
125
- const fullPath = path.join(absPages, route.file);
126
- if (!fs.existsSync(fullPath)) {
127
- continue;
128
- }
229
+ const fullPath = resolveRouteFilePath(route, pagesDir);
129
230
 
130
- let mod;
131
- try {
132
- mod = require(fullPath);
133
- } catch {
231
+ if (route.file && !fullPath) {
134
232
  continue;
135
233
  }
136
234
 
235
+ let mod = null;
137
236
  let compiled = null;
138
- try {
139
- compiled = compileSchema(fullPath, mod);
140
- } catch {
141
- compiled = null;
237
+
238
+ if (fullPath) {
239
+ try {
240
+ mod = require(fullPath);
241
+ } catch {
242
+ mod = null;
243
+ }
244
+
245
+ if (mod) {
246
+ try {
247
+ compiled = compileSchema(fullPath, mod);
248
+ } catch {
249
+ compiled = null;
250
+ }
251
+ }
142
252
  }
143
253
 
144
- const openApiPath = expressPathToOpenApi(route.pattern);
145
- const method = route.method.toLowerCase();
254
+ const routePattern = route.path || route.pattern || '';
255
+ if (!routePattern) continue;
256
+
257
+ const openApiPath = expressPathToOpenApi(routePattern);
258
+ const method = (route.method || 'GET').toLowerCase();
146
259
 
147
260
  if (!OPENAPI_METHODS.has(method)) {
148
261
  continue;
@@ -152,7 +265,7 @@ function buildOpenApiDocument(opts) {
152
265
  paths[openApiPath] = Object.create(null);
153
266
  }
154
267
 
155
- paths[openApiPath][method] = buildOperation(route, compiled);
268
+ paths[openApiPath][method] = buildOperation(route, compiled, mod);
156
269
  }
157
270
 
158
271
  const doc = {
@@ -165,6 +278,20 @@ function buildOpenApiDocument(opts) {
165
278
  servers,
166
279
  paths,
167
280
  components: {
281
+ securitySchemes: {
282
+ bearerAuth: {
283
+ type: 'http',
284
+ scheme: 'bearer',
285
+ bearerFormat: 'JWT',
286
+ description: 'Stateless JWT Bearer token authentication',
287
+ },
288
+ sessionAuth: {
289
+ type: 'apiKey',
290
+ in: 'cookie',
291
+ name: 'connect.sid',
292
+ description: 'Session cookie authentication',
293
+ },
294
+ },
168
295
  schemas: includeOrmSchemas ? generateOrmOpenApiSchemas({ exclude: ormExclude }) : {},
169
296
  },
170
297
  };
@@ -174,5 +301,9 @@ function buildOpenApiDocument(opts) {
174
301
 
175
302
  module.exports = {
176
303
  expressPathToOpenApi,
304
+ resolveRouteFilePath,
305
+ inferOperationSummary,
306
+ inferOperationTags,
307
+ buildOperation,
177
308
  buildOpenApiDocument,
178
309
  };
@@ -28,7 +28,21 @@ function stableValue(v) {
28
28
  const k = keys[0];
29
29
  return { [k]: stableValue(v[k]) };
30
30
  }
31
- keys.sort();
31
+ if (keyLen === 2) {
32
+ const k0 = keys[0];
33
+ const k1 = keys[1];
34
+ return k0 < k1
35
+ ? { [k0]: stableValue(v[k0]), [k1]: stableValue(v[k1]) }
36
+ : { [k1]: stableValue(v[k1]), [k0]: stableValue(v[k0]) };
37
+ }
38
+ let isSorted = true;
39
+ for (let i = 1; i < keyLen; i++) {
40
+ if (keys[i - 1] > keys[i]) {
41
+ isSorted = false;
42
+ break;
43
+ }
44
+ }
45
+ if (!isSorted) keys.sort();
32
46
  const out = {};
33
47
  for (let i = 0; i < keyLen; i++) {
34
48
  const k = keys[i];
@@ -56,8 +56,20 @@ function createMemoryCacheProvider(options = {}) {
56
56
  if (store.has(key)) removeKey(key);
57
57
 
58
58
  if (maxEntries > 0 && store.size >= maxEntries) {
59
- const first = store.keys().next().value;
60
- if (first !== undefined) removeKey(first);
59
+ // Active sweep: evict expired entries before falling back to FIFO eviction
60
+ const now = Date.now();
61
+ let freed = 0;
62
+ for (const [k, entry] of store.entries()) {
63
+ if (entry.expiresAt > 0 && now > entry.expiresAt) {
64
+ removeKey(k);
65
+ freed++;
66
+ if (freed >= 10 || store.size < maxEntries) break;
67
+ }
68
+ }
69
+ if (store.size >= maxEntries) {
70
+ const first = store.keys().next().value;
71
+ if (first !== undefined) removeKey(first);
72
+ }
61
73
  }
62
74
 
63
75
  const expiresAt = ttlMs > 0 ? Date.now() + ttlMs : 0;
@@ -82,6 +94,18 @@ function createMemoryCacheProvider(options = {}) {
82
94
  for (const t of tags) tagToKeys.delete(t);
83
95
  }
84
96
 
97
+ function pruneExpired() {
98
+ const now = Date.now();
99
+ let count = 0;
100
+ for (const [k, entry] of store.entries()) {
101
+ if (entry.expiresAt > 0 && now > entry.expiresAt) {
102
+ removeKey(k);
103
+ count++;
104
+ }
105
+ }
106
+ return count;
107
+ }
108
+
85
109
  function clear() {
86
110
  store.clear();
87
111
  tagToKeys.clear();
@@ -99,6 +123,7 @@ function createMemoryCacheProvider(options = {}) {
99
123
  get,
100
124
  set,
101
125
  invalidateTags,
126
+ pruneExpired,
102
127
  clear,
103
128
  getSizeStats,
104
129
  };
package/core/orm/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  const path = require('path');
8
8
  const fs = require('fs');
9
9
  const { createSchemaHelpers, extractColumnsFromSchema, getColumnMeta } = require('./schema-helpers');
10
- const { defineModel, getModel, getAllModels, hasModel, clearRegistry } = require('./model');
10
+ const { defineModel, getModel, getAllModels, hasModel, clearRegistry, resolveModelName } = require('./model');
11
11
  const { createRepository } = require('./repository');
12
12
  const { createMigrationManager } = require('./migrations');
13
13
  const { createSeeder } = require('./seeder');
@@ -172,13 +172,11 @@ function createDatabase(config) {
172
172
 
173
173
  /**
174
174
  * Get a model by name
175
- * @param {string|import('./types').ModelDefinition} nameOrModel - Model name or model definition
175
+ * @param {string|Object|import('./types').ModelDefinition} nameOrModel - Model name or model definition
176
176
  * @returns {import('./types').ModelDefinition}
177
177
  */
178
178
  function getModelInstance(nameOrModel) {
179
- const name = typeof nameOrModel === 'object' && nameOrModel !== null && nameOrModel.name
180
- ? nameOrModel.name
181
- : nameOrModel;
179
+ const name = resolveModelName(nameOrModel);
182
180
 
183
181
  // First check local registry
184
182
  let model = models.get(name);
@@ -199,11 +197,12 @@ function createDatabase(config) {
199
197
 
200
198
  /**
201
199
  * Check if a model exists
202
- * @param {string} name - Model name
200
+ * @param {string|Object} name - Model name
203
201
  * @returns {boolean}
204
202
  */
205
203
  function hasModelInstance(name) {
206
- return models.has(name);
204
+ const modelName = resolveModelName(name);
205
+ return models.has(modelName) || hasModel(modelName);
207
206
  }
208
207
 
209
208
  /**
@@ -211,6 +210,12 @@ function createDatabase(config) {
211
210
  * @returns {Array<import('./types').ModelDefinition>}
212
211
  */
213
212
  function getAllModelInstances() {
213
+ const globalModels = getAllModels();
214
+ for (const [name, model] of globalModels) {
215
+ if (!models.has(name)) {
216
+ models.set(name, model);
217
+ }
218
+ }
214
219
  return Array.from(models.values());
215
220
  }
216
221