parser.tsx 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428
  1. import * as Sentry from '@sentry/react';
  2. import merge from 'lodash/merge';
  3. import moment from 'moment-timezone';
  4. import type {LocationRange} from 'pegjs';
  5. import {t} from 'sentry/locale';
  6. import type {TagCollection} from 'sentry/types/group';
  7. import {
  8. isMeasurement,
  9. isSpanOperationBreakdownField,
  10. measurementType,
  11. } from 'sentry/utils/discover/fields';
  12. import grammar from './grammar.pegjs';
  13. import {getKeyName} from './utils';
  14. type TextFn = () => string;
  15. type LocationFn = () => LocationRange;
  16. type ListItem<V> = [
  17. space: ReturnType<TokenConverter['tokenSpaces']>,
  18. comma: string,
  19. space: ReturnType<TokenConverter['tokenSpaces']>,
  20. value?: [notComma: undefined, value: V | null],
  21. ];
  22. const listJoiner = <K,>([s1, comma, s2, value]: ListItem<K>) => {
  23. return {
  24. separator: [s1.value, comma, s2.value].join(''),
  25. value: value ? value[1] : null,
  26. };
  27. };
  28. /**
  29. * A token represents a node in the syntax tree. These are all extrapolated
  30. * from the grammar and may not be named exactly the same.
  31. */
  32. export enum Token {
  33. SPACES = 'spaces',
  34. FILTER = 'filter',
  35. FREE_TEXT = 'freeText',
  36. LOGIC_GROUP = 'logicGroup',
  37. LOGIC_BOOLEAN = 'logicBoolean',
  38. KEY_SIMPLE = 'keySimple',
  39. KEY_EXPLICIT_TAG = 'keyExplicitTag',
  40. KEY_AGGREGATE = 'keyAggregate',
  41. KEY_AGGREGATE_ARGS = 'keyAggregateArgs',
  42. KEY_AGGREGATE_PARAMS = 'keyAggregateParam',
  43. L_PAREN = 'lParen',
  44. R_PAREN = 'rParen',
  45. VALUE_ISO_8601_DATE = 'valueIso8601Date',
  46. VALUE_RELATIVE_DATE = 'valueRelativeDate',
  47. VALUE_DURATION = 'valueDuration',
  48. VALUE_SIZE = 'valueSize',
  49. VALUE_PERCENTAGE = 'valuePercentage',
  50. VALUE_BOOLEAN = 'valueBoolean',
  51. VALUE_NUMBER = 'valueNumber',
  52. VALUE_TEXT = 'valueText',
  53. VALUE_NUMBER_LIST = 'valueNumberList',
  54. VALUE_TEXT_LIST = 'valueTextList',
  55. }
  56. /**
  57. * An operator in a key value term
  58. */
  59. export enum TermOperator {
  60. DEFAULT = '',
  61. GREATER_THAN_EQUAL = '>=',
  62. LESS_THAN_EQUAL = '<=',
  63. GREATER_THAN = '>',
  64. LESS_THAN = '<',
  65. EQUAL = '=',
  66. NOT_EQUAL = '!=',
  67. }
  68. /**
  69. * Logic operators
  70. */
  71. export enum BooleanOperator {
  72. AND = 'AND',
  73. OR = 'OR',
  74. }
  75. /**
  76. * The Token.Filter may be one of many types of filters. This enum declares the
  77. * each variant filter type.
  78. */
  79. export enum FilterType {
  80. TEXT = 'text',
  81. TEXT_IN = 'textIn',
  82. DATE = 'date',
  83. SPECIFIC_DATE = 'specificDate',
  84. RELATIVE_DATE = 'relativeDate',
  85. DURATION = 'duration',
  86. SIZE = 'size',
  87. NUMERIC = 'numeric',
  88. NUMERIC_IN = 'numericIn',
  89. BOOLEAN = 'boolean',
  90. AGGREGATE_DURATION = 'aggregateDuration',
  91. AGGREGATE_SIZE = 'aggregateSize',
  92. AGGREGATE_PERCENTAGE = 'aggregatePercentage',
  93. AGGREGATE_NUMERIC = 'aggregateNumeric',
  94. AGGREGATE_DATE = 'aggregateDate',
  95. AGGREGATE_RELATIVE_DATE = 'aggregateRelativeDate',
  96. HAS = 'has',
  97. IS = 'is',
  98. }
  99. export const allOperators = [
  100. TermOperator.DEFAULT,
  101. TermOperator.GREATER_THAN_EQUAL,
  102. TermOperator.LESS_THAN_EQUAL,
  103. TermOperator.GREATER_THAN,
  104. TermOperator.LESS_THAN,
  105. TermOperator.EQUAL,
  106. TermOperator.NOT_EQUAL,
  107. ] as const;
  108. const basicOperators = [TermOperator.DEFAULT, TermOperator.NOT_EQUAL] as const;
  109. /**
  110. * Map of certain filter types to other filter types with applicable operators
  111. * e.g. SpecificDate can use the operators from Date to become a Date filter.
  112. */
  113. export const interchangeableFilterOperators = {
  114. [FilterType.SPECIFIC_DATE]: [FilterType.DATE],
  115. [FilterType.DATE]: [FilterType.SPECIFIC_DATE],
  116. };
  117. const textKeys = [Token.KEY_SIMPLE, Token.KEY_EXPLICIT_TAG] as const;
  118. /**
  119. * This constant-type configuration object declares how each filter type
  120. * operates. Including what types of keys, operators, and values it may
  121. * receive.
  122. *
  123. * This configuration is used to generate the discriminate Filter type that is
  124. * returned from the tokenFilter converter.
  125. */
  126. export const filterTypeConfig = {
  127. [FilterType.TEXT]: {
  128. validKeys: textKeys,
  129. validOps: basicOperators,
  130. validValues: [Token.VALUE_TEXT],
  131. canNegate: true,
  132. },
  133. [FilterType.TEXT_IN]: {
  134. validKeys: textKeys,
  135. validOps: basicOperators,
  136. validValues: [Token.VALUE_TEXT_LIST],
  137. canNegate: true,
  138. },
  139. [FilterType.DATE]: {
  140. validKeys: [Token.KEY_SIMPLE],
  141. validOps: allOperators,
  142. validValues: [Token.VALUE_ISO_8601_DATE],
  143. canNegate: false,
  144. },
  145. [FilterType.SPECIFIC_DATE]: {
  146. validKeys: [Token.KEY_SIMPLE],
  147. validOps: [],
  148. validValues: [Token.VALUE_ISO_8601_DATE],
  149. canNegate: false,
  150. },
  151. [FilterType.RELATIVE_DATE]: {
  152. validKeys: [Token.KEY_SIMPLE],
  153. validOps: [],
  154. validValues: [Token.VALUE_RELATIVE_DATE],
  155. canNegate: false,
  156. },
  157. [FilterType.DURATION]: {
  158. validKeys: [Token.KEY_SIMPLE],
  159. validOps: allOperators,
  160. validValues: [Token.VALUE_DURATION],
  161. canNegate: true,
  162. },
  163. [FilterType.SIZE]: {
  164. validKeys: [Token.KEY_SIMPLE],
  165. validOps: allOperators,
  166. validValues: [Token.VALUE_SIZE],
  167. canNegate: true,
  168. },
  169. [FilterType.NUMERIC]: {
  170. validKeys: [Token.KEY_SIMPLE],
  171. validOps: allOperators,
  172. validValues: [Token.VALUE_NUMBER],
  173. canNegate: true,
  174. },
  175. [FilterType.NUMERIC_IN]: {
  176. validKeys: [Token.KEY_SIMPLE],
  177. validOps: basicOperators,
  178. validValues: [Token.VALUE_NUMBER_LIST],
  179. canNegate: true,
  180. },
  181. [FilterType.BOOLEAN]: {
  182. validKeys: [Token.KEY_SIMPLE],
  183. validOps: basicOperators,
  184. validValues: [Token.VALUE_BOOLEAN],
  185. canNegate: true,
  186. },
  187. [FilterType.AGGREGATE_DURATION]: {
  188. validKeys: [Token.KEY_AGGREGATE],
  189. validOps: allOperators,
  190. validValues: [Token.VALUE_DURATION],
  191. canNegate: true,
  192. },
  193. [FilterType.AGGREGATE_SIZE]: {
  194. validKeys: [Token.KEY_AGGREGATE],
  195. validOps: allOperators,
  196. validValues: [Token.VALUE_SIZE],
  197. canNegate: true,
  198. },
  199. [FilterType.AGGREGATE_NUMERIC]: {
  200. validKeys: [Token.KEY_AGGREGATE],
  201. validOps: allOperators,
  202. validValues: [Token.VALUE_NUMBER],
  203. canNegate: true,
  204. },
  205. [FilterType.AGGREGATE_PERCENTAGE]: {
  206. validKeys: [Token.KEY_AGGREGATE],
  207. validOps: allOperators,
  208. validValues: [Token.VALUE_PERCENTAGE],
  209. canNegate: true,
  210. },
  211. [FilterType.AGGREGATE_DATE]: {
  212. validKeys: [Token.KEY_AGGREGATE],
  213. validOps: allOperators,
  214. validValues: [Token.VALUE_ISO_8601_DATE],
  215. canNegate: true,
  216. },
  217. [FilterType.AGGREGATE_RELATIVE_DATE]: {
  218. validKeys: [Token.KEY_AGGREGATE],
  219. validOps: allOperators,
  220. validValues: [Token.VALUE_RELATIVE_DATE],
  221. canNegate: true,
  222. },
  223. [FilterType.HAS]: {
  224. validKeys: [Token.KEY_SIMPLE],
  225. validOps: basicOperators,
  226. validValues: [],
  227. canNegate: true,
  228. },
  229. [FilterType.IS]: {
  230. validKeys: [Token.KEY_SIMPLE],
  231. validOps: basicOperators,
  232. validValues: [Token.VALUE_TEXT],
  233. canNegate: true,
  234. },
  235. } as const;
  236. type FilterTypeConfig = typeof filterTypeConfig;
  237. /**
  238. * The invalid reason is used to mark fields invalid fields and can be
  239. * used to determine why the field was invalid. This is primarily use for the
  240. * invalidMessages option
  241. */
  242. export enum InvalidReason {
  243. FREE_TEXT_NOT_ALLOWED = 'free-text-not-allowed',
  244. WILDCARD_NOT_ALLOWED = 'wildcard-not-allowed',
  245. LOGICAL_OR_NOT_ALLOWED = 'logic-or-not-allowed',
  246. LOGICAL_AND_NOT_ALLOWED = 'logic-and-not-allowed',
  247. NEGATION_NOT_ALLOWED = 'negation-not-allowed',
  248. MUST_BE_QUOTED = 'must-be-quoted',
  249. FILTER_MUST_HAVE_VALUE = 'filter-must-have-value',
  250. INVALID_BOOLEAN = 'invalid-boolean',
  251. INVALID_FILE_SIZE = 'invalid-file-size',
  252. INVALID_NUMBER = 'invalid-number',
  253. EMPTY_VALUE_IN_LIST_NOT_ALLOWED = 'empty-value-in-list-not-allowed',
  254. EMPTY_PARAMETER_NOT_ALLOWED = 'empty-parameter-not-allowed',
  255. INVALID_KEY = 'invalid-key',
  256. INVALID_DURATION = 'invalid-duration',
  257. INVALID_DATE_FORMAT = 'invalid-date-format',
  258. PARENS_NOT_ALLOWED = 'parens-not-allowed',
  259. }
  260. /**
  261. * Object representing an invalid filter state
  262. */
  263. type InvalidFilter = {
  264. /**
  265. * The message indicating why the filter is invalid
  266. */
  267. reason: string;
  268. /**
  269. * The invalid reason type
  270. */
  271. type: InvalidReason;
  272. /**
  273. * In the case where a filter is invalid, we may be expecting a different
  274. * type for this filter based on the key. This can be useful to hint to the
  275. * user what values they should be providing.
  276. *
  277. * This may be multiple filter types.
  278. */
  279. expectedType?: FilterType[];
  280. };
  281. type FilterMap = {
  282. [F in keyof FilterTypeConfig]: {
  283. /**
  284. * The filter type being represented
  285. */
  286. filter: F;
  287. /**
  288. * When a filter is marked as 'invalid' a reason is given. If the filter is
  289. * not invalid this will always be null
  290. */
  291. invalid: InvalidFilter | null;
  292. /**
  293. * The key of the filter
  294. */
  295. key: KVConverter<FilterTypeConfig[F]['validKeys'][number]>;
  296. /**
  297. * Indicates if the filter has been negated
  298. */
  299. negated: FilterTypeConfig[F]['canNegate'] extends true ? boolean : false;
  300. /**
  301. * The operator applied to the filter
  302. */
  303. operator: FilterTypeConfig[F]['validOps'][number];
  304. type: Token.FILTER;
  305. /**
  306. * The value of the filter
  307. */
  308. value: KVConverter<FilterTypeConfig[F]['validValues'][number]>;
  309. /**
  310. * A warning message associated with this filter
  311. */
  312. warning: React.ReactNode;
  313. };
  314. };
  315. type TextFilter = FilterMap[FilterType.TEXT];
  316. type InFilter = FilterMap[FilterType.TEXT_IN] | FilterMap[FilterType.NUMERIC_IN];
  317. type AggregateFilterType =
  318. | FilterMap[FilterType.AGGREGATE_DATE]
  319. | FilterMap[FilterType.AGGREGATE_DURATION]
  320. | FilterMap[FilterType.AGGREGATE_NUMERIC]
  321. | FilterMap[FilterType.AGGREGATE_PERCENTAGE]
  322. | FilterMap[FilterType.AGGREGATE_RELATIVE_DATE]
  323. | FilterMap[FilterType.AGGREGATE_SIZE];
  324. /**
  325. * The Filter type discriminates on the FilterType enum using the `filter` key.
  326. *
  327. * When receiving this type you may narrow it to a specific filter by checking
  328. * this field. This will give you proper types on what the key, value, and
  329. * operator results are.
  330. */
  331. type FilterResult = FilterMap[FilterType];
  332. type TokenConverterOpts = {
  333. config: SearchConfig;
  334. location: LocationFn;
  335. text: TextFn;
  336. };
  337. /**
  338. * Used to construct token results via the token grammar
  339. */
  340. export class TokenConverter {
  341. text: TextFn;
  342. location: LocationFn;
  343. config: SearchConfig;
  344. constructor({text, location, config}: TokenConverterOpts) {
  345. this.text = text;
  346. this.location = location;
  347. this.config = config;
  348. }
  349. /**
  350. * Validates various types of keys
  351. */
  352. keyValidation = {
  353. isNumeric: (key: string) =>
  354. this.config.numericKeys.has(key) ||
  355. isMeasurement(key) ||
  356. isSpanOperationBreakdownField(key),
  357. isBoolean: (key: string) => this.config.booleanKeys.has(key),
  358. isPercentage: (key: string) => this.config.percentageKeys.has(key),
  359. isDate: (key: string) => this.config.dateKeys.has(key),
  360. isDuration: (key: string) =>
  361. this.config.durationKeys.has(key) ||
  362. isSpanOperationBreakdownField(key) ||
  363. measurementType(key) === 'duration',
  364. isSize: (key: string) => this.config.sizeKeys.has(key),
  365. };
  366. /**
  367. * Creates shared `text` and `location` keys.
  368. */
  369. get defaultTokenFields() {
  370. return {
  371. text: this.text(),
  372. location: this.location(),
  373. };
  374. }
  375. tokenSpaces = (value: string) => ({
  376. ...this.defaultTokenFields,
  377. type: Token.SPACES as const,
  378. value,
  379. });
  380. tokenFilter = <T extends FilterType>(
  381. filter: T,
  382. key: FilterMap[T]['key'],
  383. value: FilterMap[T]['value'],
  384. operator: FilterMap[T]['operator'] | undefined,
  385. negated: FilterMap[T]['negated']
  386. ) => {
  387. const filterToken = {
  388. type: Token.FILTER as const,
  389. filter,
  390. key,
  391. value,
  392. negated,
  393. operator: operator ?? TermOperator.DEFAULT,
  394. invalid: this.checkInvalidFilter(filter, key, value, negated),
  395. warning: this.checkFilterWarning(key),
  396. } as FilterResult;
  397. return {
  398. ...this.defaultTokenFields,
  399. ...filterToken,
  400. };
  401. };
  402. tokenLParen = (value: '(') => ({
  403. ...this.defaultTokenFields,
  404. type: Token.L_PAREN as const,
  405. value,
  406. invalid: this.checkInvalidParen(),
  407. });
  408. tokenRParen = (value: ')') => ({
  409. ...this.defaultTokenFields,
  410. type: Token.R_PAREN as const,
  411. value,
  412. invalid: this.checkInvalidParen(),
  413. });
  414. tokenFreeText = (value: string, quoted: boolean) => ({
  415. ...this.defaultTokenFields,
  416. type: Token.FREE_TEXT as const,
  417. value,
  418. quoted,
  419. invalid: this.checkInvalidFreeText(value),
  420. });
  421. tokenLogicGroup = (
  422. inner: Array<
  423. | ReturnType<TokenConverter['tokenLogicBoolean']>
  424. | ReturnType<TokenConverter['tokenFilter']>
  425. | ReturnType<TokenConverter['tokenFreeText']>
  426. >
  427. ) => ({
  428. ...this.defaultTokenFields,
  429. type: Token.LOGIC_GROUP as const,
  430. inner,
  431. });
  432. tokenLogicBoolean = (bool: BooleanOperator) => ({
  433. ...this.defaultTokenFields,
  434. type: Token.LOGIC_BOOLEAN as const,
  435. value: bool,
  436. invalid: this.checkInvalidLogicalBoolean(bool),
  437. });
  438. tokenKeySimple = (value: string, quoted: boolean) => ({
  439. ...this.defaultTokenFields,
  440. type: Token.KEY_SIMPLE as const,
  441. value,
  442. quoted,
  443. });
  444. tokenKeyExplicitTag = (
  445. prefix: string,
  446. key: ReturnType<TokenConverter['tokenKeySimple']>
  447. ) => ({
  448. ...this.defaultTokenFields,
  449. type: Token.KEY_EXPLICIT_TAG as const,
  450. prefix,
  451. key,
  452. });
  453. tokenKeyAggregateParam = (value: string, quoted: boolean) => ({
  454. ...this.defaultTokenFields,
  455. type: Token.KEY_AGGREGATE_PARAMS as const,
  456. value,
  457. quoted,
  458. });
  459. tokenKeyAggregate = (
  460. name: ReturnType<TokenConverter['tokenKeySimple']>,
  461. args: ReturnType<TokenConverter['tokenKeyAggregateArgs']> | null,
  462. argsSpaceBefore: ReturnType<TokenConverter['tokenSpaces']>,
  463. argsSpaceAfter: ReturnType<TokenConverter['tokenSpaces']>
  464. ) => ({
  465. ...this.defaultTokenFields,
  466. type: Token.KEY_AGGREGATE as const,
  467. name,
  468. args,
  469. argsSpaceBefore,
  470. argsSpaceAfter,
  471. });
  472. tokenKeyAggregateArgs = (
  473. arg1: ReturnType<TokenConverter['tokenKeyAggregateParam']>,
  474. args: ListItem<ReturnType<TokenConverter['tokenKeyAggregateParam']>>[]
  475. ) => {
  476. return {
  477. ...this.defaultTokenFields,
  478. type: Token.KEY_AGGREGATE_ARGS as const,
  479. args: [{separator: '', value: arg1}, ...args.map(listJoiner)],
  480. };
  481. };
  482. tokenValueIso8601Date = (
  483. value: string,
  484. date: Array<string | string[]>,
  485. time?: Array<string | string[] | Array<string[]>>,
  486. tz?: Array<string | string[]>
  487. ) => ({
  488. ...this.defaultTokenFields,
  489. type: Token.VALUE_ISO_8601_DATE as const,
  490. value: value,
  491. parsed: this.config.parse ? parseDate(value) : undefined,
  492. date: date.flat().join(''),
  493. time: Array.isArray(time) ? time.flat().flat().join('').replace('T', '') : time,
  494. tz: Array.isArray(tz) ? tz.flat().join('') : tz,
  495. });
  496. tokenValueRelativeDate = (
  497. value: string,
  498. sign: '-' | '+',
  499. unit: 'w' | 'd' | 'h' | 'm'
  500. ) => ({
  501. ...this.defaultTokenFields,
  502. type: Token.VALUE_RELATIVE_DATE as const,
  503. value: value,
  504. parsed: this.config.parse ? parseRelativeDate(value, {unit, sign}) : undefined,
  505. sign,
  506. unit,
  507. });
  508. tokenValueDuration = (
  509. value: string,
  510. unit: 'ms' | 's' | 'min' | 'm' | 'hr' | 'h' | 'day' | 'd' | 'wk' | 'w'
  511. ) => ({
  512. ...this.defaultTokenFields,
  513. type: Token.VALUE_DURATION as const,
  514. value: value,
  515. parsed: this.config.parse ? parseDuration(value, unit) : undefined,
  516. unit,
  517. });
  518. tokenValueSize = (
  519. value: string,
  520. // warning: size units are case insensitive, this type is incomplete
  521. unit:
  522. | 'bit'
  523. | 'nb'
  524. | 'bytes'
  525. | 'kb'
  526. | 'mb'
  527. | 'gb'
  528. | 'tb'
  529. | 'pb'
  530. | 'eb'
  531. | 'zb'
  532. | 'yb'
  533. | 'kib'
  534. | 'mib'
  535. | 'gib'
  536. | 'tib'
  537. | 'pib'
  538. | 'eib'
  539. | 'zib'
  540. | 'yib'
  541. ) => ({
  542. ...this.defaultTokenFields,
  543. type: Token.VALUE_SIZE as const,
  544. value: value,
  545. // units are case insensitive, normalize them in their parsed representation
  546. // so that we dont have to compare all possible permutations.
  547. parsed: this.config.parse ? parseSize(value, unit) : undefined,
  548. unit,
  549. });
  550. tokenValuePercentage = (value: string) => ({
  551. ...this.defaultTokenFields,
  552. type: Token.VALUE_PERCENTAGE as const,
  553. value: value,
  554. parsed: this.config.parse ? parsePercentage(value) : undefined,
  555. });
  556. tokenValueBoolean = (value: string) => ({
  557. ...this.defaultTokenFields,
  558. type: Token.VALUE_BOOLEAN as const,
  559. value: value,
  560. parsed: this.config.parse ? parseBoolean(value) : undefined,
  561. });
  562. tokenValueNumber = (value: string, unit: 'k' | 'm' | 'b' | 'K' | 'M' | 'B') => {
  563. return {
  564. ...this.defaultTokenFields,
  565. type: Token.VALUE_NUMBER as const,
  566. value,
  567. unit,
  568. parsed: this.config.parse ? parseNumber(value, unit) : undefined,
  569. };
  570. };
  571. tokenValueNumberList = (
  572. item1: ReturnType<TokenConverter['tokenValueNumber']>,
  573. items: ListItem<ReturnType<TokenConverter['tokenValueNumber']>>[]
  574. ) => ({
  575. ...this.defaultTokenFields,
  576. type: Token.VALUE_NUMBER_LIST as const,
  577. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  578. });
  579. tokenValueTextList = (
  580. item1: ReturnType<TokenConverter['tokenValueText']>,
  581. items: ListItem<ReturnType<TokenConverter['tokenValueText']>>[]
  582. ) => ({
  583. ...this.defaultTokenFields,
  584. type: Token.VALUE_TEXT_LIST as const,
  585. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  586. });
  587. tokenValueText = (value: string, quoted: boolean) => {
  588. return {
  589. ...this.defaultTokenFields,
  590. type: Token.VALUE_TEXT as const,
  591. value,
  592. quoted,
  593. };
  594. };
  595. /**
  596. * This method is used while tokenizing to predicate whether a filter should
  597. * match or not. We do this because not all keys are valid for specific
  598. * filter types. For example, boolean filters should only match for keys
  599. * which can be filtered as booleans.
  600. *
  601. * See [0] and look for &{ predicate } to understand how predicates are
  602. * declared in the grammar
  603. *
  604. * [0]:https://pegjs.org/documentation
  605. */
  606. predicateFilter = <T extends FilterType>(type: T, key: FilterMap[T]['key']) => {
  607. const keyName = getKeyName(key);
  608. const aggregateKey = key as ReturnType<TokenConverter['tokenKeyAggregate']>;
  609. const {isNumeric, isDuration, isBoolean, isDate, isPercentage, isSize} =
  610. this.keyValidation;
  611. const checkAggregate = (check: (s: string) => boolean) =>
  612. aggregateKey.args?.args.some(arg => check(arg?.value?.value ?? ''));
  613. switch (type) {
  614. case FilterType.NUMERIC:
  615. case FilterType.NUMERIC_IN:
  616. return isNumeric(keyName);
  617. case FilterType.DURATION:
  618. return isDuration(keyName);
  619. case FilterType.SIZE:
  620. return isSize(keyName);
  621. case FilterType.BOOLEAN:
  622. return isBoolean(keyName);
  623. case FilterType.DATE:
  624. case FilterType.RELATIVE_DATE:
  625. case FilterType.SPECIFIC_DATE:
  626. return isDate(keyName);
  627. case FilterType.AGGREGATE_DURATION:
  628. return checkAggregate(isDuration);
  629. case FilterType.AGGREGATE_DATE:
  630. return checkAggregate(isDate);
  631. case FilterType.AGGREGATE_PERCENTAGE:
  632. return checkAggregate(isPercentage);
  633. default:
  634. return true;
  635. }
  636. };
  637. /**
  638. * Predicates weather a text filter have operators for specific keys.
  639. */
  640. predicateTextOperator = (key: TextFilter['key']) =>
  641. this.config.textOperatorKeys.has(getKeyName(key));
  642. /**
  643. * When flattenParenGroups is enabled, paren groups should not be parsed,
  644. * instead parsing the parens and inner group as individual tokens.
  645. */
  646. predicateParenGroup = (): boolean => {
  647. return !this.config.flattenParenGroups;
  648. };
  649. /**
  650. * Checks the validity of a free text based on the provided search configuration
  651. */
  652. checkInvalidFreeText = (value: string) => {
  653. if (this.config.disallowFreeText) {
  654. return {
  655. type: InvalidReason.FREE_TEXT_NOT_ALLOWED,
  656. reason: this.config.invalidMessages[InvalidReason.FREE_TEXT_NOT_ALLOWED],
  657. };
  658. }
  659. if (this.config.disallowWildcard && value.includes('*')) {
  660. return {
  661. type: InvalidReason.WILDCARD_NOT_ALLOWED,
  662. reason: this.config.invalidMessages[InvalidReason.WILDCARD_NOT_ALLOWED],
  663. };
  664. }
  665. return null;
  666. };
  667. /**
  668. * Checks the validity of a logical boolean filter based on the provided search configuration
  669. */
  670. checkInvalidLogicalBoolean = (value: BooleanOperator) => {
  671. if (this.config.disallowedLogicalOperators.has(value)) {
  672. if (value === BooleanOperator.OR) {
  673. return {
  674. type: InvalidReason.LOGICAL_OR_NOT_ALLOWED,
  675. reason: this.config.invalidMessages[InvalidReason.LOGICAL_OR_NOT_ALLOWED],
  676. };
  677. }
  678. if (value === BooleanOperator.AND) {
  679. return {
  680. type: InvalidReason.LOGICAL_AND_NOT_ALLOWED,
  681. reason: this.config.invalidMessages[InvalidReason.LOGICAL_AND_NOT_ALLOWED],
  682. };
  683. }
  684. }
  685. return null;
  686. };
  687. /**
  688. * Checks the validity of a parens based on the provided search configuration
  689. */
  690. checkInvalidParen = () => {
  691. if (!this.config.disallowParens) {
  692. return null;
  693. }
  694. return {
  695. type: InvalidReason.PARENS_NOT_ALLOWED,
  696. reason: this.config.invalidMessages[InvalidReason.PARENS_NOT_ALLOWED],
  697. };
  698. };
  699. /**
  700. * Checks a filter against some non-grammar validation rules
  701. */
  702. checkFilterWarning = <T extends FilterType>(key: FilterMap[T]['key']) => {
  703. if (![Token.KEY_SIMPLE, Token.KEY_EXPLICIT_TAG].includes(key.type)) {
  704. return null;
  705. }
  706. const keyName = getKeyName(
  707. key as TokenResult<Token.KEY_SIMPLE | Token.KEY_EXPLICIT_TAG>
  708. );
  709. return this.config.getFilterTokenWarning?.(keyName) ?? null;
  710. };
  711. /**
  712. * Checks a filter against some non-grammar validation rules
  713. */
  714. checkInvalidFilter = <T extends FilterType>(
  715. filter: T,
  716. key: FilterMap[T]['key'],
  717. value: FilterMap[T]['value'],
  718. negated: FilterMap[T]['negated']
  719. ) => {
  720. // Text filter is the "fall through" filter that will match when other
  721. // filter predicates fail.
  722. if (
  723. this.config.validateKeys &&
  724. this.config.supportedTags &&
  725. !this.config.supportedTags[key.text]
  726. ) {
  727. return {
  728. type: InvalidReason.INVALID_KEY,
  729. reason: t('Invalid key. "%s" is not a supported search key.', key.text),
  730. };
  731. }
  732. if (this.config.disallowNegation && negated) {
  733. return {
  734. type: InvalidReason.NEGATION_NOT_ALLOWED,
  735. reason: this.config.invalidMessages[InvalidReason.NEGATION_NOT_ALLOWED],
  736. };
  737. }
  738. if (filter === FilterType.TEXT) {
  739. return this.checkInvalidTextFilter(
  740. key as TextFilter['key'],
  741. value as TextFilter['value']
  742. );
  743. }
  744. if (filter === FilterType.IS || filter === FilterType.HAS) {
  745. return this.checkInvalidTextValue(value as TextFilter['value']);
  746. }
  747. if ([FilterType.TEXT_IN, FilterType.NUMERIC_IN].includes(filter)) {
  748. return this.checkInvalidInFilter(value as InFilter['value']);
  749. }
  750. if ('name' in key) {
  751. return this.checkInvalidAggregateKey(key);
  752. }
  753. return null;
  754. };
  755. /**
  756. * Validates text filters which may have failed predication
  757. */
  758. checkInvalidTextFilter = (key: TextFilter['key'], value: TextFilter['value']) => {
  759. // Explicit tag keys will always be treated as text filters
  760. if (key.type === Token.KEY_EXPLICIT_TAG) {
  761. return this.checkInvalidTextValue(value);
  762. }
  763. const keyName = getKeyName(key);
  764. if (this.keyValidation.isDuration(keyName)) {
  765. return {
  766. type: InvalidReason.INVALID_DURATION,
  767. reason: t('Invalid duration. Expected number followed by duration unit suffix'),
  768. expectedType: [FilterType.DURATION],
  769. };
  770. }
  771. if (this.keyValidation.isDate(keyName)) {
  772. const date = new Date();
  773. date.setSeconds(0);
  774. date.setMilliseconds(0);
  775. const example = date.toISOString();
  776. return {
  777. type: InvalidReason.INVALID_DATE_FORMAT,
  778. reason: t(
  779. 'Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. %s or %s)',
  780. example.slice(0, 10),
  781. example
  782. ),
  783. expectedType: [
  784. FilterType.DATE,
  785. FilterType.SPECIFIC_DATE,
  786. FilterType.RELATIVE_DATE,
  787. ],
  788. };
  789. }
  790. if (this.keyValidation.isBoolean(keyName)) {
  791. return {
  792. type: InvalidReason.INVALID_BOOLEAN,
  793. reason: this.config.invalidMessages[InvalidReason.INVALID_BOOLEAN],
  794. expectedType: [FilterType.BOOLEAN],
  795. };
  796. }
  797. if (this.keyValidation.isSize(keyName)) {
  798. return {
  799. type: InvalidReason.INVALID_FILE_SIZE,
  800. reason: this.config.invalidMessages[InvalidReason.INVALID_FILE_SIZE],
  801. expectedType: [FilterType.SIZE],
  802. };
  803. }
  804. if (this.keyValidation.isNumeric(keyName)) {
  805. return {
  806. type: InvalidReason.INVALID_NUMBER,
  807. reason: this.config.invalidMessages[InvalidReason.INVALID_NUMBER],
  808. expectedType: [FilterType.NUMERIC, FilterType.NUMERIC_IN],
  809. };
  810. }
  811. return this.checkInvalidTextValue(value);
  812. };
  813. /**
  814. * Validates the value of a text filter
  815. */
  816. checkInvalidTextValue = (value: TextFilter['value']) => {
  817. if (this.config.disallowWildcard && value.value.includes('*')) {
  818. return {
  819. type: InvalidReason.WILDCARD_NOT_ALLOWED,
  820. reason: this.config.invalidMessages[InvalidReason.WILDCARD_NOT_ALLOWED],
  821. };
  822. }
  823. if (!value.quoted && /(^|[^\\])"/.test(value.value)) {
  824. return {
  825. type: InvalidReason.MUST_BE_QUOTED,
  826. reason: this.config.invalidMessages[InvalidReason.MUST_BE_QUOTED],
  827. };
  828. }
  829. if (!value.quoted && value.value === '') {
  830. return {
  831. type: InvalidReason.FILTER_MUST_HAVE_VALUE,
  832. reason: this.config.invalidMessages[InvalidReason.FILTER_MUST_HAVE_VALUE],
  833. };
  834. }
  835. return null;
  836. };
  837. /**
  838. * Validates IN filter values do not have an missing elements
  839. */
  840. checkInvalidInFilter = ({items}: InFilter['value']) => {
  841. const hasEmptyValue = items.some(item => item.value === null);
  842. if (hasEmptyValue) {
  843. return {
  844. type: InvalidReason.EMPTY_VALUE_IN_LIST_NOT_ALLOWED,
  845. reason:
  846. this.config.invalidMessages[InvalidReason.EMPTY_VALUE_IN_LIST_NOT_ALLOWED],
  847. };
  848. }
  849. if (
  850. this.config.disallowWildcard &&
  851. items.some(item => item.value.value.includes('*'))
  852. ) {
  853. return {
  854. type: InvalidReason.WILDCARD_NOT_ALLOWED,
  855. reason: this.config.invalidMessages[InvalidReason.WILDCARD_NOT_ALLOWED],
  856. };
  857. }
  858. return null;
  859. };
  860. checkInvalidAggregateKey = (key: AggregateFilterType['key']) => {
  861. const hasEmptyParameter = key.args?.args.some(arg => arg.value === null);
  862. if (hasEmptyParameter) {
  863. return {
  864. type: InvalidReason.EMPTY_PARAMETER_NOT_ALLOWED,
  865. reason: this.config.invalidMessages[InvalidReason.EMPTY_PARAMETER_NOT_ALLOWED],
  866. };
  867. }
  868. return null;
  869. };
  870. }
  871. function parseDate(input: string): {value: Date} {
  872. const date = moment(input).toDate();
  873. if (isNaN(date.getTime())) {
  874. throw new Error('Invalid date');
  875. }
  876. return {value: date};
  877. }
  878. function parseRelativeDate(
  879. input: string,
  880. {sign, unit}: {sign: '-' | '+'; unit: string}
  881. ): {value: Date} {
  882. let date = new Date().getTime();
  883. const number = numeric(input);
  884. if (isNaN(date)) {
  885. throw new Error('Invalid date');
  886. }
  887. let offset: number | undefined;
  888. switch (unit) {
  889. case 'm':
  890. offset = number * 1000 * 60;
  891. break;
  892. case 'h':
  893. offset = number * 1000 * 60 * 60;
  894. break;
  895. case 'd':
  896. offset = number * 1000 * 60 * 60 * 24;
  897. break;
  898. case 'w':
  899. offset = number * 1000 * 60 * 60 * 24 * 7;
  900. break;
  901. default:
  902. throw new Error('Invalid unit');
  903. }
  904. if (offset === undefined) {
  905. throw new Error('Unreachable');
  906. }
  907. date = sign === '-' ? date - offset : date + offset;
  908. return {value: new Date(date)};
  909. }
  910. // The parser supports floats and ints, parseFloat handles both.
  911. function numeric(input: string) {
  912. const number = parseFloat(input);
  913. if (isNaN(number)) {
  914. throw new Error('Invalid number');
  915. }
  916. return number;
  917. }
  918. function parseDuration(
  919. input: string,
  920. unit: 'ms' | 's' | 'min' | 'm' | 'hr' | 'h' | 'day' | 'd' | 'wk' | 'w'
  921. ): {value: number} {
  922. let number = numeric(input);
  923. switch (unit) {
  924. case 'ms':
  925. break;
  926. case 's':
  927. number *= 1e3;
  928. break;
  929. case 'min':
  930. case 'm':
  931. number *= 1e3 * 60;
  932. break;
  933. case 'hr':
  934. case 'h':
  935. number *= 1e3 * 60 * 60;
  936. break;
  937. case 'day':
  938. case 'd':
  939. number *= 1e3 * 60 * 60 * 24;
  940. break;
  941. case 'wk':
  942. case 'w':
  943. number *= 1e3 * 60 * 60 * 24 * 7;
  944. break;
  945. default:
  946. throw new Error('Invalid unit');
  947. }
  948. return {
  949. value: number,
  950. };
  951. }
  952. function parseNumber(
  953. input: string,
  954. unit: 'k' | 'm' | 'b' | 'K' | 'M' | 'B'
  955. ): {value: number} {
  956. let number = numeric(input);
  957. switch (unit) {
  958. case 'K':
  959. case 'k':
  960. number = number * 1e3;
  961. break;
  962. case 'M':
  963. case 'm':
  964. number = number * 1e6;
  965. break;
  966. case 'B':
  967. case 'b':
  968. number = number * 1e9;
  969. break;
  970. case null:
  971. case undefined:
  972. break;
  973. default:
  974. throw new Error('Invalid unit');
  975. }
  976. return {value: number};
  977. }
  978. function parseSize(input: string, unit: string): {value: number} {
  979. if (!unit) {
  980. unit = 'bytes';
  981. }
  982. let number = numeric(input);
  983. // parser is case insensitive to units
  984. switch (unit.toLowerCase()) {
  985. case 'bit':
  986. number /= 8;
  987. break;
  988. case 'nb':
  989. number /= 2;
  990. break;
  991. case 'bytes':
  992. break;
  993. case 'kb':
  994. number *= 1000;
  995. break;
  996. case 'mb':
  997. number *= 1000 ** 2;
  998. break;
  999. case 'gb':
  1000. number *= 1000 ** 3;
  1001. break;
  1002. case 'tb':
  1003. number *= 1000 ** 4;
  1004. break;
  1005. case 'pb':
  1006. number *= 1000 ** 5;
  1007. break;
  1008. case 'eb':
  1009. number *= 1000 ** 6;
  1010. break;
  1011. case 'zb':
  1012. number *= 1000 ** 7;
  1013. break;
  1014. case 'yb':
  1015. number *= 1000 ** 8;
  1016. break;
  1017. case 'kib':
  1018. number *= 1024;
  1019. break;
  1020. case 'mib':
  1021. number *= 1024 ** 2;
  1022. break;
  1023. case 'gib':
  1024. number *= 1024 ** 3;
  1025. break;
  1026. case 'tib':
  1027. number *= 1024 ** 4;
  1028. break;
  1029. case 'pib':
  1030. number *= 1024 ** 5;
  1031. break;
  1032. case 'eib':
  1033. number *= 1024 ** 6;
  1034. break;
  1035. case 'zib':
  1036. number *= 1024 ** 7;
  1037. break;
  1038. case 'yib':
  1039. number *= 1024 ** 8;
  1040. break;
  1041. default:
  1042. throw new Error('Invalid unit');
  1043. }
  1044. return {value: number};
  1045. }
  1046. function parsePercentage(input: string): {value: number} {
  1047. return {value: numeric(input)};
  1048. }
  1049. function parseBoolean(input: string): {value: boolean} {
  1050. if (/^true$/i.test(input) || input === '1') {
  1051. return {value: true};
  1052. }
  1053. if (/^false$/i.test(input) || input === '0') {
  1054. return {value: false};
  1055. }
  1056. throw new Error('Invalid boolean');
  1057. }
  1058. /**
  1059. * Maps token conversion methods to their result types
  1060. */
  1061. type ConverterResultMap = {
  1062. [K in keyof TokenConverter & `token${string}`]: ReturnType<TokenConverter[K]>;
  1063. };
  1064. type Converter = keyof ConverterResultMap;
  1065. /**
  1066. * Converter keys specific to Key and Value tokens
  1067. */
  1068. type KVTokens = Converter & `token${'Key' | 'Value'}${string}`;
  1069. /**
  1070. * Similar to TokenResult, but only includes Key* and Value* token type
  1071. * results. This avoids a circular reference when this is used for the Filter
  1072. * token converter result
  1073. */
  1074. type KVConverter<T extends Token> = ConverterResultMap[KVTokens] & {type: T};
  1075. /**
  1076. * Each token type is discriminated by the `type` field.
  1077. */
  1078. export type TokenResult<T extends Token> = ConverterResultMap[Converter] & {type: T};
  1079. export type ParseResultToken =
  1080. | TokenResult<Token.LOGIC_BOOLEAN>
  1081. | TokenResult<Token.LOGIC_GROUP>
  1082. | TokenResult<Token.FILTER>
  1083. | TokenResult<Token.FREE_TEXT>
  1084. | TokenResult<Token.SPACES>
  1085. | TokenResult<Token.L_PAREN>
  1086. | TokenResult<Token.R_PAREN>;
  1087. /**
  1088. * Result from parsing a search query.
  1089. */
  1090. export type ParseResult = ParseResultToken[];
  1091. export type AggregateFilter = AggregateFilterType & {
  1092. location: LocationRange;
  1093. text: string;
  1094. };
  1095. /**
  1096. * Configures behavior of search parsing
  1097. */
  1098. export type SearchConfig = {
  1099. /**
  1100. * Keys considered valid for boolean filter types
  1101. */
  1102. booleanKeys: Set<string>;
  1103. /**
  1104. * Keys considered valid for date filter types
  1105. */
  1106. dateKeys: Set<string>;
  1107. /**
  1108. * Disallow free text search
  1109. */
  1110. disallowFreeText: boolean;
  1111. /**
  1112. * Disallow negation for filters
  1113. */
  1114. disallowNegation: boolean;
  1115. /**
  1116. * Disallow parens in search
  1117. */
  1118. disallowParens: boolean;
  1119. /**
  1120. * Disallow wildcards in free text search AND in tag values
  1121. */
  1122. disallowWildcard: boolean;
  1123. /**
  1124. * Disallow specific boolean operators
  1125. */
  1126. disallowedLogicalOperators: Set<BooleanOperator>;
  1127. /**
  1128. * Keys which are considered valid for duration filters
  1129. */
  1130. durationKeys: Set<string>;
  1131. /**
  1132. * Configures the associated messages for invalid reasons
  1133. */
  1134. invalidMessages: Partial<Record<InvalidReason, string>>;
  1135. /**
  1136. * Keys considered valid for numeric filter types
  1137. */
  1138. numericKeys: Set<string>;
  1139. /**
  1140. * Keys considered valid for the percentage aggregate and may have percentage
  1141. * search values
  1142. */
  1143. percentageKeys: Set<string>;
  1144. /**
  1145. * Keys considered valid for size filter types
  1146. */
  1147. sizeKeys: Set<string>;
  1148. /**
  1149. * Text filter keys we allow to have operators
  1150. */
  1151. textOperatorKeys: Set<string>;
  1152. /**
  1153. * When true, the parser will not parse paren groups and will return individual paren tokens
  1154. */
  1155. flattenParenGroups?: boolean;
  1156. /**
  1157. * A function that returns a warning message for a given filter token key
  1158. */
  1159. getFilterTokenWarning?: (key: string) => React.ReactNode;
  1160. /**
  1161. * Determines if user input values should be parsed
  1162. */
  1163. parse?: boolean;
  1164. /**
  1165. * If validateKeys is set to true, tag keys that don't exist in supportedTags will be consider invalid
  1166. */
  1167. supportedTags?: TagCollection;
  1168. /**
  1169. * If set to true, tag keys that don't exist in supportedTags will be consider invalid
  1170. */
  1171. validateKeys?: boolean;
  1172. };
  1173. export const defaultConfig: SearchConfig = {
  1174. textOperatorKeys: new Set([
  1175. 'release.version',
  1176. 'release.build',
  1177. 'release.package',
  1178. 'release.stage',
  1179. ]),
  1180. durationKeys: new Set(['transaction.duration']),
  1181. percentageKeys: new Set(['percentage']),
  1182. // do not put functions in this Set
  1183. numericKeys: new Set([
  1184. 'project_id',
  1185. 'project.id',
  1186. 'issue.id',
  1187. 'stack.colno',
  1188. 'stack.lineno',
  1189. 'stack.stack_level',
  1190. 'transaction.duration',
  1191. ]),
  1192. dateKeys: new Set([
  1193. 'start',
  1194. 'end',
  1195. 'firstSeen',
  1196. 'lastSeen',
  1197. 'last_seen()',
  1198. 'time',
  1199. 'event.timestamp',
  1200. 'timestamp',
  1201. 'timestamp.to_hour',
  1202. 'timestamp.to_day',
  1203. ]),
  1204. booleanKeys: new Set([
  1205. 'error.handled',
  1206. 'error.unhandled',
  1207. 'stack.in_app',
  1208. 'team_key_transaction',
  1209. ]),
  1210. sizeKeys: new Set([]),
  1211. disallowedLogicalOperators: new Set(),
  1212. disallowFreeText: false,
  1213. disallowWildcard: false,
  1214. disallowNegation: false,
  1215. disallowParens: false,
  1216. invalidMessages: {
  1217. [InvalidReason.FREE_TEXT_NOT_ALLOWED]: t('Free text is not supported in this search'),
  1218. [InvalidReason.WILDCARD_NOT_ALLOWED]: t('Wildcards not supported in search'),
  1219. [InvalidReason.LOGICAL_OR_NOT_ALLOWED]: t(
  1220. 'The OR operator is not allowed in this search'
  1221. ),
  1222. [InvalidReason.LOGICAL_AND_NOT_ALLOWED]: t(
  1223. 'The AND operator is not allowed in this search'
  1224. ),
  1225. [InvalidReason.MUST_BE_QUOTED]: t('Quotes must enclose text or be escaped'),
  1226. [InvalidReason.NEGATION_NOT_ALLOWED]: t('Negation is not allowed in this search.'),
  1227. [InvalidReason.FILTER_MUST_HAVE_VALUE]: t('Filter must have a value'),
  1228. [InvalidReason.INVALID_BOOLEAN]: t('Invalid boolean. Expected true, 1, false, or 0.'),
  1229. [InvalidReason.INVALID_FILE_SIZE]: t(
  1230. 'Invalid file size. Expected number followed by file size unit suffix'
  1231. ),
  1232. [InvalidReason.INVALID_NUMBER]: t(
  1233. 'Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)'
  1234. ),
  1235. [InvalidReason.EMPTY_PARAMETER_NOT_ALLOWED]: t(
  1236. 'Function parameters should not have empty values'
  1237. ),
  1238. [InvalidReason.EMPTY_VALUE_IN_LIST_NOT_ALLOWED]: t(
  1239. 'Lists should not have empty values'
  1240. ),
  1241. [InvalidReason.PARENS_NOT_ALLOWED]: t('Parentheses are not supported in this search'),
  1242. },
  1243. };
  1244. function tryParseSearch<T extends {config: SearchConfig}>(
  1245. query: string,
  1246. config: T
  1247. ): ParseResult | null {
  1248. try {
  1249. return grammar.parse(query, config);
  1250. } catch (e) {
  1251. Sentry.withScope(scope => {
  1252. scope.setFingerprint(['search-syntax-parse-error']);
  1253. scope.setExtra('message', e.message?.slice(-100));
  1254. scope.setExtra('found', e.found);
  1255. Sentry.captureException(e);
  1256. });
  1257. return null;
  1258. }
  1259. }
  1260. /**
  1261. * Parse a search query into a ParseResult. Failing to parse the search query
  1262. * will result in null.
  1263. */
  1264. export function parseSearch(
  1265. query: string,
  1266. additionalConfig?: Partial<SearchConfig>
  1267. ): ParseResult | null {
  1268. const config = additionalConfig
  1269. ? merge({...defaultConfig}, additionalConfig)
  1270. : defaultConfig;
  1271. return tryParseSearch(query, {
  1272. config,
  1273. TokenConverter,
  1274. TermOperator,
  1275. FilterType,
  1276. });
  1277. }
  1278. /**
  1279. * Join a parsed query array into a string.
  1280. * Should handle null cases to chain easily with parseSearch.
  1281. * Option to add a leading space when applicable (e.g. to combine with other strings).
  1282. * Option to add a space between elements (e.g. for when no Token.Spaces present).
  1283. */
  1284. export function joinQuery(
  1285. parsedTerms: ParseResult | null | undefined,
  1286. leadingSpace?: boolean,
  1287. additionalSpaceBetween?: boolean
  1288. ): string {
  1289. if (!parsedTerms || !parsedTerms.length) {
  1290. return '';
  1291. }
  1292. return (
  1293. (leadingSpace ? ' ' : '') +
  1294. (parsedTerms.length === 1
  1295. ? parsedTerms[0].text
  1296. : parsedTerms.map(p => p.text).join(additionalSpaceBetween ? ' ' : ''))
  1297. );
  1298. }