webspresso 0.0.89 → 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 (102) hide show
  1. package/README.md +450 -53
  2. package/bin/commands/service.js +193 -0
  3. package/bin/webspresso.js +2 -0
  4. package/core/auth/manager.js +36 -26
  5. package/core/auth/middleware.js +21 -2
  6. package/core/compression/compressible.js +51 -0
  7. package/core/compression/compression-stream.js +59 -0
  8. package/core/compression/index.js +25 -0
  9. package/core/compression/middleware.js +301 -0
  10. package/core/compression/negotiate.js +153 -0
  11. package/core/errors/base.js +31 -0
  12. package/core/errors/domain.js +193 -0
  13. package/core/errors/http.js +133 -0
  14. package/core/errors/index.js +77 -0
  15. package/core/errors/middleware.js +162 -0
  16. package/core/errors/normalize.js +171 -0
  17. package/core/orm/complexity.js +120 -0
  18. package/core/orm/events.js +64 -14
  19. package/core/orm/index.js +45 -10
  20. package/core/orm/model.js +10 -2
  21. package/core/orm/query-builder.js +30 -4
  22. package/core/orm/repository.js +57 -25
  23. package/core/orm/scopes.js +2 -1
  24. package/core/orm/transaction.js +39 -5
  25. package/core/orm/utils.js +6 -3
  26. package/core/queue/adapters/database.js +284 -0
  27. package/core/queue/adapters/memory.js +155 -0
  28. package/core/queue/adapters/redis.js +336 -0
  29. package/core/queue/index.js +21 -0
  30. package/core/queue/job.js +103 -0
  31. package/core/queue/manager.js +387 -0
  32. package/core/realtime/adapters/redis.js +431 -0
  33. package/core/realtime/adapters/socket-io.js +203 -0
  34. package/core/realtime/adapters/sse.js +210 -0
  35. package/core/realtime/adapters/websocket.js +337 -0
  36. package/core/realtime/auth-manager.js +140 -0
  37. package/core/realtime/core.js +359 -0
  38. package/core/realtime/emitter.js +98 -0
  39. package/core/realtime/errors.js +71 -0
  40. package/core/realtime/index.js +79 -0
  41. package/core/realtime/reconnect-manager.js +118 -0
  42. package/core/realtime/registry.js +243 -0
  43. package/core/shutdown/http-adapter.js +134 -0
  44. package/core/shutdown/index.js +12 -0
  45. package/core/shutdown/shutdown-manager.js +312 -0
  46. package/core/ssr/index.js +13 -0
  47. package/core/ssr/stream.js +174 -0
  48. package/index.d.ts +829 -52
  49. package/index.js +103 -0
  50. package/package.json +22 -2
  51. package/plugins/admin-panel/api.js +37 -4
  52. package/plugins/admin-panel/app.js +67 -17
  53. package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +4 -1
  54. package/plugins/admin-panel/client/parts/03-pagination-intro.js +25 -18
  55. package/plugins/admin-panel/client/parts/04-field-renderers.js +102 -58
  56. package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +37 -23
  57. package/plugins/admin-panel/client/parts/08-record-list.js +27 -34
  58. package/plugins/admin-panel/client/parts/09-record-form.js +168 -19
  59. package/plugins/admin-panel/core/admin-module.js +2 -2
  60. package/plugins/admin-panel/core/api-extensions.js +149 -0
  61. package/plugins/admin-panel/field-renderers/rich-text.js +3 -2
  62. package/plugins/admin-panel/index.js +131 -18
  63. package/plugins/admin-panel/modules/admin-users.js +770 -0
  64. package/plugins/admin-panel/modules/custom-pages.js +144 -45
  65. package/plugins/admin-panel/modules/menu.js +85 -0
  66. package/plugins/admin-panel/modules/user-management.js +5 -2
  67. package/plugins/admin-panel/vendor/mithril.min.js +1 -0
  68. package/plugins/admin-panel/vendor/quill.min.js +7 -0
  69. package/plugins/admin-panel/vendor/quill.snow.min.css +945 -0
  70. package/plugins/admin-panel/vendor/tailwind.min.js +65 -0
  71. package/plugins/basic-auth/index.js +293 -0
  72. package/plugins/content/client/inline-edit.js +41 -17
  73. package/plugins/cors.js +12 -8
  74. package/plugins/csrf/index.js +8 -8
  75. package/plugins/data-exchange/export-xlsx.js +5 -1
  76. package/plugins/data-exchange/import.js +16 -2
  77. package/plugins/data-exchange/index.js +14 -0
  78. package/plugins/email/index.js +14 -1
  79. package/plugins/index.js +29 -0
  80. package/plugins/queue/index.js +108 -0
  81. package/plugins/realtime/index.js +150 -0
  82. package/plugins/recaptcha/index.js +11 -0
  83. package/plugins/redirect/index.js +9 -1
  84. package/plugins/rest-resources/index.js +280 -11
  85. package/plugins/upload/index.js +16 -0
  86. package/src/app-context.js +46 -6
  87. package/src/file-router.js +220 -28
  88. package/src/helpers.js +24 -5
  89. package/src/plugin-manager.js +134 -7
  90. package/src/server.js +470 -44
  91. package/src/services/builtins/auth.js +273 -0
  92. package/src/services/builtins/exchange.js +229 -0
  93. package/src/services/builtins/mail.js +134 -0
  94. package/src/services/builtins/media.js +196 -0
  95. package/src/services/builtins/system.js +221 -0
  96. package/src/services/discovery.js +140 -0
  97. package/src/services/executor.js +292 -0
  98. package/src/services/index.js +70 -0
  99. package/src/services/memoize.js +256 -0
  100. package/src/services/registry.js +288 -0
  101. package/src/services/validator.js +281 -0
  102. package/templates/skills/webspresso-usage/REFERENCE-framework.md +2 -2
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Distributed Redis Queue Adapter
3
+ * @module core/queue/adapters/redis
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Job } = require('../job');
9
+
10
+ class RedisQueueAdapter {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {any} options.client - ioredis or @redis/client instance
14
+ * @param {string} [options.prefix='webspresso:queue:'] - Key prefix
15
+ */
16
+ constructor(options = {}) {
17
+ if (!options.client) {
18
+ throw new Error('RedisQueueAdapter requires a Redis client in options.client');
19
+ }
20
+ this.client = options.client;
21
+ this.prefix = options.prefix || 'webspresso:queue:';
22
+ }
23
+
24
+ _key(name) {
25
+ return this.prefix + name;
26
+ }
27
+
28
+ /**
29
+ * Enqueue a job into Redis
30
+ * @param {Job} job
31
+ * @returns {Promise<Job>}
32
+ */
33
+ async enqueue(job) {
34
+ const jobInstance = job instanceof Job ? job : new Job(job);
35
+ const jobKey = this._key('job:' + jobInstance.id);
36
+ const delayedKey = this._key('delayed');
37
+ const waitingKey = this._key('waiting');
38
+
39
+ const jobData = JSON.stringify(jobInstance.toJSON());
40
+
41
+ // Save job payload
42
+ if (typeof this.client.set === 'function') {
43
+ await this.client.set(jobKey, jobData);
44
+ }
45
+
46
+ const now = Date.now();
47
+ const runAt = jobInstance.runAt ? jobInstance.runAt.getTime() : now;
48
+
49
+ if (runAt > now) {
50
+ // Delayed job
51
+ if (typeof this.client.zadd === 'function') {
52
+ await this.client.zadd(delayedKey, runAt, String(jobInstance.id));
53
+ } else if (typeof this.client.zAdd === 'function') {
54
+ await this.client.zAdd(delayedKey, [{ score: runAt, value: String(jobInstance.id) }]);
55
+ }
56
+ } else {
57
+ // Ready to process (score is negative priority to sort highest priority first)
58
+ const score = (100 - (jobInstance.priority || 10)) * 1000000000000 + runAt;
59
+ if (typeof this.client.zadd === 'function') {
60
+ await this.client.zadd(waitingKey, score, String(jobInstance.id));
61
+ } else if (typeof this.client.zAdd === 'function') {
62
+ await this.client.zAdd(waitingKey, [{ score, value: String(jobInstance.id) }]);
63
+ }
64
+ }
65
+
66
+ return jobInstance;
67
+ }
68
+
69
+ /**
70
+ * Move due delayed jobs into waiting queue
71
+ * @private
72
+ */
73
+ async _promoteDelayed() {
74
+ const delayedKey = this._key('delayed');
75
+ const waitingKey = this._key('waiting');
76
+ const now = Date.now();
77
+
78
+ try {
79
+ let dueIds = [];
80
+ if (typeof this.client.zrangebyscore === 'function') {
81
+ dueIds = await this.client.zrangebyscore(delayedKey, 0, now);
82
+ } else if (typeof this.client.zRangeByScore === 'function') {
83
+ dueIds = await this.client.zRangeByScore(delayedKey, 0, now);
84
+ }
85
+
86
+ if (dueIds && dueIds.length > 0) {
87
+ for (const id of dueIds) {
88
+ const job = await this.getJob(id);
89
+ if (job) {
90
+ const score = (100 - (job.priority || 10)) * 1000000000000 + now;
91
+ if (typeof this.client.zadd === 'function') {
92
+ await this.client.zadd(waitingKey, score, String(id));
93
+ await this.client.zrem(delayedKey, String(id));
94
+ } else if (typeof this.client.zAdd === 'function') {
95
+ await this.client.zAdd(waitingKey, [{ score, value: String(id) }]);
96
+ await this.client.zRem(delayedKey, String(id));
97
+ }
98
+ }
99
+ }
100
+ }
101
+ } catch {}
102
+ }
103
+
104
+ /**
105
+ * Dequeue next pending job
106
+ * @param {string[]} [jobNames]
107
+ * @returns {Promise<Job|null>}
108
+ */
109
+ async dequeue(jobNames = []) {
110
+ await this._promoteDelayed();
111
+
112
+ const waitingKey = this._key('waiting');
113
+ const activeKey = this._key('active');
114
+
115
+ let ids = [];
116
+ if (typeof this.client.zrange === 'function') {
117
+ ids = await this.client.zrange(waitingKey, 0, 50);
118
+ } else if (typeof this.client.zRange === 'function') {
119
+ ids = await this.client.zRange(waitingKey, 0, 50);
120
+ }
121
+
122
+ if (!ids || ids.length === 0) return null;
123
+
124
+ for (const id of ids) {
125
+ const job = await this.getJob(id);
126
+ if (!job) {
127
+ if (typeof this.client.zrem === 'function') await this.client.zrem(waitingKey, String(id));
128
+ else if (typeof this.client.zRem === 'function') await this.client.zRem(waitingKey, String(id));
129
+ continue;
130
+ }
131
+
132
+ if (jobNames.length > 0 && !jobNames.includes(job.name)) {
133
+ continue;
134
+ }
135
+
136
+ // Pop from waiting and add to active
137
+ if (typeof this.client.zrem === 'function') {
138
+ await this.client.zrem(waitingKey, String(id));
139
+ await this.client.sadd(activeKey, String(id));
140
+ } else if (typeof this.client.zRem === 'function') {
141
+ await this.client.zRem(waitingKey, String(id));
142
+ await this.client.sAdd(activeKey, String(id));
143
+ }
144
+
145
+ job.status = 'running';
146
+ job.attempts += 1;
147
+ job.updatedAt = new Date();
148
+
149
+ const jobKey = this._key('job:' + job.id);
150
+ if (typeof this.client.set === 'function') {
151
+ await this.client.set(jobKey, JSON.stringify(job.toJSON()));
152
+ }
153
+
154
+ return job;
155
+ }
156
+
157
+ return null;
158
+ }
159
+
160
+ /**
161
+ * Mark job as completed
162
+ * @param {string|number} jobId
163
+ * @param {any} [result]
164
+ * @returns {Promise<Job|null>}
165
+ */
166
+ async complete(jobId, result = null) {
167
+ const job = await this.getJob(jobId);
168
+ if (!job) return null;
169
+
170
+ const activeKey = this._key('active');
171
+ const completedKey = this._key('completed');
172
+ const jobKey = this._key('job:' + jobId);
173
+
174
+ if (typeof this.client.srem === 'function') {
175
+ await this.client.srem(activeKey, String(jobId));
176
+ await this.client.sadd(completedKey, String(jobId));
177
+ } else if (typeof this.client.sRem === 'function') {
178
+ await this.client.sRem(activeKey, String(jobId));
179
+ await this.client.sAdd(completedKey, String(jobId));
180
+ }
181
+
182
+ job.status = 'completed';
183
+ job.result = result;
184
+ job.progressValue = 100;
185
+ job.updatedAt = new Date();
186
+
187
+ if (typeof this.client.set === 'function') {
188
+ await this.client.set(jobKey, JSON.stringify(job.toJSON()));
189
+ }
190
+
191
+ return job;
192
+ }
193
+
194
+ /**
195
+ * Mark job as failed
196
+ * @param {string|number} jobId
197
+ * @param {any} [error]
198
+ * @returns {Promise<Job|null>}
199
+ */
200
+ async fail(jobId, error = null) {
201
+ const job = await this.getJob(jobId);
202
+ if (!job) return null;
203
+
204
+ const activeKey = this._key('active');
205
+ const failedKey = this._key('failed');
206
+ const jobKey = this._key('job:' + jobId);
207
+
208
+ if (typeof this.client.srem === 'function') {
209
+ await this.client.srem(activeKey, String(jobId));
210
+ await this.client.sadd(failedKey, String(jobId));
211
+ } else if (typeof this.client.sRem === 'function') {
212
+ await this.client.sRem(activeKey, String(jobId));
213
+ await this.client.sAdd(failedKey, String(jobId));
214
+ }
215
+
216
+ job.status = 'failed';
217
+ job.error = error;
218
+ job.updatedAt = new Date();
219
+
220
+ if (typeof this.client.set === 'function') {
221
+ await this.client.set(jobKey, JSON.stringify(job.toJSON()));
222
+ }
223
+
224
+ return job;
225
+ }
226
+
227
+ /**
228
+ * Re-enqueue job for retry
229
+ * @param {string|number} jobId
230
+ * @param {number} delayMs
231
+ * @param {any} [error]
232
+ * @returns {Promise<Job|null>}
233
+ */
234
+ async retry(jobId, delayMs, error = null) {
235
+ const job = await this.getJob(jobId);
236
+ if (!job) return null;
237
+
238
+ const activeKey = this._key('active');
239
+ if (typeof this.client.srem === 'function') {
240
+ await this.client.srem(activeKey, String(jobId));
241
+ } else if (typeof this.client.sRem === 'function') {
242
+ await this.client.sRem(activeKey, String(jobId));
243
+ }
244
+
245
+ job.status = 'pending';
246
+ job.runAt = new Date(Date.now() + Math.max(0, delayMs));
247
+ job.error = error;
248
+ job.updatedAt = new Date();
249
+
250
+ await this.enqueue(job);
251
+ return job;
252
+ }
253
+
254
+ /**
255
+ * Get job by ID
256
+ * @param {string|number} jobId
257
+ * @returns {Promise<Job|null>}
258
+ */
259
+ async getJob(jobId) {
260
+ const jobKey = this._key('job:' + jobId);
261
+ let raw = null;
262
+ if (typeof this.client.get === 'function') {
263
+ raw = await this.client.get(jobKey);
264
+ }
265
+ if (!raw) return null;
266
+
267
+ try {
268
+ const parsed = typeof raw === 'string' ? JSON.parse(raw) : raw;
269
+ return new Job(parsed);
270
+ } catch {
271
+ return null;
272
+ }
273
+ }
274
+
275
+ /**
276
+ * Get stats
277
+ * @returns {Promise<{ pending: number, running: number, completed: number, failed: number, total: number }>}
278
+ */
279
+ async getStats() {
280
+ const waitingKey = this._key('waiting');
281
+ const delayedKey = this._key('delayed');
282
+ const activeKey = this._key('active');
283
+ const completedKey = this._key('completed');
284
+ const failedKey = this._key('failed');
285
+
286
+ let pending = 0;
287
+ let delayed = 0;
288
+ let running = 0;
289
+ let completed = 0;
290
+ let failed = 0;
291
+
292
+ try {
293
+ if (typeof this.client.zcard === 'function') {
294
+ pending = (await this.client.zcard(waitingKey)) || 0;
295
+ delayed = (await this.client.zcard(delayedKey)) || 0;
296
+ running = (await this.client.scard(activeKey)) || 0;
297
+ completed = (await this.client.scard(completedKey)) || 0;
298
+ failed = (await this.client.scard(failedKey)) || 0;
299
+ } else if (typeof this.client.zCard === 'function') {
300
+ pending = (await this.client.zCard(waitingKey)) || 0;
301
+ delayed = (await this.client.zCard(delayedKey)) || 0;
302
+ running = (await this.client.sCard(activeKey)) || 0;
303
+ completed = (await this.client.sCard(completedKey)) || 0;
304
+ failed = (await this.client.sCard(failedKey)) || 0;
305
+ }
306
+ } catch {}
307
+
308
+ const totalPending = pending + delayed;
309
+ return {
310
+ pending: totalPending,
311
+ running,
312
+ completed,
313
+ failed,
314
+ total: totalPending + running + completed + failed,
315
+ };
316
+ }
317
+
318
+ /**
319
+ * Clear all queue keys
320
+ * @returns {Promise<void>}
321
+ */
322
+ async clear() {
323
+ const keys = [
324
+ this._key('waiting'),
325
+ this._key('delayed'),
326
+ this._key('active'),
327
+ this._key('completed'),
328
+ this._key('failed'),
329
+ ];
330
+ for (const k of keys) {
331
+ if (typeof this.client.del === 'function') await this.client.del(k);
332
+ }
333
+ }
334
+ }
335
+
336
+ module.exports = { RedisQueueAdapter };
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Webspresso Core Background Job Queue Engine
3
+ * @module core/queue
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Job } = require('./job');
9
+ const { QueueManager, createQueueManager } = require('./manager');
10
+ const { MemoryQueueAdapter } = require('./adapters/memory');
11
+ const { DatabaseQueueAdapter } = require('./adapters/database');
12
+ const { RedisQueueAdapter } = require('./adapters/redis');
13
+
14
+ module.exports = {
15
+ Job,
16
+ QueueManager,
17
+ createQueueManager,
18
+ MemoryQueueAdapter,
19
+ DatabaseQueueAdapter,
20
+ RedisQueueAdapter,
21
+ };
@@ -0,0 +1,103 @@
1
+ /**
2
+ * Background Job Entity
3
+ * @module core/queue/job
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ class Job {
9
+ /**
10
+ * @param {Object} options
11
+ * @param {string|number} options.id
12
+ * @param {string} options.name
13
+ * @param {any} [options.data={}]
14
+ * @param {'pending'|'running'|'completed'|'failed'} [options.status='pending']
15
+ * @param {number} [options.attempts=0]
16
+ * @param {number} [options.maxAttempts=3]
17
+ * @param {number|Object} [options.backoff={ type: 'exponential', delay: 1000 }]
18
+ * @param {number} [options.priority=10]
19
+ * @param {number} [options.timeout=30000]
20
+ * @param {number} [options.progress=0]
21
+ * @param {any} [options.result=null]
22
+ * @param {any} [options.error=null]
23
+ * @param {Date|number|string} [options.runAt]
24
+ * @param {Date|number|string} [options.createdAt]
25
+ * @param {Date|number|string} [options.updatedAt]
26
+ * @param {Function} [options.onProgress]
27
+ */
28
+ constructor(options = {}) {
29
+ this.id = options.id || ('job_' + Math.random().toString(36).slice(2, 11) + Date.now().toString(36));
30
+ this.name = options.name || 'default';
31
+ this.data = options.data !== undefined ? options.data : {};
32
+ this.status = options.status || 'pending';
33
+ this.attempts = options.attempts || 0;
34
+ this.maxAttempts = options.maxAttempts !== undefined ? options.maxAttempts : 3;
35
+ this.backoff = options.backoff !== undefined ? options.backoff : { type: 'exponential', delay: 1000 };
36
+ this.priority = options.priority !== undefined ? options.priority : 10;
37
+ this.timeout = options.timeout !== undefined ? options.timeout : 30000;
38
+ this.progressValue = options.progress || 0;
39
+ this.result = options.result || null;
40
+ this.error = options.error || null;
41
+ this.createdAt = options.createdAt ? new Date(options.createdAt) : new Date();
42
+ this.updatedAt = options.updatedAt ? new Date(options.updatedAt) : new Date();
43
+ this.runAt = options.runAt ? new Date(options.runAt) : new Date();
44
+ this._onProgress = options.onProgress || null;
45
+ }
46
+
47
+ /**
48
+ * Update progress of a running job
49
+ * @param {number} percent - 0 to 100
50
+ * @param {string} [message]
51
+ */
52
+ progress(percent, message) {
53
+ this.progressValue = Math.min(100, Math.max(0, percent));
54
+ this.updatedAt = new Date();
55
+ if (typeof this._onProgress === 'function') {
56
+ this._onProgress(this, this.progressValue, message);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Compute next retry delay in milliseconds based on backoff config
62
+ * @returns {number}
63
+ */
64
+ getNextRetryDelay() {
65
+ if (typeof this.backoff === 'number') {
66
+ return this.backoff;
67
+ }
68
+ const baseDelay = (this.backoff && this.backoff.delay) || 1000;
69
+ const type = (this.backoff && this.backoff.type) || 'exponential';
70
+
71
+ if (type === 'fixed') {
72
+ return baseDelay;
73
+ }
74
+ // Exponential: baseDelay * 2^(attempts - 1)
75
+ const factor = Math.max(1, Math.pow(2, Math.max(0, this.attempts - 1)));
76
+ return Math.min(baseDelay * factor, 3600000); // Cap at 1 hour
77
+ }
78
+
79
+ /**
80
+ * Plain object serialization
81
+ */
82
+ toJSON() {
83
+ return {
84
+ id: this.id,
85
+ name: this.name,
86
+ data: this.data,
87
+ status: this.status,
88
+ attempts: this.attempts,
89
+ maxAttempts: this.maxAttempts,
90
+ backoff: this.backoff,
91
+ priority: this.priority,
92
+ timeout: this.timeout,
93
+ progress: this.progressValue,
94
+ result: this.result,
95
+ error: this.error ? (this.error.message || String(this.error)) : null,
96
+ createdAt: this.createdAt.toISOString(),
97
+ updatedAt: this.updatedAt.toISOString(),
98
+ runAt: this.runAt.toISOString(),
99
+ };
100
+ }
101
+ }
102
+
103
+ module.exports = { Job };