webspresso 0.0.97 → 0.0.99
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/ADMIN.md +163 -5
- package/.agents/AGENTS.md +1 -1
- package/.agents/ARCHITECTURE.md +10 -0
- package/.agents/AUTH.md +169 -0
- package/.agents/CLI.md +195 -1
- package/.agents/ORM.md +88 -4
- package/.agents/PLUGINS.md +221 -9
- package/.agents/QUEUE.md +127 -0
- package/.agents/ROUTING.md +127 -7
- package/AGENTS.md +1 -0
- package/bin/commands/add-esbuild.js +80 -0
- package/bin/commands/add-tailwind.js +76 -31
- package/bin/commands/analytics-prune.js +57 -0
- package/bin/commands/api.js +207 -15
- package/bin/commands/build.js +99 -0
- package/bin/commands/dev.js +64 -35
- package/bin/commands/doctor.js +26 -0
- package/bin/commands/email-prune.js +47 -0
- package/bin/commands/make-job.js +86 -0
- package/bin/commands/make-model.js +250 -0
- package/bin/commands/new.js +45 -28
- package/bin/commands/openapi.js +102 -0
- package/bin/commands/page.js +6 -3
- package/bin/commands/queue-prune.js +59 -0
- package/bin/commands/routes.js +52 -0
- package/bin/commands/service.js +6 -3
- package/bin/commands/typegen.js +106 -0
- package/bin/commands/upload-prune.js +79 -0
- package/bin/webspresso.js +22 -0
- package/core/applySchema.js +13 -6
- package/core/auth/hash.js +21 -12
- package/core/auth/index.js +60 -0
- package/core/auth/manager.js +134 -0
- package/core/auth/middleware.js +88 -0
- package/core/auth/oauth.js +517 -0
- package/core/auth/qrcode.js +499 -0
- package/core/auth/totp.js +504 -0
- package/core/compression/middleware.js +23 -17
- package/core/openapi/build-from-api-routes.js +160 -29
- package/core/orm/cache/fingerprint.js +15 -1
- package/core/orm/cache/memory-provider.js +27 -2
- package/core/orm/eager-loader.js +144 -10
- package/core/orm/index.js +20 -7
- package/core/orm/model.js +53 -7
- package/core/orm/query-builder.js +90 -4
- package/core/orm/repository.js +44 -3
- package/core/orm/search.js +213 -0
- package/core/queue/adapters/database.js +94 -0
- package/core/queue/adapters/memory.js +62 -0
- package/core/queue/adapters/redis.js +80 -0
- package/core/queue/cron.js +331 -0
- package/core/queue/index.js +8 -1
- package/core/queue/manager.js +205 -1
- package/core/queue/scheduler.js +359 -0
- package/core/sse/index.js +19 -0
- package/core/sse/stream.js +430 -0
- package/core/ssr/stream.js +11 -0
- package/index.d.ts +1188 -10
- package/index.js +88 -96
- package/package.json +1 -1
- package/plugins/admin-panel/api.js +279 -17
- package/plugins/admin-panel/app.js +114 -22
- package/plugins/admin-panel/client/parts/01-state-api-breadcrumb.js +314 -9
- package/plugins/admin-panel/client/parts/04-field-renderers.js +264 -67
- package/plugins/admin-panel/client/parts/05-rich-text-file-helpers.js +333 -36
- package/plugins/admin-panel/client/parts/06-login-setup-forms.js +119 -28
- package/plugins/admin-panel/client/parts/07-model-list.js +49 -8
- package/plugins/admin-panel/client/parts/08-record-list.js +248 -23
- package/plugins/admin-panel/client/parts/09-record-form.js +59 -12
- package/plugins/admin-panel/client/parts/10-export-registry.js +23 -0
- package/plugins/admin-panel/client/verify-spa-parts.js +2 -0
- package/plugins/admin-panel/core/admin-module.js +13 -7
- package/plugins/admin-panel/core/api-extensions.js +30 -2
- package/plugins/admin-panel/core/registry.js +96 -16
- package/plugins/admin-panel/field-renderers/file-upload.js +187 -104
- package/plugins/admin-panel/field-renderers/index.js +2 -0
- package/plugins/admin-panel/index.js +96 -0
- package/plugins/admin-panel/lib/login-rate-limiter.js +119 -0
- package/plugins/admin-panel/modules/admin-users.js +16 -1
- package/plugins/admin-panel/modules/bulk-actions.js +13 -5
- package/plugins/admin-panel/modules/custom-pages.js +492 -3
- package/plugins/admin-panel/modules/dashboard.js +283 -0
- package/plugins/admin-panel/modules/menu.js +56 -18
- package/plugins/admin-panel/modules/user-management.js +24 -6
- package/plugins/api-key/crypto.js +101 -0
- package/plugins/api-key/index.js +288 -0
- package/plugins/api-key/middleware.js +461 -0
- package/plugins/api-key/migration-template.js +48 -0
- package/plugins/api-key/model.js +50 -0
- package/plugins/api-key/scopes.js +125 -0
- package/plugins/audit-log/admin-component.js +487 -59
- package/plugins/audit-log/api-handlers.js +148 -0
- package/plugins/audit-log/index.js +153 -3
- package/plugins/audit-log/purge.js +60 -10
- package/plugins/dashboard/app.js +152 -27
- package/plugins/dashboard/index.js +143 -39
- package/plugins/dashboard/styles.js +78 -0
- package/plugins/data-exchange/export-xlsx.js +9 -2
- package/plugins/data-exchange/import.js +9 -2
- package/plugins/data-exchange/index.js +6 -3
- package/plugins/email/index.js +132 -2
- package/plugins/email/purge.js +60 -10
- package/plugins/file-manager/admin-component.js +20 -4
- package/plugins/file-manager/index.js +21 -4
- package/plugins/index.js +10 -0
- package/plugins/oauth/index.js +338 -0
- package/plugins/orm-cache-admin/index.js +4 -2
- package/plugins/queue/index.js +15 -0
- package/plugins/schema-explorer/admin-component.js +714 -0
- package/plugins/schema-explorer/relations-graph.js +205 -0
- package/plugins/schema-explorer.js +248 -69
- package/plugins/site-analytics/admin-component.js +23 -0
- package/plugins/site-analytics/api-handlers.js +42 -0
- package/plugins/site-analytics/index.js +147 -2
- package/plugins/site-analytics/purge.js +168 -0
- package/plugins/swagger.js +261 -13
- package/plugins/upload/index.js +26 -0
- package/plugins/upload/orphan-pruner.js +292 -0
- package/src/api/api-loader.js +4 -1
- package/src/bundle/index.js +517 -0
- package/src/bundle/manifest.js +165 -0
- package/src/dev/banner.js +296 -0
- package/src/dev/colors.js +70 -0
- package/src/dev/index.js +62 -0
- package/src/dev/profiler.js +354 -0
- package/src/dev/typegen.js +497 -0
- package/src/dev-reload/index.js +611 -0
- package/src/discovery/file-route-parser.js +44 -1
- package/src/discovery/scan-routes.js +2 -2
- package/src/file-router.js +347 -162
- package/src/helpers.js +967 -98
- package/src/njk-frontmatter.js +12 -0
- package/src/pages/page-loader.js +61 -11
- package/src/routing/cascading.js +378 -0
- package/src/routing/middleware-resolver.js +92 -0
- package/src/server.js +635 -63
|
@@ -0,0 +1,354 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Webspresso Development HTTP & Database Query Profiler
|
|
5
|
+
* AsyncLocalStorage-powered per-request database query tracking and N+1 query detection
|
|
6
|
+
* @module src/dev/profiler
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
10
|
+
const { colors: defaultColors, stripAnsi } = require('./colors');
|
|
11
|
+
|
|
12
|
+
// Ambient request storage for profiling queries executed during an HTTP request
|
|
13
|
+
const profilerStorage = new AsyncLocalStorage();
|
|
14
|
+
|
|
15
|
+
// Internal tables to ignore when calculating N+1 warnings
|
|
16
|
+
const IGNORED_SYSTEM_TABLES = new Set([
|
|
17
|
+
'sqlite_master',
|
|
18
|
+
'sqlite_sequence',
|
|
19
|
+
'sqlite_temp_master',
|
|
20
|
+
'knex_migrations',
|
|
21
|
+
'knex_migrations_lock',
|
|
22
|
+
'information_schema',
|
|
23
|
+
'pg_catalog',
|
|
24
|
+
'spatial_ref_sys',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Robustly extracts the primary target table name from an SQL query string
|
|
29
|
+
* @param {string} sql
|
|
30
|
+
* @returns {string|null} Table name, or null if unparseable
|
|
31
|
+
*/
|
|
32
|
+
function extractTableFromSql(sql) {
|
|
33
|
+
if (!sql || typeof sql !== 'string') return null;
|
|
34
|
+
const cleaned = sql.replace(/\s+/g, ' ').trim();
|
|
35
|
+
|
|
36
|
+
// 1. SELECT ... FROM [schema.]table
|
|
37
|
+
const fromMatch = cleaned.match(/\bfrom\s+(?:(?:[`"']?([a-zA-Z0-9_]+)[`"']?\.)?[`"']?([a-zA-Z0-9_]+)[`"']?)/i);
|
|
38
|
+
if (fromMatch) {
|
|
39
|
+
return fromMatch[2] || fromMatch[1];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// 2. INSERT INTO [schema.]table
|
|
43
|
+
const intoMatch = cleaned.match(/\binto\s+(?:(?:[`"']?([a-zA-Z0-9_]+)[`"']?\.)?[`"']?([a-zA-Z0-9_]+)[`"']?)/i);
|
|
44
|
+
if (intoMatch) {
|
|
45
|
+
return intoMatch[2] || intoMatch[1];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 3. UPDATE [schema.]table
|
|
49
|
+
const updateMatch = cleaned.match(/\bupdate\s+(?:(?:[`"']?([a-zA-Z0-9_]+)[`"']?\.)?[`"']?([a-zA-Z0-9_]+)[`"']?)/i);
|
|
50
|
+
if (updateMatch) {
|
|
51
|
+
return updateMatch[2] || updateMatch[1];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// 4. DELETE FROM [schema.]table
|
|
55
|
+
const deleteMatch = cleaned.match(/\bdelete\s+(?:from\s+)?(?:(?:[`"']?([a-zA-Z0-9_]+)[`"']?\.)?[`"']?([a-zA-Z0-9_]+)[`"']?)/i);
|
|
56
|
+
if (deleteMatch) {
|
|
57
|
+
return deleteMatch[2] || deleteMatch[1];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Analyzes collected queries for potential N+1 anti-patterns
|
|
65
|
+
* @param {Array<Object>} queries - List of executed query records
|
|
66
|
+
* @param {Object} [options]
|
|
67
|
+
* @param {number} [options.threshold=4] - Minimum repeated queries to trigger warning
|
|
68
|
+
* @returns {Array<{ table: string, count: number, totalMs: number, recommendation: string, sampleSql: string }>}
|
|
69
|
+
*/
|
|
70
|
+
function detectNPlusOne(queries = [], options = {}) {
|
|
71
|
+
const threshold = options.threshold ?? 4;
|
|
72
|
+
if (!Array.isArray(queries) || queries.length < threshold) {
|
|
73
|
+
return [];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const tableStats = new Map();
|
|
77
|
+
|
|
78
|
+
for (const q of queries) {
|
|
79
|
+
if (!q || !q.table) continue;
|
|
80
|
+
const lower = q.table.toLowerCase();
|
|
81
|
+
if (IGNORED_SYSTEM_TABLES.has(lower) || lower.startsWith('knex_')) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
let stats = tableStats.get(lower);
|
|
86
|
+
if (!stats) {
|
|
87
|
+
stats = { table: q.table, count: 0, totalMs: 0, queries: [] };
|
|
88
|
+
tableStats.set(lower, stats);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
stats.count++;
|
|
92
|
+
stats.totalMs += q.durationMs || 0;
|
|
93
|
+
stats.queries.push(q);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const warnings = [];
|
|
97
|
+
|
|
98
|
+
for (const [, stats] of tableStats) {
|
|
99
|
+
if (stats.count >= threshold) {
|
|
100
|
+
const roundedMs = Math.round(stats.totalMs * 100) / 100;
|
|
101
|
+
warnings.push({
|
|
102
|
+
table: stats.table,
|
|
103
|
+
count: stats.count,
|
|
104
|
+
totalMs: roundedMs,
|
|
105
|
+
sampleSql: stats.queries[0]?.sql || '',
|
|
106
|
+
recommendation: `Potential N+1 query pattern detected on table '${stats.table}' (${stats.count} queries in single request, ${roundedMs}ms). Consider eager loading with repo.query().include('${stats.table}') or batching.`,
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return warnings;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Formats a dev request log line with execution time, DB metrics, and optional query detail
|
|
116
|
+
* @param {Object} params
|
|
117
|
+
* @param {string} params.method
|
|
118
|
+
* @param {string} params.url
|
|
119
|
+
* @param {number} params.status
|
|
120
|
+
* @param {number} params.durationMs
|
|
121
|
+
* @param {Array<Object>} [params.queries]
|
|
122
|
+
* @param {number} [params.slowQueryMs=50]
|
|
123
|
+
* @param {boolean} [params.verbose=false]
|
|
124
|
+
* @param {Object} [params.colors]
|
|
125
|
+
* @returns {{ line: string, queryLines: string[], plainLine: string }}
|
|
126
|
+
*/
|
|
127
|
+
function formatRequestLog(params) {
|
|
128
|
+
const {
|
|
129
|
+
method = 'GET',
|
|
130
|
+
url = '/',
|
|
131
|
+
status = 200,
|
|
132
|
+
durationMs = 0,
|
|
133
|
+
queries = [],
|
|
134
|
+
slowQueryMs = 50,
|
|
135
|
+
verbose = false,
|
|
136
|
+
colors = defaultColors,
|
|
137
|
+
} = params;
|
|
138
|
+
|
|
139
|
+
// 1. Method color
|
|
140
|
+
let methodColored = method;
|
|
141
|
+
const m = method.toUpperCase();
|
|
142
|
+
if (m === 'GET') methodColored = colors.cyan(m);
|
|
143
|
+
else if (m === 'POST') methodColored = colors.green(m);
|
|
144
|
+
else if (m === 'PUT' || m === 'PATCH') methodColored = colors.yellow(m);
|
|
145
|
+
else if (m === 'DELETE') methodColored = colors.red(m);
|
|
146
|
+
else methodColored = colors.white(m);
|
|
147
|
+
|
|
148
|
+
// 2. Status color
|
|
149
|
+
let statusColored = String(status);
|
|
150
|
+
if (status >= 500) statusColored = colors.red(String(status));
|
|
151
|
+
else if (status >= 400) statusColored = colors.yellow(String(status));
|
|
152
|
+
else if (status >= 300) statusColored = colors.cyan(String(status));
|
|
153
|
+
else if (status >= 200) statusColored = colors.green(String(status));
|
|
154
|
+
|
|
155
|
+
// 3. Duration color
|
|
156
|
+
let durationColored = `${durationMs}ms`;
|
|
157
|
+
if (durationMs > 500) durationColored = colors.red(durationColored);
|
|
158
|
+
else if (durationMs > 150) durationColored = colors.yellow(durationColored);
|
|
159
|
+
else durationColored = colors.gray(durationColored);
|
|
160
|
+
|
|
161
|
+
// 4. DB Queries summary
|
|
162
|
+
let dbSummary = '';
|
|
163
|
+
const queryCount = queries.length;
|
|
164
|
+
if (queryCount > 0) {
|
|
165
|
+
const totalDbMs = Math.round(queries.reduce((acc, q) => acc + (q.durationMs || 0), 0) * 10) / 10;
|
|
166
|
+
const slowQueries = queries.filter((q) => (q.durationMs || 0) >= slowQueryMs);
|
|
167
|
+
const slowNote = slowQueries.length > 0 ? ` ${colors.yellow(`⚠️ ${slowQueries.length} slow >${slowQueryMs}ms`)}` : '';
|
|
168
|
+
dbSummary = ` ${colors.gray('|')} ${colors.magenta(`${queryCount} DB ${queryCount === 1 ? 'query' : 'queries'}`)} ${colors.gray(`(${totalDbMs}ms${slowNote})`)}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const line = `[${methodColored} ${statusColored}] ${colors.bold(url)} ${colors.gray('—')} ${durationColored}${dbSummary}`;
|
|
172
|
+
const plainLine = stripAnsi(line);
|
|
173
|
+
|
|
174
|
+
// 5. Verbose query breakdown lines
|
|
175
|
+
const queryLines = [];
|
|
176
|
+
if (verbose && queryCount > 0) {
|
|
177
|
+
for (let i = 0; i < queries.length; i++) {
|
|
178
|
+
const q = queries[i];
|
|
179
|
+
const isSlow = (q.durationMs || 0) >= slowQueryMs;
|
|
180
|
+
const msLabel = `${q.durationMs}ms`;
|
|
181
|
+
const timeTag = isSlow ? colors.yellow(msLabel) : colors.gray(msLabel);
|
|
182
|
+
const isLast = i === queries.length - 1;
|
|
183
|
+
const branch = isLast ? '└─' : '├─';
|
|
184
|
+
const cleanSql = (q.sql || '').replace(/\s+/g, ' ').slice(0, 120);
|
|
185
|
+
queryLines.push(` ${colors.gray(branch)} [DB ${timeTag}] ${colors.dim(cleanSql)}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return { line, queryLines, plainLine };
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Attaches query instrumentation listeners to a Knex instance
|
|
194
|
+
* @param {import('knex').Knex} knexInstance
|
|
195
|
+
* @returns {void}
|
|
196
|
+
*/
|
|
197
|
+
function attachProfilerToKnex(knexInstance) {
|
|
198
|
+
if (!knexInstance || typeof knexInstance.on !== 'function') return;
|
|
199
|
+
if (knexInstance.__wsDevProfilerAttached) return;
|
|
200
|
+
knexInstance.__wsDevProfilerAttached = true;
|
|
201
|
+
|
|
202
|
+
const queryStarts = new Map();
|
|
203
|
+
|
|
204
|
+
knexInstance.on('query', (queryData) => {
|
|
205
|
+
if (queryData && queryData.__knexQueryUid) {
|
|
206
|
+
queryStarts.set(queryData.__knexQueryUid, process.hrtime.bigint());
|
|
207
|
+
// Self-cleaning bound to avoid memory leak if responses fail to fire
|
|
208
|
+
if (queryStarts.size > 1000) {
|
|
209
|
+
const oldestKey = queryStarts.keys().next().value;
|
|
210
|
+
queryStarts.delete(oldestKey);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
function recordQuery(queryData, isError, error) {
|
|
216
|
+
const uid = queryData?.__knexQueryUid;
|
|
217
|
+
const start = uid ? queryStarts.get(uid) : null;
|
|
218
|
+
if (uid) queryStarts.delete(uid);
|
|
219
|
+
|
|
220
|
+
let durationMs = 0;
|
|
221
|
+
if (start) {
|
|
222
|
+
const diff = process.hrtime.bigint() - start;
|
|
223
|
+
durationMs = Number(diff) / 1e6;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const store = profilerStorage.getStore();
|
|
227
|
+
if (store && Array.isArray(store.queries)) {
|
|
228
|
+
const sql = queryData?.sql || '';
|
|
229
|
+
store.queries.push({
|
|
230
|
+
uid,
|
|
231
|
+
sql,
|
|
232
|
+
bindings: queryData?.bindings || [],
|
|
233
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
234
|
+
table: extractTableFromSql(sql),
|
|
235
|
+
isError: Boolean(isError),
|
|
236
|
+
error: error ? error.message : null,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
knexInstance.on('query-response', (response, queryData) => {
|
|
242
|
+
recordQuery(queryData, false);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
knexInstance.on('query-error', (error, queryData) => {
|
|
246
|
+
recordQuery(queryData, true, error);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Creates Express middleware for dev request logging, DB query profiling, and N+1 detection
|
|
252
|
+
* @param {Object} [options]
|
|
253
|
+
* @param {Object} [options.db] - Knex/ORM database instance
|
|
254
|
+
* @param {Object} [options.logger=console] - Output logger
|
|
255
|
+
* @param {boolean} [options.detectNPlusOne=true] - Whether to detect and warn on N+1 queries
|
|
256
|
+
* @param {number} [options.thresholdNPlusOne=4] - Threshold repeated queries
|
|
257
|
+
* @param {number} [options.slowQueryMs=50] - Threshold for slow query warning
|
|
258
|
+
* @param {boolean} [options.verbose=false] - Whether to print indented SQL queries
|
|
259
|
+
* @param {boolean} [options.colors=true] - Whether to use ANSI colors
|
|
260
|
+
* @returns {import('express').RequestHandler}
|
|
261
|
+
*/
|
|
262
|
+
function createDevProfilerMiddleware(options = {}) {
|
|
263
|
+
const {
|
|
264
|
+
db = null,
|
|
265
|
+
logger = console,
|
|
266
|
+
detectNPlusOne: shouldDetectNPlusOne = true,
|
|
267
|
+
thresholdNPlusOne = 4,
|
|
268
|
+
slowQueryMs = 50,
|
|
269
|
+
verbose = false,
|
|
270
|
+
colors: useColors = true,
|
|
271
|
+
} = options;
|
|
272
|
+
|
|
273
|
+
// Auto-instrument Knex instance if available
|
|
274
|
+
if (db && db.knex) {
|
|
275
|
+
attachProfilerToKnex(db.knex);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return function devProfilerMiddleware(req, res, next) {
|
|
279
|
+
const startHr = process.hrtime.bigint();
|
|
280
|
+
const requestId = (req.headers && req.headers['x-request-id'])
|
|
281
|
+
? String(req.headers['x-request-id'])
|
|
282
|
+
: Math.random().toString(36).slice(2, 8);
|
|
283
|
+
|
|
284
|
+
const store = {
|
|
285
|
+
requestId,
|
|
286
|
+
method: (req.method || 'GET').toUpperCase(),
|
|
287
|
+
path: req.originalUrl || req.url || '/',
|
|
288
|
+
queries: [],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
// Expose profiler details on req object for inspection
|
|
292
|
+
req.profiler = store;
|
|
293
|
+
|
|
294
|
+
profilerStorage.run(store, () => {
|
|
295
|
+
res.on('finish', () => {
|
|
296
|
+
const durationMs = Number(process.hrtime.bigint() - startHr) / 1e6;
|
|
297
|
+
const roundedDuration = Math.round(durationMs * 10) / 10;
|
|
298
|
+
const statusCode = res.statusCode || 200;
|
|
299
|
+
|
|
300
|
+
const formatted = formatRequestLog({
|
|
301
|
+
method: store.method,
|
|
302
|
+
url: store.path,
|
|
303
|
+
status: statusCode,
|
|
304
|
+
durationMs: roundedDuration,
|
|
305
|
+
queries: store.queries,
|
|
306
|
+
slowQueryMs,
|
|
307
|
+
verbose,
|
|
308
|
+
colors: useColors ? defaultColors : {
|
|
309
|
+
bold: (s) => s,
|
|
310
|
+
dim: (s) => s,
|
|
311
|
+
cyan: (s) => s,
|
|
312
|
+
green: (s) => s,
|
|
313
|
+
yellow: (s) => s,
|
|
314
|
+
red: (s) => s,
|
|
315
|
+
magenta: (s) => s,
|
|
316
|
+
gray: (s) => s,
|
|
317
|
+
white: (s) => s,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
if (logger && typeof logger.log === 'function') {
|
|
322
|
+
logger.log(formatted.line);
|
|
323
|
+
if (Array.isArray(formatted.queryLines) && formatted.queryLines.length > 0) {
|
|
324
|
+
for (const ql of formatted.queryLines) {
|
|
325
|
+
logger.log(ql);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// N+1 Query Warning Check
|
|
331
|
+
if (shouldDetectNPlusOne && store.queries.length >= thresholdNPlusOne) {
|
|
332
|
+
const warnings = detectNPlusOne(store.queries, { threshold: thresholdNPlusOne });
|
|
333
|
+
for (const warn of warnings) {
|
|
334
|
+
if (logger && typeof logger.warn === 'function') {
|
|
335
|
+
const prefix = defaultColors.yellow('⚠️ [webspresso:n+1]');
|
|
336
|
+
logger.warn(` ${prefix} ${warn.recommendation}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
|
|
342
|
+
next();
|
|
343
|
+
});
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
module.exports = {
|
|
348
|
+
profilerStorage,
|
|
349
|
+
attachProfilerToKnex,
|
|
350
|
+
extractTableFromSql,
|
|
351
|
+
detectNPlusOne,
|
|
352
|
+
formatRequestLog,
|
|
353
|
+
createDevProfilerMiddleware,
|
|
354
|
+
};
|