webspresso 0.0.90 → 0.0.91

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 (94) hide show
  1. package/README.md +358 -8
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/middleware.js +9 -0
  5. package/core/compression/compressible.js +51 -0
  6. package/core/compression/compression-stream.js +59 -0
  7. package/core/compression/index.js +25 -0
  8. package/core/compression/middleware.js +301 -0
  9. package/core/compression/negotiate.js +153 -0
  10. package/core/errors/base.js +31 -0
  11. package/core/errors/domain.js +193 -0
  12. package/core/errors/http.js +133 -0
  13. package/core/errors/index.js +77 -0
  14. package/core/errors/middleware.js +162 -0
  15. package/core/errors/normalize.js +171 -0
  16. package/core/orm/complexity.js +120 -0
  17. package/core/orm/events.js +64 -14
  18. package/core/orm/index.js +45 -10
  19. package/core/orm/model.js +2 -0
  20. package/core/orm/query-builder.js +30 -4
  21. package/core/orm/repository.js +57 -25
  22. package/core/orm/transaction.js +39 -5
  23. package/core/orm/utils.js +6 -3
  24. package/core/queue/adapters/database.js +284 -0
  25. package/core/queue/adapters/memory.js +155 -0
  26. package/core/queue/adapters/redis.js +336 -0
  27. package/core/queue/index.js +21 -0
  28. package/core/queue/job.js +103 -0
  29. package/core/queue/manager.js +387 -0
  30. package/core/realtime/adapters/redis.js +431 -0
  31. package/core/realtime/adapters/socket-io.js +203 -0
  32. package/core/realtime/adapters/sse.js +210 -0
  33. package/core/realtime/adapters/websocket.js +337 -0
  34. package/core/realtime/auth-manager.js +140 -0
  35. package/core/realtime/core.js +359 -0
  36. package/core/realtime/emitter.js +98 -0
  37. package/core/realtime/errors.js +71 -0
  38. package/core/realtime/index.js +79 -0
  39. package/core/realtime/reconnect-manager.js +118 -0
  40. package/core/realtime/registry.js +243 -0
  41. package/core/shutdown/http-adapter.js +134 -0
  42. package/core/shutdown/index.js +12 -0
  43. package/core/shutdown/shutdown-manager.js +312 -0
  44. package/core/ssr/index.js +13 -0
  45. package/core/ssr/stream.js +174 -0
  46. package/index.d.ts +829 -52
  47. package/index.js +103 -0
  48. package/package.json +22 -2
  49. package/plugins/admin-panel/api.js +37 -4
  50. package/plugins/admin-panel/app.js +57 -17
  51. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  52. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  53. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  54. package/plugins/admin-panel/client/parts/08-record-list.js +17 -24
  55. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  56. package/plugins/admin-panel/core/admin-module.js +2 -2
  57. package/plugins/admin-panel/core/api-extensions.js +149 -0
  58. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  59. package/plugins/admin-panel/index.js +90 -17
  60. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  61. package/plugins/admin-panel/modules/menu.js +85 -0
  62. package/plugins/admin-panel/modules/user-management.js +5 -2
  63. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  64. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  65. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  66. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  67. package/plugins/basic-auth/index.js +293 -0
  68. package/plugins/data-exchange/export-xlsx.js +5 -1
  69. package/plugins/data-exchange/import.js +16 -2
  70. package/plugins/data-exchange/index.js +14 -0
  71. package/plugins/email/index.js +14 -1
  72. package/plugins/index.js +29 -0
  73. package/plugins/queue/index.js +108 -0
  74. package/plugins/realtime/index.js +150 -0
  75. package/plugins/recaptcha/index.js +11 -0
  76. package/plugins/redirect/index.js +9 -1
  77. package/plugins/rest-resources/index.js +280 -11
  78. package/plugins/upload/index.js +16 -0
  79. package/src/app-context.js +46 -6
  80. package/src/file-router.js +196 -21
  81. package/src/helpers.js +20 -2
  82. package/src/plugin-manager.js +124 -4
  83. package/src/server.js +304 -44
  84. package/src/services/builtins/auth.js +273 -0
  85. package/src/services/builtins/exchange.js +229 -0
  86. package/src/services/builtins/mail.js +134 -0
  87. package/src/services/builtins/media.js +196 -0
  88. package/src/services/builtins/system.js +221 -0
  89. package/src/services/discovery.js +140 -0
  90. package/src/services/executor.js +292 -0
  91. package/src/services/index.js +70 -0
  92. package/src/services/memoize.js +256 -0
  93. package/src/services/registry.js +288 -0
  94. package/src/services/validator.js +281 -0
package/index.js CHANGED
@@ -10,9 +10,21 @@ const {
10
10
  getAppContext,
11
11
  getDb,
12
12
  hasDb,
13
+ getShutdownManager,
14
+ hasShutdownManager,
15
+ getServiceRegistry,
16
+ hasServiceRegistry,
13
17
  resetAppContext,
14
18
  setAppContext,
15
19
  } = require('./src/app-context');
20
+ const {
21
+ createServiceRegistry,
22
+ defineService,
23
+ ServiceRegistry,
24
+ } = require('./src/services');
25
+ const { ShutdownManager, NodeHttpAdapter } = require('./core/shutdown');
26
+ const compression = require('./core/compression');
27
+ const errors = require('./core/errors');
16
28
  const {
17
29
  mountPages,
18
30
  filePathToRoute,
@@ -64,6 +76,17 @@ const {
64
76
  rateLimitPlugin,
65
77
  contentPlugin,
66
78
  csrfPlugin,
79
+ corsPlugin,
80
+ emailPlugin,
81
+ basicAuthPlugin,
82
+ realtimePlugin,
83
+ realtime,
84
+ createRealtime,
85
+ websocket,
86
+ sse,
87
+ socketIo,
88
+ redis,
89
+ createRedisAdapter,
67
90
  } = require('./plugins');
68
91
 
69
92
  const content = require('./core/content');
@@ -78,8 +101,67 @@ module.exports = {
78
101
  getAppContext,
79
102
  getDb,
80
103
  hasDb,
104
+ getShutdownManager,
105
+ hasShutdownManager,
106
+ getServiceRegistry,
107
+ hasServiceRegistry,
81
108
  resetAppContext,
82
109
  setAppContext,
110
+
111
+ // Services Layer
112
+ createServiceRegistry,
113
+ defineService,
114
+ service: defineService,
115
+ ServiceRegistry,
116
+ memoize: require('./src/services').memoize,
117
+ parseTtlMs: require('./src/services').parseTtlMs,
118
+ stableCacheKey: require('./src/services').stableCacheKey,
119
+ sanitizeInput: require('./src/services').sanitizeInput,
120
+ maskSensitiveData: require('./src/services').maskSensitiveData,
121
+ discoverServices: require('./src/services').discoverServices,
122
+ filePathToServiceName: require('./src/services').filePathToServiceName,
123
+ toCamelCase: require('./src/services').toCamelCase,
124
+ createAuthServices: require('./src/services').createAuthServices,
125
+ createMailServices: require('./src/services').createMailServices,
126
+ createMediaServices: require('./src/services').createMediaServices,
127
+ createSystemServices: require('./src/services').createSystemServices,
128
+ createExchangeServices: require('./src/services').createExchangeServices,
129
+ validateServiceInput: require('./src/services').validateServiceInput,
130
+ executeService: require('./src/services').executeService,
131
+
132
+ // Shutdown & Lifecycle
133
+ ShutdownManager,
134
+ NodeHttpAdapter,
135
+
136
+ // HTTP Response Compression
137
+ compression,
138
+ createCompressionMiddleware: compression.createCompressionMiddleware,
139
+
140
+ // Exceptions & Error Handling
141
+ errors,
142
+ WebspressoError: errors.WebspressoError,
143
+ HttpError: errors.HttpError,
144
+ BadRequestError: errors.BadRequestError,
145
+ UnauthorizedError: errors.UnauthorizedError,
146
+ ForbiddenError: errors.ForbiddenError,
147
+ NotFoundError: errors.NotFoundError,
148
+ MethodNotAllowedError: errors.MethodNotAllowedError,
149
+ ConflictError: errors.ConflictError,
150
+ PayloadTooLargeError: errors.PayloadTooLargeError,
151
+ UnsupportedMediaTypeError: errors.UnsupportedMediaTypeError,
152
+ UnprocessableEntityError: errors.UnprocessableEntityError,
153
+ TooManyRequestsError: errors.TooManyRequestsError,
154
+ ValidationError: errors.ValidationError,
155
+ ConfigurationError: errors.ConfigurationError,
156
+ PluginError: errors.PluginError,
157
+ SecurityError: errors.SecurityError,
158
+ RequestError: errors.RequestError,
159
+ RequestAbortedError: errors.RequestAbortedError,
160
+ RouterError: errors.RouterError,
161
+ RouteNotFoundError: errors.RouteNotFoundError,
162
+ RouteGenerationError: errors.RouteGenerationError,
163
+ normalizeError: errors.normalizeError,
164
+ toErrorResponseObject: errors.toErrorResponseObject,
83
165
 
84
166
  // Router utilities (for advanced use)
85
167
  mountPages,
@@ -97,6 +179,11 @@ module.exports = {
97
179
  // Template helpers
98
180
  createHelpers,
99
181
  utils,
182
+
183
+ // SSR Streaming
184
+ ssr: require('./core/ssr'),
185
+ renderStream: require('./core/ssr').renderStream,
186
+ createHtmlStream: require('./core/ssr').createHtmlStream,
100
187
 
101
188
  // Asset management
102
189
  AssetManager,
@@ -135,7 +222,23 @@ module.exports = {
135
222
  rateLimitPlugin,
136
223
  contentPlugin,
137
224
  csrfPlugin,
225
+ corsPlugin,
226
+ emailPlugin,
227
+ basicAuthPlugin,
228
+ realtimePlugin,
229
+ realtime,
230
+ createRealtime,
231
+ websocket,
232
+ sse,
233
+ socketIo,
234
+ redis,
235
+ createRedisAdapter,
236
+ queuePlugin: require('./plugins/queue'),
237
+ queue: require('./core/queue'),
238
+ createQueueManager: require('./core/queue').createQueueManager,
239
+ QueueManager: require('./core/queue').QueueManager,
138
240
 
139
241
  // Schema-driven CMS core (framework-agnostic)
140
242
  content,
141
243
  };
244
+
package/package.json CHANGED
@@ -1,14 +1,34 @@
1
1
  {
2
2
  "name": "webspresso",
3
- "version": "0.0.90",
3
+ "version": "0.0.91",
4
4
  "description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ },
12
+ "./errors": {
13
+ "types": "./index.d.ts",
14
+ "default": "./core/errors/index.js"
15
+ },
16
+ "./plugins": "./plugins/index.js",
17
+ "./plugins/*": "./plugins/*",
18
+ "./realtime": "./core/realtime/index.js",
19
+ "./core/*": "./core/*",
20
+ "./src/*": "./src/*",
21
+ "./views/*": "./views/*",
22
+ "./templates/*": "./templates/*",
23
+ "./package.json": "./package.json"
24
+ },
7
25
  "bin": {
8
26
  "webspresso": "./bin/webspresso.js"
9
27
  },
10
28
  "scripts": {
11
29
  "test": "vitest run",
30
+ "test:security": "vitest run tests/security/",
31
+ "security": "npm run test:security && npm audit",
12
32
  "test:watch": "vitest",
13
33
  "test:coverage": "vitest run --coverage",
14
34
  "test:e2e": "playwright test",
@@ -62,7 +82,7 @@
62
82
  "csv-parse": "^5.6.0",
63
83
  "dayjs": "^1.11.19",
64
84
  "exceljs": "^4.4.0",
65
- "express": "^4.18.2",
85
+ "express": "^5.2.1",
66
86
  "express-session": "^1.18.0",
67
87
  "helmet": "^7.2.0",
68
88
  "inquirer": "^8.2.6",
@@ -384,6 +384,9 @@ function isColumnSortable(name, meta, model) {
384
384
 
385
385
  // Apply filters
386
386
  for (const [colName, filter] of Object.entries(filterParams)) {
387
+ if (!model.columns.has(colName) || !filter || typeof filter !== 'object') {
388
+ continue;
389
+ }
387
390
  const colMeta = model.columns.get(colName);
388
391
  const colType = colMeta?.type || 'string';
389
392
  const op = filter.op || (colType === 'boolean' ? 'eq' : 'contains');
@@ -568,6 +571,32 @@ function isColumnSortable(name, meta, model) {
568
571
  }
569
572
  }
570
573
 
574
+ /** Extract structured field errors from Zod, ORM or database validation exceptions */
575
+ function extractValidationErrors(error) {
576
+ if (!error) return null;
577
+ const fields = {};
578
+ if (error.errors && typeof error.errors === 'object') {
579
+ if (Array.isArray(error.errors)) {
580
+ for (const item of error.errors) {
581
+ if (item.path && item.message) {
582
+ const key = Array.isArray(item.path) ? item.path.join('.') : String(item.path);
583
+ fields[key] = item.message;
584
+ }
585
+ }
586
+ } else {
587
+ Object.assign(fields, error.errors);
588
+ }
589
+ } else if (error.issues && Array.isArray(error.issues)) {
590
+ for (const issue of error.issues) {
591
+ const key = issue.path && issue.path.length > 0 ? issue.path.join('.') : 'root';
592
+ fields[key] = issue.message;
593
+ }
594
+ } else if (error.fields && typeof error.fields === 'object') {
595
+ Object.assign(fields, error.fields);
596
+ }
597
+ return Object.keys(fields).length > 0 ? fields : null;
598
+ }
599
+
571
600
  /**
572
601
  * Create record
573
602
  */
@@ -588,7 +617,8 @@ function isColumnSortable(name, meta, model) {
588
617
  const value = req.body[colName];
589
618
  if (isRichTextEmpty(value)) {
590
619
  return res.status(400).json({
591
- error: `Field "${colName}" is required`
620
+ error: `Field "${colName}" is required`,
621
+ fields: { [colName]: `Field "${colName}" is required` },
592
622
  });
593
623
  }
594
624
  }
@@ -599,7 +629,8 @@ function isColumnSortable(name, meta, model) {
599
629
 
600
630
  res.status(201).json({ data: sanitizeForOutput(record, model) });
601
631
  } catch (error) {
602
- res.status(400).json({ error: error.message });
632
+ const fields = extractValidationErrors(error);
633
+ res.status(400).json({ error: error.message, ...(fields ? { fields } : {}) });
603
634
  }
604
635
  }
605
636
 
@@ -623,7 +654,8 @@ function isColumnSortable(name, meta, model) {
623
654
  const value = req.body[colName];
624
655
  if (isRichTextEmpty(value)) {
625
656
  return res.status(400).json({
626
- error: `Field "${colName}" is required`
657
+ error: `Field "${colName}" is required`,
658
+ fields: { [colName]: `Field "${colName}" is required` },
627
659
  });
628
660
  }
629
661
  }
@@ -638,7 +670,8 @@ function isColumnSortable(name, meta, model) {
638
670
 
639
671
  res.json({ data: sanitizeForOutput(record, model) });
640
672
  } catch (error) {
641
- res.status(400).json({ error: error.message });
673
+ const fields = extractValidationErrors(error);
674
+ res.status(400).json({ error: error.message, ...(fields ? { fields } : {}) });
642
675
  }
643
676
  }
644
677
 
@@ -247,36 +247,76 @@ function wrapCustomPageWithLayout(page, rawComp) {
247
247
 
248
248
  return {
249
249
  oninit: function(vnode) {
250
- vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
251
- if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
252
- return vnode.state.instance.oninit.call(this, vnode);
250
+ try {
251
+ vnode.state.instance = typeof rawComp === 'function' ? rawComp.call(this, vnode) : rawComp;
252
+ if (vnode.state.instance && typeof vnode.state.instance.oninit === 'function') {
253
+ return vnode.state.instance.oninit.call(this, vnode);
254
+ }
255
+ } catch (err) {
256
+ console.error('[Admin Panel] Error in oninit for custom page "' + page.id + '":', err);
257
+ vnode.state.renderError = err;
253
258
  }
254
259
  },
255
260
  oncreate: function(vnode) {
256
- const comp = vnode.state.instance || rawComp;
257
- if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
261
+ try {
262
+ const comp = vnode.state.instance || rawComp;
263
+ if (comp && typeof comp.oncreate === 'function') return comp.oncreate.call(this, vnode);
264
+ } catch (err) {
265
+ console.error('[Admin Panel] Error in oncreate for custom page "' + page.id + '":', err);
266
+ }
258
267
  },
259
268
  onupdate: function(vnode) {
260
- const comp = vnode.state.instance || rawComp;
261
- if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
269
+ try {
270
+ const comp = vnode.state.instance || rawComp;
271
+ if (comp && typeof comp.onupdate === 'function') return comp.onupdate.call(this, vnode);
272
+ } catch (err) {
273
+ console.error('[Admin Panel] Error in onupdate for custom page "' + page.id + '":', err);
274
+ }
262
275
  },
263
276
  onbeforeremove: function(vnode) {
264
- const comp = vnode.state.instance || rawComp;
265
- if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
277
+ try {
278
+ const comp = vnode.state.instance || rawComp;
279
+ if (comp && typeof comp.onbeforeremove === 'function') return comp.onbeforeremove.call(this, vnode);
280
+ } catch (err) {
281
+ console.error('[Admin Panel] Error in onbeforeremove for custom page "' + page.id + '":', err);
282
+ }
266
283
  },
267
284
  onremove: function(vnode) {
268
- const comp = vnode.state.instance || rawComp;
269
- if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
285
+ try {
286
+ const comp = vnode.state.instance || rawComp;
287
+ if (comp && typeof comp.onremove === 'function') return comp.onremove.call(this, vnode);
288
+ } catch (err) {
289
+ console.error('[Admin Panel] Error in onremove for custom page "' + page.id + '":', err);
290
+ }
270
291
  },
271
292
  view: function(vnode) {
293
+ if (vnode.state.renderError) {
294
+ var err = vnode.state.renderError;
295
+ var breadcrumbs = [{ label: page.title, href: page.path }];
296
+ return m(Layout, [
297
+ m(Breadcrumb, { items: breadcrumbs }),
298
+ m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
299
+ m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Yüklenirken Hata Oluştu'),
300
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
301
+ ])
302
+ ]);
303
+ }
272
304
  const comp = vnode.state.instance || rawComp;
273
305
  var content;
274
- if (comp && typeof comp.view === 'function') {
275
- content = comp.view.call(this, vnode);
276
- } else if (typeof comp === 'function') {
277
- content = comp.call(this, vnode);
278
- } else {
279
- content = comp;
306
+ try {
307
+ if (comp && typeof comp.view === 'function') {
308
+ content = comp.view.call(this, vnode);
309
+ } else if (typeof comp === 'function') {
310
+ content = comp.call(this, vnode);
311
+ } else {
312
+ content = comp;
313
+ }
314
+ } catch (err) {
315
+ console.error('[Admin Panel] Error rendering custom page "' + page.id + '":', err);
316
+ content = m('.p-6.bg-red-50.border.border-red-200.rounded-xl', [
317
+ m('h3.text-lg.font-semibold.text-red-800', 'Sayfa Çizilirken Hata Oluştu'),
318
+ m('p.text-sm.text-red-600.mt-1', err.message || String(err)),
319
+ ]);
280
320
  }
281
321
  if (content && (content.tag === Layout || (content.tag && content.tag.name === 'Layout'))) {
282
322
  return content;
@@ -18,7 +18,10 @@ const api = {
18
18
  if (response.status === 401 && path.indexOf('/auth/') !== 0) {
19
19
  redirectToLogin();
20
20
  }
21
- throw new Error(error.error || 'Request failed');
21
+ const err = new Error(error.error || 'Request failed');
22
+ if (error.fields) err.fields = error.fields;
23
+ err.response = error;
24
+ throw err;
22
25
  }
23
26
 
24
27
  return response.json();