sqlite-hub 0.16.0 → 0.17.2

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 (53) hide show
  1. package/README.md +106 -19
  2. package/bin/sqlite-hub.js +1041 -224
  3. package/frontend/index.html +1 -0
  4. package/frontend/js/api.js +34 -0
  5. package/frontend/js/app.js +481 -32
  6. package/frontend/js/components/modal.js +270 -50
  7. package/frontend/js/components/pageHeader.js +14 -16
  8. package/frontend/js/components/queryHistoryPanel.js +126 -131
  9. package/frontend/js/components/queryResults.js +18 -1
  10. package/frontend/js/components/rowEditorPanel.js +42 -34
  11. package/frontend/js/components/sidebar.js +1 -0
  12. package/frontend/js/router.js +8 -0
  13. package/frontend/js/store.js +655 -2
  14. package/frontend/js/utils/jsonPreview.js +31 -0
  15. package/frontend/js/utils/markdownDocuments.js +248 -0
  16. package/frontend/js/utils/rowEditorValues.js +41 -0
  17. package/frontend/js/utils/tableScrollState.js +39 -0
  18. package/frontend/js/views/charts.js +8 -0
  19. package/frontend/js/views/data.js +4 -1
  20. package/frontend/js/views/documents.js +300 -0
  21. package/frontend/js/views/editor.js +2 -0
  22. package/frontend/js/views/settings.js +39 -3
  23. package/frontend/styles/components.css +19 -0
  24. package/frontend/styles/tailwind.generated.css +1 -1
  25. package/frontend/styles/views.css +422 -0
  26. package/package.json +2 -1
  27. package/server/middleware/localRequestSecurity.js +84 -0
  28. package/server/routes/connections.js +18 -1
  29. package/server/routes/documents.js +163 -0
  30. package/server/routes/settings.js +22 -6
  31. package/server/server.js +18 -1
  32. package/server/services/nativeFileDialogService.js +168 -0
  33. package/server/services/sqlite/dataBrowserService.js +0 -4
  34. package/server/services/sqlite/exportService.js +5 -1
  35. package/server/services/sqlite/sqlExecutor.js +51 -2
  36. package/server/services/storage/appStateStore.js +313 -0
  37. package/server/utils/sqliteTypes.js +17 -8
  38. package/tests/cli-args.test.js +100 -0
  39. package/tests/connections-file-dialog-route.test.js +46 -0
  40. package/tests/copy-column-modal.test.js +97 -0
  41. package/tests/database-documents.test.js +85 -0
  42. package/tests/export-blob.test.js +46 -0
  43. package/tests/json-preview.test.js +49 -0
  44. package/tests/local-request-security.test.js +85 -0
  45. package/tests/markdown-documents.test.js +79 -0
  46. package/tests/native-file-dialog.test.js +78 -0
  47. package/tests/query-results-truncation.test.js +20 -0
  48. package/tests/row-editor-null-values.test.js +131 -0
  49. package/tests/settings-metadata.test.js +16 -0
  50. package/tests/settings-view.test.js +32 -0
  51. package/tests/sql-identifier-safety.test.js +9 -3
  52. package/tests/sql-result-limit.test.js +66 -0
  53. package/tests/table-scroll-state.test.js +70 -0
@@ -218,6 +218,428 @@
218
218
  min-height: 0;
219
219
  }
220
220
 
221
+ .documents-view-surface {
222
+ height: 100%;
223
+ min-height: 0;
224
+ overflow: hidden;
225
+ }
226
+
227
+ .documents-view-frame {
228
+ display: flex;
229
+ flex-direction: column;
230
+ height: 100%;
231
+ min-height: 0;
232
+ overflow: hidden;
233
+ }
234
+
235
+ .documents-view {
236
+ background: var(--color-surface);
237
+ border: 1px solid var(--border-medium);
238
+ display: flex;
239
+ flex: 1;
240
+ height: 100%;
241
+ min-height: 0;
242
+ overflow: hidden;
243
+ }
244
+
245
+ .documents-view__sidebar {
246
+ background: var(--color-surface-low);
247
+ border-right: 1px solid var(--border-medium);
248
+ display: flex;
249
+ flex: 0 0 clamp(17rem, 23vw, 21rem);
250
+ flex-direction: column;
251
+ min-height: 100%;
252
+ overflow: hidden;
253
+ width: clamp(17rem, 23vw, 21rem);
254
+ }
255
+
256
+ .documents-view__sidebar-header {
257
+ align-items: center;
258
+ border-bottom: 1px solid var(--border-medium);
259
+ display: flex;
260
+ justify-content: space-between;
261
+ padding: var(--spacing-5);
262
+ }
263
+
264
+ .documents-view__sidebar-body {
265
+ display: flex;
266
+ flex: 1;
267
+ flex-direction: column;
268
+ gap: var(--spacing-2);
269
+ min-height: 0;
270
+ overflow: hidden;
271
+ padding: var(--spacing-3);
272
+ }
273
+
274
+ .documents-list-item {
275
+ align-items: flex-start;
276
+ background: var(--color-surface-lowest);
277
+ border: 1px solid rgb(var(--rgb-outline) / 0.12);
278
+ color: var(--color-on-surface);
279
+ display: flex;
280
+ gap: var(--spacing-3);
281
+ min-width: 0;
282
+ padding: var(--spacing-3);
283
+ text-align: left;
284
+ transition:
285
+ background-color var(--transition-fast),
286
+ border-color var(--transition-fast),
287
+ color var(--transition-fast);
288
+ }
289
+
290
+ .documents-list-item:hover,
291
+ .documents-list-item.is-selected {
292
+ background: var(--color-surface-container);
293
+ border-color: var(--primary-alpha-24);
294
+ }
295
+
296
+ .documents-list-item.is-selected {
297
+ box-shadow: inset 2px 0 0 var(--color-primary-container);
298
+ }
299
+
300
+ .documents-list-item__icon {
301
+ color: var(--color-primary-container);
302
+ font-size: 1.1rem;
303
+ line-height: 1.25;
304
+ }
305
+
306
+ .documents-list-item__body {
307
+ display: block;
308
+ min-width: 0;
309
+ }
310
+
311
+ .documents-list-item__title {
312
+ display: block;
313
+ font-family: var(--font-family-mono);
314
+ font-size: 0.75rem;
315
+ font-weight: 700;
316
+ overflow: hidden;
317
+ text-overflow: ellipsis;
318
+ white-space: nowrap;
319
+ }
320
+
321
+ .documents-list-item__meta,
322
+ .documents-list-empty {
323
+ color: rgb(var(--rgb-on-surface-variant) / 0.55);
324
+ display: block;
325
+ font-family: var(--font-family-mono);
326
+ font-size: 0.625rem;
327
+ margin-top: 0.25rem;
328
+ text-transform: uppercase;
329
+ }
330
+
331
+ .documents-list-empty {
332
+ padding: var(--spacing-4);
333
+ }
334
+
335
+ .documents-view__detail {
336
+ display: flex;
337
+ flex: 1;
338
+ flex-direction: column;
339
+ min-height: 0;
340
+ min-width: 0;
341
+ overflow: hidden;
342
+ }
343
+
344
+ .documents-toolbar {
345
+ align-items: center;
346
+ border-bottom: 1px solid var(--border-medium);
347
+ display: flex;
348
+ flex-wrap: wrap;
349
+ gap: var(--spacing-4);
350
+ justify-content: space-between;
351
+ padding: var(--spacing-4);
352
+ }
353
+
354
+ .documents-filename-field {
355
+ display: flex;
356
+ flex-direction: column;
357
+ padding: var(--spacing-4);
358
+ padding-bottom: 0;
359
+ max-width: 32rem;
360
+ min-width: min(100%, 14rem);
361
+ }
362
+
363
+ .documents-filename-field span {
364
+ color: rgb(var(--rgb-on-surface-variant) / 0.6);
365
+ font-family: var(--font-family-mono);
366
+ font-size: 0.625rem;
367
+ font-weight: 700;
368
+ letter-spacing: 0.18em;
369
+ text-transform: uppercase;
370
+ }
371
+
372
+ .documents-filename-field .control-input {
373
+ background: var(--color-surface-lowest);
374
+ border: 1px solid rgb(var(--rgb-outline) / 0.18);
375
+ color: var(--color-on-surface);
376
+ height: var(--control-height);
377
+ padding: 0 var(--spacing-3);
378
+ }
379
+
380
+ .documents-filename-field .control-input:focus {
381
+ border-color: var(--color-primary-container);
382
+ }
383
+
384
+ .documents-filename-field .control-input:disabled {
385
+ color: rgb(var(--rgb-on-surface-variant) / 0.72);
386
+ opacity: 1;
387
+ }
388
+
389
+ .documents-toolbar__actions {
390
+ display: flex;
391
+ flex-wrap: wrap;
392
+ gap: var(--spacing-2);
393
+ justify-content: flex-end;
394
+ }
395
+
396
+ .documents-workspace {
397
+ display: grid;
398
+ flex: 1;
399
+ grid-template-columns: 1fr;
400
+ min-height: 0;
401
+ overflow: hidden;
402
+ }
403
+
404
+ .documents-workspace--split {
405
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
406
+ }
407
+
408
+ .documents-pane {
409
+ display: flex;
410
+ flex-direction: column;
411
+ min-height: 0;
412
+ min-width: 0;
413
+ overflow: hidden;
414
+ }
415
+
416
+ .documents-pane + .documents-pane {
417
+ border-left: 1px solid var(--border-medium);
418
+ }
419
+
420
+ .documents-pane__header {
421
+ align-items: center;
422
+ background: var(--color-surface-low);
423
+ border-bottom: 1px solid var(--border-medium);
424
+ color: rgb(var(--rgb-on-surface-variant) / 0.62);
425
+ display: flex;
426
+ font-family: var(--font-family-mono);
427
+ font-size: 0.625rem;
428
+ font-weight: 700;
429
+ justify-content: space-between;
430
+ letter-spacing: 0.18em;
431
+ padding: var(--spacing-3) var(--spacing-4);
432
+ text-transform: uppercase;
433
+ }
434
+
435
+ .documents-editor-input {
436
+ background: var(--color-surface-lowest);
437
+ border: 0;
438
+ color: var(--color-on-surface);
439
+ flex: 1;
440
+ font-family: var(--font-family-mono);
441
+ font-size: 0.875rem;
442
+ height: 100%;
443
+ line-height: 1.7;
444
+ min-height: 0;
445
+ outline: none;
446
+ overflow-y: auto;
447
+ padding: var(--spacing-5);
448
+ resize: none;
449
+ }
450
+
451
+ .documents-editor-input:focus {
452
+ box-shadow: inset 0 0 0 1px var(--color-primary-container);
453
+ }
454
+
455
+ .document-markdown-preview {
456
+ background: var(--color-surface-lowest);
457
+ color: var(--color-on-surface);
458
+ flex: 1;
459
+ font-size: 0.9375rem;
460
+ line-height: 1.7;
461
+ min-height: 0;
462
+ overflow-y: auto;
463
+ padding: var(--spacing-6);
464
+ }
465
+
466
+ .document-markdown-preview > * + * {
467
+ margin-top: 1rem;
468
+ }
469
+
470
+ .document-markdown-preview h1,
471
+ .document-markdown-preview h2,
472
+ .document-markdown-preview h3 {
473
+ color: var(--color-primary-container);
474
+ font-family: var(--font-family-headline);
475
+ font-weight: 900;
476
+ letter-spacing: 0;
477
+ line-height: 1.1;
478
+ text-transform: uppercase;
479
+ }
480
+
481
+ .document-markdown-preview h1 {
482
+ font-size: 2.25rem;
483
+ }
484
+
485
+ .document-markdown-preview h2 {
486
+ font-size: 1.5rem;
487
+ }
488
+
489
+ .document-markdown-preview h3 {
490
+ font-size: 1.15rem;
491
+ }
492
+
493
+ .document-markdown-preview a {
494
+ color: var(--color-primary-container);
495
+ text-decoration: underline;
496
+ text-underline-offset: 0.18em;
497
+ }
498
+
499
+ .document-markdown-preview code {
500
+ background: var(--color-surface-container);
501
+ color: var(--color-primary-container);
502
+ font-family: var(--font-family-mono);
503
+ font-size: 0.85em;
504
+ padding: 0.1rem 0.25rem;
505
+ }
506
+
507
+ .document-markdown-preview pre {
508
+ background: var(--color-surface-low);
509
+ border: 1px solid rgb(var(--rgb-outline) / 0.12);
510
+ overflow-x: auto;
511
+ padding: var(--spacing-4);
512
+ }
513
+
514
+ .document-markdown-preview pre code {
515
+ background: transparent;
516
+ color: inherit;
517
+ padding: 0;
518
+ }
519
+
520
+ .document-markdown-preview blockquote {
521
+ border-left: 2px solid var(--color-primary-container);
522
+ color: rgb(var(--rgb-on-surface) / 0.72);
523
+ padding-left: var(--spacing-4);
524
+ }
525
+
526
+ .document-markdown-preview table {
527
+ border-collapse: collapse;
528
+ width: 100%;
529
+ }
530
+
531
+ .document-markdown-preview th,
532
+ .document-markdown-preview td {
533
+ border: 1px solid rgb(var(--rgb-outline) / 0.16);
534
+ padding: 0.5rem 0.75rem;
535
+ text-align: left;
536
+ }
537
+
538
+ .document-markdown-preview ul,
539
+ .document-markdown-preview ol {
540
+ padding-left: 1.5rem;
541
+ }
542
+
543
+ .document-markdown-preview ul {
544
+ list-style: disc outside;
545
+ }
546
+
547
+ .document-markdown-preview ol {
548
+ list-style: decimal outside;
549
+ }
550
+
551
+ .document-markdown-preview li + li {
552
+ margin-top: 0.35rem;
553
+ }
554
+
555
+ .document-markdown-task-list {
556
+ display: flex;
557
+ flex-direction: column;
558
+ gap: var(--spacing-2);
559
+ list-style: none;
560
+ padding-left: 0;
561
+ }
562
+
563
+ .document-markdown-task-label {
564
+ align-items: flex-start;
565
+ cursor: pointer;
566
+ display: flex;
567
+ gap: var(--spacing-3);
568
+ }
569
+
570
+ .document-markdown-task-checkbox {
571
+ accent-color: var(--color-primary-container);
572
+ margin-top: 0.35rem;
573
+ }
574
+
575
+ .document-markdown-task-item.is-checked .document-markdown-task-text {
576
+ color: rgb(var(--rgb-on-surface-variant) / 0.64);
577
+ text-decoration: line-through;
578
+ }
579
+
580
+ .document-markdown-empty,
581
+ .documents-empty-state {
582
+ color: rgb(var(--rgb-on-surface-variant) / 0.55);
583
+ }
584
+
585
+ .documents-empty-state {
586
+ align-items: center;
587
+ display: flex;
588
+ flex: 1;
589
+ flex-direction: column;
590
+ justify-content: center;
591
+ padding: var(--spacing-8);
592
+ text-align: center;
593
+ }
594
+
595
+ .documents-empty-state .material-symbols-outlined {
596
+ color: rgb(var(--rgb-on-surface-variant) / 0.28);
597
+ font-size: 3.5rem;
598
+ margin-bottom: var(--spacing-3);
599
+ }
600
+
601
+ .documents-error {
602
+ background: rgb(var(--rgb-error) / 0.08);
603
+ border-bottom: 1px solid rgb(var(--rgb-error) / 0.28);
604
+ color: var(--color-error);
605
+ font-size: 0.8125rem;
606
+ padding: var(--spacing-3) var(--spacing-4);
607
+ }
608
+
609
+ @media (max-width: 980px) {
610
+ .documents-view {
611
+ min-height: 0;
612
+ }
613
+
614
+ .documents-view,
615
+ .documents-workspace--split {
616
+ grid-template-columns: 1fr;
617
+ }
618
+
619
+ .documents-view {
620
+ flex-direction: column;
621
+ }
622
+
623
+ .documents-view__sidebar {
624
+ border-bottom: 1px solid var(--border-medium);
625
+ border-right: 0;
626
+ flex: 0 0 auto;
627
+ max-height: 18rem;
628
+ min-height: 18rem;
629
+ width: 100%;
630
+ }
631
+
632
+ .documents-pane + .documents-pane {
633
+ border-left: 0;
634
+ border-top: 1px solid var(--border-medium);
635
+ }
636
+
637
+ .documents-editor-input,
638
+ .document-markdown-preview {
639
+ min-height: 0;
640
+ }
641
+ }
642
+
221
643
  .structure-sidebar__section + .structure-sidebar__section {
222
644
  margin-top: 1.5rem;
223
645
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sqlite-hub",
3
- "version": "0.16.0",
3
+ "version": "0.17.2",
4
4
  "description": "SQLite-only local management app backend and SPA shell",
5
5
  "main": "server/server.js",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "express": "^5.2.1",
27
27
  "express-rate-limit": "^8.5.2",
28
28
  "helmet": "^8.2.0",
29
+ "marked": "^18.0.5",
29
30
  "material-symbols": "^0.44.9"
30
31
  },
31
32
  "devDependencies": {
@@ -0,0 +1,84 @@
1
+ const { AppError } = require("../utils/errors");
2
+
3
+ const LOCAL_HOSTNAMES = new Set(["127.0.0.1", "localhost", "::1"]);
4
+ const LOOPBACK_HOST = "127.0.0.1";
5
+ const MUTATING_METHODS = new Set(["POST", "PUT", "PATCH", "DELETE"]);
6
+
7
+ function parseHost(value) {
8
+ try {
9
+ return new URL(`http://${String(value ?? "").trim()}`);
10
+ } catch (error) {
11
+ return null;
12
+ }
13
+ }
14
+
15
+ function isLocalHostname(hostname) {
16
+ return LOCAL_HOSTNAMES.has(String(hostname ?? "").replace(/^\[|\]$/g, "").toLowerCase());
17
+ }
18
+
19
+ function createLocalRequestError(message) {
20
+ return new AppError(message, 403, {
21
+ code: "LOCAL_REQUEST_REQUIRED",
22
+ });
23
+ }
24
+
25
+ function listenOnLoopback(app, port) {
26
+ return app.listen(port, LOOPBACK_HOST);
27
+ }
28
+
29
+ function localRequestSecurity(req, res, next) {
30
+ const requestHost = parseHost(req.get("host"));
31
+
32
+ if (!requestHost || !isLocalHostname(requestHost.hostname)) {
33
+ next(createLocalRequestError("SQLite Hub only accepts requests addressed to localhost."));
34
+ return;
35
+ }
36
+
37
+ if (!MUTATING_METHODS.has(req.method)) {
38
+ next();
39
+ return;
40
+ }
41
+
42
+ if (String(req.get("sec-fetch-site") ?? "").toLowerCase() === "cross-site") {
43
+ next(createLocalRequestError("Cross-site API requests are not allowed."));
44
+ return;
45
+ }
46
+
47
+ const origin = String(req.get("origin") ?? "").trim();
48
+
49
+ // CLI and other non-browser clients do not send Origin. Browser requests must be same-origin.
50
+ if (!origin) {
51
+ next();
52
+ return;
53
+ }
54
+
55
+ let parsedOrigin;
56
+
57
+ try {
58
+ parsedOrigin = new URL(origin);
59
+ } catch (error) {
60
+ next(createLocalRequestError("API request origin is invalid."));
61
+ return;
62
+ }
63
+
64
+ if (
65
+ !isLocalHostname(parsedOrigin.hostname) ||
66
+ parsedOrigin.host.toLowerCase() !== requestHost.host.toLowerCase() ||
67
+ parsedOrigin.protocol !== `${req.protocol}:`
68
+ ) {
69
+ next(createLocalRequestError("Cross-origin API requests are not allowed."));
70
+ return;
71
+ }
72
+
73
+ next();
74
+ }
75
+
76
+ module.exports = {
77
+ LOCAL_HOSTNAMES,
78
+ LOOPBACK_HOST,
79
+ MUTATING_METHODS,
80
+ isLocalHostname,
81
+ listenOnLoopback,
82
+ localRequestSecurity,
83
+ parseHost,
84
+ };
@@ -1,7 +1,7 @@
1
1
  const express = require("express");
2
2
  const { route, successResponse } = require("../utils/errors");
3
3
 
4
- function createConnectionsRouter({ connectionManager, importService, backupService }) {
4
+ function createConnectionsRouter({ connectionManager, importService, backupService, nativeFileDialogService }) {
5
5
  const router = express.Router();
6
6
 
7
7
  router.post(
@@ -42,6 +42,23 @@ function createConnectionsRouter({ connectionManager, importService, backupServi
42
42
  })
43
43
  );
44
44
 
45
+ router.post(
46
+ "/choose-create-path",
47
+ route(async (req, res) => {
48
+ const selectedPath = await nativeFileDialogService.chooseCreateDatabasePath();
49
+
50
+ res.json(
51
+ successResponse({
52
+ message: selectedPath ? "Database path selected." : "File selection cancelled.",
53
+ data: {
54
+ cancelled: !selectedPath,
55
+ path: selectedPath,
56
+ },
57
+ })
58
+ );
59
+ })
60
+ );
61
+
45
62
  router.post(
46
63
  "/import-sql",
47
64
  route((req, res) => {
@@ -0,0 +1,163 @@
1
+ const express = require("express");
2
+ const path = require("node:path");
3
+ const { DatabaseRequiredError, route, successResponse } = require("../utils/errors");
4
+
5
+ function getActiveDatabaseKey(connectionManager) {
6
+ return connectionManager.getActiveConnection()?.id ?? null;
7
+ }
8
+
9
+ function requireActiveDatabaseKey(connectionManager) {
10
+ const databaseKey = getActiveDatabaseKey(connectionManager);
11
+
12
+ if (!databaseKey) {
13
+ throw new DatabaseRequiredError();
14
+ }
15
+
16
+ return databaseKey;
17
+ }
18
+
19
+ function resolveActiveDatabaseDocumentName(connection) {
20
+ const label = String(connection?.label ?? "").trim();
21
+
22
+ if (label) {
23
+ return label;
24
+ }
25
+
26
+ const basename = path.basename(String(connection?.path ?? "").trim());
27
+ return basename || "Database";
28
+ }
29
+
30
+ function buildInitialDocumentPayload(connection) {
31
+ const databaseName = resolveActiveDatabaseDocumentName(connection);
32
+
33
+ return {
34
+ title: databaseName,
35
+ filename: databaseName,
36
+ content: `# ${databaseName}\n`,
37
+ };
38
+ }
39
+
40
+ function pickDocumentPatch(body = {}) {
41
+ const patch = {};
42
+
43
+ for (const field of ["title", "filename", "content"]) {
44
+ if (Object.prototype.hasOwnProperty.call(body, field)) {
45
+ patch[field] = body[field];
46
+ }
47
+ }
48
+
49
+ return patch;
50
+ }
51
+
52
+ function ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey }) {
53
+ let items = appStateStore.listDatabaseDocuments(databaseKey);
54
+
55
+ if (items.length > 0) {
56
+ return items;
57
+ }
58
+
59
+ appStateStore.createDatabaseDocument(
60
+ databaseKey,
61
+ buildInitialDocumentPayload(connectionManager.getActiveConnection())
62
+ );
63
+
64
+ return appStateStore.listDatabaseDocuments(databaseKey);
65
+ }
66
+
67
+ function createDocumentsRouter({ appStateStore, connectionManager }) {
68
+ const router = express.Router();
69
+
70
+ router.get(
71
+ "/",
72
+ route((req, res) => {
73
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
74
+
75
+ res.json(
76
+ successResponse({
77
+ data: {
78
+ items: ensureDatabaseDocuments({ appStateStore, connectionManager, databaseKey }),
79
+ },
80
+ metadata: { databaseKey },
81
+ })
82
+ );
83
+ })
84
+ );
85
+
86
+ router.post(
87
+ "/",
88
+ route((req, res) => {
89
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
90
+ const document = appStateStore.createDatabaseDocument(databaseKey, {
91
+ title: req.body?.title,
92
+ filename: req.body?.filename,
93
+ content: req.body?.content,
94
+ });
95
+
96
+ res.status(201).json(
97
+ successResponse({
98
+ message: "Document created.",
99
+ data: document,
100
+ metadata: { databaseKey },
101
+ })
102
+ );
103
+ })
104
+ );
105
+
106
+ router.get(
107
+ "/:documentId",
108
+ route((req, res) => {
109
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
110
+
111
+ res.json(
112
+ successResponse({
113
+ data: appStateStore.getDatabaseDocument(databaseKey, req.params.documentId),
114
+ metadata: { databaseKey },
115
+ })
116
+ );
117
+ })
118
+ );
119
+
120
+ router.patch(
121
+ "/:documentId",
122
+ route((req, res) => {
123
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
124
+
125
+ res.json(
126
+ successResponse({
127
+ message: "Document saved.",
128
+ data: appStateStore.updateDatabaseDocument(
129
+ databaseKey,
130
+ req.params.documentId,
131
+ pickDocumentPatch(req.body)
132
+ ),
133
+ metadata: { databaseKey },
134
+ })
135
+ );
136
+ })
137
+ );
138
+
139
+ router.delete(
140
+ "/:documentId",
141
+ route((req, res) => {
142
+ const databaseKey = requireActiveDatabaseKey(connectionManager);
143
+
144
+ res.json(
145
+ successResponse({
146
+ message: "Document deleted.",
147
+ data: appStateStore.deleteDatabaseDocument(databaseKey, req.params.documentId),
148
+ metadata: { databaseKey },
149
+ })
150
+ );
151
+ })
152
+ );
153
+
154
+ return router;
155
+ }
156
+
157
+ module.exports = {
158
+ buildInitialDocumentPayload,
159
+ createDocumentsRouter,
160
+ ensureDatabaseDocuments,
161
+ pickDocumentPatch,
162
+ resolveActiveDatabaseDocumentName,
163
+ };