@rsktash/beads-ui 0.1.40 → 0.1.42

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rsktash/beads-ui",
3
- "version": "0.1.40",
3
+ "version": "0.1.42",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/rsktash/beads-ui.git"
@@ -80,6 +80,121 @@ function buildPagination(pagination) {
80
80
  return { limitClause };
81
81
  }
82
82
 
83
+ /**
84
+ * Enrich list items with parent_title, children counts, and blocked_by.
85
+ * Runs batch queries to avoid N+1.
86
+ *
87
+ * @param {Array<Record<string, unknown>>} items
88
+ * @returns {Promise<Array<Record<string, unknown>>>}
89
+ */
90
+ export async function enrichListItems(items) {
91
+ const pool = getPool();
92
+ if (!pool || items.length === 0) {
93
+ log('enrichListItems skip: pool=%s items=%d', !!pool, items.length);
94
+ return items;
95
+ }
96
+
97
+ const ids = items.map(i => String(i.id));
98
+ log('enrichListItems: enriching %d items', ids.length);
99
+
100
+ try {
101
+ // 1. Resolve parent IDs — use the `parent` field already present from the
102
+ // LEFT JOIN in query functions, falling back to a dependencies lookup.
103
+ /** @type {Map<string, string>} */
104
+ const parentIdMap = new Map();
105
+ for (const item of items) {
106
+ const p = item.parent ?? item.parent_id;
107
+ if (p && typeof p === 'string' && p.length > 0) {
108
+ parentIdMap.set(String(item.id), p);
109
+ }
110
+ }
111
+
112
+ // If items didn't carry a `parent` column, query dependencies table
113
+ if (parentIdMap.size === 0 && ids.length > 0) {
114
+ const [parentDepRows] = await pool.query(
115
+ `SELECT issue_id, depends_on_id FROM dependencies
116
+ WHERE type = 'parent-child' AND issue_id IN (${ids.map(() => '?').join(',')})`,
117
+ ids
118
+ );
119
+ for (const r of /** @type {any[]} */ (parentDepRows)) {
120
+ parentIdMap.set(r.issue_id, r.depends_on_id);
121
+ }
122
+ }
123
+ log('enrichListItems: %d parent mappings found', parentIdMap.size);
124
+
125
+ // 2. Batch parent titles
126
+ const parentIds = [...new Set(parentIdMap.values())];
127
+ /** @type {Map<string, string>} */
128
+ const parentTitles = new Map();
129
+ if (parentIds.length > 0) {
130
+ const [rows] = await pool.query(
131
+ `SELECT id, title FROM issues WHERE id IN (${parentIds.map(() => '?').join(',')})`,
132
+ parentIds
133
+ );
134
+ for (const r of /** @type {any[]} */ (rows)) parentTitles.set(r.id, r.title);
135
+ }
136
+
137
+ // 3. Batch children counts
138
+ /** @type {Map<string, { total: number, closed: number }>} */
139
+ const childCounts = new Map();
140
+ if (ids.length > 0) {
141
+ const [childRows] = await pool.query(
142
+ `SELECT depends_on_id AS pid, COUNT(*) AS total,
143
+ SUM(CASE WHEN i.status = 'closed' THEN 1 ELSE 0 END) AS closed
144
+ FROM dependencies d JOIN issues i ON i.id = d.issue_id
145
+ WHERE d.type = 'parent-child' AND d.depends_on_id IN (${ids.map(() => '?').join(',')})
146
+ GROUP BY d.depends_on_id`,
147
+ ids
148
+ );
149
+ for (const r of /** @type {any[]} */ (childRows)) {
150
+ childCounts.set(r.pid, { total: Number(r.total), closed: Number(r.closed) });
151
+ }
152
+ }
153
+ log('enrichListItems: %d items have children', childCounts.size);
154
+
155
+ // 4. Batch blocked-by (open issues that block each item)
156
+ /** @type {Map<string, Array<{ id: string, title: string }>>} */
157
+ const blockedBy = new Map();
158
+ if (ids.length > 0) {
159
+ const [blockRows] = await pool.query(
160
+ `SELECT d.issue_id, d.depends_on_id, i.title AS blocker_title
161
+ FROM dependencies d JOIN issues i ON i.id = d.depends_on_id
162
+ WHERE d.type = 'blocks' AND d.issue_id IN (${ids.map(() => '?').join(',')})
163
+ AND i.status != 'closed'`,
164
+ ids
165
+ );
166
+ for (const r of /** @type {any[]} */ (blockRows)) {
167
+ if (!blockedBy.has(r.issue_id)) blockedBy.set(r.issue_id, []);
168
+ blockedBy.get(r.issue_id).push({ id: r.depends_on_id, title: r.blocker_title });
169
+ }
170
+ }
171
+ log('enrichListItems: %d items have blockers', blockedBy.size);
172
+
173
+ return items.map(item => {
174
+ const id = String(item.id);
175
+ const enriched = { ...item };
176
+ const pid = parentIdMap.get(id);
177
+ if (pid) {
178
+ enriched.parent_id = pid;
179
+ if (parentTitles.has(pid)) enriched.parent_title = parentTitles.get(pid);
180
+ }
181
+ const cc = childCounts.get(id);
182
+ if (cc) {
183
+ enriched.total_children = cc.total;
184
+ enriched.closed_children = cc.closed;
185
+ }
186
+ const bb = blockedBy.get(id);
187
+ if (bb && bb.length > 0) {
188
+ enriched.blocked_by = bb;
189
+ }
190
+ return enriched;
191
+ });
192
+ } catch (err) {
193
+ log('enrichListItems error: %o', err);
194
+ return items;
195
+ }
196
+ }
197
+
83
198
  /**
84
199
  * Fetch total count for a WHERE clause.
85
200
  *