@promptowl/contextnest-community 1.5.0 → 1.7.0
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/CONFIGURATION.md +181 -123
- package/LICENSE.md +142 -142
- package/README.md +224 -211
- package/dist/adapter.postgres-YOODX2BI.js +71 -0
- package/dist/{chunk-EMOE53KX.js → chunk-43DOX4LH.js} +356 -267
- package/dist/{chunk-IWA2UDAT.js → chunk-MOXICJPD.js} +116 -90
- package/dist/{chunk-RMU3LOPH.js → chunk-QMLAXQES.js} +383 -69
- package/dist/chunk-SLTQACJW.js +8 -0
- package/dist/{chunk-HIH7I232.js → chunk-VD5QX2ZQ.js} +69 -53
- package/dist/index.js +1427 -976
- package/dist/migrations.postgres-3HMGLKOV.js +279 -0
- package/dist/{review-service-XNXHF6Q7.js → review-service-2FSKW425.js} +5 -4
- package/dist/{stewardship-service-P4M5UEJW.js → stewardship-service-I2JAFYJU.js} +3 -2
- package/dist/{version-service-REGL5CUT.js → version-service-FJWVTZKR.js} +3 -2
- package/dist/web3/assets/{hootie-C2ocYkn4.svg → hootie-_U3ECslt.svg} +8 -8
- package/dist/web3/assets/index-24dBhQSQ.css +1 -0
- package/dist/web3/assets/index-DpM8J4RN.js +889 -0
- package/dist/web3/index.html +14 -14
- package/package.json +152 -150
- package/dist/web3/assets/index-C2dfT3Et.css +0 -1
- package/dist/web3/assets/index-DEKnE-ty.js +0 -887
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ANON_USER_ID
|
|
3
|
+
} from "./chunk-SLTQACJW.js";
|
|
4
|
+
|
|
1
5
|
// src/config.ts
|
|
2
6
|
import { join, dirname } from "path";
|
|
3
7
|
import { existsSync } from "fs";
|
|
@@ -38,6 +42,68 @@ var config = {
|
|
|
38
42
|
get DATABASE_PATH() {
|
|
39
43
|
return process.env.DATABASE_PATH || join(dataRoot(), "community.db");
|
|
40
44
|
},
|
|
45
|
+
/**
|
|
46
|
+
* Root directory for nest vault files. Defaults to `<DATA_ROOT>/nests`.
|
|
47
|
+
* Override (e.g. a GCS volume mount on Cloud Run) to store nest files
|
|
48
|
+
* separately from the SQLite DB. See `src/shared/paths.ts`.
|
|
49
|
+
*/
|
|
50
|
+
get NEST_STORAGE_ROOT() {
|
|
51
|
+
return process.env.NEST_STORAGE_ROOT || join(dataRoot(), "nests");
|
|
52
|
+
},
|
|
53
|
+
/**
|
|
54
|
+
* Which database backend to use.
|
|
55
|
+
* "sqlite" — default; the on-disk file at DATABASE_PATH (existing behaviour).
|
|
56
|
+
* "postgres" — Cloud SQL / PostgreSQL, for durable Cloud Run deployments.
|
|
57
|
+
* Explicit DB_DRIVER wins; otherwise we infer "postgres" when a connection
|
|
58
|
+
* is configured (DATABASE_URL or CLOUD_SQL_CONNECTION_NAME), else "sqlite".
|
|
59
|
+
* So existing deployments with no new env vars keep using SQLite untouched.
|
|
60
|
+
*/
|
|
61
|
+
get DB_DRIVER() {
|
|
62
|
+
const v = (process.env.DB_DRIVER || "").trim().toLowerCase();
|
|
63
|
+
if (v === "postgres" || v === "postgresql" || v === "pg") return "postgres";
|
|
64
|
+
if (v === "sqlite") return "sqlite";
|
|
65
|
+
return process.env.DATABASE_URL || process.env.CLOUD_SQL_CONNECTION_NAME ? "postgres" : "sqlite";
|
|
66
|
+
},
|
|
67
|
+
/** Postgres connection string, e.g. postgres://user:pass@host:5432/db (TCP). */
|
|
68
|
+
get DATABASE_URL() {
|
|
69
|
+
return process.env.DATABASE_URL || "";
|
|
70
|
+
},
|
|
71
|
+
/**
|
|
72
|
+
* Cloud SQL instance connection name (project:region:instance). When set,
|
|
73
|
+
* the server connects over the unix socket at /cloudsql/<name> created by
|
|
74
|
+
* the Cloud SQL Auth Proxy (the recommended Cloud Run setup). Combine with
|
|
75
|
+
* DB_USER / DB_PASSWORD / DB_NAME.
|
|
76
|
+
*/
|
|
77
|
+
get CLOUD_SQL_CONNECTION_NAME() {
|
|
78
|
+
return process.env.CLOUD_SQL_CONNECTION_NAME || "";
|
|
79
|
+
},
|
|
80
|
+
get DB_HOST() {
|
|
81
|
+
return process.env.DB_HOST || "";
|
|
82
|
+
},
|
|
83
|
+
get DB_PORT() {
|
|
84
|
+
return parseInt(process.env.DB_PORT || "5432", 10);
|
|
85
|
+
},
|
|
86
|
+
get DB_USER() {
|
|
87
|
+
return process.env.DB_USER || "";
|
|
88
|
+
},
|
|
89
|
+
get DB_PASSWORD() {
|
|
90
|
+
return process.env.DB_PASSWORD || "";
|
|
91
|
+
},
|
|
92
|
+
get DB_NAME() {
|
|
93
|
+
return process.env.DB_NAME || "";
|
|
94
|
+
},
|
|
95
|
+
/** Max Postgres pool connections. Keep modest — Cloud SQL tiers cap connections. */
|
|
96
|
+
get DB_POOL_MAX() {
|
|
97
|
+
return parseInt(process.env.DB_POOL_MAX || "10", 10);
|
|
98
|
+
},
|
|
99
|
+
/** Enable TLS for Postgres TCP connections (not needed over the unix socket). */
|
|
100
|
+
get DB_SSL() {
|
|
101
|
+
return process.env.DB_SSL === "true";
|
|
102
|
+
},
|
|
103
|
+
/** Optional path to a CA cert (PEM) for verify-ca/verify-full TLS. */
|
|
104
|
+
get DB_SSL_CA() {
|
|
105
|
+
return process.env.DB_SSL_CA || "";
|
|
106
|
+
},
|
|
41
107
|
get PROMPTOWL_API_URL() {
|
|
42
108
|
return process.env.PROMPTOWL_API_URL || "https://app.promptowl.ai";
|
|
43
109
|
},
|
|
@@ -146,16 +212,12 @@ var config = {
|
|
|
146
212
|
|
|
147
213
|
// src/db/client.ts
|
|
148
214
|
import Database from "better-sqlite3";
|
|
149
|
-
import { mkdirSync } from "fs";
|
|
215
|
+
import { mkdirSync, readFileSync } from "fs";
|
|
150
216
|
import { dirname as dirname2 } from "path";
|
|
151
217
|
|
|
152
|
-
// src/shared/constants.ts
|
|
153
|
-
var ANON_USER_ID = "00000000-0000-0000-0000-000000000000";
|
|
154
|
-
var ANON_EMAIL = "admin@localhost";
|
|
155
|
-
|
|
156
218
|
// src/db/migrations.ts
|
|
157
|
-
function runMigrations(
|
|
158
|
-
|
|
219
|
+
function runMigrations(db) {
|
|
220
|
+
db.exec(`
|
|
159
221
|
CREATE TABLE IF NOT EXISTS users (
|
|
160
222
|
id TEXT PRIMARY KEY,
|
|
161
223
|
email TEXT UNIQUE NOT NULL,
|
|
@@ -249,7 +311,7 @@ function runMigrations(db2) {
|
|
|
249
311
|
sent INTEGER NOT NULL DEFAULT 0
|
|
250
312
|
);
|
|
251
313
|
`);
|
|
252
|
-
|
|
314
|
+
db.exec(`
|
|
253
315
|
-- Steward assignments (mirrors PromptOwl ContextSteward model)
|
|
254
316
|
-- scope+target combination determines what the steward governs
|
|
255
317
|
-- Resolution priority: document(1) > tag(2) > nest(3)
|
|
@@ -322,47 +384,47 @@ function runMigrations(db2) {
|
|
|
322
384
|
PRIMARY KEY (nest_id, node_id)
|
|
323
385
|
);
|
|
324
386
|
`);
|
|
325
|
-
const nestCols =
|
|
387
|
+
const nestCols = db.prepare("PRAGMA table_info(nests)").all().map((c) => c.name);
|
|
326
388
|
if (!nestCols.includes("stewardship_enabled")) {
|
|
327
|
-
|
|
389
|
+
db.exec("ALTER TABLE nests ADD COLUMN stewardship_enabled INTEGER NOT NULL DEFAULT 0");
|
|
328
390
|
}
|
|
329
391
|
if (!nestCols.includes("is_imported")) {
|
|
330
|
-
|
|
392
|
+
db.exec("ALTER TABLE nests ADD COLUMN is_imported INTEGER NOT NULL DEFAULT 0");
|
|
331
393
|
}
|
|
332
394
|
if (!nestCols.includes("allow_self_approve")) {
|
|
333
|
-
|
|
395
|
+
db.exec("ALTER TABLE nests ADD COLUMN allow_self_approve INTEGER NOT NULL DEFAULT 0");
|
|
334
396
|
}
|
|
335
|
-
const userCols =
|
|
397
|
+
const userCols = db.prepare("PRAGMA table_info(users)").all().map((c) => c.name);
|
|
336
398
|
if (!userCols.includes("is_admin")) {
|
|
337
|
-
|
|
399
|
+
db.exec("ALTER TABLE users ADD COLUMN is_admin INTEGER NOT NULL DEFAULT 0");
|
|
338
400
|
}
|
|
339
401
|
if (!userCols.includes("is_invited")) {
|
|
340
|
-
|
|
402
|
+
db.exec("ALTER TABLE users ADD COLUMN is_invited INTEGER NOT NULL DEFAULT 0");
|
|
341
403
|
}
|
|
342
|
-
const stewardCols =
|
|
404
|
+
const stewardCols = db.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
343
405
|
if (stewardCols.length > 0) {
|
|
344
406
|
if (!stewardCols.includes("can_approve")) {
|
|
345
|
-
|
|
407
|
+
db.exec(
|
|
346
408
|
"ALTER TABLE stewards ADD COLUMN can_approve INTEGER NOT NULL DEFAULT 1"
|
|
347
409
|
);
|
|
348
410
|
}
|
|
349
411
|
if (!stewardCols.includes("can_reject")) {
|
|
350
|
-
|
|
412
|
+
db.exec(
|
|
351
413
|
"ALTER TABLE stewards ADD COLUMN can_reject INTEGER NOT NULL DEFAULT 1"
|
|
352
414
|
);
|
|
353
415
|
}
|
|
354
416
|
}
|
|
355
|
-
|
|
417
|
+
db.exec(`
|
|
356
418
|
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
357
419
|
id TEXT PRIMARY KEY,
|
|
358
420
|
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
359
421
|
);
|
|
360
422
|
`);
|
|
361
|
-
const hasMigration = (id) => !!
|
|
362
|
-
const recordMigration = (id) =>
|
|
423
|
+
const hasMigration = (id) => !!db.prepare("SELECT id FROM schema_migrations WHERE id = ?").get(id);
|
|
424
|
+
const recordMigration = (id) => db.prepare("INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)").run(id);
|
|
363
425
|
if (!hasMigration("002_steward_parity")) {
|
|
364
|
-
|
|
365
|
-
|
|
426
|
+
db.transaction(() => {
|
|
427
|
+
db.exec(`
|
|
366
428
|
CREATE TABLE stewards_new (
|
|
367
429
|
id TEXT PRIMARY KEY,
|
|
368
430
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
@@ -402,7 +464,7 @@ function runMigrations(db2) {
|
|
|
402
464
|
CREATE INDEX idx_stewards_email ON stewards(user_email);
|
|
403
465
|
CREATE INDEX idx_stewards_scope ON stewards(nest_id, scope);
|
|
404
466
|
`);
|
|
405
|
-
|
|
467
|
+
db.exec(`
|
|
406
468
|
CREATE UNIQUE INDEX idx_stewards_uniq_nest
|
|
407
469
|
ON stewards(nest_id, user_email)
|
|
408
470
|
WHERE scope = 'nest' AND is_active = 1;
|
|
@@ -423,7 +485,7 @@ function runMigrations(db2) {
|
|
|
423
485
|
ON stewards(nest_id, node_pattern)
|
|
424
486
|
WHERE scope = 'document' AND is_active = 1;
|
|
425
487
|
`);
|
|
426
|
-
|
|
488
|
+
db.exec(`
|
|
427
489
|
CREATE TABLE IF NOT EXISTS node_tag_index (
|
|
428
490
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
429
491
|
node_id TEXT NOT NULL,
|
|
@@ -433,7 +495,7 @@ function runMigrations(db2) {
|
|
|
433
495
|
CREATE INDEX IF NOT EXISTS idx_node_tag_by_tag
|
|
434
496
|
ON node_tag_index(nest_id, tag_name);
|
|
435
497
|
`);
|
|
436
|
-
const rows =
|
|
498
|
+
const rows = db.prepare(
|
|
437
499
|
`SELECT nv.nest_id, nv.node_id, nv.tags_json
|
|
438
500
|
FROM node_versions nv
|
|
439
501
|
JOIN (
|
|
@@ -445,7 +507,7 @@ function runMigrations(db2) {
|
|
|
445
507
|
AND latest.node_id = nv.node_id
|
|
446
508
|
AND latest.v = nv.version`
|
|
447
509
|
).all();
|
|
448
|
-
const insertTag =
|
|
510
|
+
const insertTag = db.prepare(
|
|
449
511
|
"INSERT OR IGNORE INTO node_tag_index (nest_id, node_id, tag_name) VALUES (?, ?, ?)"
|
|
450
512
|
);
|
|
451
513
|
for (const row of rows) {
|
|
@@ -467,8 +529,8 @@ function runMigrations(db2) {
|
|
|
467
529
|
})();
|
|
468
530
|
}
|
|
469
531
|
if (!hasMigration("003_sessions_and_single_api_key")) {
|
|
470
|
-
|
|
471
|
-
|
|
532
|
+
db.transaction(() => {
|
|
533
|
+
db.exec(`
|
|
472
534
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
473
535
|
id TEXT PRIMARY KEY,
|
|
474
536
|
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
@@ -480,7 +542,7 @@ function runMigrations(db2) {
|
|
|
480
542
|
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
|
481
543
|
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
482
544
|
`);
|
|
483
|
-
|
|
545
|
+
db.exec(`
|
|
484
546
|
DELETE FROM api_keys
|
|
485
547
|
WHERE id IN (
|
|
486
548
|
SELECT id FROM (
|
|
@@ -498,7 +560,7 @@ function runMigrations(db2) {
|
|
|
498
560
|
WHERE rn > 1
|
|
499
561
|
);
|
|
500
562
|
`);
|
|
501
|
-
|
|
563
|
+
db.exec(`
|
|
502
564
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_api_keys_user_unique
|
|
503
565
|
ON api_keys(user_id);
|
|
504
566
|
`);
|
|
@@ -506,25 +568,25 @@ function runMigrations(db2) {
|
|
|
506
568
|
})();
|
|
507
569
|
}
|
|
508
570
|
if (!hasMigration("004_license_cache_owner_email")) {
|
|
509
|
-
|
|
510
|
-
const cols =
|
|
571
|
+
db.transaction(() => {
|
|
572
|
+
const cols = db.prepare("PRAGMA table_info(license_cache)").all().map((c) => c.name);
|
|
511
573
|
if (!cols.includes("owner_email")) {
|
|
512
|
-
|
|
574
|
+
db.exec("ALTER TABLE license_cache ADD COLUMN owner_email TEXT");
|
|
513
575
|
}
|
|
514
576
|
recordMigration("004_license_cache_owner_email");
|
|
515
577
|
})();
|
|
516
578
|
}
|
|
517
579
|
if (!hasMigration("005_anon_nest_public_default")) {
|
|
518
|
-
|
|
519
|
-
|
|
580
|
+
db.transaction(() => {
|
|
581
|
+
db.prepare(
|
|
520
582
|
"UPDATE nests SET visibility = 'public' WHERE user_id = ? AND visibility = 'private'"
|
|
521
583
|
).run(ANON_USER_ID);
|
|
522
584
|
recordMigration("005_anon_nest_public_default");
|
|
523
585
|
})();
|
|
524
586
|
}
|
|
525
587
|
if (!hasMigration("006_node_versions_published_status")) {
|
|
526
|
-
|
|
527
|
-
|
|
588
|
+
db.transaction(() => {
|
|
589
|
+
db.exec(`
|
|
528
590
|
CREATE TABLE node_versions_new (
|
|
529
591
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
530
592
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
@@ -552,15 +614,15 @@ function runMigrations(db2) {
|
|
|
552
614
|
})();
|
|
553
615
|
}
|
|
554
616
|
if (!hasMigration("007_drop_steward_capability_flags")) {
|
|
555
|
-
const stewardCols2 =
|
|
617
|
+
const stewardCols2 = db.prepare("PRAGMA table_info(stewards)").all().map((c) => c.name);
|
|
556
618
|
const hasLegacyCols = stewardCols2.includes("can_approve") || stewardCols2.includes("can_reject");
|
|
557
619
|
if (hasLegacyCols) {
|
|
558
|
-
|
|
620
|
+
db.transaction(() => {
|
|
559
621
|
if (stewardCols2.includes("can_approve")) {
|
|
560
|
-
|
|
622
|
+
db.exec("ALTER TABLE stewards DROP COLUMN can_approve");
|
|
561
623
|
}
|
|
562
624
|
if (stewardCols2.includes("can_reject")) {
|
|
563
|
-
|
|
625
|
+
db.exec("ALTER TABLE stewards DROP COLUMN can_reject");
|
|
564
626
|
}
|
|
565
627
|
recordMigration("007_drop_steward_capability_flags");
|
|
566
628
|
})();
|
|
@@ -569,13 +631,13 @@ function runMigrations(db2) {
|
|
|
569
631
|
}
|
|
570
632
|
}
|
|
571
633
|
if (!hasMigration("008_drop_steward_folder_scope")) {
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
const tbl =
|
|
634
|
+
db.transaction(() => {
|
|
635
|
+
db.exec("DELETE FROM stewards WHERE scope = 'folder'");
|
|
636
|
+
db.exec("DROP INDEX IF EXISTS idx_stewards_uniq_folder");
|
|
637
|
+
db.exec("DROP INDEX IF EXISTS idx_stewards_folder_lookup");
|
|
638
|
+
const tbl = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='stewards'").get();
|
|
577
639
|
if (tbl?.sql && tbl.sql.includes("'folder'")) {
|
|
578
|
-
|
|
640
|
+
db.exec(`
|
|
579
641
|
CREATE TABLE stewards_new (
|
|
580
642
|
id TEXT PRIMARY KEY,
|
|
581
643
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
@@ -624,8 +686,8 @@ function runMigrations(db2) {
|
|
|
624
686
|
})();
|
|
625
687
|
}
|
|
626
688
|
if (!hasMigration("009_lowercase_emails")) {
|
|
627
|
-
|
|
628
|
-
const collisions =
|
|
689
|
+
db.transaction(() => {
|
|
690
|
+
const collisions = db.prepare(
|
|
629
691
|
`SELECT GROUP_CONCAT(email, ', ') AS emails
|
|
630
692
|
FROM users GROUP BY LOWER(email) HAVING COUNT(*) > 1`
|
|
631
693
|
).all();
|
|
@@ -634,20 +696,20 @@ function runMigrations(db2) {
|
|
|
634
696
|
`[migration 009] case-collision user rows NOT auto-merged: ${c.emails} \u2014 reconcile manually (pick the row the person logs into; reset its password; ensure collaborator/steward grants point at that user_id).`
|
|
635
697
|
);
|
|
636
698
|
}
|
|
637
|
-
|
|
699
|
+
db.exec(
|
|
638
700
|
`UPDATE users SET email = LOWER(email)
|
|
639
701
|
WHERE email <> LOWER(email)
|
|
640
702
|
AND LOWER(email) NOT IN (
|
|
641
703
|
SELECT LOWER(email) FROM users GROUP BY LOWER(email) HAVING COUNT(*) > 1
|
|
642
704
|
)`
|
|
643
705
|
);
|
|
644
|
-
const remaining =
|
|
706
|
+
const remaining = db.prepare(
|
|
645
707
|
`SELECT COUNT(*) AS c FROM (
|
|
646
708
|
SELECT 1 FROM users GROUP BY LOWER(email) HAVING COUNT(*) > 1
|
|
647
709
|
)`
|
|
648
710
|
).get().c;
|
|
649
711
|
if (remaining === 0) {
|
|
650
|
-
|
|
712
|
+
db.exec(
|
|
651
713
|
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_nocase ON users(email COLLATE NOCASE)"
|
|
652
714
|
);
|
|
653
715
|
} else {
|
|
@@ -659,8 +721,8 @@ function runMigrations(db2) {
|
|
|
659
721
|
recordMigration("009_lowercase_emails");
|
|
660
722
|
}
|
|
661
723
|
if (!hasMigration("010_sso_used_jti")) {
|
|
662
|
-
|
|
663
|
-
|
|
724
|
+
db.transaction(() => {
|
|
725
|
+
db.exec(`
|
|
664
726
|
CREATE TABLE IF NOT EXISTS sso_used_jti (
|
|
665
727
|
jti TEXT PRIMARY KEY,
|
|
666
728
|
used_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
@@ -673,8 +735,8 @@ function runMigrations(db2) {
|
|
|
673
735
|
recordMigration("010_sso_used_jti");
|
|
674
736
|
}
|
|
675
737
|
if (!hasMigration("011_comments")) {
|
|
676
|
-
|
|
677
|
-
|
|
738
|
+
db.transaction(() => {
|
|
739
|
+
db.exec(`
|
|
678
740
|
CREATE TABLE IF NOT EXISTS comments (
|
|
679
741
|
id TEXT PRIMARY KEY,
|
|
680
742
|
nest_id TEXT NOT NULL REFERENCES nests(id) ON DELETE CASCADE,
|
|
@@ -702,25 +764,277 @@ function runMigrations(db2) {
|
|
|
702
764
|
})();
|
|
703
765
|
recordMigration("011_comments");
|
|
704
766
|
}
|
|
767
|
+
if (!hasMigration("012_merge_case_colliding_users")) {
|
|
768
|
+
db.transaction(() => {
|
|
769
|
+
const { groups, merged } = mergeCaseCollidingUsers(db);
|
|
770
|
+
if (merged > 0) {
|
|
771
|
+
console.warn(
|
|
772
|
+
`[migration 012] merged ${merged} duplicate user row(s) across ${groups} case-collision group(s).`
|
|
773
|
+
);
|
|
774
|
+
}
|
|
775
|
+
db.exec(
|
|
776
|
+
"UPDATE users SET email = LOWER(email) WHERE email <> LOWER(email)"
|
|
777
|
+
);
|
|
778
|
+
db.exec(
|
|
779
|
+
"CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email_nocase ON users(email COLLATE NOCASE)"
|
|
780
|
+
);
|
|
781
|
+
})();
|
|
782
|
+
recordMigration("012_merge_case_colliding_users");
|
|
783
|
+
}
|
|
784
|
+
}
|
|
785
|
+
function mergeCaseCollidingUsers(db) {
|
|
786
|
+
const groups = db.prepare(
|
|
787
|
+
`SELECT LOWER(email) AS le
|
|
788
|
+
FROM users
|
|
789
|
+
WHERE id != ?
|
|
790
|
+
GROUP BY LOWER(email)
|
|
791
|
+
HAVING COUNT(*) > 1`
|
|
792
|
+
).all(ANON_USER_ID);
|
|
793
|
+
const emailCols = [
|
|
794
|
+
["annotation_threads", "created_by"],
|
|
795
|
+
["annotation_threads", "resolved_by"],
|
|
796
|
+
["annotation_comments", "author"],
|
|
797
|
+
["review_requests", "requested_by"],
|
|
798
|
+
["review_requests", "resolved_by"],
|
|
799
|
+
["node_versions", "author"],
|
|
800
|
+
["approved_versions", "approved_by"],
|
|
801
|
+
["comments", "author"],
|
|
802
|
+
["comments", "resolved_by"],
|
|
803
|
+
["license_cache", "owner_email"],
|
|
804
|
+
["stewards", "assigned_by"]
|
|
805
|
+
];
|
|
806
|
+
let merged = 0;
|
|
807
|
+
for (const { le } of groups) {
|
|
808
|
+
const rows = db.prepare(
|
|
809
|
+
`SELECT u.id, u.email, u.is_invited, u.is_admin, u.name, u.created_at,
|
|
810
|
+
(SELECT COUNT(*) FROM api_keys k WHERE k.user_id = u.id) AS keys,
|
|
811
|
+
(SELECT COUNT(*) FROM sessions s WHERE s.user_id = u.id) AS sessions
|
|
812
|
+
FROM users u
|
|
813
|
+
WHERE LOWER(u.email) = ? AND u.id != ?`
|
|
814
|
+
).all(le, ANON_USER_ID);
|
|
815
|
+
if (rows.length < 2) continue;
|
|
816
|
+
rows.sort((a, b) => {
|
|
817
|
+
if (a.is_invited !== b.is_invited) return a.is_invited - b.is_invited;
|
|
818
|
+
const aCred = a.keys + a.sessions > 0 ? 1 : 0;
|
|
819
|
+
const bCred = b.keys + b.sessions > 0 ? 1 : 0;
|
|
820
|
+
if (aCred !== bCred) return bCred - aCred;
|
|
821
|
+
return b.created_at.localeCompare(a.created_at);
|
|
822
|
+
});
|
|
823
|
+
const winner = rows[0];
|
|
824
|
+
const losers = rows.slice(1);
|
|
825
|
+
const anyAdmin = rows.some((r) => r.is_admin) ? 1 : 0;
|
|
826
|
+
const winnerName = winner.name ?? rows.find((r) => r.name)?.name ?? null;
|
|
827
|
+
for (const loser of losers) {
|
|
828
|
+
const w = winner.id;
|
|
829
|
+
const l = loser.id;
|
|
830
|
+
const loserLower = loser.email.toLowerCase();
|
|
831
|
+
db.prepare("UPDATE sessions SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
832
|
+
db.prepare(
|
|
833
|
+
"UPDATE nest_collaborators SET granted_by = ? WHERE granted_by = ?"
|
|
834
|
+
).run(w, l);
|
|
835
|
+
db.prepare("UPDATE stewards SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
836
|
+
db.prepare(
|
|
837
|
+
`UPDATE nests SET user_id = ?
|
|
838
|
+
WHERE user_id = ?
|
|
839
|
+
AND slug NOT IN (SELECT slug FROM nests WHERE user_id = ?)`
|
|
840
|
+
).run(w, l, w);
|
|
841
|
+
db.prepare(
|
|
842
|
+
`UPDATE nests
|
|
843
|
+
SET slug = slug || '-merged-' || substr(id, 1, 8), user_id = ?
|
|
844
|
+
WHERE user_id = ?`
|
|
845
|
+
).run(w, l);
|
|
846
|
+
db.prepare(
|
|
847
|
+
`DELETE FROM api_keys
|
|
848
|
+
WHERE user_id = ?
|
|
849
|
+
AND EXISTS (SELECT 1 FROM api_keys WHERE user_id = ?)`
|
|
850
|
+
).run(l, w);
|
|
851
|
+
db.prepare("UPDATE api_keys SET user_id = ? WHERE user_id = ?").run(w, l);
|
|
852
|
+
db.prepare(
|
|
853
|
+
`DELETE FROM nest_collaborators
|
|
854
|
+
WHERE user_id = ?
|
|
855
|
+
AND nest_id IN (SELECT nest_id FROM nest_collaborators WHERE user_id = ?)`
|
|
856
|
+
).run(l, w);
|
|
857
|
+
db.prepare(
|
|
858
|
+
"UPDATE nest_collaborators SET user_id = ? WHERE user_id = ?"
|
|
859
|
+
).run(w, l);
|
|
860
|
+
db.prepare(
|
|
861
|
+
`DELETE FROM nest_collaborators
|
|
862
|
+
WHERE user_id = ?
|
|
863
|
+
AND nest_id IN (SELECT id FROM nests WHERE user_id = ?)`
|
|
864
|
+
).run(w, w);
|
|
865
|
+
for (const [table, col] of emailCols) {
|
|
866
|
+
db.prepare(
|
|
867
|
+
`UPDATE ${table} SET ${col} = ? WHERE LOWER(${col}) = ?`
|
|
868
|
+
).run(le, loserLower);
|
|
869
|
+
}
|
|
870
|
+
db.prepare(
|
|
871
|
+
`DELETE FROM stewards
|
|
872
|
+
WHERE is_active = 1
|
|
873
|
+
AND LOWER(user_email) = ?
|
|
874
|
+
AND id NOT IN (
|
|
875
|
+
SELECT MIN(id) FROM stewards
|
|
876
|
+
WHERE is_active = 1 AND LOWER(user_email) = ?
|
|
877
|
+
GROUP BY nest_id, scope,
|
|
878
|
+
IFNULL(node_pattern, ''), IFNULL(tag_name, '')
|
|
879
|
+
)`
|
|
880
|
+
).run(le, le);
|
|
881
|
+
db.prepare(
|
|
882
|
+
"UPDATE stewards SET user_email = ?, user_id = ? WHERE LOWER(user_email) = ?"
|
|
883
|
+
).run(le, w, le);
|
|
884
|
+
db.prepare("DELETE FROM users WHERE id = ?").run(l);
|
|
885
|
+
merged++;
|
|
886
|
+
}
|
|
887
|
+
db.prepare(
|
|
888
|
+
"UPDATE users SET email = ?, is_admin = ?, name = ? WHERE id = ?"
|
|
889
|
+
).run(le, anyAdmin, winnerName, winner.id);
|
|
890
|
+
}
|
|
891
|
+
return { groups: groups.length, merged };
|
|
705
892
|
}
|
|
706
893
|
|
|
894
|
+
// src/db/adapter.sqlite.ts
|
|
895
|
+
var SqliteAdapter = class {
|
|
896
|
+
constructor(db) {
|
|
897
|
+
this.db = db;
|
|
898
|
+
}
|
|
899
|
+
db;
|
|
900
|
+
dialect = "sqlite";
|
|
901
|
+
// better-sqlite3 is one synchronous connection. Once transaction bodies are
|
|
902
|
+
// async (they may `await` between statements), two concurrent bodies could
|
|
903
|
+
// interleave their BEGIN/COMMIT on the shared connection and corrupt
|
|
904
|
+
// atomicity. This promise chain serialises transactions in-process,
|
|
905
|
+
// restoring the run-to-completion guarantee better-sqlite3's own
|
|
906
|
+
// `.transaction()` gave us synchronously.
|
|
907
|
+
txChain = Promise.resolve();
|
|
908
|
+
async get(sql, params = []) {
|
|
909
|
+
return this.db.prepare(sql).get(...params);
|
|
910
|
+
}
|
|
911
|
+
async all(sql, params = []) {
|
|
912
|
+
return this.db.prepare(sql).all(...params);
|
|
913
|
+
}
|
|
914
|
+
async run(sql, params = []) {
|
|
915
|
+
const info = this.db.prepare(sql).run(...params);
|
|
916
|
+
return { changes: info.changes };
|
|
917
|
+
}
|
|
918
|
+
async exec(sql) {
|
|
919
|
+
this.db.exec(sql);
|
|
920
|
+
}
|
|
921
|
+
async transaction(fn) {
|
|
922
|
+
const run = async () => {
|
|
923
|
+
this.db.exec("BEGIN IMMEDIATE");
|
|
924
|
+
try {
|
|
925
|
+
const result2 = await fn(this);
|
|
926
|
+
this.db.exec("COMMIT");
|
|
927
|
+
return result2;
|
|
928
|
+
} catch (err) {
|
|
929
|
+
try {
|
|
930
|
+
this.db.exec("ROLLBACK");
|
|
931
|
+
} catch {
|
|
932
|
+
}
|
|
933
|
+
throw err;
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
const result = this.txChain.then(run, run);
|
|
937
|
+
this.txChain = result.then(
|
|
938
|
+
() => void 0,
|
|
939
|
+
() => void 0
|
|
940
|
+
);
|
|
941
|
+
return result;
|
|
942
|
+
}
|
|
943
|
+
async close() {
|
|
944
|
+
this.db.close();
|
|
945
|
+
}
|
|
946
|
+
};
|
|
947
|
+
|
|
707
948
|
// src/db/client.ts
|
|
708
|
-
var
|
|
949
|
+
var adapter = null;
|
|
950
|
+
function buildPgConfig() {
|
|
951
|
+
const base = {
|
|
952
|
+
max: config.DB_POOL_MAX,
|
|
953
|
+
idleTimeoutMillis: 3e4,
|
|
954
|
+
connectionTimeoutMillis: 1e4,
|
|
955
|
+
keepAlive: true
|
|
956
|
+
};
|
|
957
|
+
if (config.CLOUD_SQL_CONNECTION_NAME) {
|
|
958
|
+
return {
|
|
959
|
+
...base,
|
|
960
|
+
host: `/cloudsql/${config.CLOUD_SQL_CONNECTION_NAME}`,
|
|
961
|
+
user: config.DB_USER || void 0,
|
|
962
|
+
password: config.DB_PASSWORD || void 0,
|
|
963
|
+
database: config.DB_NAME || void 0
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
if (config.DATABASE_URL) {
|
|
967
|
+
return { ...base, connectionString: config.DATABASE_URL, ssl: pgSsl() };
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
...base,
|
|
971
|
+
host: config.DB_HOST || "localhost",
|
|
972
|
+
port: config.DB_PORT,
|
|
973
|
+
user: config.DB_USER || void 0,
|
|
974
|
+
password: config.DB_PASSWORD || void 0,
|
|
975
|
+
database: config.DB_NAME || void 0,
|
|
976
|
+
ssl: pgSsl()
|
|
977
|
+
};
|
|
978
|
+
}
|
|
979
|
+
function pgSsl() {
|
|
980
|
+
if (!config.DB_SSL) return false;
|
|
981
|
+
const ssl = {
|
|
982
|
+
rejectUnauthorized: true
|
|
983
|
+
};
|
|
984
|
+
if (config.DB_SSL_CA) ssl.ca = readFileSync(config.DB_SSL_CA, "utf8");
|
|
985
|
+
return ssl;
|
|
986
|
+
}
|
|
987
|
+
function openSqlite() {
|
|
988
|
+
mkdirSync(dirname2(config.DATABASE_PATH), { recursive: true });
|
|
989
|
+
const raw = new Database(config.DATABASE_PATH);
|
|
990
|
+
raw.pragma("journal_mode = WAL");
|
|
991
|
+
raw.pragma("foreign_keys = ON");
|
|
992
|
+
raw.pragma("busy_timeout = 5000");
|
|
993
|
+
runMigrations(raw);
|
|
994
|
+
return new SqliteAdapter(raw);
|
|
995
|
+
}
|
|
996
|
+
async function initDb() {
|
|
997
|
+
if (adapter) return adapter;
|
|
998
|
+
if (config.DB_DRIVER === "postgres") {
|
|
999
|
+
const { Pool } = await import("pg");
|
|
1000
|
+
const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
|
|
1001
|
+
const { runPostgresMigrations } = await import("./migrations.postgres-3HMGLKOV.js");
|
|
1002
|
+
const pool = new Pool(buildPgConfig());
|
|
1003
|
+
adapter = new PostgresAdapter(pool);
|
|
1004
|
+
await runPostgresMigrations(adapter);
|
|
1005
|
+
} else {
|
|
1006
|
+
adapter = openSqlite();
|
|
1007
|
+
}
|
|
1008
|
+
return adapter;
|
|
1009
|
+
}
|
|
709
1010
|
function getDb() {
|
|
710
|
-
if (!
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
1011
|
+
if (!adapter) {
|
|
1012
|
+
if (config.DB_DRIVER === "postgres") {
|
|
1013
|
+
throw new Error(
|
|
1014
|
+
"Postgres backend not initialised \u2014 call (and await) initDb() at startup before getDb()."
|
|
1015
|
+
);
|
|
1016
|
+
}
|
|
1017
|
+
adapter = openSqlite();
|
|
717
1018
|
}
|
|
718
|
-
return
|
|
1019
|
+
return adapter;
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
// src/db/sql.ts
|
|
1023
|
+
function nowExpr(db) {
|
|
1024
|
+
return db.dialect === "sqlite" ? "datetime('now')" : "to_char((now() AT TIME ZONE 'utc'), 'YYYY-MM-DD HH24:MI:SS')";
|
|
1025
|
+
}
|
|
1026
|
+
function insertOrIgnore(db, insertSql) {
|
|
1027
|
+
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR IGNORE INTO") : `${insertSql} ON CONFLICT DO NOTHING`;
|
|
1028
|
+
}
|
|
1029
|
+
function insertOrReplace(db, insertSql, conflictCols, set) {
|
|
1030
|
+
return db.dialect === "sqlite" ? insertSql.replace(/^\s*INSERT\s+INTO/i, "INSERT OR REPLACE INTO") : `${insertSql} ON CONFLICT (${conflictCols.join(", ")}) DO UPDATE SET ${set}`;
|
|
719
1031
|
}
|
|
720
1032
|
|
|
721
1033
|
export {
|
|
722
1034
|
config,
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
1035
|
+
initDb,
|
|
1036
|
+
getDb,
|
|
1037
|
+
nowExpr,
|
|
1038
|
+
insertOrIgnore,
|
|
1039
|
+
insertOrReplace
|
|
726
1040
|
};
|