webspresso 0.0.102 → 0.0.104
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.
- package/.agents/ORM.md +13 -4
- package/.agents/skills/webspresso-orm-and-database/SKILL.md +4 -5
- package/README.md +2 -2
- package/bin/commands/dev.js +3 -2
- package/bin/utils/orm-map-html.js +2 -8
- package/core/auth/jwt.js +12 -21
- package/core/auth/oauth.js +38 -14
- package/core/auth/totp.js +3 -15
- package/core/auth/user-agent.js +59 -60
- package/core/compression/compression-stream.js +2 -2
- package/core/compression/middleware.js +1 -1
- package/core/compression/negotiate.js +11 -4
- package/core/content/renderer.js +1 -13
- package/core/errors/middleware.js +2 -8
- package/core/events/dispatcher.js +56 -33
- package/core/forms/form-builder.js +1 -9
- package/core/openapi/orm-components.js +2 -3
- package/core/orm/cache/listeners.js +12 -2
- package/core/orm/index.js +5 -0
- package/core/orm/json-fields.js +7 -1
- package/core/orm/model.js +99 -8
- package/core/orm/query-builder.js +71 -25
- package/core/orm/query-filters.js +393 -0
- package/core/orm/repository.js +42 -17
- package/core/orm/schema-helpers.js +49 -0
- package/core/orm/scopes.js +40 -9
- package/core/orm/types.js +12 -1
- package/core/queue/manager.js +5 -0
- package/core/shutdown/shutdown-manager.js +16 -16
- package/core/utils/crypto.js +73 -0
- package/core/utils/escape-html.js +7 -0
- package/core/utils/fetch.js +120 -0
- package/core/utils/get-client-ip.js +6 -0
- package/core/utils/html.js +72 -0
- package/core/utils/ip.js +86 -0
- package/core/utils/safe-compare.js +3 -0
- package/core/validation/index.js +9 -3
- package/index.d.ts +33 -6
- package/index.js +10 -0
- package/package.json +2 -1
- package/plugins/admin-panel/auth.js +15 -7
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +82 -10
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +4 -2
- package/plugins/admin-panel/client/parts/08-record-list.js +3 -2
- package/plugins/admin-panel/client/parts/10-export-registry.js +6 -0
- package/plugins/admin-panel/core/api-extensions.js +1 -75
- package/plugins/admin-panel/lib/login-rate-limiter.js +15 -13
- package/plugins/api-key/crypto.js +1 -24
- package/plugins/audit-log/api-handlers.js +11 -1
- package/plugins/audit-log/middleware.js +50 -6
- package/plugins/basic-auth/index.js +2 -26
- package/plugins/cors.js +2 -2
- package/plugins/csrf/index.js +3 -15
- package/plugins/email/service.js +32 -1
- package/plugins/file-manager/admin-component.js +3 -1
- package/plugins/file-manager/api-handlers.js +10 -17
- package/plugins/file-manager/core.js +34 -4
- package/plugins/file-manager/index.js +11 -9
- package/plugins/file-manager/security.js +18 -6
- package/plugins/mcp/auth.js +1 -13
- package/plugins/mcp/index.js +23 -1
- package/plugins/mcp/transports/sse.js +10 -1
- package/plugins/orm-cache-admin/admin-component.js +3 -2
- package/plugins/polar/src/api.js +9 -2
- package/plugins/polar/src/webhooks.js +3 -8
- package/plugins/recaptcha/helpers.js +1 -7
- package/plugins/recaptcha/verify.js +37 -23
- package/plugins/redirect/index.js +51 -3
- package/plugins/rest-resources/index.js +1 -207
- package/plugins/schema-explorer/admin-component.js +4 -2
- package/plugins/schema-explorer.js +40 -29
- package/plugins/seo-checker/index.js +5 -2
- package/plugins/site-analytics/bot-patterns.js +3 -0
- package/plugins/site-analytics/client-error-handler.js +3 -1
- package/plugins/site-analytics/index.js +10 -1
- package/plugins/site-analytics/tracking.js +265 -63
- package/plugins/sitemap.js +18 -15
- package/plugins/swagger.js +9 -14
- package/plugins/webhooks/crypto.js +1 -18
- package/plugins/webhooks/dispatcher.js +6 -1
- package/plugins/webhooks/matcher.js +25 -10
- package/plugins/webhooks/sender.js +32 -2
- package/src/api/api-loader.js +1 -1
- package/src/dev-reload/index.js +47 -6
- package/src/discovery/scan-routes.js +14 -4
- package/src/file-router.js +62 -6
- package/src/helpers.js +157 -32
- package/src/plugin-manager.js +34 -13
- package/src/routing/route-table.js +14 -4
- package/src/server.js +25 -9
- package/src/services/validator.js +57 -32
|
@@ -8,8 +8,7 @@ const { getAllModels } = require('../../core/orm/model');
|
|
|
8
8
|
const { omit } = require('../../core/orm/utils');
|
|
9
9
|
const { trimUrlPathSlashes } = require('../../core/url-path-normalize');
|
|
10
10
|
const { validateQueryComplexity } = require('../../core/orm/complexity');
|
|
11
|
-
|
|
12
|
-
const RESERVED_QUERY_KEYS = new Set(['page', 'perPage', 'sort', 'order', 'include', 'trashed']);
|
|
11
|
+
const { applyColumnFilters, RESERVED_QUERY_KEYS } = require('../../core/orm/query-filters');
|
|
13
12
|
|
|
14
13
|
/**
|
|
15
14
|
* Pluralize a PascalCase model name to a URL segment (e.g. User -> users, Company -> companies)
|
|
@@ -234,212 +233,7 @@ function parseSortParams(model, rawSort, rawOrder) {
|
|
|
234
233
|
return result;
|
|
235
234
|
}
|
|
236
235
|
|
|
237
|
-
function applyColumnFilters(query, countQuery, model, req) {
|
|
238
|
-
let q = query;
|
|
239
|
-
let c = countQuery;
|
|
240
|
-
|
|
241
|
-
function applyCondition(col, op, rawVal) {
|
|
242
|
-
if (!model.columns.has(col)) return;
|
|
243
|
-
const meta = model.columns.get(col);
|
|
244
|
-
const colType = meta?.type;
|
|
245
|
-
|
|
246
|
-
let operator = String(op || 'eq').toLowerCase();
|
|
247
|
-
let val = rawVal;
|
|
248
|
-
|
|
249
|
-
// Normalize booleans
|
|
250
|
-
if (colType === 'boolean' && typeof val === 'string') {
|
|
251
|
-
if (val === 'true' || val === '1') val = true;
|
|
252
|
-
else if (val === 'false' || val === '0') val = false;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
switch (operator) {
|
|
256
|
-
case 'eq':
|
|
257
|
-
case '=':
|
|
258
|
-
if (val !== undefined && val !== '') {
|
|
259
|
-
q = q.where(col, '=', val);
|
|
260
|
-
c = c.where(col, '=', val);
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
|
|
264
|
-
case 'ne':
|
|
265
|
-
case '!=':
|
|
266
|
-
case '<>':
|
|
267
|
-
if (val !== undefined && val !== '') {
|
|
268
|
-
q = q.where(col, '!=', val);
|
|
269
|
-
c = c.where(col, '!=', val);
|
|
270
|
-
}
|
|
271
|
-
break;
|
|
272
|
-
|
|
273
|
-
case 'gt':
|
|
274
|
-
case '>':
|
|
275
|
-
if (val !== undefined && val !== '') {
|
|
276
|
-
q = q.where(col, '>', val);
|
|
277
|
-
c = c.where(col, '>', val);
|
|
278
|
-
}
|
|
279
|
-
break;
|
|
280
|
-
|
|
281
|
-
case 'gte':
|
|
282
|
-
case '>=':
|
|
283
|
-
if (val !== undefined && val !== '') {
|
|
284
|
-
q = q.where(col, '>=', val);
|
|
285
|
-
c = c.where(col, '>=', val);
|
|
286
|
-
}
|
|
287
|
-
break;
|
|
288
|
-
|
|
289
|
-
case 'lt':
|
|
290
|
-
case '<':
|
|
291
|
-
if (val !== undefined && val !== '') {
|
|
292
|
-
q = q.where(col, '<', val);
|
|
293
|
-
c = c.where(col, '<', val);
|
|
294
|
-
}
|
|
295
|
-
break;
|
|
296
|
-
|
|
297
|
-
case 'lte':
|
|
298
|
-
case '<=':
|
|
299
|
-
if (val !== undefined && val !== '') {
|
|
300
|
-
q = q.where(col, '<=', val);
|
|
301
|
-
c = c.where(col, '<=', val);
|
|
302
|
-
}
|
|
303
|
-
break;
|
|
304
|
-
|
|
305
|
-
case 'in': {
|
|
306
|
-
const arr = Array.isArray(val)
|
|
307
|
-
? val
|
|
308
|
-
: (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
|
|
309
|
-
if (arr.length > 0) {
|
|
310
|
-
q = q.whereIn(col, arr);
|
|
311
|
-
c = c.whereIn(col, arr);
|
|
312
|
-
}
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
case 'nin':
|
|
317
|
-
case 'notin': {
|
|
318
|
-
const arr = Array.isArray(val)
|
|
319
|
-
? val
|
|
320
|
-
: (typeof val === 'string' ? val.split(',').map(s => s.trim()).filter(Boolean) : [val]);
|
|
321
|
-
if (arr.length > 0) {
|
|
322
|
-
q = q.whereNotIn(col, arr);
|
|
323
|
-
c = c.whereNotIn(col, arr);
|
|
324
|
-
}
|
|
325
|
-
break;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
case 'like':
|
|
329
|
-
if (val !== undefined && val !== '') {
|
|
330
|
-
q = q.where(col, 'like', String(val));
|
|
331
|
-
c = c.where(col, 'like', String(val));
|
|
332
|
-
}
|
|
333
|
-
break;
|
|
334
|
-
|
|
335
|
-
case 'ilike':
|
|
336
|
-
if (val !== undefined && val !== '') {
|
|
337
|
-
q = q.where(col, 'ilike', String(val));
|
|
338
|
-
c = c.where(col, 'ilike', String(val));
|
|
339
|
-
}
|
|
340
|
-
break;
|
|
341
|
-
|
|
342
|
-
case 'contains':
|
|
343
|
-
if (val !== undefined && val !== '') {
|
|
344
|
-
q = q.where(col, 'like', `%${String(val)}%`);
|
|
345
|
-
c = c.where(col, 'like', `%${String(val)}%`);
|
|
346
|
-
}
|
|
347
|
-
break;
|
|
348
|
-
|
|
349
|
-
case 'startswith':
|
|
350
|
-
if (val !== undefined && val !== '') {
|
|
351
|
-
q = q.where(col, 'like', `${String(val)}%`);
|
|
352
|
-
c = c.where(col, 'like', `${String(val)}%`);
|
|
353
|
-
}
|
|
354
|
-
break;
|
|
355
|
-
|
|
356
|
-
case 'endswith':
|
|
357
|
-
if (val !== undefined && val !== '') {
|
|
358
|
-
q = q.where(col, 'like', `%${String(val)}`);
|
|
359
|
-
c = c.where(col, 'like', `%${String(val)}`);
|
|
360
|
-
}
|
|
361
|
-
break;
|
|
362
|
-
|
|
363
|
-
case 'isnull':
|
|
364
|
-
case 'is_null': {
|
|
365
|
-
const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
|
|
366
|
-
if (isTrue) {
|
|
367
|
-
q = q.whereNull(col);
|
|
368
|
-
c = c.whereNull(col);
|
|
369
|
-
}
|
|
370
|
-
break;
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
case 'isnotnull':
|
|
374
|
-
case 'is_not_null': {
|
|
375
|
-
const isTrue = val === true || val === 'true' || val === '1' || val === 1 || val === '';
|
|
376
|
-
if (isTrue) {
|
|
377
|
-
q = q.whereNotNull(col);
|
|
378
|
-
c = c.whereNotNull(col);
|
|
379
|
-
}
|
|
380
|
-
break;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
case 'between': {
|
|
384
|
-
let parts = [];
|
|
385
|
-
if (Array.isArray(val)) {
|
|
386
|
-
parts = val;
|
|
387
|
-
} else if (typeof val === 'string') {
|
|
388
|
-
parts = val.split(',').map(s => s.trim());
|
|
389
|
-
}
|
|
390
|
-
if (parts.length === 2 && parts[0] !== '' && parts[1] !== '') {
|
|
391
|
-
q = q.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
|
|
392
|
-
c = c.where(col, '>=', parts[0]).where(col, '<=', parts[1]);
|
|
393
|
-
}
|
|
394
|
-
break;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
default:
|
|
398
|
-
if (val !== undefined && val !== '') {
|
|
399
|
-
q = q.where(col, '=', val);
|
|
400
|
-
c = c.where(col, '=', val);
|
|
401
|
-
}
|
|
402
|
-
break;
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// 1. Process dedicated filter object ?filter[col]=... or ?filter[col][op]=...
|
|
407
|
-
if (req.query.filter && typeof req.query.filter === 'object') {
|
|
408
|
-
for (const [col, colFilter] of Object.entries(req.query.filter)) {
|
|
409
|
-
if (!model.columns.has(col)) continue;
|
|
410
|
-
if (colFilter && typeof colFilter === 'object' && !Array.isArray(colFilter)) {
|
|
411
|
-
if (colFilter.op !== undefined && colFilter.value !== undefined) {
|
|
412
|
-
applyCondition(col, colFilter.op, colFilter.value);
|
|
413
|
-
} else {
|
|
414
|
-
for (const [op, val] of Object.entries(colFilter)) {
|
|
415
|
-
applyCondition(col, op, val);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
} else if (colFilter !== undefined && colFilter !== '') {
|
|
419
|
-
applyCondition(col, 'eq', colFilter);
|
|
420
|
-
}
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
236
|
|
|
424
|
-
// 2. Process top-level query parameters ?price[gte]=100 or ?status=active
|
|
425
|
-
for (const [key, value] of Object.entries(req.query)) {
|
|
426
|
-
if (RESERVED_QUERY_KEYS.has(key) || key === 'filter') {
|
|
427
|
-
continue;
|
|
428
|
-
}
|
|
429
|
-
if (!model.columns.has(key)) {
|
|
430
|
-
continue;
|
|
431
|
-
}
|
|
432
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
433
|
-
for (const [op, val] of Object.entries(value)) {
|
|
434
|
-
applyCondition(key, op, val);
|
|
435
|
-
}
|
|
436
|
-
} else if (value !== undefined && value !== '') {
|
|
437
|
-
applyCondition(key, 'eq', value);
|
|
438
|
-
}
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
return { query: q, countQuery: c };
|
|
442
|
-
}
|
|
443
237
|
|
|
444
238
|
/**
|
|
445
239
|
* @param {Object} options
|
|
@@ -20,10 +20,12 @@ function generateSchemaExplorerAdminComponent(options = {}) {
|
|
|
20
20
|
function getSchemaApi(method, path) {
|
|
21
21
|
var adminPath = window.__ADMIN_PATH__ || '/_admin';
|
|
22
22
|
var url = adminPath + '/api${apiPrefix}' + (path || '');
|
|
23
|
-
|
|
23
|
+
var fetchFn = (typeof window !== 'undefined' && window.Admin && window.Admin.fetch) ? window.Admin.fetch : fetch;
|
|
24
|
+
return fetchFn(url, {
|
|
24
25
|
method: method || 'GET',
|
|
25
26
|
credentials: 'include',
|
|
26
|
-
headers: { 'Accept': 'application/json' }
|
|
27
|
+
headers: { 'Accept': 'application/json' },
|
|
28
|
+
timeout: 30000,
|
|
27
29
|
}).then(function(res) {
|
|
28
30
|
if (!res.ok) {
|
|
29
31
|
return res.json().catch(function() { return { error: res.statusText }; }).then(function(err) {
|
|
@@ -59,6 +59,25 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
59
59
|
|
|
60
60
|
const pageTitle = title || adminTitle || menuLabel;
|
|
61
61
|
|
|
62
|
+
let cachedGraph = null;
|
|
63
|
+
let cachedGraphTime = 0;
|
|
64
|
+
const GRAPH_CACHE_TTL = 5000; // 5 seconds in dev, infinite in prod
|
|
65
|
+
|
|
66
|
+
function getEnrichedGraph() {
|
|
67
|
+
const isProd = process.env.NODE_ENV === 'production';
|
|
68
|
+
const now = Date.now();
|
|
69
|
+
if (cachedGraph && (isProd || (now - cachedGraphTime) < GRAPH_CACHE_TTL)) {
|
|
70
|
+
return cachedGraph;
|
|
71
|
+
}
|
|
72
|
+
const rawModels = serializeAllModels({ exclude, includeColumns, includeRelations, includeScopes });
|
|
73
|
+
const relationships = extractAllRelationships(rawModels);
|
|
74
|
+
const enriched = enrichModelsWithGraph(rawModels, relationships);
|
|
75
|
+
const mermaid = generateMermaidErDiagram(rawModels, relationships);
|
|
76
|
+
cachedGraph = { rawModels, relationships, enriched, mermaid };
|
|
77
|
+
cachedGraphTime = now;
|
|
78
|
+
return cachedGraph;
|
|
79
|
+
}
|
|
80
|
+
|
|
62
81
|
return {
|
|
63
82
|
name: 'schema-explorer',
|
|
64
83
|
version: '1.0.0',
|
|
@@ -66,6 +85,14 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
66
85
|
|
|
67
86
|
// Expose API for other plugins & programmatic consumption
|
|
68
87
|
api: {
|
|
88
|
+
/**
|
|
89
|
+
* Invalidate schema explorer cache
|
|
90
|
+
*/
|
|
91
|
+
invalidateCache() {
|
|
92
|
+
cachedGraph = null;
|
|
93
|
+
cachedGraphTime = 0;
|
|
94
|
+
},
|
|
95
|
+
|
|
69
96
|
/**
|
|
70
97
|
* Get all models metadata
|
|
71
98
|
* @returns {Object[]}
|
|
@@ -79,9 +106,7 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
79
106
|
* @returns {Object[]}
|
|
80
107
|
*/
|
|
81
108
|
getEnrichedModels() {
|
|
82
|
-
|
|
83
|
-
const rels = extractAllRelationships(raw);
|
|
84
|
-
return enrichModelsWithGraph(raw, rels);
|
|
109
|
+
return getEnrichedGraph().enriched;
|
|
85
110
|
},
|
|
86
111
|
|
|
87
112
|
/**
|
|
@@ -109,8 +134,7 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
109
134
|
* @returns {Object[]}
|
|
110
135
|
*/
|
|
111
136
|
getRelationships() {
|
|
112
|
-
|
|
113
|
-
return extractAllRelationships(raw);
|
|
137
|
+
return getEnrichedGraph().relationships;
|
|
114
138
|
},
|
|
115
139
|
|
|
116
140
|
/**
|
|
@@ -118,9 +142,7 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
118
142
|
* @returns {string}
|
|
119
143
|
*/
|
|
120
144
|
getMermaid() {
|
|
121
|
-
|
|
122
|
-
const rels = extractAllRelationships(raw);
|
|
123
|
-
return generateMermaidErDiagram(raw, rels);
|
|
145
|
+
return getEnrichedGraph().mermaid;
|
|
124
146
|
},
|
|
125
147
|
|
|
126
148
|
extractAllRelationships,
|
|
@@ -250,21 +272,17 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
250
272
|
method: 'get',
|
|
251
273
|
path: '',
|
|
252
274
|
handler: (req, res) => {
|
|
253
|
-
const
|
|
254
|
-
const relationships = extractAllRelationships(rawModels);
|
|
255
|
-
const models = enrichModelsWithGraph(rawModels, relationships);
|
|
256
|
-
const mermaid = generateMermaidErDiagram(rawModels, relationships);
|
|
257
|
-
|
|
275
|
+
const graph = getEnrichedGraph();
|
|
258
276
|
res.json({
|
|
259
277
|
meta: {
|
|
260
278
|
version: '1.0.0',
|
|
261
279
|
generatedAt: new Date().toISOString(),
|
|
262
|
-
modelCount:
|
|
263
|
-
relationCount: relationships.length,
|
|
280
|
+
modelCount: graph.enriched.length,
|
|
281
|
+
relationCount: graph.relationships.length,
|
|
264
282
|
},
|
|
265
|
-
models,
|
|
266
|
-
relationships,
|
|
267
|
-
mermaid,
|
|
283
|
+
models: graph.enriched,
|
|
284
|
+
relationships: graph.relationships,
|
|
285
|
+
mermaid: graph.mermaid,
|
|
268
286
|
});
|
|
269
287
|
},
|
|
270
288
|
},
|
|
@@ -272,19 +290,14 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
272
290
|
method: 'get',
|
|
273
291
|
path: '/relations',
|
|
274
292
|
handler: (req, res) => {
|
|
275
|
-
|
|
276
|
-
const relationships = extractAllRelationships(rawModels);
|
|
277
|
-
res.json({ relationships });
|
|
293
|
+
res.json({ relationships: getEnrichedGraph().relationships });
|
|
278
294
|
},
|
|
279
295
|
},
|
|
280
296
|
{
|
|
281
297
|
method: 'get',
|
|
282
298
|
path: '/mermaid',
|
|
283
299
|
handler: (req, res) => {
|
|
284
|
-
|
|
285
|
-
const relationships = extractAllRelationships(rawModels);
|
|
286
|
-
const mermaid = generateMermaidErDiagram(rawModels, relationships);
|
|
287
|
-
res.json({ mermaid });
|
|
300
|
+
res.json({ mermaid: getEnrichedGraph().mermaid });
|
|
288
301
|
},
|
|
289
302
|
},
|
|
290
303
|
{
|
|
@@ -298,10 +311,8 @@ function schemaExplorerPlugin(options = {}) {
|
|
|
298
311
|
return res.status(404).json({ error: `Model "${modelName}" not found` });
|
|
299
312
|
}
|
|
300
313
|
|
|
301
|
-
const
|
|
302
|
-
const
|
|
303
|
-
const models = enrichModelsWithGraph(rawModels, relationships);
|
|
304
|
-
const found = models.find(m => m.name === modelName);
|
|
314
|
+
const graph = getEnrichedGraph();
|
|
315
|
+
const found = graph.enriched.find(m => m.name === modelName);
|
|
305
316
|
|
|
306
317
|
res.json({ model: found });
|
|
307
318
|
},
|
|
@@ -50,12 +50,15 @@ function seoCheckerPlugin(options = {}) {
|
|
|
50
50
|
// Inject panel styles into head
|
|
51
51
|
ctx.injectHead(generatePanelStyles(), { priority: 100 });
|
|
52
52
|
|
|
53
|
+
const safeSettings = JSON.stringify(mergedSettings).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
54
|
+
const safeChecks = JSON.stringify(checkDefinitions).replace(/</g, '\\u003c').replace(/>/g, '\\u003e');
|
|
55
|
+
|
|
53
56
|
// Inject panel HTML and analyzer script into body
|
|
54
57
|
ctx.injectBody(`
|
|
55
58
|
${generatePanelHtml(checkDefinitions)}
|
|
56
59
|
<script>
|
|
57
|
-
window.__SEO_CHECKER_SETTINGS__ = ${
|
|
58
|
-
window.__SEO_CHECKER_CHECKS__ = ${
|
|
60
|
+
window.__SEO_CHECKER_SETTINGS__ = ${safeSettings};
|
|
61
|
+
window.__SEO_CHECKER_CHECKS__ = ${safeChecks};
|
|
59
62
|
</script>
|
|
60
63
|
<script>${analyzerScript}</script>
|
|
61
64
|
`);
|
|
@@ -27,9 +27,12 @@ const BOT_PATTERNS = [
|
|
|
27
27
|
{ pattern: /petalbot/i, name: 'PetalBot' },
|
|
28
28
|
{ pattern: /gptbot/i, name: 'GPTBot' },
|
|
29
29
|
{ pattern: /claudebot/i, name: 'ClaudeBot' },
|
|
30
|
+
{ pattern: /amazonbot/i, name: 'Amazon' },
|
|
30
31
|
{ pattern: /anthropic/i, name: 'Anthropic' },
|
|
31
32
|
{ pattern: /ccbot/i, name: 'CCBot' },
|
|
32
33
|
{ pattern: /bytespider/i, name: 'ByteSpider' },
|
|
34
|
+
{ pattern: /dataforseo/i, name: 'DataForSEO' },
|
|
35
|
+
{ pattern: /yisouspider/i, name: 'Yisou' },
|
|
33
36
|
{ pattern: /open-research-bot/i, name: 'open-research-bot' },
|
|
34
37
|
{ pattern: /ptb12/i, name: 'PTB12' },
|
|
35
38
|
{ pattern: /curl\//i, name: 'curl' },
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
* @module plugins/site-analytics/client-error-handler
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
const { formatDbError } = require('./tracking');
|
|
8
|
+
|
|
7
9
|
const DEFAULT_TABLE = 'analytics_client_errors';
|
|
8
10
|
|
|
9
11
|
/**
|
|
@@ -71,7 +73,7 @@ function createErrorReportHandler(options) {
|
|
|
71
73
|
|
|
72
74
|
res.status(202).json({ ok: true });
|
|
73
75
|
} catch (e) {
|
|
74
|
-
console.error('[site-analytics] Client error report failed:', e
|
|
76
|
+
console.error('[site-analytics] Client error report failed:', formatDbError(e));
|
|
75
77
|
res.status(500).json({ error: 'Report failed' });
|
|
76
78
|
}
|
|
77
79
|
};
|
|
@@ -140,6 +140,8 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
140
140
|
}
|
|
141
141
|
}
|
|
142
142
|
|
|
143
|
+
let trackingMiddleware = null;
|
|
144
|
+
|
|
143
145
|
return {
|
|
144
146
|
name: 'site-analytics',
|
|
145
147
|
version: '1.0.0',
|
|
@@ -177,13 +179,17 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
177
179
|
register(ctx) {
|
|
178
180
|
const knex = db.knex || db;
|
|
179
181
|
|
|
180
|
-
|
|
182
|
+
trackingMiddleware = createTrackingMiddleware({
|
|
181
183
|
knex,
|
|
182
184
|
excludePaths,
|
|
183
185
|
trackBots,
|
|
184
186
|
tableName,
|
|
185
187
|
batchSize: options.batchSize ?? 20,
|
|
186
188
|
flushIntervalMs: options.flushIntervalMs ?? 3000,
|
|
189
|
+
maxRetries: options.maxRetries ?? 3,
|
|
190
|
+
maxBackoffMs: options.maxBackoffMs ?? 60000,
|
|
191
|
+
maxQueueSize: options.maxQueueSize ?? 5000,
|
|
192
|
+
errorLogIntervalMs: options.errorLogIntervalMs ?? 60000,
|
|
187
193
|
});
|
|
188
194
|
|
|
189
195
|
ctx.app.use(trackingMiddleware);
|
|
@@ -199,15 +205,18 @@ function siteAnalyticsPlugin(options = {}) {
|
|
|
199
205
|
|
|
200
206
|
return () => {
|
|
201
207
|
stopCleanup();
|
|
208
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
202
209
|
};
|
|
203
210
|
},
|
|
204
211
|
|
|
205
212
|
dispose() {
|
|
206
213
|
stopCleanup();
|
|
214
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
207
215
|
},
|
|
208
216
|
|
|
209
217
|
shutdown() {
|
|
210
218
|
stopCleanup();
|
|
219
|
+
if (trackingMiddleware && trackingMiddleware.stop) trackingMiddleware.stop();
|
|
211
220
|
},
|
|
212
221
|
|
|
213
222
|
onRoutesReady(ctx) {
|