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
package/core/orm/utils.js CHANGED
@@ -107,11 +107,14 @@ function deepClone(obj) {
107
107
  if (Array.isArray(obj)) {
108
108
  return obj.map(deepClone);
109
109
  }
110
- const cloned = {};
111
- for (const key in obj) {
110
+ const cloned = Object.create(null);
111
+ for (const key of Object.keys(obj)) {
112
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
113
+ continue;
114
+ }
112
115
  cloned[key] = deepClone(obj[key]);
113
116
  }
114
- return cloned;
117
+ return { ...cloned };
115
118
  }
116
119
 
117
120
  /**
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Knex Database-Backed Queue Adapter
3
+ * @module core/queue/adapters/database
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Job } = require('../job');
9
+
10
+ class DatabaseQueueAdapter {
11
+ /**
12
+ * @param {Object} options
13
+ * @param {import('knex').Knex} options.knex - Knex instance
14
+ * @param {string} [options.tableName='webspresso_jobs'] - Queue table name
15
+ */
16
+ constructor(options = {}) {
17
+ if (!options.knex) {
18
+ throw new Error('DatabaseQueueAdapter requires a Knex instance in options.knex');
19
+ }
20
+ this.knex = options.knex;
21
+ this.tableName = options.tableName || 'webspresso_jobs';
22
+ this._tableChecked = false;
23
+ }
24
+
25
+ /**
26
+ * Ensure the jobs table exists in the database
27
+ */
28
+ async ensureTable() {
29
+ if (this._tableChecked) return;
30
+
31
+ const exists = await this.knex.schema.hasTable(this.tableName);
32
+ if (!exists) {
33
+ await this.knex.schema.createTable(this.tableName, (table) => {
34
+ table.string('id').primary();
35
+ table.string('name').notNullable().index();
36
+ table.text('data').nullable();
37
+ table.string('status').notNullable().defaultTo('pending').index();
38
+ table.integer('attempts').notNullable().defaultTo(0);
39
+ table.integer('max_attempts').notNullable().defaultTo(3);
40
+ table.text('backoff').nullable();
41
+ table.integer('priority').notNullable().defaultTo(10);
42
+ table.integer('timeout').notNullable().defaultTo(30000);
43
+ table.integer('progress').notNullable().defaultTo(0);
44
+ table.text('result').nullable();
45
+ table.text('error').nullable();
46
+ table.dateTime('run_at').notNullable().index();
47
+ table.dateTime('created_at').notNullable();
48
+ table.dateTime('updated_at').notNullable();
49
+ });
50
+ }
51
+
52
+ this._tableChecked = true;
53
+ }
54
+
55
+ /**
56
+ * Enqueue a job into the database
57
+ * @param {Job} job
58
+ * @returns {Promise<Job>}
59
+ */
60
+ async enqueue(job) {
61
+ await this.ensureTable();
62
+ const jobInstance = job instanceof Job ? job : new Job(job);
63
+
64
+ await this.knex(this.tableName).insert({
65
+ id: String(jobInstance.id),
66
+ name: jobInstance.name,
67
+ data: JSON.stringify(jobInstance.data),
68
+ status: jobInstance.status,
69
+ attempts: jobInstance.attempts,
70
+ max_attempts: jobInstance.maxAttempts,
71
+ backoff: JSON.stringify(jobInstance.backoff),
72
+ priority: jobInstance.priority,
73
+ timeout: jobInstance.timeout,
74
+ progress: jobInstance.progressValue,
75
+ result: jobInstance.result ? JSON.stringify(jobInstance.result) : null,
76
+ error: jobInstance.error ? (jobInstance.error.message || String(jobInstance.error)) : null,
77
+ run_at: jobInstance.runAt,
78
+ created_at: jobInstance.createdAt,
79
+ updated_at: jobInstance.updatedAt,
80
+ });
81
+
82
+ return jobInstance;
83
+ }
84
+
85
+ /**
86
+ * Dequeue next pending job
87
+ * @param {string[]} [jobNames]
88
+ * @returns {Promise<Job|null>}
89
+ */
90
+ async dequeue(jobNames = []) {
91
+ await this.ensureTable();
92
+ const now = new Date();
93
+
94
+ return this.knex.transaction(async (trx) => {
95
+ let query = trx(this.tableName)
96
+ .where('status', 'pending')
97
+ .where('run_at', '<=', now)
98
+ .orderBy('priority', 'desc')
99
+ .orderBy('run_at', 'asc')
100
+ .limit(1);
101
+
102
+ if (jobNames && jobNames.length > 0) {
103
+ query = query.whereIn('name', jobNames);
104
+ }
105
+
106
+ // If Postgres or MySQL support forUpdate / skipLocked
107
+ const client = this.knex.client.config.client;
108
+ if (client === 'pg' || client === 'postgres' || client === 'mysql' || client === 'mysql2') {
109
+ try {
110
+ query = query.forUpdate().skipLocked();
111
+ } catch {}
112
+ }
113
+
114
+ const rows = await query;
115
+ if (!rows || rows.length === 0) return null;
116
+
117
+ const row = rows[0];
118
+ const nextAttempts = Number(row.attempts) + 1;
119
+ const updatedAt = new Date();
120
+
121
+ await trx(this.tableName)
122
+ .where('id', row.id)
123
+ .update({
124
+ status: 'running',
125
+ attempts: nextAttempts,
126
+ updated_at: updatedAt,
127
+ });
128
+
129
+ return this._rowToJob({
130
+ ...row,
131
+ status: 'running',
132
+ attempts: nextAttempts,
133
+ updated_at: updatedAt,
134
+ });
135
+ });
136
+ }
137
+
138
+ /**
139
+ * Mark job as completed
140
+ * @param {string|number} jobId
141
+ * @param {any} [result]
142
+ * @returns {Promise<Job|null>}
143
+ */
144
+ async complete(jobId, result = null) {
145
+ await this.ensureTable();
146
+ const updatedAt = new Date();
147
+ await this.knex(this.tableName)
148
+ .where('id', String(jobId))
149
+ .update({
150
+ status: 'completed',
151
+ result: result !== undefined ? JSON.stringify(result) : null,
152
+ progress: 100,
153
+ updated_at: updatedAt,
154
+ });
155
+
156
+ return this.getJob(jobId);
157
+ }
158
+
159
+ /**
160
+ * Mark job as failed
161
+ * @param {string|number} jobId
162
+ * @param {any} [error]
163
+ * @returns {Promise<Job|null>}
164
+ */
165
+ async fail(jobId, error = null) {
166
+ await this.ensureTable();
167
+ const updatedAt = new Date();
168
+ const errorStr = error ? (error.message || String(error)) : null;
169
+
170
+ await this.knex(this.tableName)
171
+ .where('id', String(jobId))
172
+ .update({
173
+ status: 'failed',
174
+ error: errorStr,
175
+ updated_at: updatedAt,
176
+ });
177
+
178
+ return this.getJob(jobId);
179
+ }
180
+
181
+ /**
182
+ * Reschedule job for retry
183
+ * @param {string|number} jobId
184
+ * @param {number} delayMs
185
+ * @param {any} [error]
186
+ * @returns {Promise<Job|null>}
187
+ */
188
+ async retry(jobId, delayMs, error = null) {
189
+ await this.ensureTable();
190
+ const updatedAt = new Date();
191
+ const runAt = new Date(Date.now() + Math.max(0, delayMs));
192
+ const errorStr = error ? (error.message || String(error)) : null;
193
+
194
+ await this.knex(this.tableName)
195
+ .where('id', String(jobId))
196
+ .update({
197
+ status: 'pending',
198
+ run_at: runAt,
199
+ error: errorStr,
200
+ updated_at: updatedAt,
201
+ });
202
+
203
+ return this.getJob(jobId);
204
+ }
205
+
206
+ /**
207
+ * Get job by ID
208
+ * @param {string|number} jobId
209
+ * @returns {Promise<Job|null>}
210
+ */
211
+ async getJob(jobId) {
212
+ await this.ensureTable();
213
+ const row = await this.knex(this.tableName).where('id', String(jobId)).first();
214
+ if (!row) return null;
215
+ return this._rowToJob(row);
216
+ }
217
+
218
+ /**
219
+ * Get stats summary
220
+ * @returns {Promise<{ pending: number, running: number, completed: number, failed: number, total: number }>}
221
+ */
222
+ async getStats() {
223
+ await this.ensureTable();
224
+ const rows = await this.knex(this.tableName)
225
+ .select('status')
226
+ .count('* as count')
227
+ .groupBy('status');
228
+
229
+ const stats = { pending: 0, running: 0, completed: 0, failed: 0, total: 0 };
230
+ for (const r of rows) {
231
+ const count = Number(r.count || r.COUNT || 0);
232
+ if (r.status in stats) {
233
+ stats[r.status] = count;
234
+ }
235
+ stats.total += count;
236
+ }
237
+
238
+ return stats;
239
+ }
240
+
241
+ /**
242
+ * Clear all jobs from table
243
+ * @returns {Promise<void>}
244
+ */
245
+ async clear() {
246
+ await this.ensureTable();
247
+ await this.knex(this.tableName).del();
248
+ }
249
+
250
+ /**
251
+ * Convert DB row to Job instance
252
+ * @private
253
+ */
254
+ _rowToJob(row) {
255
+ let data = {};
256
+ try { data = JSON.parse(row.data); } catch {}
257
+
258
+ let backoff = { type: 'exponential', delay: 1000 };
259
+ try { if (row.backoff) backoff = JSON.parse(row.backoff); } catch {}
260
+
261
+ let result = null;
262
+ try { if (row.result) result = JSON.parse(row.result); } catch {}
263
+
264
+ return new Job({
265
+ id: row.id,
266
+ name: row.name,
267
+ data,
268
+ status: row.status,
269
+ attempts: Number(row.attempts),
270
+ maxAttempts: Number(row.max_attempts),
271
+ backoff,
272
+ priority: Number(row.priority),
273
+ timeout: Number(row.timeout),
274
+ progress: Number(row.progress),
275
+ result,
276
+ error: row.error,
277
+ runAt: row.run_at,
278
+ createdAt: row.created_at,
279
+ updatedAt: row.updated_at,
280
+ });
281
+ }
282
+ }
283
+
284
+ module.exports = { DatabaseQueueAdapter };
@@ -0,0 +1,155 @@
1
+ /**
2
+ * In-Memory Queue Adapter
3
+ * @module core/queue/adapters/memory
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const { Job } = require('../job');
9
+
10
+ class MemoryQueueAdapter {
11
+ constructor() {
12
+ /** @type {Map<string|number, Job>} */
13
+ this.jobs = new Map();
14
+ }
15
+
16
+ /**
17
+ * Enqueue a job into memory
18
+ * @param {Job} job
19
+ * @returns {Promise<Job>}
20
+ */
21
+ async enqueue(job) {
22
+ const jobInstance = job instanceof Job ? job : new Job(job);
23
+ this.jobs.set(jobInstance.id, jobInstance);
24
+ return jobInstance;
25
+ }
26
+
27
+ /**
28
+ * Dequeue the highest priority pending job matching registered job names
29
+ * @param {string[]} [jobNames]
30
+ * @returns {Promise<Job|null>}
31
+ */
32
+ async dequeue(jobNames = []) {
33
+ const now = Date.now();
34
+ let candidate = null;
35
+
36
+ for (const job of this.jobs.values()) {
37
+ if (job.status !== 'pending') continue;
38
+ if (job.runAt && job.runAt.getTime() > now) continue;
39
+ if (jobNames.length > 0 && !jobNames.includes(job.name)) continue;
40
+
41
+ if (!candidate) {
42
+ candidate = job;
43
+ } else {
44
+ // Priority (higher first) then runAt (earlier first)
45
+ if (job.priority > candidate.priority) {
46
+ candidate = job;
47
+ } else if (job.priority === candidate.priority && job.runAt.getTime() < candidate.runAt.getTime()) {
48
+ candidate = job;
49
+ }
50
+ }
51
+ }
52
+
53
+ if (candidate) {
54
+ candidate.status = 'running';
55
+ candidate.attempts += 1;
56
+ candidate.updatedAt = new Date();
57
+ return candidate;
58
+ }
59
+
60
+ return null;
61
+ }
62
+
63
+ /**
64
+ * Mark job as completed
65
+ * @param {string|number} jobId
66
+ * @param {any} [result]
67
+ * @returns {Promise<Job|null>}
68
+ */
69
+ async complete(jobId, result = null) {
70
+ const job = this.jobs.get(jobId);
71
+ if (!job) return null;
72
+ job.status = 'completed';
73
+ job.result = result;
74
+ job.progressValue = 100;
75
+ job.updatedAt = new Date();
76
+ return job;
77
+ }
78
+
79
+ /**
80
+ * Mark job as permanently failed
81
+ * @param {string|number} jobId
82
+ * @param {any} [error]
83
+ * @returns {Promise<Job|null>}
84
+ */
85
+ async fail(jobId, error = null) {
86
+ const job = this.jobs.get(jobId);
87
+ if (!job) return null;
88
+ job.status = 'failed';
89
+ job.error = error;
90
+ job.updatedAt = new Date();
91
+ return job;
92
+ }
93
+
94
+ /**
95
+ * Re-schedule job for retry
96
+ * @param {string|number} jobId
97
+ * @param {number} delayMs
98
+ * @param {any} [error]
99
+ * @returns {Promise<Job|null>}
100
+ */
101
+ async retry(jobId, delayMs, error = null) {
102
+ const job = this.jobs.get(jobId);
103
+ if (!job) return null;
104
+ job.status = 'pending';
105
+ job.runAt = new Date(Date.now() + Math.max(0, delayMs));
106
+ job.error = error;
107
+ job.updatedAt = new Date();
108
+ return job;
109
+ }
110
+
111
+ /**
112
+ * Get job by ID
113
+ * @param {string|number} jobId
114
+ * @returns {Promise<Job|null>}
115
+ */
116
+ async getJob(jobId) {
117
+ return this.jobs.get(jobId) || null;
118
+ }
119
+
120
+ /**
121
+ * Get queue statistics
122
+ * @returns {Promise<{ pending: number, running: number, completed: number, failed: number, total: number }>}
123
+ */
124
+ async getStats() {
125
+ let pending = 0;
126
+ let running = 0;
127
+ let completed = 0;
128
+ let failed = 0;
129
+
130
+ for (const job of this.jobs.values()) {
131
+ if (job.status === 'pending') pending++;
132
+ else if (job.status === 'running') running++;
133
+ else if (job.status === 'completed') completed++;
134
+ else if (job.status === 'failed') failed++;
135
+ }
136
+
137
+ return {
138
+ pending,
139
+ running,
140
+ completed,
141
+ failed,
142
+ total: this.jobs.size,
143
+ };
144
+ }
145
+
146
+ /**
147
+ * Clear all jobs
148
+ * @returns {Promise<void>}
149
+ */
150
+ async clear() {
151
+ this.jobs.clear();
152
+ }
153
+ }
154
+
155
+ module.exports = { MemoryQueueAdapter };