wuffle 0.70.0 → 0.71.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.
@@ -1,6 +1,5 @@
1
1
  import { promises as fs } from 'node:fs';
2
2
  import path from 'node:path';
3
- import { mkdirp } from 'mkdirp';
4
3
 
5
4
 
6
5
  /**
@@ -27,7 +26,7 @@ export default function DumpStoreLocal(logger, store, events) {
27
26
  // io helpers
28
27
 
29
28
  function upload(dump) {
30
- return mkdirp(path.dirname(storeLocation)).then(
29
+ return fs.mkdir(path.dirname(storeLocation), { recursive: true }).then(
31
30
  () => fs.writeFile(storeLocation, dump, 'utf8')
32
31
  );
33
32
  }
@@ -1,5 +1,3 @@
1
- import { mkdirp } from 'mkdirp';
2
-
3
1
  import path from 'node:path';
4
2
  import { promises as fs } from 'node:fs';
5
3
 
@@ -39,7 +37,7 @@ export default function LogEvents(logger, webhookEvents) {
39
37
 
40
38
  const data = JSON.stringify({ event, payload }, null, ' ');
41
39
 
42
- return mkdirp(eventsDir).then(() => {
40
+ return fs.mkdir(eventsDir, { recursive: true }).then(() => {
43
41
  const fileName = path.join(eventsDir, `${Date.now()}-${counter++}-${name}.json`);
44
42
 
45
43
  return fs.writeFile(fileName, data, 'utf8');
@@ -9,6 +9,11 @@ const CHILD_LINK_TYPES = {
9
9
 
10
10
  /**
11
11
  * @typedef { { defaultFilter?: string } } SearchConfig
12
+ * @typedef { import('../../util/search.js').SearchTerm } SearchTerm
13
+ *
14
+ * @typedef { import('../../types.js').GitHubUser } GitHubUser
15
+ *
16
+ * @typedef { (issue: any) => boolean } FilterFn
12
17
  */
13
18
 
14
19
  /**
@@ -318,71 +323,95 @@ export default function Search(config, logger, store) {
318
323
  };
319
324
  }
320
325
 
321
- function buildFilterFns(search, user) {
326
+ /**
327
+ * @param {SearchTerm} term
328
+ * @param {any} user
329
+ *
330
+ * @return {FilterFn}
331
+ */
332
+ function buildTermFn(term, user) {
333
+ let {
334
+ qualifier,
335
+ value,
336
+ negated,
337
+ exact
338
+ } = term;
322
339
 
323
- const terms = search ? parseSearch(search) : [];
340
+ const wrap = (fn) => negated ? ((issue) => !fn(issue)) : fn;
324
341
 
325
- return terms.map(term => {
326
- let {
327
- qualifier,
328
- value,
329
- negated,
330
- exact
331
- } = term;
342
+ if ([ 'or', 'and' ].includes(qualifier)) {
332
343
 
333
- if (!value) {
334
- return noopFilter();
335
- }
344
+ const childTerms = /** @type {SearchTerm[]} */ (value);
336
345
 
337
- if (value === '@me') {
338
- if (!user) {
339
- return noneFilter();
340
- }
346
+ const filterFns = childTerms.map(childTerm => buildTermFn(childTerm, user));
341
347
 
342
- value = user.login;
343
- exact = true;
344
- }
348
+ return wrap(
349
+ (issue) => filterFns[qualifier === 'or' ? 'some' : 'every'](
350
+ filterFn => filterFn(issue)
351
+ )
352
+ );
353
+ }
345
354
 
346
- const factoryFn = filters[qualifier];
355
+ if (!value) {
356
+ return noopFilter();
357
+ }
347
358
 
348
- if (!factoryFn) {
349
- return noopFilter();
359
+ if (value === '@me') {
360
+ if (!user) {
361
+ return noneFilter();
350
362
  }
351
363
 
352
- const fn = factoryFn(value, exact);
364
+ value = user.login;
365
+ exact = true;
366
+ }
353
367
 
354
- if (negated) {
355
- return function(arg) {
356
- return !fn(arg);
357
- };
358
- }
368
+ const factoryFn = filters[qualifier];
369
+
370
+ if (!factoryFn) {
371
+ return noopFilter();
372
+ }
359
373
 
360
- return fn;
361
- });
374
+ return wrap(factoryFn(value, exact));
375
+ }
376
+
377
+ function buildFilterFn(search, user) {
378
+
379
+ const term = search && parseSearch(search);
380
+
381
+ if (!term) {
382
+ return null;
383
+ }
384
+
385
+ return buildTermFn(term);
362
386
  }
363
387
 
364
388
  /**
365
389
  * Retrieve a filter function from the given search string.
366
390
  *
367
391
  * @param {string} search
368
- * @param {import('../../types.js').GitHubUser} [user]
392
+ * @param {GitHubUser} [user]
369
393
  *
370
- * @return {Function}
394
+ * @return {FilterFn}
371
395
  */
372
396
  function getSearchFilter(search, user) {
373
397
 
374
- const filterFns = buildFilterFns(search, user);
398
+ const filterFn = buildFilterFn(search, user);
375
399
 
376
- const ignoreFilterFns = buildFilterFns(config.defaultFilter, user);
400
+ const ignoreFilterFn = buildFilterFn(config.defaultFilter, user);
377
401
 
378
402
  return function(issue) {
379
403
  try {
380
- if (filterFns.length) {
381
- return filterFns.every(fn => fn(issue));
382
- } else {
404
+ if (filterFn) {
405
+ return filterFn(issue);
406
+ }
383
407
 
384
- return ignoreFilterFns.every(fn => fn(issue));
408
+ if (ignoreFilterFn) {
409
+ return ignoreFilterFn(issue);
385
410
  }
411
+
412
+ // no user search, no ignore filter,
413
+ // show all issues
414
+ return true;
386
415
  } catch (err) {
387
416
  log.warn({ issue: issueIdent(issue), err }, 'filter failed');
388
417
 
@@ -54,58 +54,149 @@ function startOfDay(time) {
54
54
  return date.getTime();
55
55
  }
56
56
 
57
+ /**
58
+ * @typedef { {
59
+ * qualifier: string,
60
+ * exact?: boolean,
61
+ * negated?: boolean,
62
+ * value?: string|SearchTerm[],
63
+ * } } SearchTerm
64
+ */
65
+
57
66
  /**
58
67
  * @param {string} str
59
68
  *
60
- * @return { {
61
- * qualifier: string,
62
- * value: string|undefined,
63
- * exact: boolean,
64
- * negated?: boolean
65
- * }[] }
69
+ * @return {SearchTerm}
66
70
  */
67
71
  export function parseSearch(str) {
68
72
 
69
- const regexp = /(?:([\w#/&]+)|"([\w#/&\s-.]+)"|([-!]?)([\w]+):(?:([\w-#/&@<>=.]+)|"([\w-#/&@:.,; ]+)")?)(?:\s|$)/g;
73
+ const regexp = /(\()|(\))|(OR)\s*|(AND)\s|([-!]|NOT\s+)?(?:"([^"]+)"|([\w#/&]+)(?:(:)(?:([\w-#/&@<>=.]+)|"([^"]+)")?)?)/g;
70
74
 
71
- const terms = [];
75
+ const stack = [ {
76
+ qualifier: 'and',
77
+ value: /** @type {SearchTerm[]} */ ([])
78
+ } ];
72
79
 
73
80
  let match;
74
81
 
75
82
  while ((match = regexp.exec(str))) {
76
83
 
84
+ const term = stack[stack.length - 1];
85
+
77
86
  const [
78
87
  _,
79
- text,
80
- textEscaped,
88
+ openGroup,
89
+ closeGroup,
90
+ or,
91
+ and,
81
92
  negated,
93
+ textEscaped,
94
+ text,
82
95
  qualifier,
83
96
  qualifierText,
84
97
  qualifierTextEscaped
85
98
  ] = match;
86
99
 
100
+ if (openGroup) {
101
+ const groupTerm = {
102
+ qualifier: 'and',
103
+ value: [],
104
+ group: true
105
+ };
87
106
 
88
- const textValue = text || textEscaped;
107
+ stack.push(groupTerm);
108
+ term.value.push(groupTerm);
89
109
 
90
- if (textValue) {
91
- terms.push({
92
- qualifier: 'text',
93
- value: textValue,
94
- exact: !!textEscaped
95
- });
110
+ continue;
111
+ }
112
+
113
+ if (closeGroup) {
114
+
115
+ let lastTerm;
116
+
117
+ do {
118
+ lastTerm = stack.pop();
119
+ } while (!lastTerm.group);
120
+
121
+ continue;
122
+ }
123
+
124
+ if (and) {
125
+
126
+ // default semantics
127
+ continue;
128
+ }
129
+
130
+ if (or) {
131
+ const nextTerm = {
132
+ qualifier: 'and',
133
+ value: []
134
+ };
135
+
136
+ const groupTerm = {
137
+ qualifier: 'or',
138
+ value: [
139
+ {
140
+ qualifier: 'and',
141
+ value: term.value.slice()
142
+ },
143
+ nextTerm
144
+ ]
145
+ };
146
+
147
+ // replace existing terms with <OR> term
148
+ term.value.length = 0;
149
+ term.value.push(groupTerm);
150
+
151
+ stack.push(nextTerm);
152
+
153
+ continue;
96
154
  }
97
155
 
156
+ const textValue = text || textEscaped;
98
157
  const qualifierValue = qualifierText || qualifierTextEscaped;
99
158
 
100
159
  if (qualifier) {
101
- terms.push({
102
- qualifier,
160
+ term.value.push({
161
+ qualifier: textValue,
103
162
  value: qualifierValue,
104
163
  negated: !!negated,
105
164
  exact: !!qualifierTextEscaped
106
165
  });
166
+ } else {
167
+ term.value.push({
168
+ qualifier: 'text',
169
+ value: textValue,
170
+ exact: !!textEscaped,
171
+ negated: !!negated
172
+ });
107
173
  }
108
174
  }
109
175
 
110
- return terms;
176
+ return collapseSearch(stack[0]);
177
+ }
178
+
179
+ /**
180
+ * @param {SearchTerm} term
181
+ *
182
+ * @return {SearchTerm}
183
+ */
184
+ export function collapseSearch(term) {
185
+
186
+ if (![ 'and', 'or' ].includes(term.qualifier)) {
187
+ return term;
188
+ }
189
+
190
+ const nestedTerms = /** @type {SearchTerm[]} */ (term.value);
191
+
192
+ const nestedTermsCollapsed = nestedTerms.map(collapseSearch);
193
+
194
+ if (term.qualifier === 'and' && nestedTermsCollapsed.length === 1) {
195
+ return nestedTermsCollapsed[0];
196
+ }
197
+
198
+ return {
199
+ ...term,
200
+ value: nestedTermsCollapsed
201
+ };
111
202
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "wuffle",
3
- "version": "0.70.0",
3
+ "version": "0.71.0",
4
4
  "description": "A multi-repository task board for GitHub issues",
5
5
  "author": {
6
6
  "name": "Nico Rehwaldt",
@@ -42,7 +42,7 @@
42
42
  "auto-test": "npm test -- --watch"
43
43
  },
44
44
  "dependencies": {
45
- "@aws-sdk/client-s3": "^3.1014.0",
45
+ "@aws-sdk/client-s3": "^3.1037.0",
46
46
  "async-didi": "^1.0.0",
47
47
  "body-parser": "^2.2.2",
48
48
  "compression": "^1.8.1",
@@ -50,7 +50,6 @@
50
50
  "fake-tag": "^5.0.0",
51
51
  "memorystore": "^1.6.7",
52
52
  "min-dash": "^5.0.0",
53
- "mkdirp": "^3.0.1",
54
53
  "p-defer": "^4.0.1",
55
54
  "prexit": "^2.3.0",
56
55
  "probot": "^13.4.7",
@@ -60,15 +59,15 @@
60
59
  "@graphql-eslint/eslint-plugin": "^4.4.0",
61
60
  "@octokit/graphql-schema": "^15.25.0",
62
61
  "@types/compression": "^1.8.1",
63
- "@types/express-session": "^1.18.2",
62
+ "@types/express-session": "^1.19.0",
64
63
  "@types/mocha": "^10.0.10",
65
64
  "chai": "^6.2.2",
66
- "graphql": "^16.13.1",
65
+ "graphql": "^16.13.2",
67
66
  "mocha": "^11.7.5",
68
- "nock": "^14.0.11",
67
+ "nock": "^14.0.13",
69
68
  "nodemon": "^3.1.14",
70
69
  "npm-run-all2": "^8.0.4",
71
- "sinon": "^21.0.3",
70
+ "sinon": "^21.1.2",
72
71
  "sinon-chai": "^4.0.0",
73
72
  "typescript": "^5.9.3"
74
73
  },