@aimeloic/monkey-tester 5.0.3 → 5.0.5

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 (3) hide show
  1. package/htmlTemplate.js +112 -103
  2. package/monkey.js +117 -49
  3. package/package.json +1 -1
package/htmlTemplate.js CHANGED
@@ -1,23 +1,13 @@
1
1
  'use strict';
2
2
 
3
- // ─── Unicode-safe base64 helpers ────────────────────────────────────────────
4
- // Use these on the CALLER side to produce endpointsJsonB64:
5
- // import { encodePayload } from './ui.js';
6
- // const endpointsJsonB64 = encodePayload(endpointsData);
7
- //
3
+ // ─── Unicode-safe base64 helpers ─────────────────────────────────────────────
8
4
  function encodePayload(obj) {
9
5
  const jsonStr = JSON.stringify(obj);
10
- // Convert string to UTF-8 bytes
11
6
  const bytes = new TextEncoder().encode(jsonStr);
12
- // Convert bytes to binary string for btoa
13
7
  const binStr = Array.from(bytes, byte => String.fromCharCode(byte)).join('');
14
8
  return btoa(binStr);
15
9
  }
16
10
 
17
- // The reciprocal decode runs inside each HTML template (see _decode below).
18
- // It is injected as a one-liner so every template is self-contained.
19
-
20
- // Replace line 18 with this version:
21
11
  const _decode = `function _decode(b64) {
22
12
  const binStr = atob(b64.replace(/\\s+/g, ''));
23
13
  const bytes = new Uint8Array(binStr.length);
@@ -27,22 +17,20 @@ const _decode = `function _decode(b64) {
27
17
  return JSON.parse(new TextDecoder().decode(bytes));
28
18
  }`;
29
19
 
30
- // ─── Runtime sandbox (injected into tester page) ─────────────────────────────
20
+ // ─── Runtime sandbox (injected into tester page) ──────────────────────────────
31
21
  function runtimeClientSandbox() {
32
- // Unicode-safe decode (whitespace-stripped for safety)
33
22
  function _decode(b64) {
34
- const binStr = atob(b64.replace(/\s+/g, ''));
35
- const bytes = new Uint8Array(binStr.length);
36
- for (let i = 0; i < binStr.length; i++) {
37
- bytes[i] = binStr.charCodeAt(i);
23
+ const binStr = atob(b64.replace(/\s+/g, ''));
24
+ const bytes = new Uint8Array(binStr.length);
25
+ for (let i = 0; i < binStr.length; i++) {
26
+ bytes[i] = binStr.charCodeAt(i);
27
+ }
28
+ return JSON.parse(new TextDecoder().decode(bytes));
38
29
  }
39
- const jsonStr = new TextDecoder().decode(bytes);
40
- return JSON.parse(jsonStr);
41
- }
42
30
 
43
31
  const ENDPOINTS = _decode(
44
- document.getElementById('__monkey_data__').textContent.trim()
45
- );
32
+ document.getElementById('__monkey_data__').textContent.trim()
33
+ );
46
34
 
47
35
  let currentKey = null;
48
36
 
@@ -218,10 +206,11 @@ function runtimeClientSandbox() {
218
206
  const UI = {
219
207
 
220
208
  // ── Tester sandbox ──────────────────────────────────────────────────────────
221
- tester: (endpointsJsonB64) => `<!DOCTYPE html>
209
+ // appName: displayed in the header logo
210
+ tester: (endpointsJsonB64, appName = 'App') => `<!DOCTYPE html>
222
211
  <html lang="en">
223
212
  <head>
224
- <meta charset="UTF-8"><title>EndtesterEnvironment Hub</title>
213
+ <meta charset="UTF-8"><title>${appName}API Tester</title>
225
214
  <link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&family=DM+Mono:wght@400;500&family=DM+Sans:wght@300;400;500&display=swap" rel="stylesheet">
226
215
  <style>
227
216
  :root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; --blue:#5a86c0; --radius:8px; }
@@ -250,7 +239,7 @@ const UI = {
250
239
  .form-section-title { font-size:11px; font-family:'DM Mono',monospace; color:var(--text-dim); text-transform:uppercase; margin-bottom:16px; border-bottom:1px solid var(--border); padding-bottom:6px; }
251
240
  .field-row { display:grid; grid-template-columns:150px 1fr; align-items:center; gap:16px; margin-bottom:14px; }
252
241
  .field-label { font-family:'DM Mono',monospace; font-size:12px; color:var(--text-dim); text-align:right; }
253
- input[type=text],input[type=password],input[type=number] { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-size:13px; padding:8px 12px; border-radius:var(--radius); width:100%; outline:none; }
242
+ input[type=text],input[type=password],input[type=number],input[type=email],input[type=date],input[type=tel],input[type=url] { background:var(--surface2); border:1px solid var(--border); color:var(--text); font-size:13px; padding:8px 12px; border-radius:var(--radius); width:100%; outline:none; }
254
243
  .btn-row { margin-top:24px; display:flex; gap:12px; }
255
244
  .btn { background:var(--accent); color:#0e0c09; border:none; padding:10px 24px; border-radius:var(--radius); font-size:13px; font-weight:500; cursor:pointer; }
256
245
  .btn-secondary { background:var(--surface2); color:var(--text-dim); border:1px solid var(--border); }
@@ -268,10 +257,9 @@ const UI = {
268
257
  </style>
269
258
  </head>
270
259
  <body>
271
- <!-- Replace the old hidden div with this -->
272
260
  <script id="__monkey_data__" type="text/plain">${endpointsJsonB64}</script>
273
261
  <header>
274
- <div class="logo">🐒 Endtester <span>Interactive API Hub</span></div>
262
+ <div class="logo">${appName} <span>API Tester</span></div>
275
263
  <div class="header-right">
276
264
  <div class="base-url-wrap"><label>HOST</label><input id="base-url" type="text" value=""></div>
277
265
  <div class="jwt-wrap"><label>BEARER AUTH</label><input id="jwt-input" type="text" placeholder="Token value..."></div>
@@ -291,19 +279,24 @@ const UI = {
291
279
  </html>`,
292
280
 
293
281
  // ── Login page ──────────────────────────────────────────────────────────────
294
- login: () => `<!DOCTYPE html>
282
+ // appName: shown as the page title and card heading
283
+ // loginPath: the API endpoint to POST credentials to (default: /api/v1/auth/login)
284
+ // redirectTo: where to send the user after a successful login (default: /dashboard)
285
+ login: (appName = 'App', loginPath = '/api/v1/auth/login', redirectTo = '/dashboard') => `<!DOCTYPE html>
295
286
  <html>
296
287
  <head>
297
- <title>Sign In</title>
288
+ <title>${appName} — Sign In</title>
298
289
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
299
290
  <style>
300
291
  body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
301
292
  .card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
302
- h2 { color:#e8a838; margin:0 0 24px; text-align:center; }
293
+ h2 { color:#e8a838; margin:0 0 8px; text-align:center; }
294
+ .subtitle { color:#9a8c78; font-size:13px; text-align:center; margin:0 0 28px; }
303
295
  .field { margin-bottom:20px; }
304
296
  label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
305
- input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
306
- button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; }
297
+ input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; font-size:14px; }
298
+ input::placeholder { color:#5a5040; }
299
+ button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; font-size:14px; }
307
300
  .footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
308
301
  a { color:#e8a838; text-decoration:none; }
309
302
  #err { color:#d45c3c; font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
@@ -311,48 +304,61 @@ const UI = {
311
304
  </head>
312
305
  <body>
313
306
  <div class="card">
314
- <h2>Sign In</h2>
307
+ <h2>${appName}</h2>
308
+ <p class="subtitle">Sign in to your account</p>
315
309
  <div id="err"></div>
316
- <div class="field"><label>Email</label><input type="email" id="email" value="admin@bakery.com"></div>
317
- <div class="field"><label>Password</label><input type="password" id="password" value="password123"></div>
318
- <button onclick="handleLogin()">Log In</button>
310
+ <div class="field"><label>Email</label><input type="email" id="email" placeholder="you@example.com" autocomplete="email"></div>
311
+ <div class="field"><label>Password</label><input type="password" id="password" placeholder="Your password" autocomplete="current-password"></div>
312
+ <button onclick="handleLogin()">Sign In</button>
319
313
  <div class="footer">Need an account? <a href="/signup">Sign up</a></div>
320
314
  </div>
321
315
  <script>
322
316
  async function handleLogin() {
323
- const email = document.getElementById('email').value;
317
+ const email = document.getElementById('email').value.trim();
324
318
  const password = document.getElementById('password').value;
325
- const res = await fetch('/api/v1/auth/login', {
326
- method: 'POST',
327
- headers: { 'Content-Type': 'application/json' },
328
- body: JSON.stringify({ email, password })
329
- });
330
- const data = await res.json();
331
- if (res.ok && data.token) {
332
- localStorage.setItem('__auth_token__', data.token);
333
- window.location.href = '/dashboard';
334
- } else {
335
- document.getElementById('err').textContent = data.error || 'Login failed';
319
+ const errDiv = document.getElementById('err');
320
+ errDiv.textContent = '';
321
+ if (!email || !password) { errDiv.textContent = 'Please fill in all fields.'; return; }
322
+ try {
323
+ const res = await fetch('${loginPath}', {
324
+ method: 'POST',
325
+ headers: { 'Content-Type': 'application/json' },
326
+ body: JSON.stringify({ email, password })
327
+ });
328
+ const data = await res.json();
329
+ if (res.ok && data.token) {
330
+ localStorage.setItem('__auth_token__', data.token);
331
+ window.location.href = '${redirectTo}';
332
+ } else {
333
+ errDiv.textContent = data.error || data.message || 'Login failed.';
334
+ }
335
+ } catch (e) {
336
+ errDiv.textContent = 'Network error — could not reach the server.';
336
337
  }
337
338
  }
339
+ document.addEventListener('keydown', e => { if (e.key === 'Enter') handleLogin(); });
338
340
  </script>
339
341
  </body>
340
342
  </html>`,
341
343
 
342
344
  // ── Signup page ─────────────────────────────────────────────────────────────
343
- signup: () => `<!DOCTYPE html>
345
+ // appName: shown as the page title and card heading
346
+ // registerPath: the API endpoint to POST to (default: /api/v1/auth/register)
347
+ signup: (appName = 'App', registerPath = '/api/v1/auth/register') => `<!DOCTYPE html>
344
348
  <html>
345
349
  <head>
346
- <title>Create Account</title>
350
+ <title>${appName} — Create Account</title>
347
351
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
348
352
  <style>
349
353
  body { background:#0e0c09; color:#f0e8d8; font-family:'DM Sans',sans-serif; display:flex; justify-content:center; align-items:center; height:100vh; margin:0; }
350
354
  .card { background:#181510; border:1px solid #3a3020; padding:40px; border-radius:12px; width:340px; }
351
- h2 { color:#e8a838; margin:0 0 24px; text-align:center; }
355
+ h2 { color:#e8a838; margin:0 0 8px; text-align:center; }
356
+ .subtitle { color:#9a8c78; font-size:13px; text-align:center; margin:0 0 28px; }
352
357
  .field { margin-bottom:20px; }
353
358
  label { display:block; font-size:11px; color:#9a8c78; text-transform:uppercase; margin-bottom:8px; }
354
- input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; }
355
- button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; }
359
+ input { background:#221d14; border:1px solid #3a3020; color:#f0e8d8; padding:12px; width:100%; box-sizing:border-box; border-radius:6px; outline:none; font-size:14px; }
360
+ input::placeholder { color:#5a5040; }
361
+ button { background:#e8a838; color:#0e0c09; border:none; padding:12px; width:100%; border-radius:6px; font-weight:600; cursor:pointer; margin-top:10px; font-size:14px; }
356
362
  .footer { text-align:center; margin-top:20px; font-size:13px; color:#9a8c78; }
357
363
  a { color:#e8a838; text-decoration:none; }
358
364
  #msg { font-size:13px; margin-bottom:15px; text-align:center; min-height:18px; }
@@ -360,44 +366,55 @@ async function handleLogin() {
360
366
  </head>
361
367
  <body>
362
368
  <div class="card">
363
- <h2>Sign Up</h2>
369
+ <h2>${appName}</h2>
370
+ <p class="subtitle">Create your account</p>
364
371
  <div id="msg"></div>
365
- <div class="field"><label>Username</label><input type="text" id="username" placeholder="Username"></div>
366
- <div class="field"><label>Email Address</label><input type="email" id="email" placeholder="Email"></div>
367
- <div class="field"><label>Password</label><input type="password" id="password"></div>
368
- <button onclick="handleRegister()">Register Account</button>
369
- <div class="footer">Have an account? <a href="/login">Sign In</a></div>
372
+ <div class="field"><label>Username</label><input type="text" id="username" placeholder="Choose a username" autocomplete="username"></div>
373
+ <div class="field"><label>Email Address</label><input type="email" id="email" placeholder="you@example.com" autocomplete="email"></div>
374
+ <div class="field"><label>Password</label><input type="password" id="password" placeholder="Choose a password" autocomplete="new-password"></div>
375
+ <button onclick="handleRegister()">Create Account</button>
376
+ <div class="footer">Have an account? <a href="/login">Sign in</a></div>
370
377
  </div>
371
378
  <script>
372
379
  async function handleRegister() {
373
- const username = document.getElementById('username').value;
374
- const email = document.getElementById('email').value;
380
+ const username = document.getElementById('username').value.trim();
381
+ const email = document.getElementById('email').value.trim();
375
382
  const password = document.getElementById('password').value;
376
383
  const msgDiv = document.getElementById('msg');
377
- const res = await fetch('/api/v1/auth/register', {
378
- method: 'POST',
379
- headers: { 'Content-Type': 'application/json' },
380
- body: JSON.stringify({ username, email, password })
381
- });
382
- const data = await res.json();
383
- if (res.ok) {
384
- msgDiv.style.color = '#6ba05a';
385
- msgDiv.textContent = 'Registration complete! Redirecting…';
386
- setTimeout(() => window.location.href = '/login', 1200);
387
- } else {
384
+ msgDiv.textContent = '';
385
+ if (!username || !email || !password) { msgDiv.style.color = '#d45c3c'; msgDiv.textContent = 'Please fill in all fields.'; return; }
386
+ try {
387
+ const res = await fetch('${registerPath}', {
388
+ method: 'POST',
389
+ headers: { 'Content-Type': 'application/json' },
390
+ body: JSON.stringify({ username, email, password })
391
+ });
392
+ const data = await res.json();
393
+ if (res.ok) {
394
+ msgDiv.style.color = '#6ba05a';
395
+ msgDiv.textContent = 'Account created! Redirecting to login…';
396
+ setTimeout(() => window.location.href = '/login', 1200);
397
+ } else {
398
+ msgDiv.style.color = '#d45c3c';
399
+ msgDiv.textContent = data.error || data.message || 'Registration failed.';
400
+ }
401
+ } catch (e) {
388
402
  msgDiv.style.color = '#d45c3c';
389
- msgDiv.textContent = data.error || 'Registration failed';
403
+ msgDiv.textContent = 'Network error could not reach the server.';
390
404
  }
391
405
  }
406
+ document.addEventListener('keydown', e => { if (e.key === 'Enter') handleRegister(); });
392
407
  </script>
393
408
  </body>
394
409
  </html>`,
395
410
 
396
411
  // ── Dashboard ────────────────────────────────────────────────────────────────
397
- dashboard: (endpointsJsonB64) => `<!DOCTYPE html>
412
+ // endpointsJsonB64: base64-encoded endpoints payload from encodePayload()
413
+ // appName: shown in the page title and header h1
414
+ dashboard: (endpointsJsonB64, appName = 'App') => `<!DOCTYPE html>
398
415
  <html>
399
416
  <head>
400
- <title>Dynamic Admin Dashboard</title>
417
+ <title>${appName} Admin Dashboard</title>
401
418
  <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;700&display=swap" rel="stylesheet">
402
419
  <style>
403
420
  :root { --bg:#0e0c09; --surface:#181510; --surface2:#221d14; --border:#3a3020; --accent:#e8a838; --text:#f0e8d8; --text-dim:#9a8c78; --red:#d45c3c; --green:#6ba05a; }
@@ -429,19 +446,18 @@ async function handleRegister() {
429
446
  </style>
430
447
  </head>
431
448
  <body>
432
- <!-- Replace the old hidden div with this -->
433
449
  <script id="__monkey_data__" type="text/plain">${endpointsJsonB64}</script>
434
450
 
435
451
  <header>
436
- <h1>Universal Management Dashboard</h1>
452
+ <h1>${appName}</h1>
437
453
  <div class="nav-links">
438
- <a href="/api/tester" target="_blank">🛠 Open Tester Sandbox</a>
454
+ <a href="/api/tester" target="_blank">🛠 API Tester</a>
439
455
  <button class="logout-btn" onclick="localStorage.removeItem('__auth_token__'); window.location.href='/login'">Log Out</button>
440
456
  </div>
441
457
  </header>
442
458
 
443
459
  <div class="selector-banner">
444
- <label style="margin:0;">Target Data Resource Collection:</label>
460
+ <label style="margin:0;">Resource collection:</label>
445
461
  <select id="route-selector" onchange="switchCollection()"></select>
446
462
  </div>
447
463
 
@@ -449,8 +465,8 @@ async function handleRegister() {
449
465
  <div class="panel">
450
466
  <h3 id="form-title">Add Entry</h3>
451
467
  <div id="dynamic-fields-container"></div>
452
- <button class="btn" id="btn-submit" onclick="submitDataForm()">Execute Submission</button>
453
- <button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel Action</button>
468
+ <button class="btn" id="btn-submit" onclick="submitDataForm()">Submit</button>
469
+ <button class="btn btn-sm btn-cancel" id="btn-cancel" style="display:none; margin-top:10px;" onclick="resetDataForm()">Cancel</button>
454
470
  </div>
455
471
  <div class="table-wrap">
456
472
  <table id="dynamic-table">
@@ -461,22 +477,14 @@ async function handleRegister() {
461
477
  </div>
462
478
 
463
479
  <script>
464
- // ── Unicode-safe decode ──────────────────────────────────────────────────────
465
- // Replace the old _decode function inside UI.dashboard with this:
466
480
  function _decode(b64) {
467
481
  const binStr = atob(b64.replace(/\s+/g, ''));
468
482
  const bytes = new Uint8Array(binStr.length);
469
- for (let i = 0; i < binStr.length; i++) {
470
- bytes[i] = binStr.charCodeAt(i);
471
- }
483
+ for (let i = 0; i < binStr.length; i++) bytes[i] = binStr.charCodeAt(i);
472
484
  return JSON.parse(new TextDecoder().decode(bytes));
473
485
  }
474
486
 
475
- // BEFORE (Broken)
476
- // AFTER (Safe from HTML parsing artifacts)
477
- const ENDPOINTS = _decode(
478
- document.getElementById('__monkey_data__').textContent.trim()
479
- );
487
+ const ENDPOINTS = _decode(document.getElementById('__monkey_data__').textContent.trim());
480
488
 
481
489
  const token = localStorage.getItem('__auth_token__');
482
490
  if (!token) window.location.href = '/login';
@@ -511,7 +519,9 @@ function resolveRoutes() {
511
519
  if (dynamicCollections[path].get) {
512
520
  const opt = document.createElement('option');
513
521
  opt.value = path;
514
- opt.textContent = path + ' (Dynamic Dataset)';
522
+ // Derive a readable label from the path: /api/v1/students Students
523
+ const label = path.split('/').filter(Boolean).pop();
524
+ opt.textContent = label.charAt(0).toUpperCase() + label.slice(1);
515
525
  selector.appendChild(opt);
516
526
  }
517
527
  });
@@ -533,7 +543,7 @@ function renderFormFields() {
533
543
  const fields = dynamicCollections[activeCollectionPath].modelFields;
534
544
 
535
545
  if (!fields || fields.length === 0) {
536
- container.innerHTML = '<p style="font-size:12px;color:var(--text-dim);">No input properties found.</p>';
546
+ container.innerHTML = '<p style="font-size:12px;color:var(--text-dim);">No writable fields detected for this endpoint.</p>';
537
547
  return;
538
548
  }
539
549
 
@@ -551,7 +561,7 @@ async function fetchData() {
551
561
  const head = document.getElementById('table-head');
552
562
  const body = document.getElementById('table-body');
553
563
  head.innerHTML = '';
554
- body.innerHTML = '<tr><td style="padding:20px;color:var(--text-dim)">Loading layout schemas…</td></tr>';
564
+ body.innerHTML = '<tr><td style="padding:20px;color:var(--text-dim)">Loading…</td></tr>';
555
565
 
556
566
  try {
557
567
  const res = await fetch(activeCollectionPath, { headers: { 'Authorization': 'Bearer ' + token } });
@@ -566,7 +576,7 @@ async function fetchData() {
566
576
  }
567
577
 
568
578
  if (!list || list.length === 0 || list[0] === null) {
569
- body.innerHTML = '<tr><td style="padding:30px;color:var(--text-dim)">No records found inside this live API container.</td></tr>';
579
+ body.innerHTML = '<tr><td style="padding:30px;color:var(--text-dim)">No records found.</td></tr>';
570
580
  return;
571
581
  }
572
582
 
@@ -592,7 +602,7 @@ async function fetchData() {
592
602
  });
593
603
 
594
604
  } catch (e) {
595
- body.innerHTML = '<tr><td style="padding:20px;color:var(--red)">Failed processing remote endpoint structure data.</td></tr>';
605
+ body.innerHTML = '<tr><td style="padding:20px;color:var(--red)">Failed to load data from this endpoint.</td></tr>';
596
606
  console.error(e);
597
607
  }
598
608
  }
@@ -622,24 +632,23 @@ async function submitDataForm() {
622
632
  });
623
633
 
624
634
  if (res.ok) { resetDataForm(); fetchData(); }
625
- else { alert('Submission declined.'); }
635
+ else { alert('Request failed — check the console for details.'); }
626
636
  }
627
637
 
628
638
  async function deleteRow(id) {
629
- if (!confirm('Execute atomic entry removal?')) return;
639
+ if (!confirm('Delete this record?')) return;
630
640
  const delTemplate = dynamicCollections[activeCollectionPath].del;
631
641
  const paramName = delTemplate.split('/:')[1];
632
642
  const url = delTemplate.replace(\`:\${paramName}\`, id);
633
643
  const res = await fetch(url, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } });
634
- if (res.ok) fetchData(); else alert('Delete request blocked.');
644
+ if (res.ok) fetchData(); else alert('Delete failed.');
635
645
  }
636
646
 
637
647
  function startRowEdit(id, encodedJson) {
638
648
  activeEditId = id;
639
- // Unicode-safe decode for row data encoded above with btoa(unescape(encodeURIComponent(...)))
640
649
  const data = JSON.parse(decodeURIComponent(escape(atob(encodedJson))));
641
- document.getElementById('form-title').textContent = \`Update Row #\${id}\`;
642
- document.getElementById('btn-submit').textContent = 'Commit Changes';
650
+ document.getElementById('form-title').textContent = \`Edit record #\${id}\`;
651
+ document.getElementById('btn-submit').textContent = 'Save Changes';
643
652
  document.getElementById('btn-cancel').style.display = 'block';
644
653
  const fields = dynamicCollections[activeCollectionPath].modelFields;
645
654
  fields.forEach(f => {
@@ -651,7 +660,7 @@ function startRowEdit(id, encodedJson) {
651
660
  function resetDataForm() {
652
661
  activeEditId = null;
653
662
  document.getElementById('form-title').textContent = 'Add Entry';
654
- document.getElementById('btn-submit').textContent = 'Execute Submission';
663
+ document.getElementById('btn-submit').textContent = 'Submit';
655
664
  document.getElementById('btn-cancel').style.display = 'none';
656
665
  (dynamicCollections[activeCollectionPath]?.modelFields || []).forEach(f => {
657
666
  const el = document.getElementById(\`input-\${f.name}\`);
package/monkey.js CHANGED
@@ -1,75 +1,129 @@
1
1
  'use strict';
2
2
 
3
- import { UI } from './htmlTemplate.js';
3
+ import { UI, encodePayload } from './htmlTemplate.js';
4
4
 
5
5
  function inferType(name) {
6
6
  const n = name.toLowerCase();
7
- if (n.includes('email')) return 'email';
8
- if (n.includes('password') || n.includes('pass')) return 'password';
9
- if (n.includes('date')) return 'date';
10
- if (n.includes('age') || n.includes('price') || n.includes('quantity') || n.includes('stock') || n === 'id' || n.endsWith('id')) return 'number';
7
+ if (n.includes('email')) return 'email';
8
+ if (n.includes('password') || n.includes('pass')) return 'password';
9
+ if (n.includes('date') || n.includes('birth')) return 'date';
10
+ if (n.includes('phone') || n.includes('tel')) return 'tel';
11
+ if (n.includes('url') || n.includes('website') || n.includes('link')) return 'url';
12
+ if (
13
+ n.includes('age') || n.includes('price') || n.includes('amount') ||
14
+ n.includes('count') || n.includes('qty') || n.includes('quantity') ||
15
+ n.includes('stock') || n.includes('salary') || n.includes('total') ||
16
+ (n === 'id') || n.endsWith('_id') || n.endsWith('Id')
17
+ ) return 'number';
11
18
  return 'text';
12
19
  }
13
20
 
14
21
  function buildField(name) {
15
- return { name, label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'), type: inferType(name), placeholder: `Enter ${name}` };
22
+ return {
23
+ name,
24
+ label: name.charAt(0).toUpperCase() + name.slice(1).replace(/([A-Z])/g, ' $1'),
25
+ type: inferType(name),
26
+ placeholder: 'Enter ' + name
27
+ };
16
28
  }
17
29
 
18
30
  function extractBodyFields(handler) {
19
31
  try {
20
32
  const source = handler.toString();
21
33
  if (!source || source.includes('[native code]')) return [];
34
+
22
35
  const seen = new Map();
23
36
  const destructRe = /(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=\s*req\.body/g;
24
37
  let m;
25
38
  while ((m = destructRe.exec(source)) !== null) {
26
- m[1].split(',').forEach(part => {
39
+ m[1].split(',').forEach(function(part) {
27
40
  const name = part.split(':')[0].split('=')[0].trim();
28
- if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) seen.set(name, buildField(name));
41
+ if (name && /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) && !seen.has(name)) {
42
+ seen.set(name, buildField(name));
43
+ }
29
44
  });
30
45
  }
31
- const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
46
+
47
+ const accessRe = /req\.body\.([a-zA-Z_$][a-zA-Z0-9_$]*)|req\.body\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
32
48
  while ((m = accessRe.exec(source)) !== null) {
33
- const name = m[1]; if (name && !seen.has(name)) seen.set(name, buildField(name));
49
+ const name = m[1] || m[2];
50
+ if (name && !seen.has(name)) seen.set(name, buildField(name));
34
51
  }
52
+
35
53
  return Array.from(seen.values());
36
- } catch { return []; }
54
+ } catch {
55
+ return [];
56
+ }
57
+ }
58
+
59
+ function fallbackFields(path) {
60
+ const p = path.toLowerCase();
61
+ if (p.includes('login') || p.includes('signin') || p.includes('auth/login')) return ['email', 'password'].map(buildField);
62
+ if (p.includes('register') || p.includes('signup') || p.includes('auth/register')) return ['username', 'email', 'password'].map(buildField);
63
+ if (p.includes('user')) return ['username', 'email', 'password'].map(buildField);
64
+ if (p.includes('product')) return ['name', 'price', 'stock'].map(buildField);
65
+ if (p.includes('order')) return ['productId', 'quantity', 'address'].map(buildField);
66
+ return [];
37
67
  }
38
68
 
39
69
  function extractRouterPrefix(layer) {
40
70
  if (!layer.regexp) return '';
41
- const m = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/].reduce((acc, re) => acc || re.exec(layer.regexp.source), null);
42
- return m && m[1] ? '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '') : '';
71
+ const src = layer.regexp.source;
72
+ const patterns = [/^\^\\\/([^\\?$]+)/, /^\^\\\/([a-zA-Z0-9_/-]+)/];
73
+ for (var i = 0; i < patterns.length; i++) {
74
+ const m = patterns[i].exec(src);
75
+ if (m && m[1]) return '/' + m[1].replace(/\\\//g, '/').replace(/\\/g, '');
76
+ }
77
+ return '';
43
78
  }
44
79
 
45
- function parseStack(stack, detectedEndpoints, prefix = '') {
80
+ function parseStack(stack, detectedEndpoints, prefix) {
46
81
  if (!Array.isArray(stack)) return;
47
- for (const layer of stack) {
82
+
83
+ for (var i = 0; i < stack.length; i++) {
84
+ var layer = stack[i];
48
85
  if (layer.route) {
49
- const rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
50
- const fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
86
+ var rawPath = typeof layer.route.path === 'string' ? layer.route.path : (layer.route.path ? String(layer.route.path) : '');
87
+ var fullPath = (prefix + rawPath).replace(/\/+/g, '/') || '/';
88
+
51
89
  if (fullPath.startsWith('/api/tester')) continue;
52
90
 
53
- const methods = Object.keys(layer.route.methods || {});
54
- for (const method of methods) {
55
- const httpMethod = method.toUpperCase();
56
- const key = `${httpMethod}::${fullPath}`;
57
- const pathParams = [];
58
- const paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
59
- let pm;
60
- while ((pm = paramRe.exec(fullPath)) !== null) {
61
- pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
62
- }
91
+ var methods = Object.keys(layer.route.methods || {});
92
+ var pathParams = [];
93
+ var paramRe = /:([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
94
+ var pm;
95
+ while ((pm = paramRe.exec(fullPath)) !== null) {
96
+ pathParams.push({ name: pm[1], label: pm[1].charAt(0).toUpperCase() + pm[1].slice(1), placeholder: 'value' });
97
+ }
63
98
 
64
- let bodyFields = [];
65
- if (['POST', 'PUT', 'PATCH'].includes(httpMethod)) {
66
- const handlers = (layer.route.stack || []).map(sl => sl.handle).filter(Boolean);
67
- for (const handler of handlers) bodyFields.push(...extractBodyFields(handler));
68
- const seen = new Map();
69
- bodyFields = bodyFields.filter(f => !seen.has(f.name) && seen.set(f.name, true));
99
+ var bodyFields = [];
100
+ if (methods.some(function(m) { return ['POST', 'PUT', 'PATCH'].indexOf(m.toUpperCase()) !== -1; })) {
101
+ var handlers = (layer.route.stack || []).map(function(sl) { return sl.handle; }).filter(Boolean);
102
+ for (var j = 0; j < handlers.length; j++) {
103
+ bodyFields.push.apply(bodyFields, extractBodyFields(handlers[j]));
70
104
  }
105
+ var seen2 = new Map();
106
+ bodyFields = bodyFields.filter(function(f) {
107
+ if (seen2.has(f.name)) return false;
108
+ seen2.set(f.name, true);
109
+ return true;
110
+ });
111
+ if (bodyFields.length === 0 && pathParams.length === 0) {
112
+ bodyFields = fallbackFields(fullPath);
113
+ }
114
+ }
71
115
 
72
- detectedEndpoints[key] = { method: httpMethod, path: fullPath, title: `${httpMethod} ${fullPath}`, desc: `Auto-discovered endpoint - ${fullPath}`, params: pathParams, fields: bodyFields };
116
+ for (var k = 0; k < methods.length; k++) {
117
+ var httpMethod = methods[k].toUpperCase();
118
+ var key = httpMethod + '::' + fullPath;
119
+ detectedEndpoints[key] = {
120
+ method: httpMethod,
121
+ path: fullPath,
122
+ title: httpMethod + ' ' + fullPath,
123
+ desc: 'Auto-discovered endpoint — ' + fullPath,
124
+ params: pathParams,
125
+ fields: bodyFields
126
+ };
73
127
  }
74
128
  } else if (layer.name === 'router' && layer.handle && layer.handle.stack) {
75
129
  parseStack(layer.handle.stack, detectedEndpoints, prefix + extractRouterPrefix(layer));
@@ -79,26 +133,40 @@ function parseStack(stack, detectedEndpoints, prefix = '') {
79
133
 
80
134
  export function endtesterExpress() {
81
135
  return function monkeyTesterMiddleware(req, res, next) {
82
- const route = req.path.toLowerCase().replace(/\/$/, '');
83
-
84
- // 1. Serve Standalone Frontend Templates Interceptions
85
- if (route === '/login') return res.send(UI.login());
86
- if (route === '/signup') return res.send(UI.signup());
87
- if (route === '/dashboard') return res.send(UI.dashboard());
88
-
89
- // 2. Serve the main Tester UI environment
90
- if (route === '/api/tester') {
91
- const app = req.app;
92
- const detectedEndpoints = {};
93
- const rootStack = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
94
- parseStack(rootStack, detectedEndpoints);
95
- const b64 = Buffer.from(JSON.stringify(detectedEndpoints)).toString('base64');
136
+ var normalized = req.path.replace(/\/+$/, '');
137
+
138
+ // Tester sandbox must be checked before static pages so query strings don't collide
139
+ if (normalized.startsWith('/api/tester')) {
140
+ var app = req.app;
141
+ var eps = app.__monkey_endpoints_cache__ || {};
142
+ if (!app.__monkey_endpoints_cache__) {
143
+ eps = {};
144
+ var raw = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
145
+ parseStack(raw, eps);
146
+ app.__monkey_endpoints_cache__ = eps;
147
+ }
148
+ var b64 = encodePayload(eps);
96
149
  res.setHeader('Content-Type', 'text/html; charset=utf-8');
97
150
  return res.send(UI.tester(b64));
98
151
  }
99
152
 
153
+ // Standalone HTML pages — resolved dynamically against discovered routes
154
+ if (normalized === '/login') return res.send(UI.login());
155
+ if (normalized === '/signup') return res.send(UI.signup());
156
+ if (normalized === '/dashboard') {
157
+ if (!app.__monkey_endpoints_cache__) {
158
+ var eps2 = {};
159
+ var raw2 = (app._router && app._router.stack) || (app.router && app.router.stack) || [];
160
+ parseStack(raw2, eps2);
161
+ app.__monkey_endpoints_cache__ = eps2;
162
+ }
163
+ var b64dash = encodePayload(app.__monkey_endpoints_cache__);
164
+ res.setHeader('Content-Type', 'text/html; charset=utf-8');
165
+ return res.send(UI.dashboard(b64dash));
166
+ }
167
+
100
168
  next();
101
169
  };
102
170
  }
103
171
 
104
- export default { endtesterExpress };
172
+ export default { endtesterExpress };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aimeloic/monkey-tester",
3
- "version": "5.0.3",
3
+ "version": "5.0.5",
4
4
  "description": "Auto route scanning visual runner dashboard.",
5
5
  "main": "index.js",
6
6
  "type": "module",