wuffle 0.71.0 → 0.73.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019-present Nico Rehwaldt
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
9
+ of the Software, and to permit persons to whom the Software is furnished to do
10
+ so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -63,13 +63,15 @@ export function filterUser(user) {
63
63
  const {
64
64
  login,
65
65
  avatar_url,
66
- html_url
66
+ html_url,
67
+ type
67
68
  } = user;
68
69
 
69
70
  return {
70
71
  login,
71
72
  avatar_url,
72
- html_url
73
+ html_url,
74
+ type
73
75
  };
74
76
  }
75
77
 
@@ -138,13 +140,15 @@ export function filterReview(review) {
138
140
  const {
139
141
  state,
140
142
  user,
141
- html_url
143
+ html_url,
144
+ author_association
142
145
  } = review;
143
146
 
144
147
  return {
145
148
  state,
146
149
  html_url,
147
- user: filterUser(user)
150
+ user: filterUser(user),
151
+ author_association
148
152
  };
149
153
  }
150
154
 
@@ -173,7 +177,8 @@ export function filterPull(pullRequest) {
173
177
  order,
174
178
  column,
175
179
  check_runs = [],
176
- statuses = []
180
+ statuses = [],
181
+ author_association
177
182
  } = pullRequest;
178
183
 
179
184
  return {
@@ -198,7 +203,8 @@ export function filterPull(pullRequest) {
198
203
  order,
199
204
  column,
200
205
  statuses: statuses.map(filterStatus),
201
- check_runs: check_runs.map(filterCheckRun)
206
+ check_runs: check_runs.map(filterCheckRun),
207
+ author_association
202
208
  };
203
209
  }
204
210
 
@@ -222,7 +228,8 @@ export function filterIssue(issue) {
222
228
  comments = [],
223
229
  links = [],
224
230
  order,
225
- column
231
+ column,
232
+ author_association
226
233
  } = issue;
227
234
 
228
235
  return {
@@ -242,7 +249,8 @@ export function filterIssue(issue) {
242
249
  html_url,
243
250
  links: links.map(filterLink),
244
251
  order,
245
- column
252
+ column,
253
+ author_association
246
254
  };
247
255
 
248
256
  }
@@ -117,7 +117,8 @@ function filterReview(review) {
117
117
  submitted_at,
118
118
  state,
119
119
  user,
120
- html_url
120
+ html_url,
121
+ author_association
121
122
  } = review;
122
123
 
123
124
  return {
@@ -128,6 +129,7 @@ function filterReview(review) {
128
129
  submitted_at,
129
130
  state: state.toLowerCase(),
130
131
  user: filterUser(user),
131
- html_url
132
+ html_url,
133
+ author_association
132
134
  };
133
135
  }
@@ -8,7 +8,11 @@ const CHILD_LINK_TYPES = {
8
8
  };
9
9
 
10
10
  /**
11
- * @typedef { { defaultFilter?: string } } SearchConfig
11
+ * @typedef { {
12
+ * defaultFilter?: string,
13
+ * treatBotsAsReviewers?: boolean
14
+ * } } SearchConfig
15
+ *
12
16
  * @typedef { import('../../util/search.js').SearchTerm } SearchTerm
13
17
  *
14
18
  * @typedef { import('../../types.js').GitHubUser } GitHubUser
@@ -31,6 +35,11 @@ export default function Search(config, logger, store) {
31
35
  name: 'wuffle:search'
32
36
  });
33
37
 
38
+ const {
39
+ treatBotsAsReviewers = false,
40
+ defaultFilter
41
+ } = config;
42
+
34
43
  function filterNoop(issue) {
35
44
  return true;
36
45
  }
@@ -67,12 +76,46 @@ export default function Search(config, logger, store) {
67
76
  return issue.pull_request;
68
77
  }
69
78
 
79
+ function isValidReview(review) {
80
+
81
+ const {
82
+ user,
83
+ author_association
84
+ } = review;
85
+
86
+ if (!user) {
87
+ return false;
88
+ }
89
+
90
+ if (user.type === 'Bot') {
91
+ return treatBotsAsReviewers;
92
+ }
93
+
94
+ // backwards compatibility - we did not have that
95
+ // data stored previously
96
+ return author_association ? [ 'COLLABORATOR', 'MEMBER', 'OWNER' ].includes(author_association) : true;
97
+ }
98
+
99
+ function getEffectiveReviews(reviews) {
100
+ const byReviewer = new Map();
101
+
102
+ for (const review of reviews) {
103
+ if (review.state === 'commented' || !isValidReview(review)) continue;
104
+
105
+ byReviewer.set(review.user?.login, review);
106
+ }
107
+
108
+ return [ ...byReviewer.values() ];
109
+ }
110
+
70
111
  function isApproved(issue) {
71
- return (issue.reviews || []).some(r => r.state === 'approved');
112
+ return getEffectiveReviews(issue.reviews || []).some(
113
+ r => r.state === 'approved'
114
+ );
72
115
  }
73
116
 
74
117
  function isReviewed(issue) {
75
- return (issue.reviews || []).length > 0;
118
+ return getEffectiveReviews(issue.reviews || []).length > 0;
76
119
  }
77
120
 
78
121
  const filters = {
@@ -158,6 +201,10 @@ export default function Search(config, logger, store) {
158
201
 
159
202
  return !links || !links.some(link => CHILD_LINK_TYPES[link.type]);
160
203
  };
204
+ case 'referenced':
205
+ return function filterReferenced(issue) {
206
+ return (issue.links || []).length > 0;
207
+ };
161
208
  default:
162
209
  return filterNoop;
163
210
  }
@@ -286,7 +333,29 @@ export default function Search(config, logger, store) {
286
333
  return function filterCreated(issue) {
287
334
  return matchTemporal(issue.updated_at);
288
335
  };
289
- })
336
+ }),
337
+
338
+ 'or': function orFilter(value, _exact, user) {
339
+
340
+ const childTerms = /** @type {SearchTerm[]} */ (value);
341
+
342
+ const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
343
+
344
+ return (issue) => filterFns.some(
345
+ filterFn => filterFn(issue)
346
+ );
347
+ },
348
+
349
+ 'and': function andFilter(value, _exact, user) {
350
+
351
+ const childTerms = /** @type {SearchTerm[]} */ (value);
352
+
353
+ const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
354
+
355
+ return (issue) => filterFns.every(
356
+ filterFn => filterFn(issue)
357
+ );
358
+ }
290
359
  };
291
360
 
292
361
  function temporalFilter(fn) {
@@ -339,19 +408,6 @@ export default function Search(config, logger, store) {
339
408
 
340
409
  const wrap = (fn) => negated ? ((issue) => !fn(issue)) : fn;
341
410
 
342
- if ([ 'or', 'and' ].includes(qualifier)) {
343
-
344
- const childTerms = /** @type {SearchTerm[]} */ (value);
345
-
346
- const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
347
-
348
- return wrap(
349
- (issue) => filterFns[qualifier === 'or' ? 'some' : 'every'](
350
- filterFn => filterFn(issue)
351
- )
352
- );
353
- }
354
-
355
411
  if (!value) {
356
412
  return noopFilter();
357
413
  }
@@ -371,7 +427,7 @@ export default function Search(config, logger, store) {
371
427
  return noopFilter();
372
428
  }
373
429
 
374
- return wrap(factoryFn(value, exact));
430
+ return wrap(factoryFn(value, exact, user));
375
431
  }
376
432
 
377
433
  function buildFilterFn(search, user) {
@@ -395,9 +451,11 @@ export default function Search(config, logger, store) {
395
451
  */
396
452
  function getSearchFilter(search, user) {
397
453
 
398
- const filterFn = buildFilterFn(search, user);
454
+ if (!search) {
455
+ search = defaultFilter;
456
+ }
399
457
 
400
- const ignoreFilterFn = buildFilterFn(config.defaultFilter, user);
458
+ const filterFn = buildFilterFn(search, user);
401
459
 
402
460
  return function(issue) {
403
461
  try {
@@ -405,12 +463,7 @@ export default function Search(config, logger, store) {
405
463
  return filterFn(issue);
406
464
  }
407
465
 
408
- if (ignoreFilterFn) {
409
- return ignoreFilterFn(issue);
410
- }
411
-
412
- // no user search, no ignore filter,
413
- // show all issues
466
+ // no search, show all issues
414
467
  return true;
415
468
  } catch (err) {
416
469
  log.warn({ issue: issueIdent(issue), err }, 'filter failed');
@@ -424,4 +477,6 @@ export default function Search(config, logger, store) {
424
477
  // api ///////////////////////
425
478
 
426
479
  this.getSearchFilter = getSearchFilter;
480
+
481
+ this.buildFilterFn = buildFilterFn;
427
482
  }
@@ -0,0 +1,31 @@
1
+ /**
2
+ * @typedef { { ignoreFilter?: string } } StoreFilterConfig
3
+ */
4
+
5
+ /**
6
+ * An component that configures the store to filter certain elements,
7
+ * effectively making them invisible to the board and its users.
8
+ *
9
+ * @param { StoreFilterConfig } config
10
+ *
11
+ * @param { import('../store.js').default } store
12
+ * @param { import('./search/Search.js').default } search
13
+ * @param { import('../types.js').Logger } logger
14
+ */
15
+ export default function StoreFilter(config, store, search, logger) {
16
+
17
+ const log = logger.child({
18
+ name: 'wuffle:store-filter'
19
+ });
20
+
21
+ if ('ignoreFilter' in config) {
22
+ const ignoreFilterFn = search.buildFilterFn(config.ignoreFilter);
23
+
24
+ if (ignoreFilterFn) {
25
+ store.setIgnoreFilter(ignoreFilterFn);
26
+ } else {
27
+ log.warn('unparseable <ignoreFilter> - please correct your board configuration');
28
+ }
29
+ }
30
+
31
+ }
package/lib/filters.js CHANGED
@@ -47,7 +47,8 @@ export function filterUser(githubUser) {
47
47
  node_id,
48
48
  login,
49
49
  avatar_url,
50
- html_url
50
+ html_url,
51
+ type
51
52
  } = githubUser;
52
53
 
53
54
  return {
@@ -55,7 +56,8 @@ export function filterUser(githubUser) {
55
56
  node_id,
56
57
  login,
57
58
  avatar_url,
58
- html_url
59
+ html_url,
60
+ type
59
61
  };
60
62
  }
61
63
 
@@ -147,7 +149,8 @@ export function filterPull(githubPull, githubRepository) {
147
149
  additions,
148
150
  deletions,
149
151
  changed_files,
150
- html_url
152
+ html_url,
153
+ author_association
151
154
  } = githubPull;
152
155
 
153
156
  // stable ID that is independent from GitHubs internal issue/pr distinction
@@ -187,7 +190,8 @@ export function filterPull(githubPull, githubRepository) {
187
190
  changed_files,
188
191
  pull_request: true,
189
192
  repository: filterRepository(githubRepository),
190
- html_url
193
+ html_url,
194
+ author_association
191
195
  };
192
196
  }
193
197
 
@@ -209,7 +213,8 @@ export function filterIssue(githubIssue, githubRepository) {
209
213
  labels,
210
214
  milestone,
211
215
  pull_request,
212
- html_url
216
+ html_url,
217
+ author_association
213
218
  } = githubIssue;
214
219
 
215
220
  // stable ID that is independent from GitHubs internal issue/pr distinction
@@ -239,7 +244,8 @@ export function filterIssue(githubIssue, githubRepository) {
239
244
  milestone: milestone ? filterMilestone(milestone) : null,
240
245
  repository: filterRepository(githubRepository),
241
246
  pull_request: !!pull_request,
242
- html_url
247
+ html_url,
248
+ author_association
243
249
  };
244
250
 
245
251
  }
package/lib/index.js CHANGED
@@ -27,7 +27,8 @@ const appModules = [
27
27
  import('./apps/auth-routes/index.js'),
28
28
  import('./apps/board-api-routes/index.js'),
29
29
  import('./apps/board-routes.js'),
30
- import('./apps/reindex-store.js')
30
+ import('./apps/reindex-store.js'),
31
+ import('./apps/store-filter.js')
31
32
  ];
32
33
 
33
34
  import loadConfig from './load-config.js';
package/lib/store.js CHANGED
@@ -4,6 +4,9 @@ import { issueIdent } from './util/index.js';
4
4
  import { findLinks } from './util/links.js';
5
5
  import { Links } from './links.js';
6
6
 
7
+ /**
8
+ * @typedef { import('./apps/search/Search.js').FilterFn } FilterFn
9
+ */
7
10
 
8
11
  /**
9
12
  * The store that holds all board data
@@ -39,6 +42,10 @@ export default class Store {
39
42
  this.updateContext = null;
40
43
  this.queuedUpdates = [];
41
44
 
45
+ /**
46
+ * @type { FilterFn }
47
+ */
48
+ this.ignoreFilter = (issue) => false;
42
49
 
43
50
  this.on('updateIssue', async event => {
44
51
 
@@ -303,7 +310,9 @@ export default class Store {
303
310
  /**
304
311
  * Update issue, tagging it as updated.
305
312
  *
306
- * @return {Promise<Object>} promise to updated issue
313
+ * If issue matches the ignore filter it will be removed as part of the update.
314
+ *
315
+ * @return {Promise<Object|undefined>} a promise to the updated issue or undefined in case the update triggered issue deletion
307
316
  */
308
317
  updateIssue(update) {
309
318
 
@@ -374,6 +383,14 @@ export default class Store {
374
383
 
375
384
  const ident = issueIdent(updatedIssue);
376
385
 
386
+ if (this.ignoreFilter(updatedIssue)) {
387
+ this.log.debug({ issue: ident }, 'issue matching ignore filter');
388
+
389
+ await this.removeIssueById(id);
390
+
391
+ return null;
392
+ }
393
+
377
394
  this.log.debug({
378
395
  issue: ident
379
396
  }, 'process update');
@@ -861,6 +878,13 @@ export default class Store {
861
878
  }, 'load JSON complete');
862
879
  }
863
880
 
881
+ /**
882
+ * @param { FilterFn } ignoreFilter
883
+ */
884
+ setIgnoreFilter(ignoreFilter) {
885
+ this.ignoreFilter = ignoreFilter;
886
+ }
887
+
864
888
  }
865
889
 
866
890
 
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "wuffle",
3
- "version": "0.71.0",
3
+ "version": "0.73.0",
4
4
  "description": "A multi-repository task board for GitHub issues",
5
5
  "author": {
6
6
  "name": "Nico Rehwaldt",
7
7
  "url": "https://github.com/nikku"
8
8
  },
9
9
  "bin": {
10
- "wuffle": "./bin/wuffle"
10
+ "wuffle": "bin/wuffle"
11
11
  },
12
12
  "exports": {
13
13
  ".": "./index.js",
@@ -19,7 +19,7 @@
19
19
  "homepage": "https://github.com/nikku/wuffle",
20
20
  "repository": {
21
21
  "type": "git",
22
- "url": "https://github.com/nikku/wuffle.git",
22
+ "url": "git+https://github.com/nikku/wuffle.git",
23
23
  "directory": "packages/app"
24
24
  },
25
25
  "keywords": [
@@ -48,7 +48,7 @@
48
48
  "compression": "^1.8.1",
49
49
  "express-session": "^1.19.0",
50
50
  "fake-tag": "^5.0.0",
51
- "memorystore": "^1.6.7",
51
+ "memorystore": "^1.6.8",
52
52
  "min-dash": "^5.0.0",
53
53
  "p-defer": "^4.0.1",
54
54
  "prexit": "^2.3.0",
@@ -81,5 +81,6 @@
81
81
  "app.yml",
82
82
  "index.js",
83
83
  "wuffle.config.example.js"
84
- ]
84
+ ],
85
+ "gitHead": "a58cc7fcd73db7a1dc7babc25b97bb590119b69d"
85
86
  }