parser.tsx 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825
  1. import moment from 'moment';
  2. import {LocationRange} from 'pegjs';
  3. import {t} from 'app/locale';
  4. import {
  5. isMeasurement,
  6. isSpanOperationBreakdownField,
  7. measurementType,
  8. } from 'app/utils/discover/fields';
  9. import grammar from './grammar.pegjs';
  10. import {getKeyName} from './utils';
  11. type TextFn = () => string;
  12. type LocationFn = () => LocationRange;
  13. type ListItem<V> = [
  14. space: ReturnType<TokenConverter['tokenSpaces']>,
  15. comma: string,
  16. space: ReturnType<TokenConverter['tokenSpaces']>,
  17. notComma: undefined,
  18. value: V | null
  19. ];
  20. const listJoiner = <K,>([s1, comma, s2, _, value]: ListItem<K>) => ({
  21. separator: [s1.value, comma, s2.value].join(''),
  22. value,
  23. });
  24. /**
  25. * A token represents a node in the syntax tree. These are all extrapolated
  26. * from the grammar and may not be named exactly the same.
  27. */
  28. export enum Token {
  29. Spaces = 'spaces',
  30. Filter = 'filter',
  31. FreeText = 'freeText',
  32. LogicGroup = 'logicGroup',
  33. LogicBoolean = 'logicBoolean',
  34. KeySimple = 'keySimple',
  35. KeyExplicitTag = 'keyExplicitTag',
  36. KeyAggregate = 'keyAggregate',
  37. KeyAggregateArgs = 'keyAggregateArgs',
  38. KeyAggregateParam = 'keyAggregateParam',
  39. ValueIso8601Date = 'valueIso8601Date',
  40. ValueRelativeDate = 'valueRelativeDate',
  41. ValueDuration = 'valueDuration',
  42. ValuePercentage = 'valuePercentage',
  43. ValueBoolean = 'valueBoolean',
  44. ValueNumber = 'valueNumber',
  45. ValueText = 'valueText',
  46. ValueNumberList = 'valueNumberList',
  47. ValueTextList = 'valueTextList',
  48. }
  49. /**
  50. * An operator in a key value term
  51. */
  52. export enum TermOperator {
  53. Default = '',
  54. GreaterThanEqual = '>=',
  55. LessThanEqual = '<=',
  56. GreaterThan = '>',
  57. LessThan = '<',
  58. Equal = '=',
  59. NotEqual = '!=',
  60. }
  61. /**
  62. * Logic operators
  63. */
  64. export enum BooleanOperator {
  65. And = 'AND',
  66. Or = 'OR',
  67. }
  68. /**
  69. * The Token.Filter may be one of many types of filters. This enum declares the
  70. * each variant filter type.
  71. */
  72. export enum FilterType {
  73. Text = 'text',
  74. TextIn = 'textIn',
  75. Date = 'date',
  76. SpecificDate = 'specificDate',
  77. RelativeDate = 'relativeDate',
  78. Duration = 'duration',
  79. Numeric = 'numeric',
  80. NumericIn = 'numericIn',
  81. Boolean = 'boolean',
  82. AggregateDuration = 'aggregateDuration',
  83. AggregatePercentage = 'aggregatePercentage',
  84. AggregateNumeric = 'aggregateNumeric',
  85. AggregateDate = 'aggregateDate',
  86. AggregateRelativeDate = 'aggregateRelativeDate',
  87. Has = 'has',
  88. Is = 'is',
  89. }
  90. const allOperators = [
  91. TermOperator.Default,
  92. TermOperator.GreaterThanEqual,
  93. TermOperator.LessThanEqual,
  94. TermOperator.GreaterThan,
  95. TermOperator.LessThan,
  96. TermOperator.Equal,
  97. TermOperator.NotEqual,
  98. ] as const;
  99. const basicOperators = [TermOperator.Default, TermOperator.NotEqual] as const;
  100. /**
  101. * Map of certain filter types to other filter types with applicable operators
  102. * e.g. SpecificDate can use the operators from Date to become a Date filter.
  103. */
  104. export const interchangeableFilterOperators = {
  105. [FilterType.SpecificDate]: [FilterType.Date],
  106. [FilterType.Date]: [FilterType.SpecificDate],
  107. };
  108. const textKeys = [Token.KeySimple, Token.KeyExplicitTag] as const;
  109. const numberUnits = {
  110. b: 1_000_000_000,
  111. m: 1_000_000,
  112. k: 1_000,
  113. };
  114. /**
  115. * This constant-type configuration object declares how each filter type
  116. * operates. Including what types of keys, operators, and values it may
  117. * receive.
  118. *
  119. * This configuration is used to generate the discriminate Filter type that is
  120. * returned from the tokenFilter converter.
  121. */
  122. export const filterTypeConfig = {
  123. [FilterType.Text]: {
  124. validKeys: textKeys,
  125. validOps: basicOperators,
  126. validValues: [Token.ValueText],
  127. canNegate: true,
  128. },
  129. [FilterType.TextIn]: {
  130. validKeys: textKeys,
  131. validOps: [],
  132. validValues: [Token.ValueTextList],
  133. canNegate: true,
  134. },
  135. [FilterType.Date]: {
  136. validKeys: [Token.KeySimple],
  137. validOps: allOperators,
  138. validValues: [Token.ValueIso8601Date],
  139. canNegate: false,
  140. },
  141. [FilterType.SpecificDate]: {
  142. validKeys: [Token.KeySimple],
  143. validOps: [],
  144. validValues: [Token.ValueIso8601Date],
  145. canNegate: false,
  146. },
  147. [FilterType.RelativeDate]: {
  148. validKeys: [Token.KeySimple],
  149. validOps: [],
  150. validValues: [Token.ValueRelativeDate],
  151. canNegate: false,
  152. },
  153. [FilterType.Duration]: {
  154. validKeys: [Token.KeySimple],
  155. validOps: allOperators,
  156. validValues: [Token.ValueDuration],
  157. canNegate: false,
  158. },
  159. [FilterType.Numeric]: {
  160. validKeys: [Token.KeySimple],
  161. validOps: allOperators,
  162. validValues: [Token.ValueNumber],
  163. canNegate: false,
  164. },
  165. [FilterType.NumericIn]: {
  166. validKeys: [Token.KeySimple],
  167. validOps: [],
  168. validValues: [Token.ValueNumberList],
  169. canNegate: false,
  170. },
  171. [FilterType.Boolean]: {
  172. validKeys: [Token.KeySimple],
  173. validOps: basicOperators,
  174. validValues: [Token.ValueBoolean],
  175. canNegate: true,
  176. },
  177. [FilterType.AggregateDuration]: {
  178. validKeys: [Token.KeyAggregate],
  179. validOps: allOperators,
  180. validValues: [Token.ValueDuration],
  181. canNegate: true,
  182. },
  183. [FilterType.AggregateNumeric]: {
  184. validKeys: [Token.KeyAggregate],
  185. validOps: allOperators,
  186. validValues: [Token.ValueNumber],
  187. canNegate: true,
  188. },
  189. [FilterType.AggregatePercentage]: {
  190. validKeys: [Token.KeyAggregate],
  191. validOps: allOperators,
  192. validValues: [Token.ValuePercentage],
  193. canNegate: true,
  194. },
  195. [FilterType.AggregateDate]: {
  196. validKeys: [Token.KeyAggregate],
  197. validOps: allOperators,
  198. validValues: [Token.ValueIso8601Date],
  199. canNegate: true,
  200. },
  201. [FilterType.AggregateRelativeDate]: {
  202. validKeys: [Token.KeyAggregate],
  203. validOps: allOperators,
  204. validValues: [Token.ValueRelativeDate],
  205. canNegate: true,
  206. },
  207. [FilterType.Has]: {
  208. validKeys: [Token.KeySimple],
  209. validOps: basicOperators,
  210. validValues: [],
  211. canNegate: true,
  212. },
  213. [FilterType.Is]: {
  214. validKeys: [Token.KeySimple],
  215. validOps: basicOperators,
  216. validValues: [Token.ValueText],
  217. canNegate: true,
  218. },
  219. } as const;
  220. type FilterTypeConfig = typeof filterTypeConfig;
  221. /**
  222. * Object representing an invalid filter state
  223. */
  224. type InvalidFilter = {
  225. /**
  226. * The message indicating why the filter is invalid
  227. */
  228. reason: string;
  229. /**
  230. * In the case where a filter is invalid, we may be expecting a different
  231. * type for this filter based on the key. This can be useful to hint to the
  232. * user what values they should be providing.
  233. *
  234. * This may be multiple filter types.
  235. */
  236. expectedType?: FilterType[];
  237. };
  238. type FilterMap = {
  239. [F in keyof FilterTypeConfig]: {
  240. type: Token.Filter;
  241. /**
  242. * The filter type being represented
  243. */
  244. filter: F;
  245. /**
  246. * The key of the filter
  247. */
  248. key: KVConverter<FilterTypeConfig[F]['validKeys'][number]>;
  249. /**
  250. * The value of the filter
  251. */
  252. value: KVConverter<FilterTypeConfig[F]['validValues'][number]>;
  253. /**
  254. * The operator applied to the filter
  255. */
  256. operator: FilterTypeConfig[F]['validOps'][number];
  257. /**
  258. * Indicates if the filter has been negated
  259. */
  260. negated: FilterTypeConfig[F]['canNegate'] extends true ? boolean : false;
  261. /**
  262. * When a filter is marked as 'invalid' a reason is given. If the filter is
  263. * not invalid this will always be null
  264. */
  265. invalid: InvalidFilter | null;
  266. };
  267. };
  268. type TextFilter = FilterMap[FilterType.Text];
  269. type InFilter = FilterMap[FilterType.TextIn] | FilterMap[FilterType.NumericIn];
  270. /**
  271. * The Filter type discriminates on the FilterType enum using the `filter` key.
  272. *
  273. * When receiving this type you may narrow it to a specific filter by checking
  274. * this field. This will give you proper types on what the key, value, and
  275. * operator results are.
  276. */
  277. type FilterResult = FilterMap[FilterType];
  278. type TokenConverterOpts = {
  279. text: TextFn;
  280. location: LocationFn;
  281. config: SearchConfig;
  282. };
  283. /**
  284. * Used to construct token results via the token grammar
  285. */
  286. export class TokenConverter {
  287. text: TextFn;
  288. location: LocationFn;
  289. config: SearchConfig;
  290. constructor({text, location, config}: TokenConverterOpts) {
  291. this.text = text;
  292. this.location = location;
  293. this.config = config;
  294. }
  295. /**
  296. * Validates various types of keys
  297. */
  298. keyValidation = {
  299. isNumeric: (key: string) => this.config.numericKeys.has(key) || isMeasurement(key),
  300. isBoolean: (key: string) => this.config.booleanKeys.has(key),
  301. isPercentage: (key: string) => this.config.percentageKeys.has(key),
  302. isDate: (key: string) => this.config.dateKeys.has(key),
  303. isDuration: (key: string) =>
  304. this.config.durationKeys.has(key) ||
  305. isSpanOperationBreakdownField(key) ||
  306. measurementType(key) === 'duration',
  307. };
  308. /**
  309. * Creates shared `text` and `location` keys.
  310. */
  311. get defaultTokenFields() {
  312. return {
  313. text: this.text(),
  314. location: this.location(),
  315. };
  316. }
  317. tokenSpaces = (value: string) => ({
  318. ...this.defaultTokenFields,
  319. type: Token.Spaces as const,
  320. value,
  321. });
  322. tokenFilter = <T extends FilterType>(
  323. filter: T,
  324. key: FilterMap[T]['key'],
  325. value: FilterMap[T]['value'],
  326. operator: FilterMap[T]['operator'] | undefined,
  327. negated: FilterMap[T]['negated']
  328. ) => {
  329. const filterToken = {
  330. type: Token.Filter as const,
  331. filter,
  332. key,
  333. value,
  334. negated,
  335. operator: operator ?? TermOperator.Default,
  336. invalid: this.checkInvalidFilter(filter, key, value),
  337. } as FilterResult;
  338. return {
  339. ...this.defaultTokenFields,
  340. ...filterToken,
  341. };
  342. };
  343. tokenFreeText = (value: string, quoted: boolean) => ({
  344. ...this.defaultTokenFields,
  345. type: Token.FreeText as const,
  346. value,
  347. quoted,
  348. });
  349. tokenLogicGroup = (
  350. inner: Array<
  351. | ReturnType<TokenConverter['tokenLogicBoolean']>
  352. | ReturnType<TokenConverter['tokenFilter']>
  353. | ReturnType<TokenConverter['tokenFreeText']>
  354. >
  355. ) => ({
  356. ...this.defaultTokenFields,
  357. type: Token.LogicGroup as const,
  358. inner,
  359. });
  360. tokenLogicBoolean = (bool: BooleanOperator) => ({
  361. ...this.defaultTokenFields,
  362. type: Token.LogicBoolean as const,
  363. value: bool,
  364. });
  365. tokenKeySimple = (value: string, quoted: boolean) => ({
  366. ...this.defaultTokenFields,
  367. type: Token.KeySimple as const,
  368. value,
  369. quoted,
  370. });
  371. tokenKeyExplicitTag = (
  372. prefix: string,
  373. key: ReturnType<TokenConverter['tokenKeySimple']>
  374. ) => ({
  375. ...this.defaultTokenFields,
  376. type: Token.KeyExplicitTag as const,
  377. prefix,
  378. key,
  379. });
  380. tokenKeyAggregateParam = (value: string, quoted: boolean) => ({
  381. ...this.defaultTokenFields,
  382. type: Token.KeyAggregateParam as const,
  383. value,
  384. quoted,
  385. });
  386. tokenKeyAggregate = (
  387. name: ReturnType<TokenConverter['tokenKeySimple']>,
  388. args: ReturnType<TokenConverter['tokenKeyAggregateArgs']> | null,
  389. argsSpaceBefore: ReturnType<TokenConverter['tokenSpaces']>,
  390. argsSpaceAfter: ReturnType<TokenConverter['tokenSpaces']>
  391. ) => ({
  392. ...this.defaultTokenFields,
  393. type: Token.KeyAggregate as const,
  394. name,
  395. args,
  396. argsSpaceBefore,
  397. argsSpaceAfter,
  398. });
  399. tokenKeyAggregateArgs = (
  400. arg1: ReturnType<TokenConverter['tokenKeyAggregateParam']>,
  401. args: ListItem<ReturnType<TokenConverter['tokenKeyAggregateParam']>>[]
  402. ) => ({
  403. ...this.defaultTokenFields,
  404. type: Token.KeyAggregateArgs as const,
  405. args: [{separator: '', value: arg1}, ...args.map(listJoiner)],
  406. });
  407. tokenValueIso8601Date = (value: string) => ({
  408. ...this.defaultTokenFields,
  409. type: Token.ValueIso8601Date as const,
  410. value: moment(value),
  411. });
  412. tokenValueRelativeDate = (
  413. value: string,
  414. sign: '-' | '+',
  415. unit: 'w' | 'd' | 'h' | 'm'
  416. ) => ({
  417. ...this.defaultTokenFields,
  418. type: Token.ValueRelativeDate as const,
  419. value: Number(value),
  420. sign,
  421. unit,
  422. });
  423. tokenValueDuration = (
  424. value: string,
  425. unit: 'ms' | 's' | 'min' | 'm' | 'hr' | 'h' | 'day' | 'd' | 'wk' | 'w'
  426. ) => ({
  427. ...this.defaultTokenFields,
  428. type: Token.ValueDuration as const,
  429. value: Number(value),
  430. unit,
  431. });
  432. tokenValuePercentage = (value: string) => ({
  433. ...this.defaultTokenFields,
  434. type: Token.ValuePercentage as const,
  435. value: Number(value),
  436. });
  437. tokenValueBoolean = (value: string) => ({
  438. ...this.defaultTokenFields,
  439. type: Token.ValueBoolean as const,
  440. value: ['1', 'true'].includes(value.toLowerCase()),
  441. });
  442. tokenValueNumber = (value: string, unit: string) => ({
  443. ...this.defaultTokenFields,
  444. type: Token.ValueNumber as const,
  445. value,
  446. rawValue: Number(value) * (numberUnits[unit] ?? 1),
  447. unit,
  448. });
  449. tokenValueNumberList = (
  450. item1: ReturnType<TokenConverter['tokenValueNumber']>,
  451. items: ListItem<ReturnType<TokenConverter['tokenValueNumber']>>[]
  452. ) => ({
  453. ...this.defaultTokenFields,
  454. type: Token.ValueNumberList as const,
  455. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  456. });
  457. tokenValueTextList = (
  458. item1: ReturnType<TokenConverter['tokenValueText']>,
  459. items: ListItem<ReturnType<TokenConverter['tokenValueText']>>[]
  460. ) => ({
  461. ...this.defaultTokenFields,
  462. type: Token.ValueTextList as const,
  463. items: [{separator: '', value: item1}, ...items.map(listJoiner)],
  464. });
  465. tokenValueText = (value: string, quoted: boolean) => ({
  466. ...this.defaultTokenFields,
  467. type: Token.ValueText as const,
  468. value,
  469. quoted,
  470. });
  471. /**
  472. * This method is used while tokenizing to predicate whether a filter should
  473. * match or not. We do this because not all keys are valid for specific
  474. * filter types. For example, boolean filters should only match for keys
  475. * which can be filtered as booleans.
  476. *
  477. * See [0] and look for &{ predicate } to understand how predicates are
  478. * declared in the grammar
  479. *
  480. * [0]:https://pegjs.org/documentation
  481. */
  482. predicateFilter = <T extends FilterType>(type: T, key: FilterMap[T]['key']) => {
  483. const keyName = getKeyName(key);
  484. const aggregateKey = key as ReturnType<TokenConverter['tokenKeyAggregate']>;
  485. const {isNumeric, isDuration, isBoolean, isDate, isPercentage} = this.keyValidation;
  486. const checkAggregate = (check: (s: string) => boolean) =>
  487. aggregateKey.args?.args.some(arg => check(arg?.value?.value ?? ''));
  488. switch (type) {
  489. case FilterType.Numeric:
  490. case FilterType.NumericIn:
  491. return isNumeric(keyName);
  492. case FilterType.Duration:
  493. return isDuration(keyName);
  494. case FilterType.Boolean:
  495. return isBoolean(keyName);
  496. case FilterType.Date:
  497. case FilterType.RelativeDate:
  498. case FilterType.SpecificDate:
  499. return isDate(keyName);
  500. case FilterType.AggregateDuration:
  501. return checkAggregate(isDuration);
  502. case FilterType.AggregateDate:
  503. return checkAggregate(isDate);
  504. case FilterType.AggregatePercentage:
  505. return checkAggregate(isPercentage);
  506. default:
  507. return true;
  508. }
  509. };
  510. /**
  511. * Predicates weather a text filter have operators for specific keys.
  512. */
  513. predicateTextOperator = (key: TextFilter['key']) =>
  514. this.config.textOperatorKeys.has(getKeyName(key));
  515. /**
  516. * Checks a filter against some non-grammar validation rules
  517. */
  518. checkInvalidFilter = <T extends FilterType>(
  519. filter: T,
  520. key: FilterMap[T]['key'],
  521. value: FilterMap[T]['value']
  522. ) => {
  523. // Text filter is the "fall through" filter that will match when other
  524. // filter predicates fail.
  525. if (filter === FilterType.Text) {
  526. return this.checkInvalidTextFilter(
  527. key as TextFilter['key'],
  528. value as TextFilter['value']
  529. );
  530. }
  531. if (filter === FilterType.Is || filter === FilterType.Has) {
  532. return this.checkInvalidTextValue(value as TextFilter['value']);
  533. }
  534. if ([FilterType.TextIn, FilterType.NumericIn].includes(filter)) {
  535. return this.checkInvalidInFilter(value as InFilter['value']);
  536. }
  537. return null;
  538. };
  539. /**
  540. * Validates text filters which may have failed predication
  541. */
  542. checkInvalidTextFilter = (key: TextFilter['key'], value: TextFilter['value']) => {
  543. // Explicit tag keys will always be treated as text filters
  544. if (key.type === Token.KeyExplicitTag) {
  545. return this.checkInvalidTextValue(value);
  546. }
  547. const keyName = getKeyName(key);
  548. if (this.keyValidation.isDuration(keyName)) {
  549. return {
  550. reason: t('Invalid duration. Expected number followed by duration unit suffix'),
  551. expectedType: [FilterType.Duration],
  552. };
  553. }
  554. if (this.keyValidation.isDate(keyName)) {
  555. const date = new Date();
  556. date.setSeconds(0);
  557. date.setMilliseconds(0);
  558. const example = date.toISOString();
  559. return {
  560. reason: t(
  561. 'Invalid date format. Expected +/-duration (e.g. +1h) or ISO 8601-like (e.g. %s or %s)',
  562. example.slice(0, 10),
  563. example
  564. ),
  565. expectedType: [FilterType.Date, FilterType.SpecificDate, FilterType.RelativeDate],
  566. };
  567. }
  568. if (this.keyValidation.isBoolean(keyName)) {
  569. return {
  570. reason: t('Invalid boolean. Expected true, 1, false, or 0.'),
  571. expectedType: [FilterType.Boolean],
  572. };
  573. }
  574. if (this.keyValidation.isNumeric(keyName)) {
  575. return {
  576. reason: t(
  577. 'Invalid number. Expected number then optional k, m, or b suffix (e.g. 500k)'
  578. ),
  579. expectedType: [FilterType.Numeric, FilterType.NumericIn],
  580. };
  581. }
  582. return this.checkInvalidTextValue(value);
  583. };
  584. /**
  585. * Validates the value of a text filter
  586. */
  587. checkInvalidTextValue = (value: TextFilter['value']) => {
  588. if (!value.quoted && /(^|[^\\])"/.test(value.value)) {
  589. return {reason: t('Quotes must enclose text or be escaped')};
  590. }
  591. if (!value.quoted && value.value === '') {
  592. return {reason: t('Filter must have a value')};
  593. }
  594. return null;
  595. };
  596. /**
  597. * Validates IN filter values do not have an missing elements
  598. */
  599. checkInvalidInFilter = ({items}: InFilter['value']) => {
  600. const hasEmptyValue = items.some(item => item.value === null);
  601. if (hasEmptyValue) {
  602. return {reason: t('Lists should not have empty values')};
  603. }
  604. return null;
  605. };
  606. }
  607. /**
  608. * Maps token conversion methods to their result types
  609. */
  610. type ConverterResultMap = {
  611. [K in keyof TokenConverter & `token${string}`]: ReturnType<TokenConverter[K]>;
  612. };
  613. type Converter = keyof ConverterResultMap;
  614. /**
  615. * Converter keys specific to Key and Value tokens
  616. */
  617. type KVTokens = Converter & `token${'Key' | 'Value'}${string}`;
  618. /**
  619. * Similar to TokenResult, but only includes Key* and Value* token type
  620. * results. This avoids a circular reference when this is used for the Filter
  621. * token converter result
  622. */
  623. type KVConverter<T extends Token> = ConverterResultMap[KVTokens] & {type: T};
  624. /**
  625. * Each token type is discriminated by the `type` field.
  626. */
  627. export type TokenResult<T extends Token> = ConverterResultMap[Converter] & {type: T};
  628. /**
  629. * Result from parsing a search query.
  630. */
  631. export type ParseResult = Array<
  632. | TokenResult<Token.LogicBoolean>
  633. | TokenResult<Token.LogicGroup>
  634. | TokenResult<Token.Filter>
  635. | TokenResult<Token.FreeText>
  636. | TokenResult<Token.Spaces>
  637. >;
  638. /**
  639. * Configures behavior of search parsing
  640. */
  641. export type SearchConfig = {
  642. /**
  643. * Keys which are considered valid for duration filters
  644. */
  645. durationKeys: Set<string>;
  646. /**
  647. * Text filter keys we allow to have operators
  648. */
  649. textOperatorKeys: Set<string>;
  650. /**
  651. * Keys considered valid for the percentage aggregate and may have percentage
  652. * search values
  653. */
  654. percentageKeys: Set<string>;
  655. /**
  656. * Keys considered valid for numeric filter types
  657. */
  658. numericKeys: Set<string>;
  659. /**
  660. * Keys considered valid for date filter types
  661. */
  662. dateKeys: Set<string>;
  663. /**
  664. * Keys considered valid for boolean filter types
  665. */
  666. booleanKeys: Set<string>;
  667. /**
  668. * Enables boolean filtering (AND / OR)
  669. */
  670. allowBoolean: boolean;
  671. };
  672. const defaultConfig: SearchConfig = {
  673. textOperatorKeys: new Set([
  674. 'release.version',
  675. 'release.build',
  676. 'release.package',
  677. 'release.stage',
  678. ]),
  679. durationKeys: new Set(['transaction.duration']),
  680. percentageKeys: new Set(['percentage']),
  681. numericKeys: new Set([
  682. 'project_id',
  683. 'project.id',
  684. 'issue.id',
  685. 'stack.colno',
  686. 'stack.lineno',
  687. 'stack.stack_level',
  688. 'transaction.duration',
  689. 'apdex',
  690. 'p75',
  691. 'p95',
  692. 'p99',
  693. 'failure_rate',
  694. 'count_miserable',
  695. 'user_misery',
  696. 'count_miserable_new',
  697. 'user_miser_new',
  698. ]),
  699. dateKeys: new Set([
  700. 'start',
  701. 'end',
  702. 'first_seen',
  703. 'last_seen',
  704. 'time',
  705. 'event.timestamp',
  706. 'timestamp',
  707. 'timestamp.to_hour',
  708. 'timestamp.to_day',
  709. 'transaction.start_time',
  710. 'transaction.end_time',
  711. ]),
  712. booleanKeys: new Set([
  713. 'error.handled',
  714. 'error.unhandled',
  715. 'stack.in_app',
  716. 'key_transaction',
  717. 'team_key_transaction',
  718. ]),
  719. allowBoolean: true,
  720. };
  721. const options = {
  722. TokenConverter,
  723. TermOperator,
  724. FilterType,
  725. config: defaultConfig,
  726. };
  727. /**
  728. * Parse a search query into a ParseResult. Failing to parse the search query
  729. * will result in null.
  730. */
  731. export function parseSearch(query: string): ParseResult | null {
  732. try {
  733. return grammar.parse(query, options);
  734. } catch (e) {
  735. // TODO(epurkhiser): Should we capture these errors somewhere?
  736. }
  737. return null;
  738. }