pierre-review 0.1.72 → 0.1.73

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.
@@ -45,11 +45,57 @@ export function createUserResolver() {
45
45
  const cached = cache.get(login);
46
46
  if (cached !== undefined)
47
47
  return cached;
48
+ const nodeId = actor?.id ?? null;
49
+ // The GitHub node id is the STABLE identity; the login is NOT. The same
50
+ // account can surface under two different logins — even inside one sync
51
+ // payload: a bot's Bot-typed author field reads `dependabot[bot]` while its
52
+ // commit-author field reads the bare `dependabot`, both carrying the same
53
+ // node id; and humans get renamed. Because BOTH `github_login` AND
54
+ // `github_node_id` are UNIQUE, the login-keyed upsert below is unsafe when a
55
+ // DIFFERENT row already owns this node id: matching the login (or inserting a
56
+ // new one) while `coalesce`-stamping the node raises "UNIQUE constraint
57
+ // failed: users.github_node_id". So when we have a node id, resolve on it
58
+ // FIRST and reuse the row that owns it — the colliding insert/update is never
59
+ // issued. Only a brand-new (or absent) node id falls through to the
60
+ // login-keyed upsert, where stamping the node can't collide.
61
+ if (nodeId) {
62
+ const owner = (await exec
63
+ .select({
64
+ id: users.id,
65
+ isBot: users.isBot,
66
+ isBotOverridden: users.isBotOverridden,
67
+ })
68
+ .from(users)
69
+ .where(eq(users.githubNodeId, nodeId))
70
+ .limit(1)
71
+ .execute())[0];
72
+ if (owner) {
73
+ // Refresh volatile metadata but NEVER rewrite the login: the row already
74
+ // holds a canonical login for this node id, and rewriting it would risk the
75
+ // github_login UNIQUE (when the incoming login belongs to another live row)
76
+ // and thrash a bot that alternates login forms across a single sync. Only
77
+ // set fields the actor actually carries (a commit-author resolve has no
78
+ // name/avatar/typename) so we never clobber a known value with null — the
79
+ // same "coalesce" intent as the upsert below, via plain portable value
80
+ // binds (a raw `sql` boolean can't bind on sqlite; a bare null param can
81
+ // confuse pg type inference). isBot honours a manual override.
82
+ const set = { isBot: owner.isBotOverridden ? owner.isBot : isLikelyBot(login) };
83
+ if (actor?.name != null)
84
+ set.displayName = actor.name;
85
+ if (actor?.avatarUrl != null)
86
+ set.avatarUrl = actor.avatarUrl;
87
+ if (actor?.__typename != null)
88
+ set.githubType = actor.__typename;
89
+ await exec.update(users).set(set).where(eq(users.id, owner.id)).execute();
90
+ cache.set(login, owner.id);
91
+ return owner.id;
92
+ }
93
+ }
48
94
  const row = (await exec
49
95
  .insert(users)
50
96
  .values({
51
97
  githubLogin: login,
52
- githubNodeId: actor?.id ?? null,
98
+ githubNodeId: nodeId,
53
99
  displayName: actor?.name ?? null,
54
100
  avatarUrl: actor?.avatarUrl ?? null,
55
101
  // GraphQL __typename ('User'|'Bot'|…) when the actor carried it (the fat
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pierre-review",
3
- "version": "0.1.72",
3
+ "version": "0.1.73",
4
4
  "description": "Dashboard for tracking your team's GitHub PR activity across repos — local (SQLite + gh) or self-hosted multi-tenant cloud (Postgres + GitHub App).",
5
5
  "type": "module",
6
6
  "author": "Alex Wakeman",