@promptowl/contextnest-community 1.6.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.
@@ -764,6 +764,131 @@ function runMigrations(db) {
764
764
  })();
765
765
  recordMigration("011_comments");
766
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 };
767
892
  }
768
893
 
769
894
  // src/db/adapter.sqlite.ts
@@ -873,7 +998,7 @@ async function initDb() {
873
998
  if (config.DB_DRIVER === "postgres") {
874
999
  const { Pool } = await import("pg");
875
1000
  const { PostgresAdapter } = await import("./adapter.postgres-YOODX2BI.js");
876
- const { runPostgresMigrations } = await import("./migrations.postgres-NVJAGBSF.js");
1001
+ const { runPostgresMigrations } = await import("./migrations.postgres-3HMGLKOV.js");
877
1002
  const pool = new Pool(buildPgConfig());
878
1003
  adapter = new PostgresAdapter(pool);
879
1004
  await runPostgresMigrations(adapter);
@@ -2,7 +2,7 @@ import {
2
2
  getDb,
3
3
  insertOrReplace,
4
4
  nowExpr
5
- } from "./chunk-7V33Z6CS.js";
5
+ } from "./chunk-QMLAXQES.js";
6
6
 
7
7
  // src/governance/version-service.ts
8
8
  import { createHash } from "crypto";